385 lines
15 KiB
C#
385 lines
15 KiB
C#
using PkmnLib.Dynamic.Models;
|
|
using PkmnLib.Dynamic.Models.Choices;
|
|
using PkmnLib.Dynamic.ScriptHandling;
|
|
using PkmnLib.Plugin.Gen7.Scripts.Moves;
|
|
using PkmnLib.Plugin.Gen7.Scripts.Pokemon;
|
|
using PkmnLib.Static;
|
|
using PkmnLib.Static.Moves;
|
|
using PkmnLib.Static.Species;
|
|
using PkmnLib.Static.Utils;
|
|
|
|
namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
|
|
|
|
/// <summary>
|
|
/// Tests for the <see cref="KingsShield"/> move script and the <see cref="KingsShield"/> volatile
|
|
/// script. Behavior is verified against the Bulbapedia page for King's Shield (Generation VII).
|
|
/// </summary>
|
|
public class KingsShieldTests
|
|
{
|
|
/// <summary>
|
|
/// Creates a fully mocked setup for driving <see cref="KingsShield"/>'s inherited
|
|
/// <see cref="ProtectionScript.OnSecondaryEffect"/>. The <c>target</c> of the secondary effect is the
|
|
/// Pokémon using King's Shield itself, as the move is self-targeted, so the returned <c>user</c> is
|
|
/// passed as both the move's user and the secondary effect's target.
|
|
/// </summary>
|
|
private static (KingsShield script, IExecutingMove move, IPokemon user, IHitData hitData, IScriptSet volatileSet,
|
|
IForm defaultForm) CreateProtectSetup(bool userMovesLast = false, float randomRoll = 0.0f,
|
|
string speciesName = "mimikyu", string formName = "default")
|
|
{
|
|
var script = new KingsShield();
|
|
var move = Substitute.For<IExecutingMove>();
|
|
var user = Substitute.For<IPokemon>();
|
|
var hitData = Substitute.For<IHitData>();
|
|
// Mirror the real HitData behavior: once Fail() is called, HasFailed reports true.
|
|
hitData.When(h => h.Fail()).Do(_ => hitData.HasFailed.Returns(true));
|
|
move.GetHitData(user, 0).Returns(hitData);
|
|
move.User.Returns(user);
|
|
|
|
var defaultForm = Substitute.For<IForm>();
|
|
defaultForm.Name.Returns(new StringKey("default"));
|
|
var species = Substitute.For<ISpecies>();
|
|
species.Name.Returns(new StringKey(speciesName));
|
|
species.GetDefaultForm().Returns(defaultForm);
|
|
user.Species.Returns(species);
|
|
|
|
var currentForm = Substitute.For<IForm>();
|
|
currentForm.Name.Returns(new StringKey(formName));
|
|
user.Form.Returns(currentForm);
|
|
|
|
// A queue with a remaining choice means another Pokémon still has to move after the user.
|
|
var queue = userMovesLast ? new BattleChoiceQueue([]) : new BattleChoiceQueue([Substitute.For<IMoveChoice>()]);
|
|
|
|
var random = Substitute.For<IBattleRandom>();
|
|
random.GetFloat().Returns(randomRoll);
|
|
|
|
var battle = Substitute.For<IBattle>();
|
|
battle.ChoiceQueue.Returns(queue);
|
|
battle.Random.Returns(random);
|
|
|
|
var battleData = Substitute.For<IPokemonBattleData>();
|
|
battleData.Battle.Returns(battle);
|
|
user.BattleData.Returns(battleData);
|
|
|
|
// Give the mock a real script iterator (used by the volatile add hook) and a real volatile script set.
|
|
user.GetScripts().Returns(_ => new ScriptIterator([]));
|
|
IScriptSet volatileSet = new ScriptSet(user);
|
|
user.Volatile.Returns(volatileSet);
|
|
|
|
return (script, move, user, hitData, volatileSet, defaultForm);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a fully mocked setup for driving <see cref="KingsShieldEffect.BlockIncomingHit"/>, the
|
|
/// volatile script that King's Shield attaches to its user.
|
|
/// </summary>
|
|
private static (KingsShieldEffect effect, IExecutingMove move, IPokemon target, IPokemon attacker) CreateBlockSetup(
|
|
bool isContact, bool hasProtectFlag, MoveCategory category = MoveCategory.Physical)
|
|
{
|
|
var effect = new KingsShieldEffect();
|
|
var move = Substitute.For<IExecutingMove>();
|
|
var target = Substitute.For<IPokemon>();
|
|
var hitData = Substitute.For<IHitData>();
|
|
hitData.IsContact.Returns(isContact);
|
|
move.GetHitData(target, 0).Returns(hitData);
|
|
|
|
var useMove = Substitute.For<IMoveData>();
|
|
useMove.HasFlag(new StringKey("protect")).Returns(hasProtectFlag);
|
|
useMove.Category.Returns(category);
|
|
move.UseMove.Returns(useMove);
|
|
|
|
var attacker = Substitute.For<IPokemon>();
|
|
attacker.GetScripts().Returns(_ => new ScriptIterator(Array.Empty<IEnumerable<ScriptContainer>>()));
|
|
move.User.Returns(attacker);
|
|
|
|
target.BattleData.Returns(Substitute.For<IPokemonBattleData>());
|
|
|
|
return (effect, move, target, attacker);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper to extract the statistic and stage change from a Pokémon's received
|
|
/// <see cref="IPokemon.ChangeStatBoost"/> calls.
|
|
/// </summary>
|
|
private static (Statistic stat, sbyte change)? GetStatBoostCall(IPokemon pokemon)
|
|
{
|
|
var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "ChangeStatBoost");
|
|
return call != null ? ((Statistic)call.GetArguments()[0]!, (sbyte)call.GetArguments()[1]!) : null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper that checks whether a Pokémon received a <see cref="IPokemon.ChangeForm"/> call.
|
|
/// </summary>
|
|
private static bool ReceivedChangeForm(IPokemon pokemon) =>
|
|
pokemon.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "ChangeForm");
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "King's Shield protects the user from all effects of physical and special moves that target
|
|
/// it during the turn it is used."
|
|
/// Using the move must attach the <see cref="KingsShield"/> volatile script to the user, so that
|
|
/// blocked contact moves lower the attacker's Attack.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnSecondaryEffect_FirstUse_AddsKingsShieldEffect()
|
|
{
|
|
// Arrange
|
|
var (script, move, user, hitData, volatileSet, _) = CreateProtectSetup();
|
|
|
|
// Act
|
|
script.OnSecondaryEffect(move, user, 0);
|
|
|
|
// Assert
|
|
await Assert.That(volatileSet.Get<KingsShieldEffect>()).IsNotNull();
|
|
hitData.DidNotReceive().Fail();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "If the user goes last in the turn, the move will fail."
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnSecondaryEffect_UserMovesLast_Fails()
|
|
{
|
|
// Arrange
|
|
var (script, move, user, hitData, volatileSet, _) = CreateProtectSetup(true);
|
|
|
|
// Act
|
|
script.OnSecondaryEffect(move, user, 0);
|
|
|
|
// Assert
|
|
hitData.Received(1).Fail();
|
|
await Assert.That(volatileSet.Get<KingsShieldEffect>()).IsNull();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "The chance that King's Shield will succeed also drops each time the user successfully and
|
|
/// consecutively uses Endure, any protection move that only affects the user, Quick Guard, or Wide Guard."
|
|
/// A successful use registers a consecutive protection turn on the <see cref="ProtectionFailureScript"/>.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnSecondaryEffect_FirstUse_RegistersConsecutiveProtectionUse()
|
|
{
|
|
// Arrange
|
|
var (script, move, user, _, volatileSet, _) = CreateProtectSetup();
|
|
|
|
// Act
|
|
script.OnSecondaryEffect(move, user, 0);
|
|
|
|
// Assert
|
|
var failure = volatileSet.Get<ProtectionFailureScript>();
|
|
await Assert.That(failure).IsNotNull();
|
|
await Assert.That(failure!.ProtectTurns).IsEqualTo(1);
|
|
await Assert.That(failure.UsedProtect).IsTrue();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "Each time, the chance of success is divided by 3."
|
|
/// After one consecutive protection use the chance is 1/3, so a roll above 1/3 makes the move fail.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnSecondaryEffect_SecondConsecutiveUse_RollAboveOneThird_Fails()
|
|
{
|
|
// Arrange
|
|
var (script, move, user, hitData, volatileSet, _) = CreateProtectSetup(randomRoll: 0.5f);
|
|
volatileSet.Add(new ProtectionFailureScript { ProtectTurns = 1 });
|
|
|
|
// Act
|
|
script.OnSecondaryEffect(move, user, 0);
|
|
|
|
// Assert
|
|
hitData.Received(1).Fail();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "King's Shield also triggers Aegislash's form change. If Aegislash has Stance Change and
|
|
/// uses King's Shield while in Blade Forme, it will change to Shield Forme before using King's Shield."
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnSecondaryEffect_AegislashInBladeForme_ChangesToDefaultForm()
|
|
{
|
|
// Arrange
|
|
var (script, move, user, _, _, defaultForm) = CreateProtectSetup(speciesName: "aegislash", formName: "blade");
|
|
|
|
// Act
|
|
script.OnSecondaryEffect(move, user, 0);
|
|
|
|
// Assert
|
|
user.Received(1).ChangeForm(defaultForm);
|
|
await Assert.That(ReceivedChangeForm(user)).IsTrue();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "If Aegislash has Stance Change and uses King's Shield while in Blade Forme, it will change
|
|
/// to Shield Forme".
|
|
/// An Aegislash that is already in its default (Shield) Forme must not change form again.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnSecondaryEffect_AegislashInDefaultForm_DoesNotChangeForm()
|
|
{
|
|
// Arrange
|
|
var (script, move, user, _, _, _) = CreateProtectSetup(speciesName: "aegislash", formName: "default");
|
|
|
|
// Act
|
|
script.OnSecondaryEffect(move, user, 0);
|
|
|
|
// Assert
|
|
await Assert.That(ReceivedChangeForm(user)).IsFalse();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "King's Shield also triggers Aegislash's form change."
|
|
/// The form change is specific to Aegislash; any other species using the move must not change form.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnSecondaryEffect_NonAegislashUser_DoesNotChangeForm()
|
|
{
|
|
// Arrange
|
|
var (script, move, user, _, _, _) = CreateProtectSetup(speciesName: "mimikyu", formName: "blade");
|
|
|
|
// Act
|
|
script.OnSecondaryEffect(move, user, 0);
|
|
|
|
// Assert
|
|
await Assert.That(ReceivedChangeForm(user)).IsFalse();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia (Generation VI section): "Also in this generation only, Aegislash will change form if
|
|
/// King's Shield was selected but fails to execute. This may occur if it goes last or it is prevented by a
|
|
/// status condition."
|
|
/// In Generation VII the form change therefore must not happen when the move fails because the user goes
|
|
/// last in the turn.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task OnSecondaryEffect_MoveFailsBecauseUserMovesLast_AegislashDoesNotChangeForm()
|
|
{
|
|
// Arrange
|
|
var (script, move, user, hitData, _, _) = CreateProtectSetup(true, speciesName: "aegislash", formName: "blade");
|
|
|
|
// Act
|
|
script.OnSecondaryEffect(move, user, 0);
|
|
|
|
// Assert - the move failed, so no form change may happen
|
|
hitData.Received(1).Fail();
|
|
await Assert.That(ReceivedChangeForm(user)).IsFalse();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "King's Shield protects the user from all effects of physical and special moves that target
|
|
/// it during the turn it is used."
|
|
/// A physical move that can be protected against (it has the protect flag) is blocked by the
|
|
/// <see cref="KingsShield"/>.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task BlockIncomingHit_PhysicalMoveWithProtectFlag_BlocksHit()
|
|
{
|
|
// Arrange
|
|
var (effect, move, target, _) = CreateBlockSetup(false, true);
|
|
var block = false;
|
|
|
|
// Act
|
|
effect.BlockIncomingHit(move, target, 0, ref block);
|
|
|
|
// Assert
|
|
await Assert.That(block).IsTrue();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "Some moves will do damage through King's Shield."
|
|
/// Moves that bypass protection (they lack the protect flag) are not blocked by the
|
|
/// <see cref="KingsShield"/>.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task BlockIncomingHit_MoveWithoutProtectFlag_DoesNotBlock()
|
|
{
|
|
// Arrange
|
|
var (effect, move, target, _) = CreateBlockSetup(false, false);
|
|
var block = false;
|
|
|
|
// Act
|
|
effect.BlockIncomingHit(move, target, 0, ref block);
|
|
|
|
// Assert
|
|
await Assert.That(block).IsFalse();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "King's Shield protects the user from all effects of physical and special moves that target
|
|
/// it during the turn it is used."
|
|
/// Unlike Protect, King's Shield does not protect against status moves, so those must not be blocked.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task BlockIncomingHit_StatusMoveWithProtectFlag_DoesNotBlock()
|
|
{
|
|
// Arrange
|
|
var (effect, move, target, _) = CreateBlockSetup(false, true, MoveCategory.Status);
|
|
var block = false;
|
|
|
|
// Act
|
|
effect.BlockIncomingHit(move, target, 0, ref block);
|
|
|
|
// Assert
|
|
await Assert.That(block).IsFalse();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "If King's Shield blocks an attack that would make contact with the user, the attacker's
|
|
/// Attack stat is lowered—in Generation VI and VII, it is lowered by 2 stages".
|
|
/// </summary>
|
|
[Test]
|
|
public async Task BlockIncomingHit_BlockedContactMove_LowersAttackerAttackByTwoStages()
|
|
{
|
|
// Arrange
|
|
var (effect, move, target, attacker) = CreateBlockSetup(true, true);
|
|
var block = false;
|
|
|
|
// Act
|
|
effect.BlockIncomingHit(move, target, 0, ref block);
|
|
|
|
// Assert
|
|
await Assert.That(block).IsTrue();
|
|
var statBoost = GetStatBoostCall(attacker);
|
|
await Assert.That(statBoost).IsNotNull();
|
|
await Assert.That(statBoost!.Value.stat).IsEqualTo(Statistic.Attack);
|
|
await Assert.That(statBoost.Value.change).IsEqualTo((sbyte)-2);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "If King's Shield blocks an attack that would make contact with the user, the attacker's
|
|
/// Attack stat is lowered".
|
|
/// A blocked attack that does not make contact must not change the attacker's stats.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task BlockIncomingHit_BlockedNonContactMove_DoesNotChangeAttackerStats()
|
|
{
|
|
// Arrange
|
|
var (effect, move, target, attacker) = CreateBlockSetup(false, true);
|
|
var block = false;
|
|
|
|
// Act
|
|
effect.BlockIncomingHit(move, target, 0, ref block);
|
|
|
|
// Assert
|
|
await Assert.That(block).IsTrue();
|
|
await Assert.That(GetStatBoostCall(attacker)).IsNull();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulbapedia: "If King's Shield blocks an attack that would make contact with the user, the attacker's
|
|
/// Attack stat is lowered".
|
|
/// A contact move that bypasses the protection (it lacks the protect flag, e.g. Shadow Force) is not
|
|
/// blocked, so the attacker's stats must not be changed.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task BlockIncomingHit_ContactMoveThatBypassesProtection_DoesNotChangeAttackerStats()
|
|
{
|
|
// Arrange
|
|
var (effect, move, target, attacker) = CreateBlockSetup(true, false);
|
|
var block = false;
|
|
|
|
// Act
|
|
effect.BlockIncomingHit(move, target, 0, ref block);
|
|
|
|
// Assert - the hit was not blocked, so no stat drop is applied
|
|
await Assert.That(block).IsFalse();
|
|
await Assert.That(GetStatBoostCall(attacker)).IsNull();
|
|
}
|
|
} |