234 lines
8.7 KiB
C#
234 lines
8.7 KiB
C#
using PkmnLib.Dynamic.Models;
|
|
using PkmnLib.Dynamic.ScriptHandling;
|
|
using PkmnLib.Plugin.Gen7.Scripts.Abilities;
|
|
using PkmnLib.Plugin.Gen7.Scripts.Moves;
|
|
using PkmnLib.Static.Utils;
|
|
|
|
namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
|
|
|
|
/// <summary>
|
|
/// Tests for the <see cref="FlareBlitz"/> move script.
|
|
/// Gen VII Bulbapedia behavior: "Flare Blitz inflicts damage, and the user receives recoil damage equal to
|
|
/// ⅓ of the damage done to the target. This move has a 10% chance of burning the target."
|
|
/// </summary>
|
|
public class FlareBlitzTests
|
|
{
|
|
/// <summary>
|
|
/// Creates a fully mocked test setup for Flare Blitz tests, where the hit dealt
|
|
/// <paramref name="damage"/> damage to the target.
|
|
/// </summary>
|
|
private static (FlareBlitz script, IExecutingMove move, IPokemon user, IPokemon target, IBattleRandom random)
|
|
CreateTestSetup(uint damage, Script[]? moveScripts = null)
|
|
{
|
|
var script = new FlareBlitz();
|
|
var move = Substitute.For<IExecutingMove>();
|
|
var user = Substitute.For<IPokemon>();
|
|
var target = Substitute.For<IPokemon>();
|
|
|
|
var hitData = Substitute.For<IHitData>();
|
|
hitData.Damage.Returns(damage);
|
|
move.GetHitData(target, 0).Returns(hitData);
|
|
|
|
var battle = Substitute.For<IBattle>();
|
|
var random = Substitute.For<IBattleRandom>();
|
|
battle.Random.Returns(random);
|
|
var battleData = Substitute.For<IPokemonBattleData>();
|
|
battleData.Battle.Returns(battle);
|
|
target.BattleData.Returns(battleData);
|
|
|
|
// RunScriptHook iterates the move's scripts; give the mock a real iterator so the hook pass runs
|
|
// (empty unless the test attaches scripts such as Rock Head).
|
|
var containers = (moveScripts ?? []).Select(IEnumerable<ScriptContainer> (s) => new ScriptContainer(s))
|
|
.ToArray();
|
|
move.GetScripts().Returns(_ => new ScriptIterator(containers));
|
|
|
|
move.User.Returns(user);
|
|
|
|
return (script, move, user, target, random);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper to extract the damage amount from a Pokémon's received Damage calls, or 0 if none was received.
|
|
/// </summary>
|
|
private static uint GetDamageAmount(IPokemon pokemon)
|
|
{
|
|
var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage");
|
|
return call != null ? (uint)call.GetArguments()[0]! : 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper to extract the damage source from a Pokémon's received Damage calls.
|
|
/// </summary>
|
|
private static DamageSource? GetDamageSource(IPokemon pokemon)
|
|
{
|
|
var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage");
|
|
return call != null ? (DamageSource)call.GetArguments()[1]! : null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper that checks whether a Pokémon received a SetStatus call for the given status.
|
|
/// </summary>
|
|
private static bool ReceivedSetStatus(IPokemon pokemon, string status) =>
|
|
pokemon.ReceivedCalls().Any(c =>
|
|
c.GetMethodInfo().Name == "SetStatus" && (StringKey)c.GetArguments()[0]! == new StringKey(status));
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "the user receives recoil damage equal to ⅓ of the damage done to the target."
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnSecondaryEffect_DamageDealt_UserTakesOneThirdRecoil()
|
|
{
|
|
// Arrange
|
|
var (script, move, user, target, _) = CreateTestSetup(90);
|
|
|
|
// Act
|
|
script.OnSecondaryEffect(move, target, 0);
|
|
|
|
// Assert - 90 / 3 = 30
|
|
await Assert.That(GetDamageAmount(user)).IsEqualTo(30u);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "the user receives recoil damage equal to ⅓ of the damage done to the target."
|
|
/// Tests various damage values to ensure proper integer truncation of the one-third amount.
|
|
/// </summary>
|
|
[Test, Arguments(90u, 30u), Arguments(100u, 33u), Arguments(120u, 40u), Arguments(2u, 0u), Arguments(301u, 100u)]
|
|
public async Task OnSecondaryEffect_DamageDealt_RecoilCalculation(uint damage, uint expectedRecoil)
|
|
{
|
|
// Arrange
|
|
var (script, move, user, target, _) = CreateTestSetup(damage);
|
|
|
|
// Act
|
|
script.OnSecondaryEffect(move, target, 0);
|
|
|
|
// Assert
|
|
await Assert.That(GetDamageAmount(user)).IsEqualTo(expectedRecoil);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "the user receives recoil damage". Recoil is indirect damage, so it is dealt with
|
|
/// <see cref="DamageSource.Misc"/> rather than move damage.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnSecondaryEffect_DamageDealt_RecoilUsesMiscDamageSource()
|
|
{
|
|
// Arrange
|
|
var (script, move, user, target, _) = CreateTestSetup(90);
|
|
|
|
// Act
|
|
script.OnSecondaryEffect(move, target, 0);
|
|
|
|
// Assert
|
|
await Assert.That(GetDamageSource(user)!.Value).IsEqualTo(DamageSource.Misc);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "This move has a 10% chance of burning the target."
|
|
/// When the effect chance roll succeeds, the target is burned.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnSecondaryEffect_EffectChanceSucceeds_BurnsTarget()
|
|
{
|
|
// Arrange
|
|
var (script, move, _, target, random) = CreateTestSetup(90);
|
|
random.EffectChance(10f, move, target, 0).Returns(true);
|
|
|
|
// Act
|
|
script.OnSecondaryEffect(move, target, 0);
|
|
|
|
// Assert
|
|
await Assert.That(ReceivedSetStatus(target, "burned")).IsTrue();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "This move has a 10% chance of burning the target."
|
|
/// When the effect chance roll fails, the target is not burned.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnSecondaryEffect_EffectChanceFails_DoesNotBurnTarget()
|
|
{
|
|
// Arrange
|
|
var (script, move, _, target, random) = CreateTestSetup(90);
|
|
random.EffectChance(10f, move, target, 0).Returns(false);
|
|
|
|
// Act
|
|
script.OnSecondaryEffect(move, target, 0);
|
|
|
|
// Assert
|
|
await Assert.That(ReceivedSetStatus(target, "burned")).IsFalse();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "This move has a 10% chance of burning the target."
|
|
/// The 10% chance is rolled through the battle's <see cref="IBattleRandom.EffectChance"/> so that
|
|
/// effect-chance-modifying effects (such as Serene Grace) apply.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnSecondaryEffect_DamageDealt_RollsTenPercentEffectChance()
|
|
{
|
|
// Arrange
|
|
var (script, move, _, target, random) = CreateTestSetup(90);
|
|
|
|
// Act
|
|
script.OnSecondaryEffect(move, target, 0);
|
|
|
|
// Assert
|
|
random.Received(1).EffectChance(10f, move, target, 0);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The recoil can be prevented by recoil-negating effects (Bulbapedia: Rock Head "prevents the Pokémon
|
|
/// from taking recoil damage from most moves"). When a script such as <see cref="RockHead"/> flags the
|
|
/// recoil as prevented, the user takes no recoil damage.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnSecondaryEffect_RecoilPrevented_UserTakesNoRecoilDamage()
|
|
{
|
|
// Arrange
|
|
var (script, move, user, target, _) = CreateTestSetup(90, [new RockHead()]);
|
|
|
|
// Act
|
|
script.OnSecondaryEffect(move, target, 0);
|
|
|
|
// Assert
|
|
await Assert.That(user.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Damage")).IsFalse();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "This move has a 10% chance of burning the target." The burn chance is independent of the
|
|
/// recoil: Rock Head only "prevents the Pokémon from taking recoil damage", so preventing the recoil must
|
|
/// not suppress the burn chance.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnSecondaryEffect_RecoilPrevented_BurnChanceStillApplies()
|
|
{
|
|
// Arrange
|
|
var (script, move, _, target, random) = CreateTestSetup(90, [new RockHead()]);
|
|
random.EffectChance(10f, move, target, 0).Returns(true);
|
|
|
|
// Act
|
|
script.OnSecondaryEffect(move, target, 0);
|
|
|
|
// Assert
|
|
await Assert.That(ReceivedSetStatus(target, "burned")).IsTrue();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Technical test: when the target has no <see cref="IPokemon.BattleData"/>, the script does nothing;
|
|
/// no recoil is taken and no burn is rolled.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnSecondaryEffect_TargetHasNoBattleData_NoRecoilOrBurn()
|
|
{
|
|
// Arrange
|
|
var (script, move, user, target, _) = CreateTestSetup(90);
|
|
target.BattleData.Returns((IPokemonBattleData?)null);
|
|
|
|
// Act
|
|
script.OnSecondaryEffect(move, target, 0);
|
|
|
|
// Assert
|
|
await Assert.That(user.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Damage")).IsFalse();
|
|
await Assert.That(ReceivedSetStatus(target, "burned")).IsFalse();
|
|
}
|
|
} |