Many more tests and fixes
All checks were successful
Build / Build (push) Successful in 3m12s

This commit is contained in:
2026-08-27 17:11:12 +02:00
parent 32b3ef9c4a
commit d2a82b5fe3
204 changed files with 20255 additions and 242 deletions

View File

@@ -0,0 +1,237 @@
using PkmnLib.Dynamic.Models;
using PkmnLib.Dynamic.Models.Choices;
using PkmnLib.Dynamic.ScriptHandling;
using PkmnLib.Plugin.Gen7.Scripts.Moves;
using PkmnLib.Plugin.Gen7.Scripts.Pokemon;
using PkmnLib.Static;
using PkmnLib.Static.Moves;
namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
/// <summary>
/// Tests for the <see cref="Ingrain"/> move script and its <see cref="IngrainEffect"/> volatile.
/// Gen VII Bulbapedia behavior: the user plants roots, restoring 1/16th of its maximum HP each turn while
/// becoming unable to switch out; forced-switch moves like Roar fail against it. From Generation IV onwards
/// the move grounds the affected Pokémon, making them susceptible to Ground moves even if Flying-type.
/// Generation VI onwards: "Ghost-type Pokémon under the effects of Ingrain are now able to switch out."
/// </summary>
public class IngrainTests
{
/// <summary>
/// Creates a rooted Pokémon substitute with the given maximum HP, together with the
/// <see cref="IngrainEffect"/> that roots it.
/// </summary>
private static (IngrainEffect effect, IPokemon owner) CreateRootedPokemon(uint maxHealth = 100)
{
var owner = Substitute.For<IPokemon>();
owner.BoostedStats.Returns(new StatisticSet<uint>(maxHealth, 1, 1, 1, 1, 1));
return (new IngrainEffect(owner), owner);
}
/// <summary>
/// Helper to extract the heal amount from a Pokémon's received Heal calls.
/// </summary>
private static uint? GetHealAmount(IPokemon pokemon)
{
var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Heal");
return call != null ? (uint)call.GetArguments()[0]! : null;
}
/// <summary>
/// Bulbapedia: "The user plants roots" — using the move attaches the <see cref="IngrainEffect"/>
/// volatile to the user.
/// </summary>
[Test]
public async Task OnSecondaryEffect_Used_AddsIngrainEffectToUser()
{
// Arrange
var script = new Ingrain();
var move = Substitute.For<IExecutingMove>();
var user = Substitute.For<IPokemon>();
user.GetScripts().Returns(_ => new ScriptIterator(new List<IEnumerable<ScriptContainer>>()));
var userVolatile = new ScriptSet(user);
user.Volatile.Returns(userVolatile);
move.User.Returns(user);
var target = Substitute.For<IPokemon>();
// Act
script.OnSecondaryEffect(move, target, 0);
// Assert
await Assert.That(userVolatile.Get<IngrainEffect>()).IsNotNull();
}
/// <summary>
/// Bulbapedia: the roots restore "1/16th maximum HP each turn".
/// </summary>
[Test, Arguments(160u, 10u), Arguments(100u, 6u), Arguments(17u, 1u)]
public async Task OnEndTurn_RootedPokemon_HealsOneSixteenthOfMaxHp(uint maxHealth, uint expectedHeal)
{
// Arrange
var (effect, owner) = CreateRootedPokemon(maxHealth);
// Act
effect.OnEndTurn(owner, Substitute.For<IBattle>());
// Assert
await Assert.That(GetHealAmount(owner)!.Value).IsEqualTo(expectedHeal);
}
/// <summary>
/// Bulbapedia: the rooted user becomes "unable to switch out".
/// </summary>
[Test]
public async Task PreventSelfSwitch_RootedPokemon_CannotSwitchOut()
{
// Arrange
var (effect, _) = CreateRootedPokemon();
var prevent = false;
// Act
effect.PreventSelfSwitch(Substitute.For<ISwitchChoice>(), ref prevent);
// Assert
await Assert.That(prevent).IsTrue();
}
/// <summary>
/// Bulbapedia: the user is rooted in place; a rooted wild Pokémon cannot flee the battle.
/// </summary>
[Test]
public async Task PreventSelfRunAway_RootedPokemon_CannotFlee()
{
// Arrange
var (effect, _) = CreateRootedPokemon();
var prevent = false;
// Act
effect.PreventSelfRunAway(Substitute.For<IFleeChoice>(), ref prevent);
// Assert
await Assert.That(prevent).IsTrue();
}
/// <summary>
/// Bulbapedia: the rooted user cannot be forced out by "forced-switch moves like Roar" — Roar and
/// Whirlwind fail against it.
/// </summary>
[Test, Arguments("roar"), Arguments("whirlwind")]
public async Task FailIncomingMove_ForcedSwitchMove_MoveFails(string moveName)
{
// Arrange
var (effect, owner) = CreateRootedPokemon();
var incomingMove = Substitute.For<IExecutingMove>();
var moveData = Substitute.For<IMoveData>();
moveData.Name.Returns(new Static.Utils.StringKey(moveName));
incomingMove.UseMove.Returns(moveData);
var fail = false;
// Act
effect.FailIncomingMove(incomingMove, owner, ref fail);
// Assert
await Assert.That(fail).IsTrue();
}
/// <summary>
/// Bulbapedia: only forced-switch moves are blocked by the roots; any other incoming move works
/// normally.
/// </summary>
[Test]
public async Task FailIncomingMove_RegularMove_MoveDoesNotFail()
{
// Arrange
var (effect, owner) = CreateRootedPokemon();
var incomingMove = Substitute.For<IExecutingMove>();
var moveData = Substitute.For<IMoveData>();
moveData.Name.Returns(new Static.Utils.StringKey("tackle"));
incomingMove.UseMove.Returns(moveData);
var fail = false;
// Act
effect.FailIncomingMove(incomingMove, owner, ref fail);
// Assert
await Assert.That(fail).IsFalse();
}
/// <summary>
/// Bulbapedia (Generation IV onwards): the move grounds the affected Pokémon, making them susceptible to
/// Ground moves even if Flying-type. For an incoming Ground-type move, the rooted Pokémon's
/// Ground-immune types are ignored.
/// </summary>
[Test]
public async Task ChangeTypesForIncomingMove_GroundMoveAgainstFlyingType_RemovesGroundImmuneType()
{
// Arrange
var (effect, owner) = CreateRootedPokemon();
var library = LibraryHelpers.LoadLibrary();
owner.Library.Returns(library);
var types = library.StaticLibrary.Types;
await Assert.That(types.TryGetTypeIdentifier("ground", out var ground)).IsTrue();
await Assert.That(types.TryGetTypeIdentifier("flying", out var flying)).IsTrue();
await Assert.That(types.TryGetTypeIdentifier("grass", out var grass)).IsTrue();
var incomingMove = Substitute.For<IExecutingMove>();
var moveData = Substitute.For<IMoveData>();
moveData.MoveType.Returns(ground);
incomingMove.UseMove.Returns(moveData);
var targetTypes = new List<TypeIdentifier> { flying, grass };
// Act
effect.ChangeTypesForIncomingMove(incomingMove, owner, 0, targetTypes);
// Assert
await Assert.That(targetTypes.Contains(flying)).IsFalse();
await Assert.That(targetTypes.Contains(grass)).IsTrue();
}
/// <summary>
/// The grounding only matters for Ground-type moves: any other incoming move sees the rooted Pokémon's
/// types unchanged.
/// </summary>
[Test]
public async Task ChangeTypesForIncomingMove_NonGroundMove_TypesUnchanged()
{
// Arrange
var (effect, owner) = CreateRootedPokemon();
var library = LibraryHelpers.LoadLibrary();
owner.Library.Returns(library);
var types = library.StaticLibrary.Types;
await Assert.That(types.TryGetTypeIdentifier("water", out var water)).IsTrue();
await Assert.That(types.TryGetTypeIdentifier("flying", out var flying)).IsTrue();
var incomingMove = Substitute.For<IExecutingMove>();
var moveData = Substitute.For<IMoveData>();
moveData.MoveType.Returns(water);
incomingMove.UseMove.Returns(moveData);
var targetTypes = new List<TypeIdentifier> { flying };
// Act
effect.ChangeTypesForIncomingMove(incomingMove, owner, 0, targetTypes);
// Assert
await Assert.That(targetTypes.Contains(flying)).IsTrue();
}
/// <summary>
/// Bulbapedia (Generation VI onwards): "Ghost-type Pokémon under the effects of Ingrain are now able to
/// switch out."
/// </summary>
[Test]
public async Task PreventSelfSwitch_GhostTypeRooted_CanStillSwitchOut()
{
// Arrange
var (effect, owner) = CreateRootedPokemon();
owner.Types.Returns(new List<TypeIdentifier> { new(8, "ghost") });
var prevent = false;
var choice = Substitute.For<ISwitchChoice>();
choice.User.Returns(owner);
// Act
effect.PreventSelfSwitch(choice, ref prevent);
// Assert
await Assert.That(prevent).IsFalse();
}
}