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(); } }