Move all battle state from IPokemon to an ephemeral IBattlePokemon wrapper
This commit is contained in:
160
PkmnLib.Tests/Dynamic/BattleLifecycleTests.cs
Normal file
160
PkmnLib.Tests/Dynamic/BattleLifecycleTests.cs
Normal file
@@ -0,0 +1,160 @@
|
||||
using PkmnLib.Dynamic.Libraries;
|
||||
using PkmnLib.Dynamic.Models;
|
||||
using PkmnLib.Static;
|
||||
using PkmnLib.Static.Species;
|
||||
using PkmnLib.Tests.Integration;
|
||||
|
||||
namespace PkmnLib.Tests.Dynamic;
|
||||
|
||||
/// <summary>
|
||||
/// Regression tests for the battle lifecycle of <see cref="IBattlePokemon"/>: battle-only state must die
|
||||
/// with the battle, and the underlying <see cref="IPokemon"/> must come out of a battle unchanged except
|
||||
/// for the deliberately persistent parts (health, PP, non-volatile status, experience).
|
||||
/// </summary>
|
||||
public class BattleLifecycleTests
|
||||
{
|
||||
private static IPokemon CreatePokemon(IDynamicLibrary library, string speciesName)
|
||||
{
|
||||
if (!library.StaticLibrary.Species.TryGet(speciesName, out var species))
|
||||
throw new InvalidOperationException($"Failed to load {speciesName} species.");
|
||||
return new PokemonImpl(library, species, species.GetDefaultForm(), new AbilityIndex
|
||||
{
|
||||
Index = 0,
|
||||
IsHidden = false,
|
||||
}, 50, 0, Gender.Male, 0, "hardy");
|
||||
}
|
||||
|
||||
private static IBattle CreateBattle(IDynamicLibrary library, IPokemon own, IPokemon opponent,
|
||||
bool isWildBattle = false, int seed = 0)
|
||||
{
|
||||
var party = new PokemonPartyImpl(1);
|
||||
party.SwapInto(own, 0);
|
||||
var opponentParty = new PokemonPartyImpl(1);
|
||||
opponentParty.SwapInto(opponent, 0);
|
||||
var parties = new[]
|
||||
{
|
||||
new BattlePartyImpl(party, [new ResponsibleIndex(0, 0)]),
|
||||
new BattlePartyImpl(opponentParty, [new ResponsibleIndex(1, 0)]),
|
||||
};
|
||||
var battle = new BattleImpl(library, parties, false, 2, 1, isWildBattle, "grass", seed);
|
||||
battle.Sides[0].SendOut(0, own);
|
||||
battle.Sides[1].SendOut(0, opponent);
|
||||
return battle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The original bug this refactor removes by construction: a captured Pokémon must not carry any
|
||||
/// battle state into the party of its captor. The capture is reported through
|
||||
/// <see cref="BattleResult.CapturedPokemon"/> as the persistent Pokémon.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task Capture_ReportsPersistentPokemonAndLeavesItUsable()
|
||||
{
|
||||
var library = LibraryHelpers.LoadLibrary();
|
||||
if (!library.StaticLibrary.Items.TryGet("master_ball", out var masterBall))
|
||||
throw new InvalidOperationException("Failed to load master ball.");
|
||||
|
||||
// The capture roll is random; find a seed where the capture succeeds. The loop is deterministic,
|
||||
// so the test always exercises the same battle.
|
||||
var result = CaptureResult.Failed;
|
||||
IBattle? battle = null;
|
||||
IPokemon? wildPokemon = null;
|
||||
for (var seed = 0; seed < 100 && !result.IsCaught; seed++)
|
||||
{
|
||||
battle?.Dispose();
|
||||
var own = CreatePokemon(library, "bulbasaur");
|
||||
wildPokemon = CreatePokemon(library, "caterpie");
|
||||
battle = CreateBattle(library, own, wildPokemon, true, seed);
|
||||
var wildBattlePokemon = battle.GetPokemon(1, 0)!;
|
||||
wildBattlePokemon.Damage(wildBattlePokemon.CurrentHealth - 1, DamageSource.MoveDamage);
|
||||
result = battle.AttempCapture(1, 0, masterBall);
|
||||
}
|
||||
|
||||
await Assert.That(result.IsCaught).IsTrue();
|
||||
await Assert.That(battle!.HasEnded).IsTrue();
|
||||
await Assert.That(battle.Result!.Value.CapturedPokemon).Contains(wildPokemon!);
|
||||
|
||||
battle.Dispose();
|
||||
|
||||
// The persistent Pokémon left the battle without any battle state: it is usable and simply damaged.
|
||||
await Assert.That(wildPokemon!.IsUsable).IsTrue();
|
||||
await Assert.That(wildPokemon.CurrentHealth).IsEqualTo(1u);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Items removed or stolen during a battle are a battle-only overlay: after the battle, the victim
|
||||
/// still holds its item.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task StolenHeldItem_IsRestoredAfterBattle()
|
||||
{
|
||||
var library = LibraryHelpers.LoadLibrary();
|
||||
if (!library.StaticLibrary.Items.TryGet("oran_berry", out var berry))
|
||||
throw new InvalidOperationException("Failed to load oran berry.");
|
||||
var own = CreatePokemon(library, "bulbasaur");
|
||||
_ = own.ForceSetHeldItem(berry);
|
||||
var opponent = CreatePokemon(library, "charmander");
|
||||
|
||||
var battle = CreateBattle(library, own, opponent);
|
||||
var battlePokemon = battle.GetPokemon(0, 0)!;
|
||||
await Assert.That(battlePokemon.TryStealHeldItem(out _)).IsTrue();
|
||||
await Assert.That(battlePokemon.HeldItem).IsNull();
|
||||
|
||||
battle.Dispose();
|
||||
|
||||
await Assert.That(own.HeldItem).IsSameReferenceAs(berry);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A battle-only form (such as a mega evolution) reverts to the original form when the battle ends.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task BattleOnlyForm_RevertsWhenBattleEnds()
|
||||
{
|
||||
var library = LibraryHelpers.LoadLibrary();
|
||||
var own = CreatePokemon(library, "absol");
|
||||
var originalForm = own.Form;
|
||||
if (!own.Species.TryGetForm("mega", out var megaForm))
|
||||
throw new InvalidOperationException("Absol has no mega form.");
|
||||
await Assert.That(megaForm.IsBattleOnlyForm).IsTrue();
|
||||
var opponent = CreatePokemon(library, "charmander");
|
||||
|
||||
var battle = CreateBattle(library, own, opponent);
|
||||
var battlePokemon = battle.GetPokemon(0, 0)!;
|
||||
battlePokemon.ChangeForm(megaForm);
|
||||
await Assert.That(own.Form).IsSameReferenceAs(megaForm);
|
||||
|
||||
battle.Dispose();
|
||||
|
||||
await Assert.That(own.Form).IsSameReferenceAs(originalForm);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A second battle with the same party starts with fresh battle state: no seen opponents or stale
|
||||
/// original species from the previous battle.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task SecondBattleWithSameParty_StartsWithFreshBattleState()
|
||||
{
|
||||
var library = LibraryHelpers.LoadLibrary();
|
||||
var own = CreatePokemon(library, "bulbasaur");
|
||||
var opponent1 = CreatePokemon(library, "charmander");
|
||||
|
||||
var battle1 = CreateBattle(library, own, opponent1);
|
||||
var firstWrapper = battle1.GetPokemon(0, 0)!;
|
||||
await Assert.That(firstWrapper.SeenOpponents.Count).IsEqualTo(1);
|
||||
firstWrapper.ChangeStatBoost(Statistic.Attack, 3, true, false);
|
||||
battle1.Dispose();
|
||||
|
||||
var opponent2 = CreatePokemon(library, "squirtle");
|
||||
var battle2 = CreateBattle(library, own, opponent2);
|
||||
var secondWrapper = battle2.GetPokemon(0, 0)!;
|
||||
|
||||
await Assert.That(secondWrapper).IsNotSameReferenceAs(firstWrapper);
|
||||
await Assert.That(secondWrapper.SeenOpponents.Count).IsEqualTo(1);
|
||||
await Assert.That(secondWrapper.SeenOpponents[0].UnderlyingPokemon).IsSameReferenceAs(opponent2);
|
||||
await Assert.That(secondWrapper.StatBoost.Attack).IsEqualTo((sbyte)0);
|
||||
await Assert.That(secondWrapper.OriginalSpecies).IsSameReferenceAs(own.Species);
|
||||
battle2.Dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user