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

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

View File

@@ -0,0 +1,219 @@
using PkmnLib.Dynamic.Models;
using PkmnLib.Static;
using PkmnLib.Static.Species;
using PkmnLib.Tests.Integration;
namespace PkmnLib.Tests.Dynamic;
/// <summary>
/// Tests for the ephemeral <see cref="IBattlePokemon"/> wrapper. Battle-only state must live on the wrapper
/// and never leak into the underlying <see cref="IPokemon"/>; dropping the wrapper is all the cleanup a
/// battle needs.
/// </summary>
public class BattlePokemonTests
{
private static (IBattle battle, IBattlePokemon wrapper, IPokemon inner) CreateBattleWithWrapper(
string heldItem = "oran_berry")
{
var library = LibraryHelpers.LoadLibrary();
if (!library.StaticLibrary.Species.TryGet("bulbasaur", out var bulbasaur))
throw new InvalidOperationException("Failed to load bulbasaur species.");
if (!library.StaticLibrary.Species.TryGet("charmander", out var charmander))
throw new InvalidOperationException("Failed to load charmander species.");
if (!library.StaticLibrary.Items.TryGet(heldItem, out var item))
throw new InvalidOperationException($"Failed to load item {heldItem}.");
var pokemon = new PokemonImpl(library, bulbasaur, bulbasaur.GetDefaultForm(), new AbilityIndex
{
Index = 0,
IsHidden = false,
}, 50, 0, Gender.Male, 0, "hardy");
pokemon.LearnMove("tackle", MoveLearnMethod.LevelUp, 0);
_ = pokemon.ForceSetHeldItem(item);
var opponent = new PokemonImpl(library, charmander, charmander.GetDefaultForm(), new AbilityIndex
{
Index = 0,
IsHidden = false,
}, 50, 0, Gender.Male, 0, "hardy");
var party1 = new PokemonPartyImpl(6);
party1.SwapInto(pokemon, 0);
var party2 = new PokemonPartyImpl(6);
party2.SwapInto(opponent, 0);
var parties = new[]
{
new BattlePartyImpl(party1, [new ResponsibleIndex(0, 0)]),
new BattlePartyImpl(party2, [new ResponsibleIndex(1, 0)]),
};
var battle = new BattleImpl(library, parties, false, 2, 1, false, "grass", 0);
var wrapper = parties[0].GetBattlePokemon(pokemon) ??
throw new InvalidOperationException("Wrapper not created.");
return (battle, wrapper, pokemon);
}
[Test]
public async Task PersistentMembersAreProxiedToUnderlyingPokemon()
{
var (_, wrapper, inner) = CreateBattleWithWrapper();
await Assert.That(wrapper.UnderlyingPokemon).IsSameReferenceAs(inner);
await Assert.That(wrapper.Species).IsSameReferenceAs(inner.Species);
await Assert.That(wrapper.Form).IsSameReferenceAs(inner.Form);
await Assert.That(wrapper.Level).IsEqualTo(inner.Level);
await Assert.That(wrapper.CurrentHealth).IsEqualTo(inner.CurrentHealth);
await Assert.That(wrapper.Nature).IsSameReferenceAs(inner.Nature);
await Assert.That(wrapper.OriginalSpecies).IsSameReferenceAs(inner.Species);
await Assert.That(wrapper.OriginalForm).IsSameReferenceAs(inner.Form);
}
[Test]
public async Task TypeOverlayDoesNotTouchUnderlyingPokemon()
{
var (_, wrapper, inner) = CreateBattleWithWrapper();
var originalTypes = inner.Types.ToList();
wrapper.SetTypes([new TypeIdentifier(18, "water")]);
await Assert.That(wrapper.Types.Count).IsEqualTo(1);
await Assert.That(inner.Types.SequenceEqual(originalTypes)).IsTrue();
wrapper.OnSwitchedOut();
await Assert.That(wrapper.Types.SequenceEqual(inner.Form.Types)).IsTrue();
}
[Test]
public async Task TemporaryMoveOverlayDoesNotTouchUnderlyingPokemonAndResetsOnSwitchOut()
{
var (_, wrapper, inner) = CreateBattleWithWrapper();
wrapper.LearnTemporaryMove("swords_dance", MoveLearnMethod.Mimic, 0);
await Assert.That(wrapper.Moves[0]!.MoveData.Name.ToString()).IsEqualTo("swords_dance");
await Assert.That(wrapper.BaseMoves[0]!.MoveData.Name.ToString()).IsEqualTo("tackle");
await Assert.That(inner.Moves[0]!.MoveData.Name.ToString()).IsEqualTo("tackle");
wrapper.OnSwitchedOut();
await Assert.That(wrapper.Moves[0]!.MoveData.Name.ToString()).IsEqualTo("tackle");
}
[Test]
public async Task StatBoostLivesOnWrapperAndResetsOnSwitchOut()
{
var (_, wrapper, inner) = CreateBattleWithWrapper();
var innerFlatAttack = inner.FlatStats.Attack;
var changed = wrapper.ChangeStatBoost(Statistic.Attack, 2, true, false);
await Assert.That(changed).IsTrue();
await Assert.That(wrapper.StatBoost.Attack).IsEqualTo((sbyte)2);
await Assert.That(wrapper.BoostedStats.Attack > wrapper.FlatStats.Attack).IsTrue();
await Assert.That(inner.FlatStats.Attack).IsEqualTo(innerFlatAttack);
wrapper.OnSwitchedOut();
await Assert.That(wrapper.StatBoost.Attack).IsEqualTo((sbyte)0);
await Assert.That(wrapper.BoostedStats.Attack).IsEqualTo(wrapper.FlatStats.Attack);
}
[Test]
public async Task HeldItemOverlayDoesNotTouchUnderlyingPokemon()
{
var (_, wrapper, inner) = CreateBattleWithWrapper();
var innerItem = inner.HeldItem;
await Assert.That(innerItem).IsNotNull();
var removed = wrapper.RemoveHeldItemForBattle();
await Assert.That(removed).IsSameReferenceAs(innerItem);
await Assert.That(wrapper.HeldItem).IsNull();
await Assert.That(wrapper.HasItemBeenRemovedForBattle).IsTrue();
await Assert.That(inner.HeldItem).IsSameReferenceAs(innerItem);
wrapper.RestoreRemovedHeldItem();
await Assert.That(wrapper.HeldItem).IsSameReferenceAs(innerItem);
await Assert.That(wrapper.HasItemBeenRemovedForBattle).IsFalse();
}
[Test]
public async Task StealingHeldItemDoesNotTouchUnderlyingPokemon()
{
var (_, wrapper, inner) = CreateBattleWithWrapper();
var innerItem = inner.HeldItem;
var stolen = wrapper.TryStealHeldItem(out var item);
await Assert.That(stolen).IsTrue();
await Assert.That(item).IsSameReferenceAs(innerItem);
await Assert.That(wrapper.HeldItem).IsNull();
await Assert.That(inner.HeldItem).IsSameReferenceAs(innerItem);
}
[Test]
public async Task MarkAsCaughtOnlyAffectsWrapper()
{
var (_, wrapper, inner) = CreateBattleWithWrapper();
wrapper.MarkAsCaught();
await Assert.That(wrapper.IsCaught).IsTrue();
await Assert.That(wrapper.IsUsable).IsFalse();
await Assert.That(inner.IsUsable).IsTrue();
}
[Test]
public async Task AbilityOverrideAndSuppressionLiveOnWrapper()
{
var (_, wrapper, inner) = CreateBattleWithWrapper();
var naturalAbility = inner.Ability;
await Assert.That(naturalAbility).IsNotNull();
wrapper.SuppressAbility();
await Assert.That(wrapper.AbilitySuppressed).IsTrue();
await Assert.That(wrapper.ActiveAbility).IsNull();
await Assert.That(inner.Ability).IsSameReferenceAs(naturalAbility);
wrapper.OnSwitchedOut();
await Assert.That(wrapper.AbilitySuppressed).IsFalse();
await Assert.That(wrapper.ActiveAbility).IsSameReferenceAs(naturalAbility);
}
[Test]
public async Task GetOwnScriptsUsesUnderlyingStatusScript()
{
var (_, wrapper, inner) = CreateBattleWithWrapper();
var scripts = new List<IEnumerable<PkmnLib.Dynamic.ScriptHandling.ScriptContainer>>();
wrapper.GetOwnScripts(scripts);
await Assert.That(scripts.Count).IsEqualTo(4);
await Assert.That(scripts[0]).IsSameReferenceAs(wrapper.HeldItemTriggerScript);
await Assert.That(scripts[1]).IsSameReferenceAs(wrapper.AbilityScript);
await Assert.That(scripts[2]).IsSameReferenceAs(inner.StatusScript);
await Assert.That(scripts[3]).IsSameReferenceAs(wrapper.Volatile);
}
[Test]
public async Task GetBattlePokemonFindsWrapperByInnerAndByWrapper()
{
var (battle, wrapper, inner) = CreateBattleWithWrapper();
var party = battle.Parties[0];
await Assert.That(party.GetBattlePokemon(inner)).IsSameReferenceAs(wrapper);
await Assert.That(party.GetBattlePokemon(wrapper)).IsSameReferenceAs(wrapper);
await Assert.That(battle.Parties[1].GetBattlePokemon(inner)).IsNull();
}
[Test]
public async Task OnSwitchedInTracksTurnAndPosition()
{
var (_, wrapper, _) = CreateBattleWithWrapper();
await Assert.That(wrapper.IsOnBattlefield).IsFalse();
wrapper.OnSwitchedIn(0);
await Assert.That(wrapper.IsOnBattlefield).IsTrue();
await Assert.That(wrapper.Position).IsEqualTo((byte)0);
// Bulbasaur's ability (Overgrow) has no script in the Gen7 plugin, so the ability script stays empty.
await Assert.That(wrapper.AbilityScript.IsEmpty).IsTrue();
}
}

