using PkmnLib.Dynamic.Libraries;
using PkmnLib.Dynamic.Models;
using PkmnLib.Static;
using PkmnLib.Static.Species;
using PkmnLib.Tests.Integration;
namespace PkmnLib.Tests.Dynamic;
///
/// Regression tests for the battle lifecycle of : battle-only state must die
/// with the battle, and the underlying must come out of a battle unchanged except
/// for the deliberately persistent parts (health, PP, non-volatile status, experience).
///
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;
}
///
/// 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
/// as the persistent Pokémon.
///
[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);
}
///
/// Items removed or stolen during a battle are a battle-only overlay: after the battle, the victim
/// still holds its item.
///
[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);
}
///
/// A battle-only form (such as a mega evolution) reverts to the original form when the battle ends.
///
[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);
}
///
/// A second battle with the same party starts with fresh battle state: no seen opponents or stale
/// original species from the previous battle.
///
[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();
}
}