using PkmnLib.Dynamic.Models; using PkmnLib.Plugin.Gen7.Scripts.Moves; using PkmnLib.Static; namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves; /// /// Tests for the 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. /// public class FlameBurstTests { /// /// Creates a mocked battle Pokémon that lives in on the given side and position. /// private static IBattlePokemon CreateBattlePokemon(IBattle battle, byte sideIndex, byte position, uint maxHp = 100) { var pokemon = Substitute.For(); pokemon.Battle.Returns(battle); pokemon.SideIndex.Returns(sideIndex); pokemon.Position.Returns(position); pokemon.BoostedStats.Returns(new StatisticSet(maxHp, 0, 0, 0, 0, 0)); pokemon.MaxHealth.Returns(maxHp); return pokemon; } /// /// Configures the two sides of with the given Pokémon lists. /// private static void SetSides(IBattle battle, List side0Pokemon, List side1Pokemon) { var side0 = Substitute.For(); side0.Pokemon.Returns(side0Pokemon); var side1 = Substitute.For(); side1.Pokemon.Returns(side1Pokemon); battle.Sides.Returns(new[] { side0, side1 }); } /// /// 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. /// private static (FlameBurst script, IExecutingMove move, IBattlePokemon user, IBattlePokemon userAlly, IBattlePokemon target, IBattlePokemon targetAlly) CreateDoubleBattleSetup(uint targetAllyMaxHp = 160) { var script = new FlameBurst(); var battle = Substitute.For(); 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(); move.User.Returns(user); return (script, move, user, userAlly, target, targetAlly); } /// /// Helper to extract the damage amount from a Pokémon's received Damage calls, or 0 if none was received. /// private static uint GetDamageAmount(IBattlePokemon pokemon) { var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage"); return call != null ? (uint)call.GetArguments()[0]! : 0; } /// /// Helper to extract the damage source from a Pokémon's received Damage calls. /// private static DamageSource? GetDamageSource(IBattlePokemon pokemon) { var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage"); return call != null ? (DamageSource)call.GetArguments()[1]! : null; } /// /// Helper that checks whether a Pokémon received any Damage call. /// private static bool WasDamaged(IBattlePokemon pokemon) => pokemon.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Damage"); /// /// 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. /// [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); } /// /// Bulbapedia: the splash damage is "equal to 1/16 of their respective maximum HP". /// Tests various maximum HP values to ensure proper integer truncation. /// [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); } /// /// 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. /// [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(); } /// /// Bulbapedia: "this extra damage is classified as effect damage", so the splash damage is dealt with /// rather than move damage. /// [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); } /// /// 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. /// [Test] public async Task OnSecondaryEffect_TripleBattleTargetInMiddle_BothAdjacentAlliesDamaged() { // Arrange var script = new FlameBurst(); var battle = Substitute.For(); 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(); 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); } /// /// 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. /// [Test] public async Task OnSecondaryEffect_TripleBattleTargetAtEdge_NonAdjacentAllyNotDamaged() { // Arrange var script = new FlameBurst(); var battle = Substitute.For(); 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(); 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(); } /// /// 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. /// [Test] public async Task OnSecondaryEffect_SingleBattle_NoSplashDamage() { // Arrange var script = new FlameBurst(); var battle = Substitute.For(); 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(); 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(); } /// /// Technical test: an empty adjacent position (a fainted or absent ally) does not cause an exception and /// simply receives no splash damage. /// [Test] public async Task OnSecondaryEffect_AdjacentPositionEmpty_DoesNotThrow() { // Arrange var script = new FlameBurst(); var battle = Substitute.For(); 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(); move.User.Returns(user); // Act & Assert await Assert.That(() => script.OnSecondaryEffect(move, target, 0)).ThrowsNothing(); } }