Files
PkmnLib.NET/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/FlameBurstTests.cs

267 lines
10 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 IBattlePokemon CreateBattlePokemon(IBattle battle, byte sideIndex, byte position, uint maxHp = 100)
{
var pokemon = Substitute.For<IBattlePokemon>();
pokemon.Battle.Returns(battle);
pokemon.SideIndex.Returns(sideIndex);
pokemon.Position.Returns(position);
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<IBattlePokemon?> side0Pokemon, List<IBattlePokemon?> 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, IBattlePokemon user, IBattlePokemon userAlly, IBattlePokemon
target, IBattlePokemon 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(IBattlePokemon 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(IBattlePokemon 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(IBattlePokemon 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();
}
}