using PkmnLib.Dynamic.Models; using PkmnLib.Dynamic.ScriptHandling; using PkmnLib.Dynamic.ScriptHandling.Registry; using PkmnLib.Plugin.Gen7.Common; using PkmnLib.Plugin.Gen7.Scripts.Moves; using PkmnLib.Plugin.Gen7.Scripts.Pokemon; using PkmnLib.Plugin.Gen7.Scripts.Side; using PkmnLib.Static; using PkmnLib.Static.Species; using PkmnLib.Static.Utils; namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves; /// /// Tests for the move script. /// Gen VII Bulbapedia behavior: "Hyperspace Fury inflicts damage, then lowers the user's Defense stat by one /// stage." "It bypasses accuracy checks to always hit, unless the target is in the semi-invulnerable turn of /// a move such as Dig or Fly." "It will also hit the target even if it is protected by Protect, Detect, /// Spiky Shield, King's Shield, Mat Block, or Baneful Bunker, and lifts the effects of those moves for the /// remainder of the turn. It also removes the effects of Quick Guard, Wide Guard, and Crafty Shield from the /// target's side of the field." "It hits through a substitute." "Hyperspace Fury can only be successfully /// used by Hoopa as Hoopa Unbound (or a Pokémon that has transformed into Hoopa Unbound)." /// public class HyperspaceFuryTests { /// /// Creates a fully mocked test setup where the target has a real as its volatile /// script set. /// private static (HyperspaceFury script, IExecutingMove move, IPokemon target, IPokemon user, IScriptSet volatileSet) CreateTestSetup() { var script = new HyperspaceFury(); var move = Substitute.For(); var target = Substitute.For(); target.GetScripts().Returns(_ => new ScriptIterator(Array.Empty>())); IScriptSet volatileSet = new ScriptSet(target); target.Volatile.Returns(volatileSet); var user = Substitute.For(); move.User.Returns(user); return (script, move, target, user, volatileSet); } /// /// Extends the test setup with a battle side that has a real as its volatile /// scripts, so side-wide protections such as can be attached. /// private static IScriptSet AttachSide(IPokemon target) { var side = Substitute.For(); side.GetScripts().Returns(_ => new ScriptIterator(Array.Empty>())); IScriptSet sideScripts = new ScriptSet(side); side.VolatileScripts.Returns(sideScripts); var battle = Substitute.For(); battle.Sides.Returns(new[] { side }); var battleData = Substitute.For(); battleData.Battle.Returns(battle); battleData.SideIndex.Returns((byte)0); battleData.BattleSide.Returns(side); target.BattleData.Returns(battleData); return sideScripts; } /// /// Bulbapedia: "It will also hit the target even if it is protected by Protect, Detect, Spiky Shield, /// King's Shield, Mat Block, or Baneful Bunker, and lifts the effects of those moves for the remainder /// of the turn." — the target's (Protect/Detect) is removed. /// [Test] public async Task OnSecondaryEffect_TargetUsedProtect_ProtectionEffectRemoved() { // Arrange var (script, move, target, _, volatileSet) = CreateTestSetup(); volatileSet.Add(new ProtectionEffectScript()); // Act script.OnSecondaryEffect(move, target, 0); // Assert await Assert.That(volatileSet.Contains(ScriptUtils.ResolveName())).IsFalse(); } /// /// Bulbapedia: "It will also hit the target even if it is protected by Protect, Detect, Spiky Shield, /// King's Shield, Mat Block, or Baneful Bunker, and lifts the effects of those moves for the remainder /// of the turn." — a target under Spiky Shield has its removed. /// [Test] public async Task OnSecondaryEffect_TargetUnderSpikyShield_ProtectionEffectRemoved() { // Arrange var (script, move, target, _, volatileSet) = CreateTestSetup(); volatileSet.Add(new SpikyShieldEffect()); // Act script.OnSecondaryEffect(move, target, 0); // Assert await Assert.That(volatileSet.Contains(ScriptUtils.ResolveName())).IsFalse(); } /// /// Bulbapedia: "It will also hit the target even if it is protected by Protect, Detect, Spiky Shield, /// King's Shield, Mat Block, or Baneful Bunker, and lifts the effects of those moves for the remainder /// of the turn." — a target under King's Shield has its effect removed. /// [Test] public async Task OnSecondaryEffect_TargetUnderKingsShield_ProtectionEffectRemoved() { // Arrange var (script, move, target, _, volatileSet) = CreateTestSetup(); volatileSet.Add(new KingsShieldEffect()); // Act script.OnSecondaryEffect(move, target, 0); // Assert await Assert.That(volatileSet.Contains(ScriptUtils.ResolveName())).IsFalse(); } /// /// Bulbapedia: "It will also hit the target even if it is protected by Protect, Detect, Spiky Shield, /// King's Shield, Mat Block, or Baneful Bunker, and lifts the effects of those moves for the remainder /// of the turn." — a target under Baneful Bunker has its removed. /// [Test] public async Task OnSecondaryEffect_TargetUnderBanefulBunker_ProtectionEffectRemoved() { // Arrange var (script, move, target, _, volatileSet) = CreateTestSetup(); volatileSet.Add(new BanefulBunkerEffect()); // Act script.OnSecondaryEffect(move, target, 0); // Assert await Assert.That(volatileSet.Contains(ScriptUtils.ResolveName())).IsFalse(); } /// /// With no protection effect on the target, the secondary effect simply does nothing and does not throw. /// [Test] public async Task OnSecondaryEffect_TargetNotProtected_DoesNothing() { // Arrange var (script, move, target, _, volatileSet) = CreateTestSetup(); // Act script.OnSecondaryEffect(move, target, 0); // Assert await Assert.That(volatileSet.Count).IsEqualTo(0); } /// /// Bulbapedia: "Hyperspace Fury inflicts damage, then lowers the user's Defense stat by one stage." /// The self-inflicted Defense drop is applied via . /// [Test] public async Task OnSecondaryEffect_DamageDealt_LowersUserDefenseByOneStage() { // Arrange var (script, move, target, user, _) = CreateTestSetup(); // Act script.OnSecondaryEffect(move, target, 0); // Assert - the user received a self-inflicted ChangeStatBoost(Defense, -1) call. var call = user.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "ChangeStatBoost"); await Assert.That(call).IsNotNull(); var arguments = call!.GetArguments(); await Assert.That((Statistic)arguments[0]!).IsEqualTo(Statistic.Defense); await Assert.That((sbyte)arguments[1]!).IsEqualTo((sbyte)-1); await Assert.That((bool)arguments[2]!).IsTrue(); } /// /// Gives the mocked user an explicit species and form name. /// private static void SetUserSpeciesForm(IPokemon user, string speciesName, string formName) { var species = Substitute.For(); species.Name.Returns(new StringKey(speciesName)); var form = Substitute.For(); form.Name.Returns(new StringKey(formName)); user.Species.Returns(species); user.Form.Returns(form); } /// /// Bulbapedia: "Hyperspace Fury can only be successfully used by Hoopa as Hoopa Unbound (or a Pokémon /// that has transformed into Hoopa Unbound)." — used by any other Pokémon, the move fails /// ("But <Pokémon> can't use the move!"). /// [Test] public async Task FailMove_UserNotHoopaUnbound_MoveFails() { // Arrange - the user is a different species entirely. var (script, move, _, user, _) = CreateTestSetup(); SetUserSpeciesForm(user, "garchomp", "default"); var fail = false; // Act ((IScriptFailMove)script).FailMove(move, ref fail); // Assert await Assert.That(fail).IsTrue(); } /// /// Bulbapedia: "Hyperspace Fury can only be successfully used by Hoopa as Hoopa Unbound" — used by /// Hoopa in its Confined (default) form, the move fails ("But Hoopa can't use it the way it is now!"). /// [Test] public async Task FailMove_UserHoopaConfined_MoveFails() { // Arrange - the user is Hoopa, but in its default (Confined) form. var (script, move, _, user, _) = CreateTestSetup(); SetUserSpeciesForm(user, "hoopa", "default"); var fail = false; // Act ((IScriptFailMove)script).FailMove(move, ref fail); // Assert await Assert.That(fail).IsTrue(); } /// /// Bulbapedia: "Hyperspace Fury can only be successfully used by Hoopa as Hoopa Unbound" — used by /// Hoopa Unbound itself, the move does not fail. /// [Test] public async Task FailMove_UserHoopaUnbound_MoveDoesNotFail() { // Arrange - the user is Hoopa in its Unbound form. var (script, move, _, user, _) = CreateTestSetup(); SetUserSpeciesForm(user, "hoopa", "unbound"); var fail = false; // Act ((IScriptFailMove)script).FailMove(move, ref fail); // Assert await Assert.That(fail).IsFalse(); } /// /// Bulbapedia: "It also removes the effects of Quick Guard, Wide Guard, and Crafty Shield from the /// target's side of the field." — the is removed from the target's side. /// [Test] public async Task OnSecondaryEffect_QuickGuardOnTargetSide_SideEffectRemoved() { // Arrange var (script, move, target, _, _) = CreateTestSetup(); var sideScripts = AttachSide(target); sideScripts.Add(new QuickGuardEffect()); // Act script.OnSecondaryEffect(move, target, 0); // Assert await Assert.That(sideScripts.Contains(ScriptUtils.ResolveName())).IsFalse(); } /// /// Bulbapedia: "It also removes the effects of Quick Guard, Wide Guard, and Crafty Shield from the /// target's side of the field." — the is removed from the target's side. /// [Test] public async Task OnSecondaryEffect_WideGuardOnTargetSide_SideEffectRemoved() { // Arrange var (script, move, target, _, _) = CreateTestSetup(); var sideScripts = AttachSide(target); sideScripts.Add(new WideGuardEffect()); // Act script.OnSecondaryEffect(move, target, 0); // Assert await Assert.That(sideScripts.Contains(ScriptUtils.ResolveName())).IsFalse(); } /// /// Bulbapedia: "It also removes the effects of Quick Guard, Wide Guard, and Crafty Shield from the /// target's side of the field." — the is removed from the target's side. /// [Test] public async Task OnSecondaryEffect_CraftyShieldOnTargetSide_SideEffectRemoved() { // Arrange var (script, move, target, _, _) = CreateTestSetup(); var sideScripts = AttachSide(target); sideScripts.Add(new CraftyShieldEffect()); // Act script.OnSecondaryEffect(move, target, 0); // Assert await Assert.That(sideScripts.Contains(ScriptUtils.ResolveName())).IsFalse(); } /// /// Bulbapedia: "It will also hit the target even if it is protected by Protect, Detect, Spiky Shield, /// King's Shield, Mat Block, or Baneful Bunker, and lifts the effects of those moves for the remainder /// of the turn." — Mat Block is a side-wide effect in this implementation /// (), and it is lifted as well. /// [Test] public async Task OnSecondaryEffect_MatBlockOnTargetSide_SideEffectRemoved() { // Arrange var (script, move, target, _, _) = CreateTestSetup(); var sideScripts = AttachSide(target); sideScripts.Add(new MatBlockEffect()); // Act script.OnSecondaryEffect(move, target, 0); // Assert await Assert.That(sideScripts.Contains(ScriptUtils.ResolveName())).IsFalse(); } /// /// Bulbapedia: "It bypasses accuracy checks to always hit, unless the target is in the semi-invulnerable /// turn of a move such as Dig or Fly." /// Integration check: the Gen7 data gives hyperspace_fury an accuracy of 255, which the engine treats as /// an unmodifiable always-hit. /// [Test] public async Task MoveData_HyperspaceFury_BypassesAccuracyChecks() { // Arrange var library = LibraryHelpers.LoadLibrary(); // Act var found = library.StaticLibrary.Moves.TryGet("hyperspace_fury", out var moveData); // Assert await Assert.That(found).IsTrue(); await Assert.That(moveData!.Accuracy).IsEqualTo((byte)255); } /// /// Bulbapedia: "It hits through a substitute." /// Integration check: the Gen7 data gives hyperspace_fury the /// flag. /// [Test] public async Task MoveData_HyperspaceFury_IgnoresSubstitute() { // Arrange var library = LibraryHelpers.LoadLibrary(); // Act var found = library.StaticLibrary.Moves.TryGet("hyperspace_fury", out var moveData); // Assert await Assert.That(found).IsTrue(); await Assert.That(moveData!.HasFlag(MoveFlags.IgnoreSubstitute)).IsTrue(); } /// /// Bulbapedia: "It will also hit the target even if it is protected by Protect, Detect, Spiky Shield, /// King's Shield, Mat Block, or Baneful Bunker". /// Integration check: blocks any move carrying the /// flag before can run, /// so hyperspace_fury's data must not carry that flag for the move to hit protected targets. /// [Test] public async Task MoveData_HyperspaceFury_NotBlockedByProtection() { // Arrange var library = LibraryHelpers.LoadLibrary(); // Act var found = library.StaticLibrary.Moves.TryGet("hyperspace_fury", out var moveData); // Assert await Assert.That(found).IsTrue(); await Assert.That(moveData!.HasFlag(MoveFlags.Protect)).IsFalse(); } }