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; /// /// Tests for the move script. /// Behavior is verified against the Bulbapedia page for Forest's Curse (Generation VII behavior). /// 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"); /// /// 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. /// 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(); var target = Substitute.For(); var hitData = Substitute.For(); move.GetHitData(target, 0).Returns(hitData); var typeLibrary = Substitute.For(); if (grassTypeInLibrary) { typeLibrary.TryGetTypeIdentifier(new StringKey("grass"), out Arg.Any()).Returns(x => { x[1] = GrassType; return true; }); } var staticLibrary = Substitute.For(); staticLibrary.Types.Returns(typeLibrary); var library = Substitute.For(); library.StaticLibrary.Returns(staticLibrary); var battle = Substitute.For(); battle.Library.Returns(library); var battleData = Substitute.For(); battleData.Battle.Returns(battle); target.BattleData.Returns(battleData); target.Types.Returns(targetTypes ?? [NormalType]); return (script, move, target, hitData); } /// /// Helper to extract the type passed to the target's call. /// private static TypeIdentifier? GetAddedType(IPokemon target) { var call = target.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "AddType"); return call != null ? (TypeIdentifier)call.GetArguments()[0]! : null; } /// /// Bulbapedia: "Forest's Curse adds the Grass type to the target, in addition to the Pokémon's /// original type(s)." /// [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); } /// /// 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 must not be used. /// [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(); } /// /// 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. /// [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(); } /// /// If the target has no , the script cannot resolve the type library, /// so no type may be added. /// [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(); } /// /// Technical test: when the type library cannot resolve the Grass type identifier, the script must /// bail out without adding a type. /// [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(); } /// /// 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 must end up with the Grass type and /// without the Ghost type, while keeping its original type. The mocked target uses a real /// for its volatile scripts, so the HasHadTypeAddedEffect marker set by /// Trick-or-Treat is visible to Forest's Curse, and reacts to and /// with a backing type list, mirroring the real Pokemon implementation, /// so the resulting type list is observable. /// [Test] public async Task OnSecondaryEffect_TargetHasTrickOrTreatGhostType_GhostReplacedByGrass() { // Arrange var forestsCurse = new ForestsCurse(); var trickOrTreat = new TrickOrTreat(); var move = Substitute.For(); var target = Substitute.For(); var typeLibrary = Substitute.For(); typeLibrary.TryGetTypeIdentifier(new StringKey("grass"), out Arg.Any()).Returns(x => { x[1] = GrassType; return true; }); typeLibrary.TryGetTypeIdentifier(new StringKey("ghost"), out Arg.Any()).Returns(x => { x[1] = GhostType; return true; }); var staticLibrary = Substitute.For(); staticLibrary.Types.Returns(typeLibrary); var library = Substitute.For(); library.StaticLibrary.Returns(staticLibrary); var battle = Substitute.For(); battle.Library.Returns(library); move.Battle.Returns(battle); var battleData = Substitute.For(); 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>())); 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 { NormalType }; target.Types.Returns(_ => types); target.AddType(Arg.Any()).Returns(ci => { var type = ci.Arg(); if (types.Contains(type)) return false; types.Add(type); return true; }); target.RemoveType(Arg.Any()).Returns(ci => types.Remove(ci.Arg())); // 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(); } }