Files
PkmnLib.NET/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/ForestsCurseTests.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

226 lines
8.9 KiB
C#

using PkmnLib.Dynamic.Libraries;
using PkmnLib.Dynamic.Models;
using PkmnLib.Dynamic.ScriptHandling;
using PkmnLib.Plugin.Gen7.Scripts.Moves;
using PkmnLib.Static;
using PkmnLib.Static.Libraries;
using PkmnLib.Static.Utils;
namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
/// <summary>
/// Tests for the <see cref="ForestsCurse"/> move script.
/// Behavior is verified against the Bulbapedia page for Forest's Curse (Generation VII behavior).
/// </summary>
public class ForestsCurseTests
{
private static readonly TypeIdentifier GrassType = new(5, "grass");
private static readonly TypeIdentifier NormalType = new(1, "normal");
private static readonly TypeIdentifier GhostType = new(8, "ghost");
/// <summary>
/// Creates a fully mocked test setup for Forest's Curse tests. The target is in battle, with a mocked
/// battle library chain that can resolve the Grass type identifier.
/// </summary>
private static (ForestsCurse script, IExecutingMove move, IPokemon target, IHitData hitData) CreateTestSetup(
bool grassTypeInLibrary = true, TypeIdentifier[]? targetTypes = null)
{
var script = new ForestsCurse();
var move = Substitute.For<IExecutingMove>();
var target = Substitute.For<IPokemon>();
var hitData = Substitute.For<IHitData>();
move.GetHitData(target, 0).Returns(hitData);
var typeLibrary = Substitute.For<IReadOnlyTypeLibrary>();
if (grassTypeInLibrary)
{
typeLibrary.TryGetTypeIdentifier(new StringKey("grass"), out Arg.Any<TypeIdentifier>()).Returns(x =>
{
x[1] = GrassType;
return true;
});
}
var staticLibrary = Substitute.For<IStaticLibrary>();
staticLibrary.Types.Returns(typeLibrary);
var library = Substitute.For<IDynamicLibrary>();
library.StaticLibrary.Returns(staticLibrary);
var battle = Substitute.For<IBattle>();
battle.Library.Returns(library);
var battleData = Substitute.For<IPokemonBattleData>();
battleData.Battle.Returns(battle);
target.BattleData.Returns(battleData);
target.Types.Returns(targetTypes ?? [NormalType]);
return (script, move, target, hitData);
}
/// <summary>
/// Helper to extract the type passed to the target's <see cref="IPokemon.AddType"/> call.
/// </summary>
private static TypeIdentifier? GetAddedType(IPokemon target)
{
var call = target.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "AddType");
return call != null ? (TypeIdentifier)call.GetArguments()[0]! : null;
}
/// <summary>
/// Bulbapedia: "Forest's Curse adds the Grass type to the target, in addition to the Pokémon's
/// original type(s)."
/// </summary>
[Test]
public async Task OnSecondaryEffect_TargetInBattle_AddsGrassTypeToTarget()
{
// Arrange
var (script, move, target, _) = CreateTestSetup();
// Act
script.OnSecondaryEffect(move, target, 0);
// Assert
var addedType = GetAddedType(target);
await Assert.That(addedType).IsNotNull();
await Assert.That(addedType!.Value).IsEqualTo(GrassType);
}
/// <summary>
/// Bulbapedia: the Grass type is added "in addition to the Pokémon's original type(s)".
/// The target's original types may not be replaced, so <see cref="IPokemon.SetTypes"/> must not be used.
/// </summary>
[Test]
public async Task OnSecondaryEffect_TargetInBattle_OriginalTypesNotReplaced()
{
// Arrange
var (script, move, target, _) = CreateTestSetup();
// Act
script.OnSecondaryEffect(move, target, 0);
// Assert
await Assert.That(target.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "SetTypes")).IsFalse();
}
/// <summary>
/// Bulbapedia: Forest's Curse fails if the target is already Grass-type ("The move fails against
/// Grass-type targets"), so the hit must be marked as failed and no type may be added.
/// </summary>
[Test]
public async Task OnSecondaryEffect_TargetAlreadyGrassType_HitFails()
{
// Arrange
var (script, move, target, hitData) = CreateTestSetup(targetTypes: [GrassType]);
// Act
script.OnSecondaryEffect(move, target, 0);
// Assert
await Assert.That(hitData.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Fail")).IsTrue();
}
/// <summary>
/// If the target has no <see cref="IPokemon.BattleData"/>, the script cannot resolve the type library,
/// so no type may be added.
/// </summary>
[Test]
public async Task OnSecondaryEffect_NullBattleData_DoesNotAddType()
{
// Arrange
var (script, move, target, _) = CreateTestSetup();
target.BattleData.Returns((IPokemonBattleData?)null);
// Act
script.OnSecondaryEffect(move, target, 0);
// Assert
await Assert.That(target.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "AddType")).IsFalse();
}
/// <summary>
/// Technical test: when the type library cannot resolve the Grass type identifier, the script must
/// bail out without adding a type.
/// </summary>
[Test]
public async Task OnSecondaryEffect_GrassTypeMissingFromLibrary_DoesNotAddType()
{
// Arrange
var (script, move, target, _) = CreateTestSetup(false);
// Act
script.OnSecondaryEffect(move, target, 0);
// Assert
await Assert.That(target.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "AddType")).IsFalse();
}
/// <summary>
/// Bulbapedia: "If the target already has an additional type added to it by Trick-or-Treat, that type is
/// replaced with the Grass type."
/// A target whose Ghost type was added by <see cref="TrickOrTreat"/> must end up with the Grass type and
/// without the Ghost type, while keeping its original type. The mocked target uses a real
/// <see cref="ScriptSet"/> for its volatile scripts, so the <c>HasHadTypeAddedEffect</c> marker set by
/// Trick-or-Treat is visible to Forest's Curse, and reacts to <see cref="IPokemon.AddType"/> and
/// <see cref="IPokemon.RemoveType"/> with a backing type list, mirroring the real Pokemon implementation,
/// so the resulting type list is observable.
/// </summary>
[Test]
public async Task OnSecondaryEffect_TargetHasTrickOrTreatGhostType_GhostReplacedByGrass()
{
// Arrange
var forestsCurse = new ForestsCurse();
var trickOrTreat = new TrickOrTreat();
var move = Substitute.For<IExecutingMove>();
var target = Substitute.For<IPokemon>();
var typeLibrary = Substitute.For<IReadOnlyTypeLibrary>();
typeLibrary.TryGetTypeIdentifier(new StringKey("grass"), out Arg.Any<TypeIdentifier>()).Returns(x =>
{
x[1] = GrassType;
return true;
});
typeLibrary.TryGetTypeIdentifier(new StringKey("ghost"), out Arg.Any<TypeIdentifier>()).Returns(x =>
{
x[1] = GhostType;
return true;
});
var staticLibrary = Substitute.For<IStaticLibrary>();
staticLibrary.Types.Returns(typeLibrary);
var library = Substitute.For<IDynamicLibrary>();
library.StaticLibrary.Returns(staticLibrary);
var battle = Substitute.For<IBattle>();
battle.Library.Returns(library);
move.Battle.Returns(battle);
var battleData = Substitute.For<IPokemonBattleData>();
battleData.Battle.Returns(battle);
target.BattleData.Returns(battleData);
// Real script set so the HasHadTypeAddedEffect marker added by Trick-or-Treat is visible
// to Forest's Curse.
target.GetScripts().Returns(_ => new ScriptIterator(new List<IEnumerable<ScriptContainer>>()));
IScriptSet targetVolatile = new ScriptSet(target);
target.Volatile.Returns(targetVolatile);
// Backing type list that reacts to AddType/RemoveType like the real Pokemon implementation.
var types = new List<TypeIdentifier> { NormalType };
target.Types.Returns(_ => types);
target.AddType(Arg.Any<TypeIdentifier>()).Returns(ci =>
{
var type = ci.Arg<TypeIdentifier>();
if (types.Contains(type))
return false;
types.Add(type);
return true;
});
target.RemoveType(Arg.Any<TypeIdentifier>()).Returns(ci => types.Remove(ci.Arg<TypeIdentifier>()));
// Trick-or-Treat first adds the Ghost type to the target.
trickOrTreat.OnSecondaryEffect(move, target, 0);
// Act
forestsCurse.OnSecondaryEffect(move, target, 0);
// Assert - the Ghost type added by Trick-or-Treat is replaced by Grass; the original type remains.
await Assert.That(types.Contains(GrassType)).IsTrue();
await Assert.That(types.Contains(GhostType)).IsFalse();
await Assert.That(types.Contains(NormalType)).IsTrue();
}
}