This commit is contained in:
@@ -0,0 +1,238 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user