Files
PkmnLib.NET/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/HighJumpKickTests.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

238 lines
9.5 KiB
C#

using PkmnLib.Dynamic.Models;
using PkmnLib.Dynamic.ScriptHandling;
using PkmnLib.Plugin.Gen7.Scripts.Moves;
using PkmnLib.Static;
namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
/// <summary>
/// Tests for the <see cref="HighJumpKick"/> 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 <see cref="IScriptOnAfterHits"/> and applies crash damage when none of the move's
/// hits executed with damage.
/// </summary>
public class HighJumpKickTests
{
/// <summary>
/// Creates a fully mocked test setup for High Jump Kick tests, with the given user max HP
/// (<see cref="IPokemon.BoostedStats"/>) 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
/// <see cref="IHitData.HasExecuted"/> set to false.
/// </summary>
private static (HighJumpKick script, IExecutingMove move, IPokemon target, IPokemon user) CreateTestSetup(
uint maxHp, params (bool hasExecuted, uint damage)[] hits)
{
var script = new HighJumpKick();
var move = Substitute.For<IExecutingMove>();
var target = Substitute.For<IPokemon>();
var hitData = hits.Select(h =>
{
var hit = Substitute.For<IHitData>();
hit.HasExecuted.Returns(h.hasExecuted);
hit.Damage.Returns(h.damage);
return hit;
}).ToArray();
move.Hits.Returns(hitData);
var user = Substitute.For<IPokemon>();
user.BoostedStats.Returns(new StatisticSet<uint>(maxHp, 0, 0, 0, 0, 0));
move.User.Returns(user);
return (script, move, target, user);
}
/// <summary>
/// Helper to extract the damage amount from the user's received Damage calls.
/// </summary>
private static uint? GetDamageAmount(IPokemon user)
{
var call = user.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage");
return call != null ? (uint)call.GetArguments()[0]! : null;
}
/// <summary>
/// Helper to extract the damage source from the user's received Damage calls.
/// </summary>
private static DamageSource? GetDamageSource(IPokemon user)
{
var call = user.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage");
return call != null ? (DamageSource)call.GetArguments()[1]! : null;
}
/// <summary>
/// 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
/// <see cref="IScriptOnAfterHits"/> and the user takes half of its own max HP as crash damage.
/// </summary>
[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);
}
/// <summary>
/// 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.
/// </summary>
[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);
}
/// <summary>
/// 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.
/// </summary>
[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();
}
/// <summary>
/// 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.
/// </summary>
[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);
}
/// <summary>
/// When the move hits and deals damage, the user takes no crash damage.
/// </summary>
[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();
}
/// <summary>
/// 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 <see cref="DamageSource.Misc"/>.
/// </summary>
[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);
}
/// <summary>
/// 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 <see cref="IScriptOnAfterHits"/>; the blocked hit's would-be
/// damage was already calculated but the hit never executed, so crash damage applies.
/// </summary>
[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);
}
/// <summary>
/// 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; <see cref="IScriptOnAfterHits"/> still runs and applies crash damage.
/// </summary>
[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);
}
/// <summary>
/// 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
/// (<see cref="IScriptIsInvulnerableToMove"/>) the engine skips the hit loop entirely and invokes
/// <see cref="IScriptOnAfterHits"/> before returning, so the hit data is left untouched (not executed,
/// no damage calculated) and the script applies crash damage.
/// </summary>
[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);
}
}