Move all battle state from IPokemon to an ephemeral IBattlePokemon wrapper

This commit is contained in:
2026-08-28 15:20:26 +02:00
parent 942be8eaeb
commit 8a2733a0a9
859 changed files with 4408 additions and 5331 deletions

View File

@@ -14,12 +14,12 @@ public class AftermathTests
/// <summary>
/// Creates a fully mocked test setup for Aftermath tests.
/// </summary>
private static (Aftermath aftermath, IExecutingMove move, IPokemon target, IPokemon user, EventHook eventHook,
IBattle battle) CreateFullTestSetup(bool isContact, uint userMaxHealth = 100)
private static (Aftermath aftermath, IExecutingMove move, IBattlePokemon target, IBattlePokemon user, EventHook
eventHook, IBattle battle) CreateFullTestSetup(bool isContact, uint userMaxHealth = 100)
{
var aftermath = new Aftermath();
var move = Substitute.For<IExecutingMove>();
var target = Substitute.For<IPokemon>();
var target = Substitute.For<IBattlePokemon>();
var hitData = Substitute.For<IHitData>();
hitData.IsContact.Returns(isContact);
move.GetHitData(target, 0).Returns(hitData);
@@ -30,20 +30,14 @@ public class AftermathTests
// Setup empty sides by default (no Damp on field)
var side = Substitute.For<IBattleSide>();
side.Pokemon.Returns(new List<IPokemon?>());
side.Pokemon.Returns(new List<IBattlePokemon?>());
battle.Sides.Returns(new[] { side });
var battleData = Substitute.For<IPokemonBattleData>();
battleData.Battle.Returns(battle);
var user = Substitute.For<IPokemon>();
var user = Substitute.For<IBattlePokemon>();
user.IsUsable.Returns(true);
user.Battle.Returns(battle);
user.MaxHealth.Returns(userMaxHealth);
// Configure BattleData return value using NSubstitute's callback pattern
// First return null to suppress auto-substitution, then configure actual value
user.BattleData.Returns(battleData);
move.User.Returns(user);
return (aftermath, move, target, user, eventHook, battle);
@@ -52,7 +46,7 @@ public class AftermathTests
/// <summary>
/// Helper to extract the damage amount from a substitute's received Damage calls.
/// </summary>
private static uint GetDamageDealt(IPokemon user)
private static uint GetDamageDealt(IBattlePokemon user)
{
var damageCall = user.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage");
return damageCall != null ? (uint)damageCall.GetArguments()[0]! : 0;
@@ -61,7 +55,7 @@ public class AftermathTests
/// <summary>
/// Helper to extract the damage source from a substitute's received Damage calls.
/// </summary>
private static DamageSource? GetDamageSource(IPokemon user)
private static DamageSource? GetDamageSource(IBattlePokemon user)
{
var damageCall = user.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage");
return damageCall != null ? (DamageSource)damageCall.GetArguments()[1]! : null;
@@ -151,12 +145,12 @@ public class AftermathTests
// Arrange
var aftermath = new Aftermath();
var move = Substitute.For<IExecutingMove>();
var target = Substitute.For<IPokemon>();
var target = Substitute.For<IBattlePokemon>();
var hitData = Substitute.For<IHitData>();
hitData.IsContact.Returns(true);
move.GetHitData(target, 0).Returns(hitData);
var user = Substitute.For<IPokemon>();
var user = Substitute.For<IBattlePokemon>();
user.IsUsable.Returns(false); // Attacker already fainted
move.User.Returns(user);
@@ -168,34 +162,6 @@ public class AftermathTests
await Assert.That(user.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Damage")).IsFalse();
}
/// <summary>
/// Bulbapedia: "the attacking Pokémon takes damage".
/// If the attacker has no battle data, no damage should be dealt.
/// </summary>
[Test]
public async Task OnFaint_AttackerHasNoBattleData_DoesNotDealDamage()
{
// Arrange
var aftermath = new Aftermath();
var move = Substitute.For<IExecutingMove>();
var target = Substitute.For<IPokemon>();
var hitData = Substitute.For<IHitData>();
hitData.IsContact.Returns(true);
move.GetHitData(target, 0).Returns(hitData);
var user = Substitute.For<IPokemon>();
user.IsUsable.Returns(true);
user.BattleData.Returns((IPokemonBattleData?)null);
move.User.Returns(user);
// Act
aftermath.OnIncomingHit(move, target, 0);
aftermath.OnFaint(target, DamageSource.MoveDamage);
// Assert - Damage should never be called because BattleData is null
await Assert.That(user.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Damage")).IsFalse();
}
/// <summary>
/// Technical test: Verifies OnFaint handles the case where no attack was received.
/// </summary>
@@ -204,7 +170,7 @@ public class AftermathTests
{
// Arrange
var aftermath = new Aftermath();
var target = Substitute.For<IPokemon>();
var target = Substitute.For<IBattlePokemon>();
// Act & Assert - Should not throw when _lastAttack is null
aftermath.OnFaint(target, DamageSource.MoveDamage);
@@ -220,19 +186,16 @@ public class AftermathTests
{
// Arrange
var aftermath = new Aftermath();
var target = Substitute.For<IPokemon>();
var target = Substitute.For<IBattlePokemon>();
var battle = Substitute.For<IBattle>();
var eventHook = new EventHook();
battle.EventHook.Returns(eventHook);
// Setup empty sides (no Damp on field)
var side = Substitute.For<IBattleSide>();
side.Pokemon.Returns(new List<IPokemon?>());
side.Pokemon.Returns(new List<IBattlePokemon?>());
battle.Sides.Returns(new[] { side });
var battleData = Substitute.For<IPokemonBattleData>();
battleData.Battle.Returns(battle);
// First hit - non-contact
var move1 = Substitute.For<IExecutingMove>();
var hitData1 = Substitute.For<IHitData>();
@@ -244,10 +207,10 @@ public class AftermathTests
var hitData2 = Substitute.For<IHitData>();
hitData2.IsContact.Returns(true);
move2.GetHitData(target, 0).Returns(hitData2);
var user = Substitute.For<IPokemon>();
var user = Substitute.For<IBattlePokemon>();
user.IsUsable.Returns(true);
user.BattleData.Returns(battleData);
user.MaxHealth.Returns(100u);
user.Battle.Returns(battle);
move2.User.Returns(user);
// Act
@@ -268,7 +231,7 @@ public class AftermathTests
{
// Arrange
var aftermath = new Aftermath();
var target = Substitute.For<IPokemon>();
var target = Substitute.For<IBattlePokemon>();
// First hit - contact
var move1 = Substitute.For<IExecutingMove>();
@@ -346,14 +309,14 @@ public class AftermathTests
var (aftermath, move, target, user, _, battle) = CreateFullTestSetup(true, 100);
// Create a Pokemon with Damp ability on the field
var dampPokemon = Substitute.For<IPokemon>();
var dampPokemon = Substitute.For<IBattlePokemon>();
var dampAbility = Substitute.For<IAbility>();
dampAbility.Name.Returns(new StringKey("damp"));
dampPokemon.ActiveAbility.Returns(dampAbility);
// Update battle sides to include the Damp Pokemon
var side = Substitute.For<IBattleSide>();
side.Pokemon.Returns(new List<IPokemon?> { dampPokemon });
side.Pokemon.Returns(new List<IBattlePokemon?> { dampPokemon });
battle.Sides.Returns(new[] { side });
// Act