251 lines
9.7 KiB
C#
251 lines
9.7 KiB
C#
using PkmnLib.Dynamic.Models;
|
|
using PkmnLib.Dynamic.ScriptHandling;
|
|
using PkmnLib.Dynamic.ScriptHandling.Registry;
|
|
using PkmnLib.Plugin.Gen7.Scripts.Moves;
|
|
using PkmnLib.Plugin.Gen7.Scripts.Side;
|
|
using PkmnLib.Static;
|
|
using PkmnLib.Static.Utils;
|
|
|
|
namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
|
|
|
|
/// <summary>
|
|
/// Tests for the <see cref="HealingWish"/> move script and its side effect script
|
|
/// <see cref="HealingWishEffect"/>.
|
|
/// Gen VII Bulbapedia behavior: "Healing Wish causes the user to faint, then switches in the new Pokémon.
|
|
/// The new Pokémon's HP is restored and it is cured of any status conditions upon being sent out.", where
|
|
/// from Generation V to VII "The receiving Pokémon is now sent out at the end of the turn rather than
|
|
/// immediately."
|
|
/// </summary>
|
|
public class HealingWishTests
|
|
{
|
|
/// <summary>
|
|
/// Creates a fully mocked test setup for the <see cref="HealingWish"/> move script, with the user at
|
|
/// side 0 and the given position, and the side's volatile scripts backed by a real
|
|
/// <see cref="ScriptSet"/>.
|
|
/// </summary>
|
|
private static (HealingWish script, IExecutingMove move, IBattlePokemon user, IScriptSet sideVolatile)
|
|
CreateMoveSetup(byte position = 1, bool hasUsablePartyMembers = true)
|
|
{
|
|
var script = new HealingWish();
|
|
var move = Substitute.For<IExecutingMove>();
|
|
|
|
var side = Substitute.For<IBattleSide>();
|
|
// ScriptSet.Add runs the IScriptPreventVolatileAdd hook over the owner's scripts; give the mock a
|
|
// real (empty) iterator so the hook pass runs.
|
|
side.GetScripts().Returns(_ => new ScriptIterator(Array.Empty<IEnumerable<ScriptContainer>>()));
|
|
IScriptSet sideVolatile = new ScriptSet(side);
|
|
side.VolatileScripts.Returns(sideVolatile);
|
|
|
|
var party = Substitute.For<IBattleParty>();
|
|
party.IsResponsibleForIndex(Arg.Any<ResponsibleIndex>()).Returns(true);
|
|
party.HasUsablePokemonNotInField().Returns(hasUsablePartyMembers);
|
|
|
|
var battle = Substitute.For<IBattle>();
|
|
battle.Sides.Returns(new[] { side });
|
|
battle.Parties.Returns(new[] { party });
|
|
|
|
var user = Substitute.For<IBattlePokemon>();
|
|
move.User.Returns(user);
|
|
user.Position.Returns(position);
|
|
user.SideIndex.Returns((byte)0);
|
|
user.Battle.Returns(battle);
|
|
|
|
return (script, move, user, sideVolatile);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a mocked replacement Pokémon with the given maximum HP for
|
|
/// <see cref="HealingWishEffect"/> tests.
|
|
/// </summary>
|
|
private static IBattlePokemon CreateReplacement(uint maxHp = 200)
|
|
{
|
|
var pokemon = Substitute.For<IBattlePokemon>();
|
|
pokemon.BoostedStats.Returns(new StatisticSet<uint>(maxHp, 0, 0, 0, 0, 0));
|
|
return pokemon;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper to extract the heal amount from a substitute's received Heal calls, or null when Heal was
|
|
/// never called.
|
|
/// </summary>
|
|
private static uint? GetHealAmount(IBattlePokemon pokemon)
|
|
{
|
|
var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Heal");
|
|
return call != null ? (uint)call.GetArguments()[0]! : null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "Healing Wish causes the user to faint".
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnSecondaryEffect_UserInBattle_UserFaints()
|
|
{
|
|
// Arrange
|
|
var (script, move, user, _) = CreateMoveSetup();
|
|
|
|
// Act
|
|
script.OnSecondaryEffect(move, Substitute.For<IBattlePokemon>(), 0);
|
|
|
|
// Assert
|
|
await Assert.That(user.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Faint")).IsTrue();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia (Generations V to VII): "The receiving Pokémon is now sent out at the end of the turn
|
|
/// rather than immediately." — using the move leaves a pending <see cref="HealingWishEffect"/> on the
|
|
/// user's side instead of healing anything directly.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnSecondaryEffect_UserInBattle_AddsHealingWishEffectToUserSide()
|
|
{
|
|
// Arrange
|
|
var (script, move, _, sideVolatile) = CreateMoveSetup();
|
|
|
|
// Act
|
|
script.OnSecondaryEffect(move, Substitute.For<IBattlePokemon>(), 0);
|
|
|
|
// Assert
|
|
await Assert.That(sideVolatile.Contains(ScriptUtils.ResolveName<HealingWishEffect>())).IsTrue();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: the wish benefits the Pokémon "sent out" in the user's place — the pending effect is
|
|
/// bound to the position the user fainted at, so a replacement entering that position is healed.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnSecondaryEffect_UserAtPosition_EffectTargetsUsersPosition()
|
|
{
|
|
// Arrange
|
|
var (script, move, _, sideVolatile) = CreateMoveSetup(1);
|
|
script.OnSecondaryEffect(move, Substitute.For<IBattlePokemon>(), 0);
|
|
var effect = (HealingWishEffect)sideVolatile.Get(new StringKey("healing_wish"))!.Script!;
|
|
var replacement = CreateReplacement();
|
|
|
|
// Act
|
|
effect.OnSwitchIn(replacement, 1);
|
|
|
|
// Assert
|
|
await Assert.That(GetHealAmount(replacement)).IsNotNull();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "Healing Wish will fail if its Trainer has no other conscious Pokémon in their party".
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnSecondaryEffect_NoOtherConsciousPartyMembers_MoveFails()
|
|
{
|
|
// Arrange
|
|
var (script, move, _, _) = CreateMoveSetup(hasUsablePartyMembers: false);
|
|
var target = Substitute.For<IBattlePokemon>();
|
|
var hitData = Substitute.For<IHitData>();
|
|
move.GetHitData(target, 0).Returns(hitData);
|
|
|
|
// Act
|
|
script.OnSecondaryEffect(move, target, 0);
|
|
|
|
// Assert
|
|
await Assert.That(hitData.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Fail")).IsTrue();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: when the move fails for lack of other conscious party members, the user does not faint
|
|
/// and no pending effect is left on the side.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnSecondaryEffect_NoOtherConsciousPartyMembers_UserDoesNotFaint()
|
|
{
|
|
// Arrange
|
|
var (script, move, user, sideVolatile) = CreateMoveSetup(hasUsablePartyMembers: false);
|
|
var target = Substitute.For<IBattlePokemon>();
|
|
move.GetHitData(target, 0).Returns(Substitute.For<IHitData>());
|
|
|
|
// Act
|
|
script.OnSecondaryEffect(move, target, 0);
|
|
|
|
// Assert
|
|
await Assert.That(user.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Faint")).IsFalse();
|
|
await Assert.That(sideVolatile.Contains(ScriptUtils.ResolveName<HealingWishEffect>())).IsFalse();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "The new Pokémon's HP is restored ... upon being sent out." — the replacement switching
|
|
/// into the wisher's position is healed by its full maximum HP.
|
|
/// </summary>
|
|
[Test, Arguments(200u), Arguments(1u), Arguments(999u)]
|
|
public async Task OnSwitchIn_MatchingPosition_HealsReplacementToFullHp(uint maxHp)
|
|
{
|
|
// Arrange
|
|
var effect = new HealingWishEffect(1);
|
|
var replacement = CreateReplacement(maxHp);
|
|
|
|
// Act
|
|
effect.OnSwitchIn(replacement, 1);
|
|
|
|
// Assert
|
|
await Assert.That(GetHealAmount(replacement)!.Value).IsEqualTo(maxHp);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "it is cured of any status conditions upon being sent out."
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnSwitchIn_MatchingPosition_CuresStatus()
|
|
{
|
|
// Arrange
|
|
var effect = new HealingWishEffect(1);
|
|
var replacement = CreateReplacement();
|
|
|
|
// Act
|
|
effect.OnSwitchIn(replacement, 1);
|
|
|
|
// Assert
|
|
await Assert.That(replacement.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "ClearStatus")).IsTrue();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: the wish applies to the Pokémon sent out in the user's place — a Pokémon switching in
|
|
/// at a different position on the side is not healed and does not consume the effect.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnSwitchIn_DifferentPosition_DoesNotHealAndEffectPersists()
|
|
{
|
|
// Arrange
|
|
var side = Substitute.For<IBattleSide>();
|
|
side.GetScripts().Returns(_ => new ScriptIterator(Array.Empty<IEnumerable<ScriptContainer>>()));
|
|
IScriptSet sideVolatile = new ScriptSet(side);
|
|
var effect = new HealingWishEffect(1);
|
|
sideVolatile.Add(effect);
|
|
var otherPokemon = CreateReplacement();
|
|
|
|
// Act
|
|
effect.OnSwitchIn(otherPokemon, 0);
|
|
|
|
// Assert
|
|
await Assert.That(GetHealAmount(otherPokemon)).IsNull();
|
|
await Assert.That(otherPokemon.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "ClearStatus")).IsFalse();
|
|
await Assert.That(sideVolatile.Contains(ScriptUtils.ResolveName<HealingWishEffect>())).IsTrue();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia (Generations V to VII): the wish is consumed by the Pokémon sent out at the position —
|
|
/// unlike Generation VIII onwards there is no check on the replacement's HP or status, so the effect
|
|
/// is removed from the side after the first switch-in even if the replacement was already at full
|
|
/// health.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnSwitchIn_MatchingPosition_EffectIsRemovedFromSide()
|
|
{
|
|
// Arrange
|
|
var side = Substitute.For<IBattleSide>();
|
|
side.GetScripts().Returns(_ => new ScriptIterator(Array.Empty<IEnumerable<ScriptContainer>>()));
|
|
IScriptSet sideVolatile = new ScriptSet(side);
|
|
var effect = new HealingWishEffect(1);
|
|
sideVolatile.Add(effect);
|
|
|
|
// Act
|
|
effect.OnSwitchIn(CreateReplacement(), 1);
|
|
|
|
// Assert
|
|
await Assert.That(sideVolatile.Contains(ScriptUtils.ResolveName<HealingWishEffect>())).IsFalse();
|
|
}
|
|
} |