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();
|
||||
}
|
||||
}
|
||||
219
PkmnLib.Tests/Dynamic/BattlePokemonTests.cs
Normal file
219
PkmnLib.Tests/Dynamic/BattlePokemonTests.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ public class SetPokemonAction : IntegrationTestAction
|
||||
|
||||
public override Task Execute(IBattle battle)
|
||||
{
|
||||
var mon = battle.Parties[FromParty[0]].Party[FromParty[1]];
|
||||
var mon = battle.Parties[FromParty[0]].BattlePokemon[FromParty[1]];
|
||||
battle.Sides[Place[0]].SwapPokemon(Place[1], mon);
|
||||
Console.WriteLine($"Set: {mon} to place {Place[0]}:{Place[1]}");
|
||||
return Task.CompletedTask;
|
||||
|
||||
@@ -110,11 +110,12 @@ public class DeepCloneTests
|
||||
new BattlePartyImpl(party2, [new ResponsibleIndex(1, 0)]),
|
||||
};
|
||||
using var battle = new BattleImpl(library, parties, false, 2, 3, false, "grass", 0);
|
||||
battle.Sides[0].SwapPokemon(0, party1[0]);
|
||||
battle.Sides[1].SwapPokemon(0, party2[0]);
|
||||
party1[0]!.ChangeStatBoost(Statistic.Defense, 2, true, false);
|
||||
await Assert.That(party1[0]!.StatBoost.Defense).IsEqualTo((sbyte)2);
|
||||
party1[0]!.Volatile.Add(new ChargeBounceEffect(party1[0]!));
|
||||
battle.Sides[0].SendOut(0, party1[0]!);
|
||||
battle.Sides[1].SendOut(0, party2[0]!);
|
||||
var battlePokemon1 = parties[0].GetBattlePokemon(party1[0]!)!;
|
||||
battlePokemon1.ChangeStatBoost(Statistic.Defense, 2, true, false);
|
||||
await Assert.That(battlePokemon1.StatBoost.Defense).IsEqualTo((sbyte)2);
|
||||
battlePokemon1.Volatile.Add(new ChargeBounceEffect(battlePokemon1));
|
||||
|
||||
var clone = battle.DeepClone();
|
||||
await Assert.That(clone).IsNotEqualTo(battle);
|
||||
@@ -129,11 +130,10 @@ public class DeepCloneTests
|
||||
var pokemon = clone.Sides[0].Pokemon[0]!;
|
||||
await Assert.That(pokemon).IsNotNull();
|
||||
await Assert.That(pokemon).IsNotEqualTo(battle.Sides[0].Pokemon[0]!);
|
||||
await Assert.That(pokemon.BattleData).IsNotNull();
|
||||
await Assert.That(pokemon.BattleData).IsNotEqualTo(battle.Sides[0].Pokemon[0]!.BattleData!);
|
||||
await Assert.That(pokemon.BattleData!.Battle).IsEqualTo((IBattle)clone);
|
||||
await Assert.That(pokemon.BattleData!.SeenOpponents).Contains(clone.Sides[1].Pokemon[0]!);
|
||||
await Assert.That(pokemon.BattleData!.SeenOpponents).DoesNotContain(battle.Sides[1].Pokemon[0]!);
|
||||
await Assert.That(pokemon.UnderlyingPokemon).IsNotEqualTo(battle.Sides[0].Pokemon[0]!.UnderlyingPokemon);
|
||||
await Assert.That(pokemon.Battle).IsEqualTo((IBattle)clone);
|
||||
await Assert.That(pokemon.SeenOpponents).Contains(clone.Sides[1].Pokemon[0]!);
|
||||
await Assert.That(pokemon.SeenOpponents).DoesNotContain(battle.Sides[1].Pokemon[0]!);
|
||||
await Assert.That(pokemon.StatBoost.Defense).IsEqualTo((sbyte)2);
|
||||
await Assert.That(pokemon.Volatile.Get<ChargeBounceEffect>()).IsNotNull();
|
||||
await Assert.That(pokemon.Volatile.Get<ChargeBounceEffect>()).IsNotEqualTo(
|
||||
@@ -142,7 +142,7 @@ public class DeepCloneTests
|
||||
var ownerGetter =
|
||||
typeof(ChargeBounceEffect).GetField("_owner", BindingFlags.NonPublic | BindingFlags.Instance)!;
|
||||
var owner = ownerGetter.GetValue(pokemon.Volatile.Get<ChargeBounceEffect>()!);
|
||||
await Assert.That((IPokemon)owner!).IsEqualTo(pokemon);
|
||||
await Assert.That((IBattlePokemon)owner!).IsEqualTo(pokemon);
|
||||
pokemon.Volatile.Remove<ChargeBounceEffect>();
|
||||
|
||||
await Assert.That(pokemon.Volatile.Get<ChargeBounceEffect>()).IsNull();
|
||||
|
||||
Reference in New Issue
Block a user