204 lines
8.1 KiB
C#
204 lines
8.1 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, IBattlePokemon target, IHitData hitData) CreateTestSetup(
|
|
bool grassTypeInLibrary = true, TypeIdentifier[]? targetTypes = null)
|
|
{
|
|
var script = new ForestsCurse();
|
|
var move = Substitute.For<IExecutingMove>();
|
|
var target = Substitute.For<IBattlePokemon>();
|
|
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);
|
|
target.Battle.Returns(battle);
|
|
target.Types.Returns(targetTypes ?? [NormalType]);
|
|
|
|
return (script, move, target, hitData);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper to extract the type passed to the target's <see cref="IBattlePokemon.AddType"/> call.
|
|
/// </summary>
|
|
private static TypeIdentifier? GetAddedType(IBattlePokemon 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="IBattlePokemon.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>
|
|
/// 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="IBattlePokemon.AddType"/> and
|
|
/// <see cref="IBattlePokemon.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<IBattlePokemon>();
|
|
|
|
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);
|
|
target.Battle.Returns(battle);
|
|
|
|
// 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();
|
|
}
|
|
} |