View File

@@ -8,8 +8,8 @@ public class ChoiceQueueTests
[Test]
public async Task ChoiceQueue_HighSpeedFirstWhenPriorityEqual()
{
var pokemon1 = Substitute.For<IPokemon>();
var pokemon2 = Substitute.For<IPokemon>();
var pokemon1 = Substitute.For<IBattlePokemon>();
var pokemon2 = Substitute.For<IBattlePokemon>();
var choice1 = Substitute.For<IMoveChoice>();
choice1.User.Returns(pokemon1);
@@ -28,8 +28,8 @@ public class ChoiceQueueTests
[Test]
public async Task ChoiceQueue_HighPriorityFirst()
{
var pokemon1 = Substitute.For<IPokemon>();
var pokemon2 = Substitute.For<IPokemon>();
var pokemon1 = Substitute.For<IBattlePokemon>();
var pokemon2 = Substitute.For<IBattlePokemon>();
var choice1 = Substitute.For<IMoveChoice>();
choice1.User.Returns(pokemon1);
@@ -48,10 +48,10 @@ public class ChoiceQueueTests
[Test]
public async Task ChoiceQueue_MovePokemonChoiceNext()
{
var pokemon1 = Substitute.For<IPokemon>();
var pokemon2 = Substitute.For<IPokemon>();
var pokemon3 = Substitute.For<IPokemon>();
var pokemon4 = Substitute.For<IPokemon>();
var pokemon1 = Substitute.For<IBattlePokemon>();
var pokemon2 = Substitute.For<IBattlePokemon>();
var pokemon3 = Substitute.For<IBattlePokemon>();
var pokemon4 = Substitute.For<IBattlePokemon>();
var choice1 = Substitute.For<IMoveChoice>();
choice1.User.Returns(pokemon1);
@@ -75,10 +75,10 @@ public class ChoiceQueueTests
[Test]
public async Task ChoiceQueue_MovePokemonChoiceNextFailsIfAlreadyExecuted()
{
var pokemon1 = Substitute.For<IPokemon>();
var pokemon2 = Substitute.For<IPokemon>();
var pokemon3 = Substitute.For<IPokemon>();
var pokemon4 = Substitute.For<IPokemon>();
var pokemon1 = Substitute.For<IBattlePokemon>();
var pokemon2 = Substitute.For<IBattlePokemon>();
var pokemon3 = Substitute.For<IBattlePokemon>();
var pokemon4 = Substitute.For<IBattlePokemon>();
var choice1 = Substitute.For<IMoveChoice>();
choice1.User.Returns(pokemon1);
@@ -103,10 +103,10 @@ public class ChoiceQueueTests
[Test]
public async Task ChoiceQueue_MovePokemonChoiceLast()
{
var pokemon1 = Substitute.For<IPokemon>();
var pokemon2 = Substitute.For<IPokemon>();
var pokemon3 = Substitute.For<IPokemon>();
var pokemon4 = Substitute.For<IPokemon>();
var pokemon1 = Substitute.For<IBattlePokemon>();
var pokemon2 = Substitute.For<IBattlePokemon>();
var pokemon3 = Substitute.For<IBattlePokemon>();
var pokemon4 = Substitute.For<IBattlePokemon>();
var choice1 = Substitute.For<IMoveChoice>();
choice1.User.Returns(pokemon1);
@@ -133,10 +133,10 @@ public class ChoiceQueueTests
[Test]
public async Task ChoiceQueue_MovePokemonChoiceLastFailsIfAlreadyExecuted()
{
var pokemon1 = Substitute.For<IPokemon>();
var pokemon2 = Substitute.For<IPokemon>();
var pokemon3 = Substitute.For<IPokemon>();
var pokemon4 = Substitute.For<IPokemon>();
var pokemon1 = Substitute.For<IBattlePokemon>();
var pokemon2 = Substitute.For<IBattlePokemon>();
var pokemon3 = Substitute.For<IBattlePokemon>();
var pokemon4 = Substitute.For<IBattlePokemon>();
var choice1 = Substitute.For<IMoveChoice>();
choice1.User.Returns(pokemon1);

View File

@@ -7,16 +7,31 @@ namespace PkmnLib.Tests.Dynamic;
public class PokemonStatBoostTests
{
private static IPokemon CreatePokemon()
private static IBattlePokemon CreatePokemon()
{
var library = LibraryHelpers.LoadLibrary();
if (!library.StaticLibrary.Species.TryGet("bulbasaur", out var species))
throw new InvalidOperationException("Failed to load bulbasaur species.");
return new PokemonImpl(library, species, species.GetDefaultForm(), new AbilityIndex
var pokemon = new PokemonImpl(library, species, species.GetDefaultForm(), new AbilityIndex
{
Index = 0,
IsHidden = false,
}, 50, 0, Gender.Male, 0, "hardy");
var party = new PokemonPartyImpl(1);
party.SwapInto(pokemon, 0);
var opponentParty = new PokemonPartyImpl(1);
opponentParty.SwapInto(new PokemonImpl(library, species, species.GetDefaultForm(), new AbilityIndex
{
Index = 0,
IsHidden = false,
}, 50, 0, Gender.Male, 0, "hardy"), 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, false, "grass", 0);
return battle.Parties[0].GetBattlePokemon(pokemon)!;
}
[Test]

View File

@@ -6,65 +6,78 @@ using PkmnLib.Tests.Integration;
namespace PkmnLib.Tests.Dynamic;
/// <summary>
/// Tests for the temporary move overlay (<see cref="IPokemon.LearnTemporaryMove"/>), used by effects such as
/// Mimic. The permanently learned moves must never be mutated by a temporary move, and the overlay must be
/// discarded by the engine itself when the Pokemon leaves the battlefield.
/// Tests for the temporary move overlay (<see cref="IBattlePokemon.LearnTemporaryMove"/>), used by effects
/// such as Mimic. The permanently learned moves must never be mutated by a temporary move, and the overlay
/// must be discarded by the engine itself when the Pokemon leaves the battlefield.
/// </summary>
public class PokemonTemporaryMoveTests
{
private static IPokemon CreatePokemon()
private static (IBattle battle, IBattlePokemon battlePokemon, IPokemon pokemon) CreateBattlePokemon()
{
var library = LibraryHelpers.LoadLibrary();
if (!library.StaticLibrary.Species.TryGet("bulbasaur", out var species))
throw new InvalidOperationException("Failed to load bulbasaur species.");
var pokemon = new PokemonImpl(library, species, species.GetDefaultForm(), new AbilityIndex
{
Index = 0,
IsHidden = false,
}, 50, 0, Gender.Male, 0, "hardy");
IPokemon CreateBulbasaur() =>
new PokemonImpl(library, species, species.GetDefaultForm(), new AbilityIndex
{
Index = 0,
IsHidden = false,
}, 50, 0, Gender.Male, 0, "hardy");
var pokemon = CreateBulbasaur();
pokemon.LearnMove("tackle", MoveLearnMethod.LevelUp, 0);
return pokemon;
var party = new PokemonPartyImpl(1);
party.SwapInto(pokemon, 0);
var opponentParty = new PokemonPartyImpl(1);
opponentParty.SwapInto(CreateBulbasaur(), 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, false, "grass", 0);
return (battle, battle.Parties[0].GetBattlePokemon(pokemon)!, pokemon);
}
[Test]
public async Task LearnTemporaryMove_ReplacesMoveInMovesButNotInBaseMoves()
{
var pokemon = CreatePokemon();
var (_, battlePokemon, pokemon) = CreateBattlePokemon();
pokemon.LearnTemporaryMove("swords_dance", MoveLearnMethod.Mimic, 0);
battlePokemon.LearnTemporaryMove("swords_dance", MoveLearnMethod.Mimic, 0);
await Assert.That(pokemon.Moves[0]!.MoveData.Name.ToString()).IsEqualTo("swords_dance");
await Assert.That(pokemon.Moves[0]!.LearnMethod).IsEqualTo(MoveLearnMethod.Mimic);
await Assert.That(pokemon.BaseMoves[0]!.MoveData.Name.ToString()).IsEqualTo("tackle");
await Assert.That(battlePokemon.Moves[0]!.MoveData.Name.ToString()).IsEqualTo("swords_dance");
await Assert.That(battlePokemon.Moves[0]!.LearnMethod).IsEqualTo(MoveLearnMethod.Mimic);
await Assert.That(battlePokemon.BaseMoves[0]!.MoveData.Name.ToString()).IsEqualTo("tackle");
await Assert.That(pokemon.Moves[0]!.MoveData.Name.ToString()).IsEqualTo("tackle");
}
[Test]
public async Task SetOnBattlefield_LeavingField_RestoresOriginalMoveWithItsPP()
public async Task OnSwitchedOut_RestoresOriginalMoveWithItsPP()
{
var pokemon = CreatePokemon();
pokemon.SetBattleData(Substitute.For<IBattle>(), 0);
pokemon.SetOnBattlefield(true);
var (_, battlePokemon, _) = CreateBattlePokemon();
battlePokemon.OnSwitchedIn(0);
// Use the original move once, so we can verify its PP survives the temporary replacement untouched.
var originalMove = pokemon.Moves[0]!;
var originalMove = battlePokemon.Moves[0]!;
originalMove.TryUse();
var expectedPp = originalMove.CurrentPp;
pokemon.LearnTemporaryMove("swords_dance", MoveLearnMethod.Mimic, 0);
pokemon.SetOnBattlefield(false);
battlePokemon.LearnTemporaryMove("swords_dance", MoveLearnMethod.Mimic, 0);
battlePokemon.OnSwitchedOut();
await Assert.That(ReferenceEquals(pokemon.Moves[0], originalMove)).IsTrue();
await Assert.That(pokemon.Moves[0]!.CurrentPp).IsEqualTo(expectedPp);
await Assert.That(ReferenceEquals(battlePokemon.Moves[0], originalMove)).IsTrue();
await Assert.That(battlePokemon.Moves[0]!.CurrentPp).IsEqualTo(expectedPp);
}
[Test]
public async Task ClearBattleData_RestoresOriginalMove()
public async Task BattleEnd_LeavesOriginalMovesUntouched()
{
var pokemon = CreatePokemon();
pokemon.SetBattleData(Substitute.For<IBattle>(), 0);
pokemon.LearnTemporaryMove("swords_dance", MoveLearnMethod.Mimic, 0);
var (battle, battlePokemon, pokemon) = CreateBattlePokemon();
battlePokemon.LearnTemporaryMove("swords_dance", MoveLearnMethod.Mimic, 0);
pokemon.ClearBattleData();
battle.Dispose();
await Assert.That(pokemon.Moves[0]!.MoveData.Name.ToString()).IsEqualTo("tackle");
}
@@ -72,10 +85,10 @@ public class PokemonTemporaryMoveTests
[Test]
public async Task Serialize_WithActiveTemporaryMove_WritesOriginalMove()
{
var pokemon = CreatePokemon();
pokemon.LearnTemporaryMove("swords_dance", MoveLearnMethod.Mimic, 0);
var (_, battlePokemon, _) = CreateBattlePokemon();
battlePokemon.LearnTemporaryMove("swords_dance", MoveLearnMethod.Mimic, 0);
var serialized = pokemon.Serialize();
var serialized = battlePokemon.Serialize();
await Assert.That(serialized.Moves[0]!.MoveName.ToString()).IsEqualTo("tackle");
}