301 lines
12 KiB
C#
301 lines
12 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="MetalBurst"/> script, which implements Metal Burst.
|
|
/// Behavior is verified against the Bulbapedia page for Metal Burst (Generation VII).
|
|
/// </summary>
|
|
public class MetalBurstTests
|
|
{
|
|
/// <summary>
|
|
/// Creates a Metal Burst user whose damage-tracking volatile has been installed through
|
|
/// <see cref="MetalBurst.OnBeforeTurnStart"/>. The user's <see cref="IPokemon.Volatile"/> is a real
|
|
/// <see cref="ScriptSet"/> so the internal tracker script can actually be stored and retrieved.
|
|
/// </summary>
|
|
private static (MetalBurst metalBurst, IPokemon user, IScriptSet userVolatile) CreateUserWithTracker()
|
|
{
|
|
var metalBurst = new MetalBurst();
|
|
var user = Substitute.For<IPokemon>();
|
|
var userVolatile = new ScriptSet(user);
|
|
user.Volatile.Returns(userVolatile);
|
|
// ScriptSet.Add runs the IScriptPreventVolatileAdd hook pass over the owner's scripts; give the mock a
|
|
// real (empty) iterator so that pass can run.
|
|
user.GetScripts().Returns(_ => new ScriptIterator(new List<IEnumerable<ScriptContainer>>()));
|
|
|
|
var choice = Substitute.For<ITurnChoice>();
|
|
choice.User.Returns(user);
|
|
metalBurst.OnBeforeTurnStart(choice);
|
|
return (metalBurst, user, userVolatile);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates an opposing Pokémon with a real (empty) <see cref="ScriptSet"/> as its volatile collection.
|
|
/// </summary>
|
|
private static IPokemon CreateOpponent()
|
|
{
|
|
var opponent = Substitute.For<IPokemon>();
|
|
opponent.Volatile.Returns(new ScriptSet(opponent));
|
|
return opponent;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Simulates the user being hit by an opponent's move; the damage tracker installed on the user observes
|
|
/// this through its <see cref="IScriptOnIncomingHit"/> hook.
|
|
/// </summary>
|
|
private static void HitUser(IPokemon user, IPokemon attacker, uint damage,
|
|
MoveCategory category = MoveCategory.Physical, byte hit = 0)
|
|
{
|
|
var attackMove = Substitute.For<IExecutingMove>();
|
|
attackMove.User.Returns(attacker);
|
|
var moveData = Substitute.For<IMoveData>();
|
|
moveData.Category.Returns(category);
|
|
attackMove.UseMove.Returns(moveData);
|
|
var hitData = Substitute.For<IHitData>();
|
|
hitData.Damage.Returns(damage);
|
|
attackMove.GetHitData(user, hit).Returns(hitData);
|
|
|
|
var tracker = user.Volatile.Select(c => c.Script).OfType<IScriptOnIncomingHit>().Single();
|
|
tracker.OnIncomingHit(attackMove, user, hit);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates the executing Metal Burst move used by <paramref name="user"/> against <paramref name="target"/>.
|
|
/// </summary>
|
|
private static (IExecutingMove move, IHitData hitData) CreateMetalBurstMove(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>
|
|
/// Helper that checks whether <see cref="IHitData.Fail"/> was called on the hit data.
|
|
/// </summary>
|
|
private static bool ReceivedFail(IHitData hitData) =>
|
|
hitData.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Fail");
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "Metal Burst returns 1.5 times the damage dealt by the foe's last attack."
|
|
/// To know that damage, the script installs a damage-tracking volatile on the user just before the turn
|
|
/// starts.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnBeforeTurnStart_Called_AddsDamageTrackerToUserVolatile()
|
|
{
|
|
// Arrange & Act
|
|
var (_, _, userVolatile) = CreateUserWithTracker();
|
|
|
|
// Assert
|
|
await Assert.That(userVolatile.Count).IsEqualTo(1);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "Metal Burst returns 1.5 times the damage dealt by the foe's last attack."
|
|
/// The user is hit by the target, then retaliates with Metal Burst against that target; the damage must be
|
|
/// 1.5 times the damage taken (fractions truncated).
|
|
/// </summary>
|
|
[Test, Arguments(100u, 150u), Arguments(101u, 151u), Arguments(33u, 49u), Arguments(1u, 1u)]
|
|
public async Task ChangeMoveDamage_UserWasHitByTarget_DealsOneAndAHalfTimesDamageTaken(uint damageTaken,
|
|
uint expectedDamage)
|
|
{
|
|
// Arrange
|
|
var (metalBurst, user, _) = CreateUserWithTracker();
|
|
var attacker = CreateOpponent();
|
|
HitUser(user, attacker, damageTaken);
|
|
var (move, _) = CreateMetalBurstMove(user, attacker);
|
|
|
|
// Act
|
|
uint damage = 0;
|
|
metalBurst.ChangeMoveDamage(move, attacker, 0, ref damage);
|
|
|
|
// Assert
|
|
await Assert.That(damage).IsEqualTo(expectedDamage);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "Metal Burst returns 1.5 times the damage dealt by the foe's last attack."
|
|
/// When the user was hit by the target this turn, Metal Burst succeeds, so the hit must not be failed.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task ChangeMoveDamage_UserWasHitByTarget_DoesNotFailHit()
|
|
{
|
|
// Arrange
|
|
var (metalBurst, user, _) = CreateUserWithTracker();
|
|
var attacker = CreateOpponent();
|
|
HitUser(user, attacker, 100);
|
|
var (move, hitData) = CreateMetalBurstMove(user, attacker);
|
|
|
|
// Act
|
|
uint damage = 0;
|
|
metalBurst.ChangeMoveDamage(move, attacker, 0, ref damage);
|
|
|
|
// Assert
|
|
await Assert.That(ReceivedFail(hitData)).IsFalse();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "if the user acts before it is hit by an opponent's damaging move, Metal Burst will fail."
|
|
/// If no damaging move hit the user this turn, the hit is failed and no damage is dealt.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task ChangeMoveDamage_UserNotHitThisTurn_FailsHit()
|
|
{
|
|
// Arrange
|
|
var (metalBurst, user, _) = CreateUserWithTracker();
|
|
var opponent = CreateOpponent();
|
|
var (move, hitData) = CreateMetalBurstMove(user, opponent);
|
|
|
|
// Act
|
|
uint damage = 0;
|
|
metalBurst.ChangeMoveDamage(move, opponent, 0, ref damage);
|
|
|
|
// Assert
|
|
await Assert.That(ReceivedFail(hitData)).IsTrue();
|
|
await Assert.That(damage).IsEqualTo(0u);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "if the user acts before it is hit by an opponent's damaging move, Metal Burst will fail."
|
|
/// A status move is not a damaging move, so being targeted by only a status move must still make Metal
|
|
/// Burst fail.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task ChangeMoveDamage_UserOnlyHitByStatusMove_FailsHit()
|
|
{
|
|
// Arrange
|
|
var (metalBurst, user, _) = CreateUserWithTracker();
|
|
var attacker = CreateOpponent();
|
|
HitUser(user, attacker, 0, MoveCategory.Status);
|
|
var (move, hitData) = CreateMetalBurstMove(user, attacker);
|
|
|
|
// Act
|
|
uint damage = 0;
|
|
metalBurst.ChangeMoveDamage(move, attacker, 0, ref damage);
|
|
|
|
// Assert
|
|
await Assert.That(ReceivedFail(hitData)).IsTrue();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "Only the last damage taken will be counted, if the user is hit by a multi-hit move, only
|
|
/// the damage from the final hit will be counted."
|
|
/// </summary>
|
|
[Test]
|
|
public async Task ChangeMoveDamage_UserHitByMultiHitMove_OnlyFinalHitCounted()
|
|
{
|
|
// Arrange
|
|
var (metalBurst, user, _) = CreateUserWithTracker();
|
|
var attacker = CreateOpponent();
|
|
HitUser(user, attacker, 40, hit: 0);
|
|
HitUser(user, attacker, 100, hit: 1);
|
|
var (move, _) = CreateMetalBurstMove(user, attacker);
|
|
|
|
// Act
|
|
uint damage = 0;
|
|
metalBurst.ChangeMoveDamage(move, attacker, 0, ref damage);
|
|
|
|
// Assert - 1.5 times the final hit's 100 damage, not the earlier 40.
|
|
await Assert.That(damage).IsEqualTo(150u);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "In battles involving multiple Pokémon, Metal Burst will hit the last opponent that dealt
|
|
/// damage to the user".
|
|
/// When two opponents hit the user, retaliating against the last one uses that opponent's damage.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task ChangeMoveDamage_UserHitByTwoOpponents_UsesDamageOfLastAttacker()
|
|
{
|
|
// Arrange
|
|
var (metalBurst, user, _) = CreateUserWithTracker();
|
|
var firstAttacker = CreateOpponent();
|
|
var lastAttacker = CreateOpponent();
|
|
HitUser(user, firstAttacker, 50);
|
|
HitUser(user, lastAttacker, 80);
|
|
var (move, _) = CreateMetalBurstMove(user, lastAttacker);
|
|
|
|
// Act
|
|
uint damage = 0;
|
|
metalBurst.ChangeMoveDamage(move, lastAttacker, 0, ref damage);
|
|
|
|
// Assert
|
|
await Assert.That(damage).IsEqualTo(120u);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "Metal Burst will hit the last opponent that dealt damage to the user".
|
|
/// Hitting an opponent that is not the last Pokémon that damaged the user must fail.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task ChangeMoveDamage_TargetIsNotLastAttacker_FailsHit()
|
|
{
|
|
// Arrange
|
|
var (metalBurst, user, _) = CreateUserWithTracker();
|
|
var firstAttacker = CreateOpponent();
|
|
var lastAttacker = CreateOpponent();
|
|
HitUser(user, firstAttacker, 50);
|
|
HitUser(user, lastAttacker, 80);
|
|
var (move, hitData) = CreateMetalBurstMove(user, firstAttacker);
|
|
|
|
// Act
|
|
uint damage = 0;
|
|
metalBurst.ChangeMoveDamage(move, firstAttacker, 0, ref damage);
|
|
|
|
// Assert
|
|
await Assert.That(ReceivedFail(hitData)).IsTrue();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "In battles involving multiple Pokémon, Metal Burst will hit the last opponent that dealt
|
|
/// damage to the user" — like <see cref="Counter"/>, the script must implement
|
|
/// <see cref="IScriptChangeTargets"/> and redirect the move at the last attacker.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task ChangeTargets_UserHitByOpponents_RedirectsToLastAttacker()
|
|
{
|
|
// Arrange
|
|
var (metalBurst, user, _) = CreateUserWithTracker();
|
|
var firstAttacker = CreateOpponent();
|
|
var lastAttacker = CreateOpponent();
|
|
HitUser(user, firstAttacker, 50);
|
|
HitUser(user, lastAttacker, 80);
|
|
var choice = Substitute.For<IMoveChoice>();
|
|
choice.User.Returns(user);
|
|
IReadOnlyList<IPokemon?> targets = [firstAttacker];
|
|
|
|
// Act & Assert - the script must expose the redirection hook and point the move at the last attacker
|
|
var changeTargets = metalBurst 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(lastAttacker);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "Metal Burst returns 1.5 times the damage dealt by the foe's last attack." combined with
|
|
/// "if the user acts before it is hit by an opponent's damaging move, Metal Burst will fail."
|
|
/// Only damage taken during the current turn counts, so the damage tracker must be cleaned up at the end
|
|
/// of the turn.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnEndTurn_Called_RemovesDamageTrackerFromUser()
|
|
{
|
|
// Arrange
|
|
var (_, user, userVolatile) = CreateUserWithTracker();
|
|
var tracker = userVolatile.At(0).Script!;
|
|
|
|
// Act
|
|
((IScriptOnEndTurn)tracker).OnEndTurn(user, Substitute.For<IBattle>());
|
|
|
|
// Assert
|
|
await Assert.That(userVolatile.Get(new StringKey("metal_burst_helper"))).IsNull();
|
|
}
|
|
} |