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; /// /// Tests for the move script and the volatile /// script. Behavior is verified against the Bulbapedia page for King's Shield (Generation VII). /// public class KingsShieldTests { /// /// Creates a fully mocked setup for driving 's inherited /// . The target of the secondary effect is the /// Pokémon using King's Shield itself, as the move is self-targeted, so the returned user is /// passed as both the move's user and the secondary effect's target. /// 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(); var user = Substitute.For(); var hitData = Substitute.For(); // 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(); defaultForm.Name.Returns(new StringKey("default")); var species = Substitute.For(); species.Name.Returns(new StringKey(speciesName)); species.GetDefaultForm().Returns(defaultForm); user.Species.Returns(species); var currentForm = Substitute.For(); 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()]); var random = Substitute.For(); random.GetFloat().Returns(randomRoll); var battle = Substitute.For(); battle.ChoiceQueue.Returns(queue); battle.Random.Returns(random); var battleData = Substitute.For(); 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); } /// /// Creates a fully mocked setup for driving , the /// volatile script that King's Shield attaches to its user. /// 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(); var target = Substitute.For(); var hitData = Substitute.For(); hitData.IsContact.Returns(isContact); move.GetHitData(target, 0).Returns(hitData); var useMove = Substitute.For(); useMove.HasFlag(new StringKey("protect")).Returns(hasProtectFlag); useMove.Category.Returns(category); move.UseMove.Returns(useMove); var attacker = Substitute.For(); attacker.GetScripts().Returns(_ => new ScriptIterator(Array.Empty>())); move.User.Returns(attacker); target.BattleData.Returns(Substitute.For()); return (effect, move, target, attacker); } /// /// Helper to extract the statistic and stage change from a Pokémon's received /// calls. /// 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; } /// /// Helper that checks whether a Pokémon received a call. /// private static bool ReceivedChangeForm(IPokemon pokemon) => pokemon.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "ChangeForm"); /// /// 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 volatile script to the user, so that /// blocked contact moves lower the attacker's Attack. /// [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()).IsNotNull(); hitData.DidNotReceive().Fail(); } /// /// Bulbapedia: "If the user goes last in the turn, the move will fail." /// [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()).IsNull(); } /// /// 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 . /// [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(); await Assert.That(failure).IsNotNull(); await Assert.That(failure!.ProtectTurns).IsEqualTo(1); await Assert.That(failure.UsedProtect).IsTrue(); } /// /// 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. /// [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(); } /// /// 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." /// [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(); } /// /// 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. /// [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(); } /// /// 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. /// [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(); } /// /// 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. /// [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(); } /// /// 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 /// . /// [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(); } /// /// Bulbapedia: "Some moves will do damage through King's Shield." /// Moves that bypass protection (they lack the protect flag) are not blocked by the /// . /// [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(); } /// /// 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. /// [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(); } /// /// 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". /// [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); } /// /// 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. /// [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(); } /// /// 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. /// [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(); } }