using PkmnLib.Dynamic.Models;
using PkmnLib.Dynamic.ScriptHandling;
using PkmnLib.Plugin.Gen7.Scripts.Moves;
using PkmnLib.Static;
namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
///
/// Tests for the move script.
/// Gen VII Bulbapedia behavior: if the move misses, the user takes crash damage. Generation V onwards:
/// "The crash damage is now (always) equal to half of the user's max HP, rounded down."
/// Crash damage also applies when the move is protected against (Generation II onwards: "The user will take
/// crash damage if Hi Jump Kick is protected against by a move such as Protect") and due to a target's type
/// immunity (Generation IV onwards: "The user can now crash due to a target's type immunity").
/// The script implements and applies crash damage when none of the move's
/// hits executed with damage.
///
public class HighJumpKickTests
{
///
/// Creates a fully mocked test setup for High Jump Kick tests, with the given user max HP
/// () and hit data. The engine calculates a hit's damage before the
/// accuracy and block checks, so a missed or blocked hit still carries a damage value but has
/// set to false.
///
private static (HighJumpKick script, IExecutingMove move, IBattlePokemon target, IBattlePokemon user)
CreateTestSetup(uint maxHp, params (bool hasExecuted, uint damage)[] hits)
{
var script = new HighJumpKick();
var move = Substitute.For();
var target = Substitute.For();
var hitData = hits.Select(h =>
{
var hit = Substitute.For();
hit.HasExecuted.Returns(h.hasExecuted);
hit.Damage.Returns(h.damage);
return hit;
}).ToArray();
move.Hits.Returns(hitData);
var user = Substitute.For();
user.BoostedStats.Returns(new StatisticSet(maxHp, 0, 0, 0, 0, 0));
move.User.Returns(user);
return (script, move, target, user);
}
///
/// Helper to extract the damage amount from the user's received Damage calls.
///
private static uint? GetDamageAmount(IBattlePokemon user)
{
var call = user.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage");
return call != null ? (uint)call.GetArguments()[0]! : null;
}
///
/// Helper to extract the damage source from the user's received Damage calls.
///
private static DamageSource? GetDamageSource(IBattlePokemon user)
{
var call = user.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage");
return call != null ? (DamageSource)call.GetArguments()[1]! : null;
}
///
/// Bulbapedia (Generation V onwards): "The crash damage is now (always) equal to half of the user's max
/// HP, rounded down."
/// When the move misses, the hit never executes; after the hit loop the engine invokes
/// and the user takes half of its own max HP as crash damage.
///
[Test]
public async Task OnAfterHits_MoveMissed_UserTakesHalfMaxHpCrashDamage()
{
// Arrange - the missed hit has its would-be damage calculated but never executed.
var (script, move, target, user) = CreateTestSetup(200, (false, 400));
// Act
script.OnAfterHits(move, target);
// Assert
await Assert.That(GetDamageAmount(user)!.Value).IsEqualTo(100u);
}
///
/// Bulbapedia (Generation V onwards): "The crash damage is now (always) equal to half of the user's max
/// HP, rounded down."
/// Odd max HP values are rounded down when halved.
///
[Test, Arguments(200u, 100u), Arguments(301u, 150u), Arguments(75u, 37u)]
public async Task OnAfterHits_MoveMissed_CrashDamageIsHalfMaxHpRoundedDown(uint maxHp, uint expectedCrash)
{
// Arrange
var (script, move, target, user) = CreateTestSetup(maxHp, (false, 400));
// Act
script.OnAfterHits(move, target);
// Assert
await Assert.That(GetDamageAmount(user)!.Value).IsEqualTo(expectedCrash);
}
///
/// With a max HP of 1 the crash damage rounds down to 0, and the script does not invoke a zero-damage
/// call on the user at all.
///
[Test]
public async Task OnAfterHits_MaxHpOne_NoDamageCallOnUser()
{
// Arrange
var (script, move, target, user) = CreateTestSetup(1, (false, 400));
// Act
script.OnAfterHits(move, target);
// Assert
await Assert.That(GetDamageAmount(user)).IsNull();
}
///
/// Bulbapedia (Generation V onwards): "The crash damage is now (always) equal to half of the user's max
/// HP, rounded down."
/// Since Generation V the crash damage no longer depends on the damage the move would have dealt (that
/// was the Generation III/IV mechanic), so a low would-be damage still results in half the user's max HP.
///
[Test]
public async Task OnAfterHits_LowWouldBeDamage_CrashDamageStillHalfMaxHp()
{
// Arrange
var (script, move, target, user) = CreateTestSetup(200, (false, 10));
// Act
script.OnAfterHits(move, target);
// Assert
await Assert.That(GetDamageAmount(user)!.Value).IsEqualTo(100u);
}
///
/// When the move hits and deals damage, the user takes no crash damage.
///
[Test]
public async Task OnAfterHits_MoveHitWithDamage_NoCrashDamage()
{
// Arrange
var (script, move, target, user) = CreateTestSetup(200, (true, 400));
// Act
script.OnAfterHits(move, target);
// Assert
await Assert.That(GetDamageAmount(user)).IsNull();
}
///
/// Bulbapedia (Generation V onwards): "The crash damage is now (always) equal to half of the user's max
/// HP, rounded down."
/// Crash damage is indirect damage, not move damage, so it uses .
///
[Test]
public async Task OnAfterHits_MoveMissed_UsesMiscDamageSource()
{
// Arrange
var (script, move, target, user) = CreateTestSetup(200, (false, 400));
// Act
script.OnAfterHits(move, target);
// Assert
await Assert.That(GetDamageSource(user)!.Value).IsEqualTo(DamageSource.Misc);
}
///
/// Bulbapedia (Generation II onwards): "The user will take crash damage if Hi Jump Kick is protected
/// against by a move such as Protect".
/// When the hit is blocked by protection, the engine (MoveTurnExecutor) breaks out of the hit loop
/// without executing the hit, then invokes ; the blocked hit's would-be
/// damage was already calculated but the hit never executed, so crash damage applies.
///
[Test]
public async Task OnAfterHits_MoveBlockedByProtect_UserTakesCrashDamage()
{
// Arrange - a blocked hit carries its calculated would-be damage but never executed.
var (script, move, target, user) = CreateTestSetup(200, (false, 400));
// Act
script.OnAfterHits(move, target);
// Assert
var damage = GetDamageAmount(user);
await Assert.That(damage).IsNotNull();
await Assert.That(damage!.Value).IsEqualTo(100u);
}
///
/// Bulbapedia (Generation IV onwards): "The user can now crash due to a target's type immunity", with the
/// Generation V onwards amount: "The crash damage is now (always) equal to half of the user's max HP,
/// rounded down."
/// Against a Ghost-type target the hit deals no damage (effectiveness 0), so the engine never marks the
/// hit as executed; still runs and applies crash damage.
///
[Test]
public async Task OnAfterHits_TargetImmuneToMove_UserTakesCrashDamage()
{
// Arrange - a Ghost-type target: the hit deals no damage and is never marked as executed.
var (script, move, target, user) = CreateTestSetup(200, (false, 0));
// Act
script.OnAfterHits(move, target);
// Assert
var damage = GetDamageAmount(user);
await Assert.That(damage).IsNotNull();
await Assert.That(damage!.Value).IsEqualTo(100u);
}
///
/// A miss caused by the target being in the semi-invulnerable turn of a move such as Fly or Dig is still
/// a miss, so per Bulbapedia (Generation V onwards) the user takes crash damage "equal to half of the
/// user's max HP, rounded down." For a semi-invulnerable target
/// () the engine skips the hit loop entirely and invokes
/// before returning, so the hit data is left untouched (not executed,
/// no damage calculated) and the script applies crash damage.
///
[Test]
public async Task OnAfterHits_MoveAvoidedBySemiInvulnerableTarget_UserTakesCrashDamage()
{
// Arrange - the hit loop never ran, so the hit was never executed and carries no damage.
var (script, move, target, user) = CreateTestSetup(200, (false, 0));
// Act
script.OnAfterHits(move, target);
// Assert
var damage = GetDamageAmount(user);
await Assert.That(damage).IsNotNull();
await Assert.That(damage!.Value).IsEqualTo(100u);
}
}