using PkmnLib.Dynamic.Models; using PkmnLib.Dynamic.Models.Choices; using PkmnLib.Dynamic.ScriptHandling; using PkmnLib.Plugin.Gen7.Scripts.Moves; using PkmnLib.Static.Moves; using PkmnLib.Static.Utils; namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves; /// /// Tests for the move script. /// Gen VII Bulbapedia behavior: "If the last amount of damage done to the user before the use of Mirror /// Coat is greater than 0 and was dealt by a special move, Mirror Coat will do twice as much damage as /// taken by that attack to the opponent." Mirror Coat fails if the user was not hit, or was last hit by /// a physical move. /// The turn-scoped bookkeeping is implemented by a private "mirror_coat_helper" volatile script that the /// move script attaches to its user at the start of the turn; because that helper type is private, it is /// driven here through its and /// interfaces after retrieving it from the user's volatile script set. /// public class MirrorCoatTests { private static readonly StringKey HelperName = "mirror_coat_helper"; /// /// Creates a Pokémon substitute with a real (empty) volatile script set, so that volatile lookups /// behave like they would on a real Pokémon. /// private static IBattlePokemon CreatePokemonWithEmptyVolatile() { var pokemon = Substitute.For(); var volatileSet = new ScriptSet(pokemon); pokemon.Volatile.Returns(volatileSet); pokemon.GetScripts().Returns(_ => new ScriptIterator(new List>())); return pokemon; } /// /// Creates a Mirror Coat user that has declared Mirror Coat this turn: 's /// OnBeforeTurnStart has run, so the helper volatile script is attached to the user. /// private static (MirrorCoat script, IBattlePokemon user) CreateMirrorCoatUser() { var script = new MirrorCoat(); var user = CreatePokemonWithEmptyVolatile(); var choice = Substitute.For(); choice.User.Returns(user); script.OnBeforeTurnStart(choice); return (script, user); } /// /// Simulates the user being hit by a move of the given category dealing the given damage, by invoking /// the helper volatile script's hook. /// private static void ReceiveHit(IBattlePokemon user, IBattlePokemon attacker, uint damage, MoveCategory category = MoveCategory.Special) { var incoming = Substitute.For(); var useMove = Substitute.For(); useMove.Category.Returns(category); incoming.UseMove.Returns(useMove); incoming.User.Returns(attacker); var hitData = Substitute.For(); hitData.Damage.Returns(damage); incoming.GetHitData(user, 0).Returns(hitData); var helper = (IScriptOnIncomingHit)user.Volatile.Get(HelperName)!.Script!; helper.OnIncomingHit(incoming, user, 0); } /// /// Creates the executing Mirror Coat move of against . /// private static (IExecutingMove move, IHitData hitData) CreateExecutingMirrorCoat(IBattlePokemon user, IBattlePokemon target) { var move = Substitute.For(); move.User.Returns(user); var hitData = Substitute.For(); move.GetHitData(target, 0).Returns(hitData); return (move, hitData); } /// /// Bulbapedia: "If the last amount of damage done to the user before the use of Mirror Coat is /// greater than 0 and was dealt by a special move, Mirror Coat will do twice as much damage as taken /// by that attack to the opponent." /// Mirror Coat watches for those hits over the whole turn: at the start of the turn it attaches its /// helper volatile script to the user. /// [Test] public async Task OnBeforeTurnStart_Always_AddsMirrorCoatHelperToUser() { // Arrange & Act var (_, user) = CreateMirrorCoatUser(); // Assert await Assert.That(user.Volatile.Contains(HelperName)).IsTrue(); } /// /// Bulbapedia: "If the last amount of damage done to the user before the use of Mirror Coat is /// greater than 0 and was dealt by a special move, Mirror Coat will do twice as much damage as taken /// by that attack to the opponent." /// [Test, Arguments(40u, 80u), Arguments(1u, 2u), Arguments(123u, 246u)] public async Task ChangeMoveDamage_UserHitBySpecialMove_DealsDoubleTheDamageTaken(uint damageTaken, uint expectedDamage) { // Arrange var (script, user) = CreateMirrorCoatUser(); var attacker = CreatePokemonWithEmptyVolatile(); ReceiveHit(user, attacker, damageTaken); var (move, hitData) = CreateExecutingMirrorCoat(user, attacker); uint damage = 0; // Act script.ChangeMoveDamage(move, attacker, 0, ref damage); // Assert await Assert.That(damage).IsEqualTo(expectedDamage); hitData.DidNotReceive().Fail(); } /// /// Bulbapedia: if the user "is hit multiple times by a multistrike move", only the final strike is /// countered — the helper records the most recent hit, so Mirror Coat returns double the damage of /// the last strike only. /// [Test] public async Task ChangeMoveDamage_UserHitMultipleTimes_CountersOnlyTheLastHit() { // Arrange var (script, user) = CreateMirrorCoatUser(); var attacker = CreatePokemonWithEmptyVolatile(); ReceiveHit(user, attacker, 30); ReceiveHit(user, attacker, 50); var (move, _) = CreateExecutingMirrorCoat(user, attacker); uint damage = 0; // Act script.ChangeMoveDamage(move, attacker, 0, ref damage); // Assert - double the last hit (50), not the first (30) or the sum await Assert.That(damage).IsEqualTo(100u); } /// /// Bulbapedia: Mirror Coat fails if the user was not hit by an attack before it executes ("If the /// last amount of damage done to the user before the use of Mirror Coat is greater than 0..."). /// [Test] public async Task ChangeMoveDamage_UserNotHitThisTurn_FailsHit() { // Arrange var (script, user) = CreateMirrorCoatUser(); var target = CreatePokemonWithEmptyVolatile(); var (move, hitData) = CreateExecutingMirrorCoat(user, target); uint damage = 0; // Act script.ChangeMoveDamage(move, target, 0, ref damage); // Assert hitData.Received(1).Fail(); await Assert.That(damage).IsEqualTo(0u); } /// /// Bulbapedia: Mirror Coat only counters special moves ("...and was dealt by a special move"); /// it fails when the user was hit by a physical move instead. /// [Test] public async Task ChangeMoveDamage_UserHitByPhysicalMoveOnly_FailsHit() { // Arrange var (script, user) = CreateMirrorCoatUser(); var attacker = CreatePokemonWithEmptyVolatile(); ReceiveHit(user, attacker, 40, MoveCategory.Physical); var (move, hitData) = CreateExecutingMirrorCoat(user, attacker); uint damage = 0; // Act script.ChangeMoveDamage(move, attacker, 0, ref damage); // Assert hitData.Received(1).Fail(); await Assert.That(damage).IsEqualTo(0u); } /// /// Bulbapedia: "Mirror Coat will target the last opponent that dealt special damage to the attacker" — /// against any Pokémon other than that last attacker, the hit fails. /// [Test] public async Task ChangeMoveDamage_TargetIsNotTheLastAttacker_FailsHit() { // Arrange var (script, user) = CreateMirrorCoatUser(); var attacker = CreatePokemonWithEmptyVolatile(); ReceiveHit(user, attacker, 40); var someoneElse = CreatePokemonWithEmptyVolatile(); var (move, hitData) = CreateExecutingMirrorCoat(user, someoneElse); uint damage = 0; // Act script.ChangeMoveDamage(move, someoneElse, 0, ref damage); // Assert hitData.Received(1).Fail(); await Assert.That(damage).IsEqualTo(0u); } /// /// Bulbapedia: "Mirror Coat will target the last opponent that dealt special damage to the attacker" — /// like , the script must implement and redirect /// the move at the Pokémon whose special move last damaged the user. /// [Test] public async Task ChangeTargets_UserHitBySpecialMove_RedirectsToLastSpecialAttacker() { // Arrange var (script, user) = CreateMirrorCoatUser(); var attacker = CreatePokemonWithEmptyVolatile(); ReceiveHit(user, attacker, 40); var choice = Substitute.For(); choice.User.Returns(user); IReadOnlyList targets = [CreatePokemonWithEmptyVolatile()]; // Act & Assert - the script must expose the redirection hook and point the move at the attacker var changeTargets = script as IScriptChangeTargets; await Assert.That(changeTargets).IsNotNull(); changeTargets!.ChangeTargets(choice, ref targets); await Assert.That(targets.Count).IsEqualTo(1); await Assert.That(targets[0]).IsEqualTo(attacker); } /// /// Bulbapedia (Generation IV onwards): "If the Pokémon using Mirror Coat is hit by a special move that /// deals 0 damage, Mirror Coat becomes a special move with 1 base power." Being hit by a 0-damage /// special move therefore must not make Mirror Coat fail. /// [Test] public async Task ChangeMoveDamage_UserHitBySpecialMoveDealingZeroDamage_DoesNotFail() { // Arrange var (script, user) = CreateMirrorCoatUser(); var attacker = CreatePokemonWithEmptyVolatile(); ReceiveHit(user, attacker, 0); var (move, hitData) = CreateExecutingMirrorCoat(user, attacker); uint damage = 0; // Act script.ChangeMoveDamage(move, attacker, 0, ref damage); // Assert await Assert.That(hitData.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Fail")).IsFalse(); } /// /// Bulbapedia: Mirror Coat counters "the last amount of damage done to the user before the use of /// Mirror Coat" — i.e. damage taken within the same turn. The helper volatile script therefore /// removes itself at the end of the turn, so a hit from a previous turn is never countered. /// [Test] public async Task MirrorCoatHelper_OnEndTurn_RemovesItselfFromUser() { // Arrange var (_, user) = CreateMirrorCoatUser(); var helper = (IScriptOnEndTurn)user.Volatile.Get(HelperName)!.Script!; // Act helper.OnEndTurn(user, Substitute.For()); // Assert await Assert.That(user.Volatile.Contains(HelperName)).IsFalse(); } }