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 script, which implements Metal Burst.
/// Behavior is verified against the Bulbapedia page for Metal Burst (Generation VII).
///
public class MetalBurstTests
{
///
/// Creates a Metal Burst user whose damage-tracking volatile has been installed through
/// . The user's is a real
/// so the internal tracker script can actually be stored and retrieved.
///
private static (MetalBurst metalBurst, IBattlePokemon user, IScriptSet userVolatile) CreateUserWithTracker()
{
var metalBurst = new MetalBurst();
var user = Substitute.For();
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>()));
var choice = Substitute.For();
choice.User.Returns(user);
metalBurst.OnBeforeTurnStart(choice);
return (metalBurst, user, userVolatile);
}
///
/// Creates an opposing Pokémon with a real (empty) as its volatile collection.
///
private static IBattlePokemon CreateOpponent()
{
var opponent = Substitute.For();
opponent.Volatile.Returns(new ScriptSet(opponent));
return opponent;
}
///
/// Simulates the user being hit by an opponent's move; the damage tracker installed on the user observes
/// this through its hook.
///
private static void HitUser(IBattlePokemon user, IBattlePokemon attacker, uint damage,
MoveCategory category = MoveCategory.Physical, byte hit = 0)
{
var attackMove = Substitute.For();
attackMove.User.Returns(attacker);
var moveData = Substitute.For();
moveData.Category.Returns(category);
attackMove.UseMove.Returns(moveData);
var hitData = Substitute.For();
hitData.Damage.Returns(damage);
attackMove.GetHitData(user, hit).Returns(hitData);
var tracker = user.Volatile.Select(c => c.Script).OfType().Single();
tracker.OnIncomingHit(attackMove, user, hit);
}
///
/// Creates the executing Metal Burst move used by against .
///
private static (IExecutingMove move, IHitData hitData) CreateMetalBurstMove(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);
}
///
/// Helper that checks whether was called on the hit data.
///
private static bool ReceivedFail(IHitData hitData) =>
hitData.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Fail");
///
/// 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.
///
[Test]
public async Task OnBeforeTurnStart_Called_AddsDamageTrackerToUserVolatile()
{
// Arrange & Act
var (_, _, userVolatile) = CreateUserWithTracker();
// Assert
await Assert.That(userVolatile.Count).IsEqualTo(1);
}
///
/// 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).
///
[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);
}
///
/// 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.
///
[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();
}
///
/// 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.
///
[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);
}
///
/// 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.
///
[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();
}
///
/// 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."
///
[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);
}
///
/// 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.
///
[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);
}
///
/// 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.
///
[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();
}
///
/// Bulbapedia: "In battles involving multiple Pokémon, Metal Burst will hit the last opponent that dealt
/// damage to the user" — like , the script must implement
/// and redirect the move at the last attacker.
///
[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();
choice.User.Returns(user);
IReadOnlyList 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);
}
///
/// 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.
///
[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());
// Assert
await Assert.That(userVolatile.Get(new StringKey("metal_burst_helper"))).IsNull();
}
}