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; /// /// Tests for the 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." /// 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[] sidePokemon) { var script = new FlowerShield(); var move = Substitute.For(); var user = Substitute.For(); move.User.Returns(user); var library = LibraryHelpers.LoadLibrary(); var battle = Substitute.For(); battle.Library.Returns(library); var sides = sidePokemon.Select(pokemon => { var side = Substitute.For(); side.Pokemon.Returns(pokemon); return side; }).ToArray(); battle.Sides.Returns(sides); var battleData = Substitute.For(); battleData.Battle.Returns(battle); var target = Substitute.For(); target.BattleData.Returns(battleData); return (script, move, target, battle); } private static IPokemon CreatePokemon(TypeIdentifier type, bool isFainted = false) { var pokemon = Substitute.For(); 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"); /// /// 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 parameter cannot be bound by Arg.Any (its /// parameterless constructor initializes a fresh id, so it never equals the matcher's default value). /// private static object?[]? GetStatBoostArgs(IPokemon pokemon) { var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "ChangeStatBoost"); return call?.GetArguments(); } /// /// 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. /// [Test] public async Task OnSecondaryEffect_GrassTypeOnField_DefenseRaisedByOneStage() { // Arrange var grassPokemon = CreatePokemon(GrassType); var (script, move, target, _) = CreateTestSetup(new List { 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 } /// /// 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. /// [Test] public async Task OnSecondaryEffect_GrassTypesOnBothSides_AllAreRaised() { // Arrange var allyGrass = CreatePokemon(GrassType); var opposingGrass = CreatePokemon(GrassType); var (script, move, target, _) = CreateTestSetup(new List { allyGrass }, new List { opposingGrass }); // Act script.OnSecondaryEffect(move, target, 0); // Assert await Assert.That(ReceivedStatBoost(allyGrass)).IsTrue(); await Assert.That(ReceivedStatBoost(opposingGrass)).IsTrue(); } /// /// Bulbapedia: only "Grass-type Pokémon on the field" are affected; a non-Grass-type Pokémon does not /// have its Defense raised. /// [Test] public async Task OnSecondaryEffect_NonGrassType_DefenseNotRaised() { // Arrange var waterPokemon = CreatePokemon(WaterType); var (script, move, target, _) = CreateTestSetup(new List { waterPokemon }); // Act script.OnSecondaryEffect(move, target, 0); // Assert await Assert.That(ReceivedStatBoost(waterPokemon)).IsFalse(); } /// /// Bulbapedia: the boost applies to Pokémon "on the field"; a fainted Grass-type Pokémon is no longer /// battling and is not affected. /// [Test] public async Task OnSecondaryEffect_FaintedGrassType_DefenseNotRaised() { // Arrange var faintedGrass = CreatePokemon(GrassType, true); var (script, move, target, _) = CreateTestSetup(new List { faintedGrass }); // Act script.OnSecondaryEffect(move, target, 0); // Assert await Assert.That(ReceivedStatBoost(faintedGrass)).IsFalse(); } /// /// 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. /// [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(); side.Pokemon.Returns(new List { 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 } /// /// 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 /// volatile) must not have its Defense raised. /// [Test] public async Task OnSecondaryEffect_SemiInvulnerableGrassType_DefenseNotRaised() { // Arrange var flyingGrass = CreatePokemon(GrassType); flyingGrass.GetScripts().Returns(_ => new ScriptIterator(new List>())); var flyingGrassVolatile = new ScriptSet(flyingGrass); flyingGrassVolatile.Add(new ChargeFlyEffect(flyingGrass)); flyingGrass.Volatile.Returns(flyingGrassVolatile); var (script, move, target, _) = CreateTestSetup(new List { flyingGrass }); // Act script.OnSecondaryEffect(move, target, 0); // Assert await Assert.That(ReceivedStatBoost(flyingGrass)).IsFalse(); } /// /// 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. /// [Test] public async Task OnSecondaryEffect_NoGrassTypesOnField_HitFails() { // Arrange var waterPokemon = CreatePokemon(WaterType); var (script, move, target, _) = CreateTestSetup(new List { waterPokemon }); var hitData = Substitute.For(); 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(); } /// /// Technical test: empty (null) slots on a side are skipped without throwing. /// [Test] public async Task OnSecondaryEffect_EmptyPokemonSlot_IsSkipped() { // Arrange var grassPokemon = CreatePokemon(GrassType); var (script, move, target, _) = CreateTestSetup(new List { null, grassPokemon }); // Act script.OnSecondaryEffect(move, target, 0); // Assert await Assert.That(ReceivedStatBoost(grassPokemon)).IsTrue(); } /// /// Technical test: when the target has no (it is not in a battle), /// the script does nothing instead of throwing. /// [Test] public async Task OnSecondaryEffect_NullBattleData_DoesNothing() { // Arrange var grassPokemon = CreatePokemon(GrassType); var (script, move, _, _) = CreateTestSetup(new List { grassPokemon }); var target = Substitute.For(); target.BattleData.Returns((IPokemonBattleData?)null); // Act script.OnSecondaryEffect(move, target, 0); // Assert await Assert.That(ReceivedStatBoost(grassPokemon)).IsFalse(); } }