using PkmnLib.Dynamic.Events; using PkmnLib.Dynamic.Models; using PkmnLib.Dynamic.Models.Choices; 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.Static.Moves; using PkmnLib.Static.Utils; namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves; /// /// Tests for the move script. /// Gen VII Bulbapedia behavior: "The user gains the Sky-High status on the turn this move is used, then /// attacks on the following turn, inflicting damage with a 30% chance of paralyzing the target. While in /// Sky-High status, the user is invulnerable to most moves, but can still be hit by Gust, Twister, Thunder, /// and Sky Uppercut", where (from Generation V onward) for Gust and Twister "the damage dealt will be doubled". /// public class BounceTests { private static (Bounce script, IExecutingMove move, IPokemon user, ScriptSet userVolatile, IMoveChoice moveChoice, IBattleRandom random) CreateTestSetup() { var script = new Bounce(); var move = Substitute.For(); var user = Substitute.For(); // Use a real script set so the charge volatile added by Bounce can be inspected afterwards. var userVolatile = new ScriptSet(user); user.Volatile.Returns(userVolatile); user.GetScripts().Returns(_ => new ScriptIterator(new List>())); move.User.Returns(user); var moveChoice = Substitute.For(); move.MoveChoice.Returns(moveChoice); var battle = Substitute.For(); var random = Substitute.For(); battle.Random.Returns(random); battle.EventHook.Returns(new EventHook()); var battleData = Substitute.For(); battleData.Battle.Returns(battle); user.BattleData.Returns(battleData); return (script, move, user, userVolatile, moveChoice, random); } /// /// Bulbapedia: "The user gains the Sky-High status on the turn this move is used, then attacks on the /// following turn". On the first turn the move is prevented from executing. /// [Test] public async Task PreventMove_FirstUse_PreventsMoveForChargeTurn() { // Arrange var (script, move, _, _, _, _) = CreateTestSetup(); var prevent = false; // Act script.PreventMove(move, ref prevent); // Assert await Assert.That(prevent).IsTrue(); } /// /// Bulbapedia: "The user gains the Sky-High status on the turn this move is used". /// The charge turn attaches the volatile script to the user. /// [Test] public async Task PreventMove_FirstUse_AddsChargeBounceEffectToUser() { // Arrange var (script, move, _, userVolatile, _, _) = CreateTestSetup(); var prevent = false; // Act script.PreventMove(move, ref prevent); // Assert await Assert.That(userVolatile.Contains(ScriptUtils.ResolveName())).IsTrue(); } /// /// Bulbapedia: "The user gains the Sky-High status on the turn this move is used". /// The charge turn is marked on the so the /// knows this choice was the charging turn. /// [Test] public async Task PreventMove_FirstUse_MarksMoveChoiceAsChargeTurn() { // Arrange var (script, move, _, _, moveChoice, _) = CreateTestSetup(); var prevent = false; // Act script.PreventMove(move, ref prevent); // Assert moveChoice.Received(1).SetAdditionalData(new StringKey("bounce_charge"), true); await Assert.That(prevent).IsTrue(); } /// /// Bulbapedia: "then attacks on the following turn". When the user already has the /// from the charge turn, the move is not prevented again. /// [Test] public async Task PreventMove_SecondTurn_DoesNotPreventMove() { // Arrange var (script, move, user, userVolatile, _, _) = CreateTestSetup(); userVolatile.Add(new ChargeBounceEffect(user)); var prevent = false; // Act script.PreventMove(move, ref prevent); // Assert await Assert.That(prevent).IsFalse(); } /// /// Bulbapedia: "then attacks on the following turn". Once the attack executes, the Sky-High state /// () is removed from the user. /// [Test] public async Task OnBeforeMove_ChargeCompleted_RemovesChargeBounceEffect() { // Arrange var (script, move, user, userVolatile, _, _) = CreateTestSetup(); userVolatile.Add(new ChargeBounceEffect(user)); // Act script.OnBeforeMove(move); // Assert await Assert.That(userVolatile.Contains(ScriptUtils.ResolveName())).IsFalse(); } /// /// Bulbapedia: "inflicting damage with a 30% chance of paralyzing the target." /// When the effect chance roll succeeds, the target is paralyzed. /// [Test] public async Task OnSecondaryEffect_EffectChanceSucceeds_ParalyzesTarget() { // Arrange var (script, move, user, _, _, random) = CreateTestSetup(); var target = Substitute.For(); random.EffectChance(30, move, target, 0).Returns(true); // Act script.OnSecondaryEffect(move, target, 0); // Assert target.Received(1).SetStatus(new StringKey("paralyzed"), user); await Assert.That(target.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "SetStatus")).IsTrue(); } /// /// Bulbapedia: "inflicting damage with a 30% chance of paralyzing the target." /// When the effect chance roll fails, the target is not paralyzed. /// [Test] public async Task OnSecondaryEffect_EffectChanceFails_DoesNotParalyzeTarget() { // Arrange var (script, move, _, _, _, random) = CreateTestSetup(); var target = Substitute.For(); random.EffectChance(30, move, target, 0).Returns(false); // Act script.OnSecondaryEffect(move, target, 0); // Assert await Assert.That(target.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "SetStatus")).IsFalse(); } /// /// Bulbapedia: "a 30% chance of paralyzing the target." /// The paralysis roll is made with exactly a 30% chance. /// [Test] public async Task OnSecondaryEffect_ParalysisRoll_UsesThirtyPercentChance() { // Arrange var (script, move, _, _, _, random) = CreateTestSetup(); var target = Substitute.For(); // Act script.OnSecondaryEffect(move, target, 0); // Assert random.Received(1).EffectChance(30, move, target, 0); await Assert.That(random.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "EffectChance")).IsTrue(); } /// /// Technical test: outside of battle (no battle data) the secondary effect does nothing and does not throw. /// [Test] public async Task OnSecondaryEffect_NoBattleData_DoesNotParalyzeTarget() { // Arrange var (script, move, user, _, _, _) = CreateTestSetup(); user.BattleData.Returns((IPokemonBattleData?)null); var target = Substitute.For(); // Act script.OnSecondaryEffect(move, target, 0); // Assert await Assert.That(target.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "SetStatus")).IsFalse(); } /// /// Bulbapedia: "While in Sky-High status, the user is invulnerable to most moves". /// The added by the charge turn blocks incoming hits from moves that /// cannot hit semi-invulnerable flying Pokémon. /// [Test] public async Task BlockIncomingHit_MoveCannotHitSkyHighUser_BlocksHit() { // Arrange var (script, move, user, userVolatile, _, _) = CreateTestSetup(); var prevent = false; script.PreventMove(move, ref prevent); await Assert.That(userVolatile.TryGet(out var effect)).IsTrue(); var incomingMove = Substitute.For(); var incomingMoveData = Substitute.For(); incomingMoveData.HasFlag(MoveFlags.HitFlying).Returns(false); incomingMove.UseMove.Returns(incomingMoveData); // Act var block = false; effect!.BlockIncomingHit(incomingMove, user, 0, ref block); // Assert await Assert.That(block).IsTrue(); } /// /// Bulbapedia: "but can still be hit by Gust, Twister, Thunder, and Sky Uppercut". /// Moves flagged as able to hit semi-invulnerable flying Pokémon are not blocked by the /// . /// [Test] public async Task BlockIncomingHit_MoveCanHitSkyHighUser_DoesNotBlockHit() { // Arrange var (script, move, user, userVolatile, _, _) = CreateTestSetup(); var prevent = false; script.PreventMove(move, ref prevent); await Assert.That(userVolatile.TryGet(out var effect)).IsTrue(); var incomingMove = Substitute.For(); var incomingMoveData = Substitute.For(); incomingMoveData.HasFlag(MoveFlags.HitFlying).Returns(true); incomingMove.UseMove.Returns(incomingMoveData); // Act var block = false; effect!.BlockIncomingHit(incomingMove, user, 0, ref block); // Assert await Assert.That(block).IsFalse(); } /// /// Bulbapedia (Generation V onward): "If the user is hit by Gust or Twister while the user has the /// Sky-High status, the damage dealt will be doubled". Gust and Twister carry the /// flag, so their damage against the Sky-High user doubles. /// [Test] public async Task ChangeIncomingMoveDamage_GustLikeMove_DamageIsDoubled() { // Arrange var (script, move, user, userVolatile, _, _) = CreateTestSetup(); var prevent = false; script.PreventMove(move, ref prevent); await Assert.That(userVolatile.TryGet(out var effect)).IsTrue(); var incomingMove = Substitute.For(); var incomingMoveData = Substitute.For(); incomingMoveData.HasFlag(MoveFlags.HitFlying).Returns(true); incomingMoveData.HasFlag(MoveFlags.EffectiveAgainstFly).Returns(true); incomingMove.UseMove.Returns(incomingMoveData); uint damage = 100; // Act effect!.ChangeIncomingMoveDamage(incomingMove, user, 0, ref damage); // Assert await Assert.That(damage).IsEqualTo(200u); } /// /// Bulbapedia (Generation V onward): only for Gust and Twister "the damage dealt will be doubled". /// Other moves that can hit the Sky-High user (e.g. Thunder, Sky Uppercut) deal regular damage; they do /// not carry the flag. /// [Test] public async Task ChangeIncomingMoveDamage_ThunderLikeMove_DamageIsUnchanged() { // Arrange var (script, move, user, userVolatile, _, _) = CreateTestSetup(); var prevent = false; script.PreventMove(move, ref prevent); await Assert.That(userVolatile.TryGet(out var effect)).IsTrue(); var incomingMove = Substitute.For(); var incomingMoveData = Substitute.For(); incomingMoveData.HasFlag(MoveFlags.HitFlying).Returns(true); incomingMoveData.HasFlag(MoveFlags.EffectiveAgainstFly).Returns(false); incomingMove.UseMove.Returns(incomingMoveData); uint damage = 100; // Act effect!.ChangeIncomingMoveDamage(incomingMove, user, 0, ref damage); // Assert await Assert.That(damage).IsEqualTo(100u); } }