using PkmnLib.Dynamic.Libraries; using PkmnLib.Dynamic.Models; using PkmnLib.Dynamic.ScriptHandling; using PkmnLib.Plugin.Gen7.Scripts.Moves; using PkmnLib.Plugin.Gen7.Scripts.Pokemon; 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 Foresight (Generation VII behavior: the /// Generation II base effect with the Generation III, IV and V-to-VII changes applied). /// The lasting part of the effect is implemented by the volatile script, /// which the move script is responsible for attaching to the target. /// public class ForesightTests { /// /// Creates a fully mocked test setup for Foresight tests. The target is in battle, with a mocked /// battle library chain and the given evasion stat stage. /// private static (Foresight script, IExecutingMove move, IPokemon target, IScriptSet volatileSet, IHitData hitData) CreateTestSetup(sbyte evasionStage = 0) { var script = new Foresight(); var move = Substitute.For(); var target = Substitute.For(); var hitData = Substitute.For(); move.GetHitData(target, 0).Returns(hitData); var typeLibrary = Substitute.For(); 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); var volatileSet = Substitute.For(); target.Volatile.Returns(volatileSet); target.StatBoost.Returns(new StatBoostStatisticSet { Evasion = evasionStage, }); return (script, move, target, volatileSet, hitData); } /// /// Helper to extract all stat boost changes requested on the target through /// . /// private static IReadOnlyList<(Statistic stat, sbyte amount)> GetStatBoostChanges(IPokemon target) => target.ReceivedCalls().Where(c => c.GetMethodInfo().Name == "ChangeStatBoost") .Select(c => ((Statistic)c.GetArguments()[0]!, (sbyte)c.GetArguments()[1]!)).ToList(); /// /// Bulbapedia: "Foresight also removes the Ghost type's immunity to Fighting and Normal moves for any /// moves used against the target. This effect remains until the target switches out". /// The lasting effect is implemented by attaching a volatile script /// to the target. /// [Test] public async Task OnSecondaryEffect_TargetInBattle_AddsForesightEffectToTargetVolatile() { // Arrange var (script, move, target, volatileSet, _) = CreateTestSetup(); // Act script.OnSecondaryEffect(move, target, 0); // Assert var addCall = volatileSet.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Add"); await Assert.That(addCall).IsNotNull(); await Assert.That(addCall!.GetArguments()[0] is ForesightEffect).IsTrue(); } /// /// Bulbapedia (Generation III): "Foresight now causes all accuracy checks against the target to ignore /// any changes to the target's evasion stat stages." /// The engine models this by resetting a raised evasion stat stage back to zero (and preventing further /// changes through ). /// [Test, Arguments((sbyte)1, (sbyte)-1), Arguments((sbyte)3, (sbyte)-3), Arguments((sbyte)6, (sbyte)-6)] public async Task OnSecondaryEffect_PositiveEvasionStage_EvasionStageResetToZero(sbyte evasionStage, sbyte expectedChange) { // Arrange var (script, move, target, _, _) = CreateTestSetup(evasionStage); // Act script.OnSecondaryEffect(move, target, 0); // Assert var changes = GetStatBoostChanges(target); await Assert.That(changes.Count).IsEqualTo(1); await Assert.That(changes[0].stat).IsEqualTo(Statistic.Evasion); await Assert.That(changes[0].amount).IsEqualTo(expectedChange); } /// /// Bulbapedia (Generation IV): "Foresight now only causes accuracy checks against the target to ignore /// changes to its evasion stat stages if its evasion stat stage is greater than 0." /// A lowered evasion stat stage therefore remains in effect: Foresight must not raise it back to zero. /// [Test, Arguments((sbyte)-1), Arguments((sbyte)-6)] public async Task OnSecondaryEffect_NegativeEvasionStage_EvasionNotRaised(sbyte evasionStage) { // Arrange var (script, move, target, _, _) = CreateTestSetup(evasionStage); // Act script.OnSecondaryEffect(move, target, 0); // Assert - a negative evasion stage may not be raised back towards zero var changes = GetStatBoostChanges(target); await Assert.That(changes.Any(c => c.stat == Statistic.Evasion && c.amount > 0)).IsFalse(); } /// /// Bulbapedia (Generation III): "Foresight now causes all accuracy checks against the target to ignore /// any changes to the target's evasion stat stages." /// With an unchanged (zero) evasion stat stage there is nothing to reset, so no effective stat change /// may be requested. /// [Test] public async Task OnSecondaryEffect_ZeroEvasionStage_NoEffectiveEvasionChange() { // Arrange var (script, move, target, _, _) = CreateTestSetup(); // Act script.OnSecondaryEffect(move, target, 0); // Assert var changes = GetStatBoostChanges(target); await Assert.That(changes.Any(c => c.amount != 0)).IsFalse(); } /// /// If the target has no , the script cannot resolve the type library, /// so nothing may happen: no volatile script is added and no stat stage is changed. /// [Test] public async Task OnSecondaryEffect_NullBattleData_NoEffect() { // Arrange var (script, move, target, volatileSet, _) = CreateTestSetup(2); target.BattleData.Returns((IPokemonBattleData?)null); // Act script.OnSecondaryEffect(move, target, 0); // Assert await Assert.That(volatileSet.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Add")).IsFalse(); await Assert.That(target.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "ChangeStatBoost")).IsFalse(); } /// /// Bulbapedia (Generations V to VII): "Foresight will once again fail if used against a Pokémon already /// under its effect." /// When the target already has the volatile script, the hit must fail. /// [Test] public async Task OnSecondaryEffect_TargetAlreadyUnderEffect_HitFails() { // Arrange var (script, move, target, volatileSet, hitData) = CreateTestSetup(); volatileSet.Contains().Returns(true); volatileSet.Contains(Arg.Any()).Returns(true); // Act script.OnSecondaryEffect(move, target, 0); // Assert await Assert.That(hitData.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Fail")).IsTrue(); } /// /// Bulbapedia (Generation III): "Foresight now causes all accuracy checks against the target to ignore /// any changes to the target's evasion stat stages." /// For the evasion reset to actually take effect in a real battle, it must not be blocked by the move's /// own : that script's PreventStatBoostChange hook prevents all /// evasion changes once attached. The reset must therefore either happen before the volatile script is /// added to the target, or be requested with force: true. /// [Test] public async Task OnSecondaryEffect_PositiveEvasionStage_ResetNotBlockedByOwnEffect() { // Arrange var (script, move, target, volatileSet, _) = CreateTestSetup(2); var callOrder = new List(); volatileSet.Add(null!).ReturnsForAnyArgs(ci => { callOrder.Add("Add"); return (ScriptContainer?)null; }); target.ChangeStatBoost(default, default, default, default).ReturnsForAnyArgs(ci => { callOrder.Add(ci.ArgAt(3) ? "ChangeStatBoost:forced" : "ChangeStatBoost"); return true; }); // Act script.OnSecondaryEffect(move, target, 0); // Assert - the evasion reset must be able to take effect: either it happens before the // ForesightEffect volatile (which prevents evasion changes) is attached, or it is forced. var boostIndex = callOrder.FindIndex(c => c.StartsWith("ChangeStatBoost")); var addIndex = callOrder.IndexOf("Add"); await Assert.That(boostIndex).IsNotEqualTo(-1); await Assert.That(addIndex).IsNotEqualTo(-1); await Assert.That(callOrder.Contains("ChangeStatBoost:forced") || boostIndex < addIndex).IsTrue(); } }