using PkmnLib.Dynamic.Models; using PkmnLib.Dynamic.ScriptHandling; using PkmnLib.Dynamic.ScriptHandling.Registry; using PkmnLib.Plugin.Gen7.Scripts.Moves; using PkmnLib.Plugin.Gen7.Scripts.Side; using PkmnLib.Static.Utils; namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves; /// /// Tests for the move script and its side effect script /// . /// Gen VII Bulbapedia behavior: "Lunar Dance causes the user to faint, but restores the HP and PP of the /// Pokémon that is sent out to take its place as well as removing all status conditions (except /// fainting).", where from Generation V to VII "The receiving Pokémon is now sent out at the end of the /// turn after all other Pokémon have moved." /// public class LunarDanceTests { /// /// Creates a fully mocked test setup for the move script, with the user at the /// given position and the side's volatile scripts backed by a real . /// private static (LunarDance script, IExecutingMove move, IPokemon user, IScriptSet sideVolatile) CreateMoveSetup( byte position = 1) { var script = new LunarDance(); var move = Substitute.For(); var side = Substitute.For(); // ScriptSet.Add runs the IScriptPreventVolatileAdd hook over the owner's scripts; give the mock a // real (empty) iterator so the hook pass runs. side.GetScripts().Returns(_ => new ScriptIterator(Array.Empty>())); IScriptSet sideVolatile = new ScriptSet(side); side.VolatileScripts.Returns(sideVolatile); var battleData = Substitute.For(); battleData.BattleSide.Returns(side); battleData.Position.Returns(position); var user = Substitute.For(); user.BattleData.Returns(battleData); move.User.Returns(user); return (script, move, user, sideVolatile); } /// /// Creates a mocked replacement Pokémon with the given maximum HP for /// tests. /// private static IPokemon CreateReplacement(uint maxHp = 200) { var pokemon = Substitute.For(); pokemon.MaxHealth.Returns(maxHp); return pokemon; } /// /// Helper to extract the heal amount from a substitute's received Heal calls, or null when Heal was /// never called. /// private static uint? GetHealAmount(IPokemon pokemon) { var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Heal"); return call != null ? (uint)call.GetArguments()[0]! : null; } /// /// Bulbapedia (Generations V to VII): "The receiving Pokémon is now sent out at the end of the turn" — /// using the move leaves a pending on the user's side instead of healing /// anything directly. /// [Test] public async Task OnSecondaryEffect_UserInBattle_AddsLunarDanceEffectToUserSide() { // Arrange var (script, move, _, sideVolatile) = CreateMoveSetup(); // Act script.OnSecondaryEffect(move, Substitute.For(), 0); // Assert await Assert.That(sideVolatile.Contains(ScriptUtils.ResolveName())).IsTrue(); } /// /// Bulbapedia: "Lunar Dance causes the user to faint". /// [Test] public async Task OnSecondaryEffect_UserInBattle_UserFaints() { // Arrange var (script, move, user, _) = CreateMoveSetup(); // Act script.OnSecondaryEffect(move, Substitute.For(), 0); // Assert await Assert.That(user.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Faint")).IsTrue(); } /// /// Bulbapedia: the dance benefits the Pokémon "sent out to take its place" — the pending effect is /// bound to the position the user occupied, so a replacement entering that position is healed. /// [Test] public async Task OnSecondaryEffect_UserAtPosition_EffectTargetsUsersPosition() { // Arrange var (script, move, _, sideVolatile) = CreateMoveSetup(1); script.OnSecondaryEffect(move, Substitute.For(), 0); var effect = (LunarDanceEffect)sideVolatile.Get(new StringKey("lunar_dance"))!.Script!; var replacement = CreateReplacement(); // Act effect.OnSwitchIn(replacement, 1); // Assert await Assert.That(GetHealAmount(replacement)).IsNotNull(); } /// /// Technical test: outside of battle (no ) the hook does nothing and /// does not throw. /// [Test] public async Task OnSecondaryEffect_NoBattleData_DoesNothing() { // Arrange var script = new LunarDance(); var move = Substitute.For(); var user = Substitute.For(); user.BattleData.Returns((IPokemonBattleData?)null); move.User.Returns(user); // Act script.OnSecondaryEffect(move, Substitute.For(), 0); // Assert await Assert.That(user.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Faint")).IsFalse(); } /// /// Bulbapedia: "restores the HP ... of the Pokémon that is sent out to take its place" — the /// replacement switching into the dancer's position is healed by its full maximum HP. /// [Test, Arguments(200u), Arguments(1u), Arguments(999u)] public async Task OnSwitchIn_MatchingPosition_HealsReplacementToFullHp(uint maxHp) { // Arrange var effect = new LunarDanceEffect(1); var replacement = CreateReplacement(maxHp); // Act effect.OnSwitchIn(replacement, 1); // Assert await Assert.That(GetHealAmount(replacement)!.Value).IsEqualTo(maxHp); } /// /// Bulbapedia: "restores the HP and PP of the Pokémon that is sent out" — unlike Healing Wish, Lunar /// Dance also restores the replacement's PP. /// [Test] public async Task OnSwitchIn_MatchingPosition_RestoresPP() { // Arrange var effect = new LunarDanceEffect(1); var replacement = CreateReplacement(); // Act effect.OnSwitchIn(replacement, 1); // Assert await Assert.That(replacement.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "RestoreAllPP")).IsTrue(); } /// /// Bulbapedia: "removing all status conditions (except fainting)". /// [Test] public async Task OnSwitchIn_MatchingPosition_CuresStatus() { // Arrange var effect = new LunarDanceEffect(1); var replacement = CreateReplacement(); // Act effect.OnSwitchIn(replacement, 1); // Assert await Assert.That(replacement.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "ClearStatus")).IsTrue(); } /// /// Bulbapedia (Generations V to VII): the dance is consumed by the Pokémon sent out at the position — /// unlike Generation VIII onwards there is no check on the replacement's HP, status or PP, so the /// effect is removed from the side after the first switch-in. /// [Test] public async Task OnSwitchIn_MatchingPosition_EffectIsRemovedFromSide() { // Arrange var side = Substitute.For(); side.GetScripts().Returns(_ => new ScriptIterator(Array.Empty>())); IScriptSet sideVolatile = new ScriptSet(side); var effect = new LunarDanceEffect(1); sideVolatile.Add(effect); // Act effect.OnSwitchIn(CreateReplacement(), 1); // Assert await Assert.That(sideVolatile.Contains(ScriptUtils.ResolveName())).IsFalse(); } /// /// Bulbapedia: the dance applies to the Pokémon sent out in the user's place — a Pokémon switching in /// at a different position on the side is not restored and does not consume the effect. /// [Test] public async Task OnSwitchIn_DifferentPosition_DoesNotRestoreAndEffectPersists() { // Arrange var side = Substitute.For(); side.GetScripts().Returns(_ => new ScriptIterator(Array.Empty>())); IScriptSet sideVolatile = new ScriptSet(side); var effect = new LunarDanceEffect(1); sideVolatile.Add(effect); var otherPokemon = CreateReplacement(); // Act effect.OnSwitchIn(otherPokemon, 0); // Assert await Assert.That(GetHealAmount(otherPokemon)).IsNull(); await Assert.That(otherPokemon.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "ClearStatus")).IsFalse(); await Assert.That(sideVolatile.Contains(ScriptUtils.ResolveName())).IsTrue(); } }