288 lines
11 KiB
C#
288 lines
11 KiB
C#
using PkmnLib.Dynamic.Models;
|
||
using PkmnLib.Plugin.Gen7.Scripts.Moves;
|
||
using PkmnLib.Static;
|
||
|
||
namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
|
||
|
||
/// <summary>
|
||
/// Tests for the <see cref="FlameBurst"/> move script.
|
||
/// Gen V–VII Bulbapedia behavior: "Flame Burst deals damage to the target, then inflicts damage to up to two
|
||
/// Pokémon adjacent to that target (including the user if targeting an ally) equal to 1/16 of their
|
||
/// respective maximum HP." The splash damage is classified as effect damage rather than move damage.
|
||
/// </summary>
|
||
public class FlameBurstTests
|
||
{
|
||
/// <summary>
|
||
/// Creates a mocked battle Pokémon that lives in <paramref name="battle"/> on the given side and position.
|
||
/// </summary>
|
||
private static IPokemon CreateBattlePokemon(IBattle battle, byte sideIndex, byte position, uint maxHp = 100)
|
||
{
|
||
var pokemon = Substitute.For<IPokemon>();
|
||
var battleData = Substitute.For<IPokemonBattleData>();
|
||
battleData.Battle.Returns(battle);
|
||
battleData.SideIndex.Returns(sideIndex);
|
||
battleData.Position.Returns(position);
|
||
pokemon.BattleData.Returns(battleData);
|
||
pokemon.BoostedStats.Returns(new StatisticSet<uint>(maxHp, 0, 0, 0, 0, 0));
|
||
pokemon.MaxHealth.Returns(maxHp);
|
||
return pokemon;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Configures the two sides of <paramref name="battle"/> with the given Pokémon lists.
|
||
/// </summary>
|
||
private static void SetSides(IBattle battle, List<IPokemon?> side0Pokemon, List<IPokemon?> side1Pokemon)
|
||
{
|
||
var side0 = Substitute.For<IBattleSide>();
|
||
side0.Pokemon.Returns(side0Pokemon);
|
||
var side1 = Substitute.For<IBattleSide>();
|
||
side1.Pokemon.Returns(side1Pokemon);
|
||
battle.Sides.Returns(new[] { side0, side1 });
|
||
}
|
||
|
||
/// <summary>
|
||
/// Creates a fully mocked double battle: the user and its ally on side 0, the target and its ally on
|
||
/// side 1. Flame Burst is used by the user against the target.
|
||
/// </summary>
|
||
private static (FlameBurst script, IExecutingMove move, IPokemon user, IPokemon userAlly, IPokemon target, IPokemon
|
||
targetAlly) CreateDoubleBattleSetup(uint targetAllyMaxHp = 160)
|
||
{
|
||
var script = new FlameBurst();
|
||
var battle = Substitute.For<IBattle>();
|
||
battle.PositionsPerSide.Returns((byte)2);
|
||
|
||
var user = CreateBattlePokemon(battle, 0, 0);
|
||
var userAlly = CreateBattlePokemon(battle, 0, 1);
|
||
var target = CreateBattlePokemon(battle, 1, 0);
|
||
var targetAlly = CreateBattlePokemon(battle, 1, 1, targetAllyMaxHp);
|
||
SetSides(battle, [user, userAlly], [target, targetAlly]);
|
||
|
||
var move = Substitute.For<IExecutingMove>();
|
||
move.User.Returns(user);
|
||
|
||
return (script, move, user, userAlly, target, targetAlly);
|
||
}
|
||
|
||
/// <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 any Damage call.
|
||
/// </summary>
|
||
private static bool WasDamaged(IPokemon pokemon) =>
|
||
pokemon.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Damage");
|
||
|
||
/// <summary>
|
||
/// Bulbapedia: "Flame Burst deals damage to the target, then inflicts damage to up to two Pokémon adjacent
|
||
/// to that target ... equal to 1/16 of their respective maximum HP."
|
||
/// In a double battle, the target's ally is adjacent to the target and takes 1/16 of its own maximum HP.
|
||
/// </summary>
|
||
[Test]
|
||
public async Task OnSecondaryEffect_DoubleBattle_TargetAllyTakesOneSixteenthOfItsMaxHp()
|
||
{
|
||
// Arrange
|
||
var (script, move, _, _, target, targetAlly) = CreateDoubleBattleSetup(160);
|
||
|
||
// Act
|
||
script.OnSecondaryEffect(move, target, 0);
|
||
|
||
// Assert - 160 / 16 = 10
|
||
await Assert.That(GetDamageAmount(targetAlly)).IsEqualTo(10u);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Bulbapedia: the splash damage is "equal to 1/16 of their respective maximum HP".
|
||
/// Tests various maximum HP values to ensure proper integer truncation.
|
||
/// </summary>
|
||
[Test, Arguments(160u, 10u), Arguments(100u, 6u), Arguments(32u, 2u), Arguments(15u, 0u)]
|
||
public async Task OnSecondaryEffect_DoubleBattle_SplashDamageCalculation(uint allyMaxHp, uint expectedDamage)
|
||
{
|
||
// Arrange
|
||
var (script, move, _, _, target, targetAlly) = CreateDoubleBattleSetup(allyMaxHp);
|
||
|
||
// Act
|
||
script.OnSecondaryEffect(move, target, 0);
|
||
|
||
// Assert
|
||
await Assert.That(GetDamageAmount(targetAlly)).IsEqualTo(expectedDamage);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Bulbapedia: the splash damage hits Pokémon "adjacent to that target". The user's own ally is not
|
||
/// adjacent to the target and must not be damaged when targeting an opponent.
|
||
/// </summary>
|
||
[Test]
|
||
public async Task OnSecondaryEffect_DoubleBattle_UserAllyIsNotDamaged()
|
||
{
|
||
// Arrange
|
||
var (script, move, _, userAlly, target, _) = CreateDoubleBattleSetup();
|
||
|
||
// Act
|
||
script.OnSecondaryEffect(move, target, 0);
|
||
|
||
// Assert
|
||
await Assert.That(WasDamaged(userAlly)).IsFalse();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Bulbapedia: "this extra damage is classified as effect damage", so the splash damage is dealt with
|
||
/// <see cref="DamageSource.Misc"/> rather than move damage.
|
||
/// </summary>
|
||
[Test]
|
||
public async Task OnSecondaryEffect_DoubleBattle_SplashDamageUsesMiscDamageSource()
|
||
{
|
||
// Arrange
|
||
var (script, move, _, _, target, targetAlly) = CreateDoubleBattleSetup();
|
||
|
||
// Act
|
||
script.OnSecondaryEffect(move, target, 0);
|
||
|
||
// Assert
|
||
await Assert.That(GetDamageSource(targetAlly)!.Value).IsEqualTo(DamageSource.Misc);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Bulbapedia: "inflicts damage to up to two Pokémon adjacent to that target".
|
||
/// In a triple battle with the target in the middle, both of the target's allies are adjacent and both
|
||
/// take splash damage.
|
||
/// </summary>
|
||
[Test]
|
||
public async Task OnSecondaryEffect_TripleBattleTargetInMiddle_BothAdjacentAlliesDamaged()
|
||
{
|
||
// Arrange
|
||
var script = new FlameBurst();
|
||
var battle = Substitute.For<IBattle>();
|
||
battle.PositionsPerSide.Returns((byte)3);
|
||
|
||
var user = CreateBattlePokemon(battle, 0, 1);
|
||
var target = CreateBattlePokemon(battle, 1, 1);
|
||
var allyLeft = CreateBattlePokemon(battle, 1, 0, 96);
|
||
var allyRight = CreateBattlePokemon(battle, 1, 2, 64);
|
||
SetSides(battle, [null, user, null], [allyLeft, target, allyRight]);
|
||
|
||
var move = Substitute.For<IExecutingMove>();
|
||
move.User.Returns(user);
|
||
|
||
// Act
|
||
script.OnSecondaryEffect(move, target, 0);
|
||
|
||
// Assert - 96 / 16 = 6 and 64 / 16 = 4
|
||
await Assert.That(GetDamageAmount(allyLeft)).IsEqualTo(6u);
|
||
await Assert.That(GetDamageAmount(allyRight)).IsEqualTo(4u);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Bulbapedia: only Pokémon "adjacent to that target" take splash damage.
|
||
/// In a triple battle with the target at the edge, the ally two positions away is not adjacent and takes
|
||
/// no damage.
|
||
/// </summary>
|
||
[Test]
|
||
public async Task OnSecondaryEffect_TripleBattleTargetAtEdge_NonAdjacentAllyNotDamaged()
|
||
{
|
||
// Arrange
|
||
var script = new FlameBurst();
|
||
var battle = Substitute.For<IBattle>();
|
||
battle.PositionsPerSide.Returns((byte)3);
|
||
|
||
var user = CreateBattlePokemon(battle, 0, 0);
|
||
var target = CreateBattlePokemon(battle, 1, 0);
|
||
var allyMiddle = CreateBattlePokemon(battle, 1, 1, 160);
|
||
var allyFar = CreateBattlePokemon(battle, 1, 2, 160);
|
||
SetSides(battle, [user, null, null], [target, allyMiddle, allyFar]);
|
||
|
||
var move = Substitute.For<IExecutingMove>();
|
||
move.User.Returns(user);
|
||
|
||
// Act
|
||
script.OnSecondaryEffect(move, target, 0);
|
||
|
||
// Assert - only the adjacent ally is hit
|
||
await Assert.That(GetDamageAmount(allyMiddle)).IsEqualTo(10u);
|
||
await Assert.That(WasDamaged(allyFar)).IsFalse();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Bulbapedia: the splash damage hits Pokémon "adjacent to that target". In a single battle there are no
|
||
/// Pokémon adjacent to the target, so nothing else is damaged.
|
||
/// </summary>
|
||
[Test]
|
||
public async Task OnSecondaryEffect_SingleBattle_NoSplashDamage()
|
||
{
|
||
// Arrange
|
||
var script = new FlameBurst();
|
||
var battle = Substitute.For<IBattle>();
|
||
battle.PositionsPerSide.Returns((byte)1);
|
||
|
||
var user = CreateBattlePokemon(battle, 0, 0);
|
||
var target = CreateBattlePokemon(battle, 1, 0);
|
||
SetSides(battle, [user], [target]);
|
||
|
||
var move = Substitute.For<IExecutingMove>();
|
||
move.User.Returns(user);
|
||
|
||
// Act
|
||
script.OnSecondaryEffect(move, target, 0);
|
||
|
||
// Assert - neither the user nor the target receives effect damage
|
||
await Assert.That(WasDamaged(user)).IsFalse();
|
||
await Assert.That(WasDamaged(target)).IsFalse();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Technical test: an empty adjacent position (a fainted or absent ally) does not cause an exception and
|
||
/// simply receives no splash damage.
|
||
/// </summary>
|
||
[Test]
|
||
public async Task OnSecondaryEffect_AdjacentPositionEmpty_DoesNotThrow()
|
||
{
|
||
// Arrange
|
||
var script = new FlameBurst();
|
||
var battle = Substitute.For<IBattle>();
|
||
battle.PositionsPerSide.Returns((byte)2);
|
||
|
||
var user = CreateBattlePokemon(battle, 0, 0);
|
||
var userAlly = CreateBattlePokemon(battle, 0, 1);
|
||
var target = CreateBattlePokemon(battle, 1, 0);
|
||
SetSides(battle, [user, userAlly], [target, null]);
|
||
|
||
var move = Substitute.For<IExecutingMove>();
|
||
move.User.Returns(user);
|
||
|
||
// Act & Assert
|
||
await Assert.That(() => script.OnSecondaryEffect(move, target, 0)).ThrowsNothing();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Technical test: when no battle data is available the script does nothing instead of throwing.
|
||
/// </summary>
|
||
[Test]
|
||
public async Task OnSecondaryEffect_NoBattleData_DoesNotThrow()
|
||
{
|
||
// Arrange
|
||
var script = new FlameBurst();
|
||
var move = Substitute.For<IExecutingMove>();
|
||
var user = Substitute.For<IPokemon>();
|
||
user.BattleData.Returns((IPokemonBattleData?)null);
|
||
move.User.Returns(user);
|
||
var target = Substitute.For<IPokemon>();
|
||
target.BattleData.Returns((IPokemonBattleData?)null);
|
||
|
||
// Act & Assert
|
||
await Assert.That(() => script.OnSecondaryEffect(move, target, 0)).ThrowsNothing();
|
||
}
|
||
} |