Many more tests and fixes
All checks were successful
Build / Build (push) Successful in 3m12s

This commit is contained in:
2026-08-27 17:11:12 +02:00
parent 32b3ef9c4a
commit d2a82b5fe3
204 changed files with 20255 additions and 242 deletions

View File

@@ -0,0 +1,288 @@
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 VVII 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();
}
}