278 lines
11 KiB
C#
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();
|
|
}
|
|
} |