Files
PkmnLib.NET/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/MirrorCoatTests.cs
Deukhoofd d2a82b5fe3
All checks were successful
Build / Build (push) Successful in 3m12s
Many more tests and fixes
2026-08-27 17:11:12 +02:00

278 lines
11 KiB
C#

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;
/// <summary>
/// Tests for the <see cref="MirrorCoat"/> 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 <see cref="IScriptOnIncomingHit"/> and <see cref="IScriptOnEndTurn"/>
/// interfaces after retrieving it from the user's volatile script set.
/// </summary>
public class MirrorCoatTests
{
private static readonly StringKey HelperName = "mirror_coat_helper";
/// <summary>
/// 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.
/// </summary>
private static IPokemon CreatePokemonWithEmptyVolatile()
{
var pokemon = Substitute.For<IPokemon>();
var volatileSet = new ScriptSet(pokemon);
pokemon.Volatile.Returns(volatileSet);
pokemon.GetScripts().Returns(_ => new ScriptIterator(new List<IEnumerable<ScriptContainer>>()));
return pokemon;
}
/// <summary>
/// Creates a Mirror Coat user that has declared Mirror Coat this turn: <see cref="MirrorCoat"/>'s
/// <c>OnBeforeTurnStart</c> has run, so the helper volatile script is attached to the user.
/// </summary>
private static (MirrorCoat script, IPokemon user) CreateMirrorCoatUser()
{
var script = new MirrorCoat();
var user = CreatePokemonWithEmptyVolatile();
var choice = Substitute.For<IMoveChoice>();
choice.User.Returns(user);
script.OnBeforeTurnStart(choice);
return (script, user);
}
/// <summary>
/// Simulates the user being hit by a move of the given category dealing the given damage, by invoking
/// the helper volatile script's <see cref="IScriptOnIncomingHit.OnIncomingHit"/> hook.
/// </summary>
private static void ReceiveHit(IPokemon user, IPokemon attacker, uint damage,
MoveCategory category = MoveCategory.Special)
{
var incoming = Substitute.For<IExecutingMove>();
var useMove = Substitute.For<IMoveData>();
useMove.Category.Returns(category);
incoming.UseMove.Returns(useMove);
incoming.User.Returns(attacker);
var hitData = Substitute.For<IHitData>();
hitData.Damage.Returns(damage);
incoming.GetHitData(user, 0).Returns(hitData);
var helper = (IScriptOnIncomingHit)user.Volatile.Get(HelperName)!.Script!;
helper.OnIncomingHit(incoming, user, 0);
}
/// <summary>
/// Creates the executing Mirror Coat move of <paramref name="user"/> against <paramref name="target"/>.
/// </summary>
private static (IExecutingMove move, IHitData hitData) CreateExecutingMirrorCoat(IPokemon user, IPokemon target)
{
var move = Substitute.For<IExecutingMove>();
move.User.Returns(user);
var hitData = Substitute.For<IHitData>();
move.GetHitData(target, 0).Returns(hitData);
return (move, hitData);
}
/// <summary>
/// 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.
/// </summary>
[Test]
public async Task OnBeforeTurnStart_Always_AddsMirrorCoatHelperToUser()
{
// Arrange & Act
var (_, user) = CreateMirrorCoatUser();
// Assert
await Assert.That(user.Volatile.Contains(HelperName)).IsTrue();
}
/// <summary>
/// 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."
/// </summary>
[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();
}
/// <summary>
/// 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.
/// </summary>
[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);
}
/// <summary>
/// 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...").
/// </summary>
[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);
}
/// <summary>
/// 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.
/// </summary>
[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);
}
/// <summary>
/// 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.
/// </summary>
[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);
}
/// <summary>
/// Bulbapedia: "Mirror Coat will target the last opponent that dealt special damage to the attacker" —
/// like <see cref="Counter"/>, the script must implement <see cref="IScriptChangeTargets"/> and redirect
/// the move at the Pokémon whose special move last damaged the user.
/// </summary>
[Test]
public async Task ChangeTargets_UserHitBySpecialMove_RedirectsToLastSpecialAttacker()
{
// Arrange
var (script, user) = CreateMirrorCoatUser();
var attacker = CreatePokemonWithEmptyVolatile();
ReceiveHit(user, attacker, 40);
var choice = Substitute.For<IMoveChoice>();
choice.User.Returns(user);
IReadOnlyList<IPokemon?> 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);
}
/// <summary>
/// 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.
/// </summary>
[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();
}
/// <summary>
/// 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.
/// </summary>
[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<IBattle>());
// Assert
await Assert.That(user.Volatile.Contains(HelperName)).IsFalse();
}
}