Files
PkmnLib.NET/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/FlowerShieldTests.cs
Deukhoofd d2a82b5fe3
All checks were successful
Build / Build (push) Successful in 3m12s
Many more tests and fixes
2026-08-27 17:11:12 +02:00

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();
}
}