263 lines
9.9 KiB
C#
263 lines
9.9 KiB
C#
using PkmnLib.Dynamic.Events;
|
|
using PkmnLib.Dynamic.Models;
|
|
using PkmnLib.Dynamic.ScriptHandling;
|
|
using PkmnLib.Plugin.Gen7.Scripts.Moves;
|
|
using PkmnLib.Plugin.Gen7.Scripts.Pokemon;
|
|
using PkmnLib.Static;
|
|
|
|
namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
|
|
|
|
/// <summary>
|
|
/// Tests for the <see cref="FlowerShield"/> move script.
|
|
/// Gen VII Bulbapedia behavior: "Flower Shield raises the Defense stat of all Grass-type Pokémon on the
|
|
/// field by one stage, except those that are in the semi-invulnerable turn of a move such as Fly or Dig."
|
|
/// </summary>
|
|
public class FlowerShieldTests
|
|
{
|
|
private static readonly TypeIdentifier GrassType;
|
|
private static readonly TypeIdentifier WaterType;
|
|
|
|
static FlowerShieldTests()
|
|
{
|
|
var library = LibraryHelpers.LoadLibrary();
|
|
library.StaticLibrary.Types.TryGetTypeIdentifier("grass", out GrassType);
|
|
library.StaticLibrary.Types.TryGetTypeIdentifier("water", out WaterType);
|
|
}
|
|
|
|
private static (FlowerShield script, IExecutingMove move, IPokemon target, IBattle battle) CreateTestSetup(
|
|
params IReadOnlyList<IPokemon?>[] sidePokemon)
|
|
{
|
|
var script = new FlowerShield();
|
|
var move = Substitute.For<IExecutingMove>();
|
|
var user = Substitute.For<IPokemon>();
|
|
move.User.Returns(user);
|
|
|
|
var library = LibraryHelpers.LoadLibrary();
|
|
var battle = Substitute.For<IBattle>();
|
|
battle.Library.Returns(library);
|
|
var sides = sidePokemon.Select(pokemon =>
|
|
{
|
|
var side = Substitute.For<IBattleSide>();
|
|
side.Pokemon.Returns(pokemon);
|
|
return side;
|
|
}).ToArray();
|
|
battle.Sides.Returns(sides);
|
|
|
|
var battleData = Substitute.For<IPokemonBattleData>();
|
|
battleData.Battle.Returns(battle);
|
|
var target = Substitute.For<IPokemon>();
|
|
target.BattleData.Returns(battleData);
|
|
|
|
return (script, move, target, battle);
|
|
}
|
|
|
|
private static IPokemon CreatePokemon(TypeIdentifier type, bool isFainted = false)
|
|
{
|
|
var pokemon = Substitute.For<IPokemon>();
|
|
pokemon.Types.Returns(new[] { type });
|
|
pokemon.IsFainted.Returns(isFainted);
|
|
return pokemon;
|
|
}
|
|
|
|
private static bool ReceivedStatBoost(IPokemon pokemon) =>
|
|
pokemon.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "ChangeStatBoost");
|
|
|
|
/// <summary>
|
|
/// Helper to extract the arguments of the received ChangeStatBoost call, or null when no stat boost
|
|
/// was requested. Received-call inspection is used instead of NSubstitute argument matchers because
|
|
/// the trailing <see cref="EventBatchId"/> parameter cannot be bound by <c>Arg.Any</c> (its
|
|
/// parameterless constructor initializes a fresh id, so it never equals the matcher's default value).
|
|
/// </summary>
|
|
private static object?[]? GetStatBoostArgs(IPokemon pokemon)
|
|
{
|
|
var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "ChangeStatBoost");
|
|
return call?.GetArguments();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "Flower Shield raises the Defense stat of all Grass-type Pokémon on the field by one
|
|
/// stage". The boost on another Pokémon is not self-inflicted.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnSecondaryEffect_GrassTypeOnField_DefenseRaisedByOneStage()
|
|
{
|
|
// Arrange
|
|
var grassPokemon = CreatePokemon(GrassType);
|
|
var (script, move, target, _) = CreateTestSetup(new List<IPokemon?> { grassPokemon });
|
|
|
|
// Act
|
|
script.OnSecondaryEffect(move, target, 0);
|
|
|
|
// Assert
|
|
var args = GetStatBoostArgs(grassPokemon);
|
|
await Assert.That(args).IsNotNull();
|
|
await Assert.That((Statistic)args![0]!).IsEqualTo(Statistic.Defense);
|
|
await Assert.That((sbyte)args[1]!).IsEqualTo((sbyte)1);
|
|
await Assert.That((bool)args[2]!).IsFalse(); // not self-inflicted
|
|
await Assert.That((bool)args[3]!).IsFalse(); // not forced
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: the Defense boost applies to "all Grass-type Pokémon on the field", so Grass-types on
|
|
/// every side (allies and opponents alike) are raised.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnSecondaryEffect_GrassTypesOnBothSides_AllAreRaised()
|
|
{
|
|
// Arrange
|
|
var allyGrass = CreatePokemon(GrassType);
|
|
var opposingGrass = CreatePokemon(GrassType);
|
|
var (script, move, target, _) = CreateTestSetup(new List<IPokemon?> { allyGrass },
|
|
new List<IPokemon?> { opposingGrass });
|
|
|
|
// Act
|
|
script.OnSecondaryEffect(move, target, 0);
|
|
|
|
// Assert
|
|
await Assert.That(ReceivedStatBoost(allyGrass)).IsTrue();
|
|
await Assert.That(ReceivedStatBoost(opposingGrass)).IsTrue();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: only "Grass-type Pokémon on the field" are affected; a non-Grass-type Pokémon does not
|
|
/// have its Defense raised.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnSecondaryEffect_NonGrassType_DefenseNotRaised()
|
|
{
|
|
// Arrange
|
|
var waterPokemon = CreatePokemon(WaterType);
|
|
var (script, move, target, _) = CreateTestSetup(new List<IPokemon?> { waterPokemon });
|
|
|
|
// Act
|
|
script.OnSecondaryEffect(move, target, 0);
|
|
|
|
// Assert
|
|
await Assert.That(ReceivedStatBoost(waterPokemon)).IsFalse();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: the boost applies to Pokémon "on the field"; a fainted Grass-type Pokémon is no longer
|
|
/// battling and is not affected.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnSecondaryEffect_FaintedGrassType_DefenseNotRaised()
|
|
{
|
|
// Arrange
|
|
var faintedGrass = CreatePokemon(GrassType, true);
|
|
var (script, move, target, _) = CreateTestSetup(new List<IPokemon?> { faintedGrass });
|
|
|
|
// Act
|
|
script.OnSecondaryEffect(move, target, 0);
|
|
|
|
// Assert
|
|
await Assert.That(ReceivedStatBoost(faintedGrass)).IsFalse();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "Flower Shield raises the Defense stat of all Grass-type Pokémon on the field",
|
|
/// including the user itself. For the user the boost counts as self-inflicted.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnSecondaryEffect_UserIsGrassType_BoostIsSelfInflicted()
|
|
{
|
|
// Arrange
|
|
var (script, move, target, battle) = CreateTestSetup();
|
|
var user = move.User;
|
|
user.Types.Returns(new[] { GrassType });
|
|
user.IsFainted.Returns(false);
|
|
var side = Substitute.For<IBattleSide>();
|
|
side.Pokemon.Returns(new List<IPokemon?> { user });
|
|
battle.Sides.Returns(new[] { side });
|
|
|
|
// Act
|
|
script.OnSecondaryEffect(move, target, 0);
|
|
|
|
// Assert
|
|
var args = GetStatBoostArgs(user);
|
|
await Assert.That(args).IsNotNull();
|
|
await Assert.That((Statistic)args![0]!).IsEqualTo(Statistic.Defense);
|
|
await Assert.That((sbyte)args[1]!).IsEqualTo((sbyte)1);
|
|
await Assert.That((bool)args[2]!).IsTrue(); // self-inflicted for the user itself
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: Grass-type Pokémon are raised "except those that are in the semi-invulnerable turn of a
|
|
/// move such as Fly or Dig". A Grass-type currently flying (holding the <see cref="ChargeFlyEffect"/>
|
|
/// volatile) must not have its Defense raised.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnSecondaryEffect_SemiInvulnerableGrassType_DefenseNotRaised()
|
|
{
|
|
// Arrange
|
|
var flyingGrass = CreatePokemon(GrassType);
|
|
flyingGrass.GetScripts().Returns(_ => new ScriptIterator(new List<IEnumerable<ScriptContainer>>()));
|
|
var flyingGrassVolatile = new ScriptSet(flyingGrass);
|
|
flyingGrassVolatile.Add(new ChargeFlyEffect(flyingGrass));
|
|
flyingGrass.Volatile.Returns(flyingGrassVolatile);
|
|
var (script, move, target, _) = CreateTestSetup(new List<IPokemon?> { flyingGrass });
|
|
|
|
// Act
|
|
script.OnSecondaryEffect(move, target, 0);
|
|
|
|
// Assert
|
|
await Assert.That(ReceivedStatBoost(flyingGrass)).IsFalse();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "The move fails if no eligible Grass-type Pokémon are present." When no Grass-type is
|
|
/// on the field, the hit must be marked as failed.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnSecondaryEffect_NoGrassTypesOnField_HitFails()
|
|
{
|
|
// Arrange
|
|
var waterPokemon = CreatePokemon(WaterType);
|
|
var (script, move, target, _) = CreateTestSetup(new List<IPokemon?> { waterPokemon });
|
|
var hitData = Substitute.For<IHitData>();
|
|
move.GetHitData(target, 0).Returns(hitData);
|
|
|
|
// Act
|
|
script.OnSecondaryEffect(move, target, 0);
|
|
|
|
// Assert
|
|
await Assert.That(hitData.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Fail")).IsTrue();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Technical test: empty (null) slots on a side are skipped without throwing.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnSecondaryEffect_EmptyPokemonSlot_IsSkipped()
|
|
{
|
|
// Arrange
|
|
var grassPokemon = CreatePokemon(GrassType);
|
|
var (script, move, target, _) = CreateTestSetup(new List<IPokemon?> { null, grassPokemon });
|
|
|
|
// Act
|
|
script.OnSecondaryEffect(move, target, 0);
|
|
|
|
// Assert
|
|
await Assert.That(ReceivedStatBoost(grassPokemon)).IsTrue();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Technical test: when the target has no <see cref="IPokemon.BattleData"/> (it is not in a battle),
|
|
/// the script does nothing instead of throwing.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnSecondaryEffect_NullBattleData_DoesNothing()
|
|
{
|
|
// Arrange
|
|
var grassPokemon = CreatePokemon(GrassType);
|
|
var (script, move, _, _) = CreateTestSetup(new List<IPokemon?> { grassPokemon });
|
|
var target = Substitute.For<IPokemon>();
|
|
target.BattleData.Returns((IPokemonBattleData?)null);
|
|
|
|
// Act
|
|
script.OnSecondaryEffect(move, target, 0);
|
|
|
|
// Assert
|
|
await Assert.That(ReceivedStatBoost(grassPokemon)).IsFalse();
|
|
}
|
|
} |