using PkmnLib.Dynamic.Models; using PkmnLib.Dynamic.Models.Choices; using PkmnLib.Dynamic.ScriptHandling; using PkmnLib.Plugin.Gen7.Scripts.Moves; using PkmnLib.Plugin.Gen7.Scripts.Pokemon; using PkmnLib.Plugin.Gen7.Scripts.Utils; using PkmnLib.Static; using PkmnLib.Static.Utils; namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves; /// /// Tests for the move script. /// Behavior is verified against the Bulbapedia page for Baton Pass (Generation VII). /// In this codebase, volatile scripts that are transferred by Baton Pass are marked with /// (e.g. ). /// public class BatonPassTests { /// /// Creates a mocked Pokémon with a real volatile and a real /// . /// private static IPokemon CreateMockPokemon(out IScriptSet volatileSet) { var pokemon = Substitute.For(); pokemon.GetScripts().Returns(_ => new ScriptIterator(Array.Empty>())); var set = new ScriptSet(pokemon); pokemon.Volatile.Returns(set); pokemon.StatBoost.Returns(new StatBoostStatisticSet()); volatileSet = set; return pokemon; } /// /// Creates a fully mocked test setup for Baton Pass tests. The Pokémon to switch in is stored in the /// move choice's under the to_switch key. /// private static (BatonPass script, IExecutingMove move, IPokemon user, IPokemon toSwitch, IBattleSide side, IScriptSet userVolatile, IScriptSet switchInVolatile) CreateTestSetup() { var script = new BatonPass(); var move = Substitute.For(); var user = CreateMockPokemon(out var userVolatile); var toSwitch = CreateMockPokemon(out var switchInVolatile); var moveChoice = Substitute.For(); moveChoice.AdditionalData.Returns(new Dictionary { { "to_switch", toSwitch } }); move.MoveChoice.Returns(moveChoice); var side = Substitute.For(); var battle = Substitute.For(); battle.Sides.Returns(new[] { side }); var battleData = Substitute.For(); battleData.Battle.Returns(battle); battleData.SideIndex.Returns((byte)0); battleData.Position.Returns((byte)1); user.BattleData.Returns(battleData); move.User.Returns(user); return (script, move, user, toSwitch, side, userVolatile, switchInVolatile); } /// /// Bulbapedia: "Baton Pass switches out the user and passes several effects to the Pokémon switched in /// its place." /// The user is swapped out for the chosen Pokémon on its own field position. /// [Test] public void OnSecondaryEffect_SwitchTargetChosen_SwitchesOutUser() { // Arrange var (script, move, user, toSwitch, side, _, _) = CreateTestSetup(); // Act script.OnSecondaryEffect(move, user, 0); // Assert side.Received(1).SwapPokemon((byte)1, toSwitch); } /// /// Bulbapedia: "Baton Pass transfers all temporary stat stage increases and decreases". /// Each stat stage (including evasion and accuracy) is copied to the Pokémon that is switched in. /// [Test, Arguments(Statistic.Attack, (sbyte)6), Arguments(Statistic.Defense, (sbyte)-6), Arguments(Statistic.SpecialAttack, (sbyte)2), Arguments(Statistic.SpecialDefense, (sbyte)-1), Arguments(Statistic.Speed, (sbyte)3), Arguments(Statistic.Evasion, (sbyte)1), Arguments(Statistic.Accuracy, (sbyte)-2)] public async Task OnSecondaryEffect_StatBoost_TransferredToSwitchIn(Statistic stat, sbyte boost) { // Arrange var (script, move, user, toSwitch, _, _, _) = CreateTestSetup(); user.StatBoost.SetStatistic(stat, boost); // Act script.OnSecondaryEffect(move, user, 0); // Assert await Assert.That(toSwitch.StatBoost.GetStatistic(stat)).IsEqualTo(boost); } /// /// Bulbapedia: "Baton Pass transfers all temporary stat stage increases and decreases". /// After the stat stages are copied, the switched-in Pokémon's boosted stats are recalculated. /// [Test] public void OnSecondaryEffect_StatBoostsTransferred_RecalculatesSwitchInStats() { // Arrange var (script, move, user, toSwitch, _, _, _) = CreateTestSetup(); // Act script.OnSecondaryEffect(move, user, 0); // Assert toSwitch.Received(1).RecalculateBoostedStats(); } /// /// Bulbapedia: "Baton Pass switches out the user and passes several effects to the Pokémon switched in /// its place." /// A volatile script that is marked as transferable (, such as /// ) is passed to the switched-in Pokémon. /// [Test] public async Task OnSecondaryEffect_TransferableVolatile_PassedToSwitchIn() { // Arrange var (script, move, user, _, _, userVolatile, switchInVolatile) = CreateTestSetup(); userVolatile.Add(new AutotomizeEffect()); // Act script.OnSecondaryEffect(move, user, 0); // Assert await Assert.That(switchInVolatile.Get()).IsNotNull(); } /// /// Bulbapedia: "Baton Pass also passes some volatile status conditions, namely confusion, getting pumped, /// escape prevention, seeding, Curse, Substitute, Ingrain, Aqua Ring" — effects outside that list are not /// passed. The consecutive-protection counter () is not transferable, /// so it must not end up on the switched-in Pokémon. /// [Test] public async Task OnSecondaryEffect_NonTransferableVolatile_NotPassedToSwitchIn() { // Arrange var (script, move, user, _, _, userVolatile, switchInVolatile) = CreateTestSetup(); userVolatile.Add(new ProtectionFailureScript()); // Act script.OnSecondaryEffect(move, user, 0); // Assert await Assert.That(switchInVolatile.Get()).IsNull(); } /// /// Bulbapedia: "Baton Pass switches out the user and passes several effects to the Pokémon switched in /// its place." /// A transferable volatile must be passed even when the user also has a non-transferable volatile that /// was added before it. /// [Test] public async Task OnSecondaryEffect_TransferableVolatileAfterNonTransferable_StillPassedToSwitchIn() { // Arrange var (script, move, user, _, _, userVolatile, switchInVolatile) = CreateTestSetup(); userVolatile.Add(new ProtectionFailureScript()); userVolatile.Add(new AutotomizeEffect()); // Act script.OnSecondaryEffect(move, user, 0); // Assert await Assert.That(switchInVolatile.Get()).IsNotNull(); await Assert.That(switchInVolatile.Get()).IsNull(); } /// /// Bulbapedia: "Baton Pass switches out the user". /// The user's volatile scripts leave the field with it: after the switch its volatile containers are empty. /// [Test] public async Task OnSecondaryEffect_UserSwitchedOut_UserVolatilesCleared() { // Arrange var (script, move, user, _, _, userVolatile, _) = CreateTestSetup(); userVolatile.Add(new AutotomizeEffect()); // Act script.OnSecondaryEffect(move, user, 0); // Assert await Assert.That(userVolatile.Get()).IsNull(); } /// /// Technical test: without any there is no Pokémon to switch to, /// so no switch happens. /// [Test] public void OnSecondaryEffect_NoAdditionalData_DoesNotSwitch() { // Arrange var (script, move, user, _, side, _, _) = CreateTestSetup(); move.MoveChoice.AdditionalData.Returns((Dictionary?)null); // Act script.OnSecondaryEffect(move, user, 0); // Assert side.DidNotReceive().SwapPokemon(Arg.Any(), Arg.Any()); } /// /// Technical test: if the stored switch target is not a Pokémon, no switch happens. /// [Test] public void OnSecondaryEffect_SwitchTargetNotAPokemon_DoesNotSwitch() { // Arrange var (script, move, user, _, side, _, _) = CreateTestSetup(); move.MoveChoice.AdditionalData.Returns(new Dictionary { { "to_switch", null } }); // Act script.OnSecondaryEffect(move, user, 0); // Assert side.DidNotReceive().SwapPokemon(Arg.Any(), Arg.Any()); } /// /// Technical test: if the user has no , the script returns without /// switching and without clearing the user's volatile scripts. /// [Test] public async Task OnSecondaryEffect_NoBattleData_DoesNotSwitch() { // Arrange var (script, move, user, _, side, userVolatile, _) = CreateTestSetup(); user.BattleData.Returns((IPokemonBattleData?)null); userVolatile.Add(new AutotomizeEffect()); // Act script.OnSecondaryEffect(move, user, 0); // Assert side.DidNotReceive().SwapPokemon(Arg.Any(), Arg.Any()); await Assert.That(userVolatile.Get()).IsNotNull(); } }