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.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 and its . /// Gen VII Bulbapedia behavior: "On the turn that Fly is selected, the user will fly up high and become /// semi-invulnerable"; it attacks on the second turn. From Gen II onwards the user "can now be hit by Gust, /// Thunder, Twister, and Whirlwind during the semi-invulnerable turn of Fly, and will receive double damage /// from Gust and Twister"; from Gen V onwards it "can also be hit with Hurricane, Smack Down, and Thousand /// Arrows". /// public class FlyTests { private static (Fly script, IExecutingMove move, IBattlePokemon user, ScriptSet userVolatile, IMoveChoice moveChoice , EventHook eventHook) CreateTestSetup() { var script = new Fly(); var move = Substitute.For(); var user = Substitute.For(); // Use a real script set so the charge volatile added by Fly 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 eventHook = new EventHook(); var battle = Substitute.For(); battle.EventHook.Returns(eventHook); user.Battle.Returns(battle); return (script, move, user, userVolatile, moveChoice, eventHook); } /// /// Bulbapedia: "On the turn that Fly is selected, the user will fly up high". 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 will "become semi-invulnerable" on the charge turn. The charge turn attaches /// the volatile script to the user. /// [Test] public async Task PreventMove_FirstUse_AddsChargeFlyEffectToUser() { // Arrange var (script, move, _, userVolatile, _, _) = CreateTestSetup(); var prevent = false; // Act script.PreventMove(move, ref prevent); // Assert await Assert.That(userVolatile.Contains(ScriptUtils.ResolveName())).IsTrue(); } /// /// The charge turn is marked on the so the /// knows this choice was the charging turn and does not remove itself for it. /// [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("fly_charge"), true); await Assert.That(prevent).IsTrue(); } /// /// Bulbapedia: "the user will fly up high" — the charge turn announces itself through a /// so the battle log can show the fly-up message. /// [Test] public async Task PreventMove_FirstUse_FiresFlyChargeDialogEvent() { // Arrange var (script, move, user, _, _, eventHook) = CreateTestSetup(); DialogEvent? captured = null; eventHook.Handler += (_, args) => { if (args is DialogEvent dialogEvent) captured = dialogEvent; }; var prevent = false; // Act script.PreventMove(move, ref prevent); // Assert await Assert.That(captured).IsNotNull(); await Assert.That(captured!.Message).IsEqualTo("fly_charge"); var userParameter = captured!.Parameters?.GetValueOrDefault("user"); await Assert.That(ReferenceEquals(userParameter, user)).IsTrue(); } /// /// Bulbapedia: Fly attacks on the second 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 ChargeFlyEffect(user)); var prevent = false; // Act script.PreventMove(move, ref prevent); // Assert await Assert.That(prevent).IsFalse(); } /// /// Bulbapedia: the attack executes on the second turn, ending the semi-invulnerable state. Once the /// attack executes, the is removed from the user. /// [Test] public async Task OnBeforeMove_ChargeCompleted_RemovesChargeFlyEffect() { // Arrange var (script, move, user, userVolatile, _, _) = CreateTestSetup(); userVolatile.Add(new ChargeFlyEffect(user)); // Act script.OnBeforeMove(move); // Assert await Assert.That(userVolatile.Contains(ScriptUtils.ResolveName())).IsFalse(); } /// /// Bulbapedia: while flying up high the user is "semi-invulnerable". The /// blocks incoming hits from moves that cannot hit flying Pokémon. /// [Test] public async Task BlockIncomingHit_MoveCannotHitFlying_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(new StringKey("hit_flying")).Returns(false); incomingMove.UseMove.Returns(incomingMoveData); // Act var block = false; effect!.BlockIncomingHit(incomingMove, user, 0, ref block); // Assert await Assert.That(block).IsTrue(); } /// /// Bulbapedia: the user "can now be hit by Gust, Thunder, Twister, and Whirlwind during the /// semi-invulnerable turn of Fly" (and from Gen V onwards also "Hurricane, Smack Down, and Thousand /// Arrows"). Moves flagged as able to hit flying Pokémon are not blocked. /// [Test] public async Task BlockIncomingHit_MoveCanHitFlying_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(new StringKey("hit_flying")).Returns(true); incomingMove.UseMove.Returns(incomingMoveData); // Act var block = false; effect!.BlockIncomingHit(incomingMove, user, 0, ref block); // Assert await Assert.That(block).IsFalse(); } /// /// Bulbapedia: the user "will receive double damage from Gust and Twister" while in the /// semi-invulnerable turn of Fly. A move flagged as effective against flying Pokémon deals doubled /// damage to the flying user. /// [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(new StringKey("hit_flying")).Returns(true); incomingMoveData.HasFlag(new StringKey("effective_against_fly")).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: only Gust and Twister deal "double damage" to the flying user; other moves that can hit /// during the semi-invulnerable turn (such as Thunder, Hurricane, and Smack Down) deal regular damage. /// [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(new StringKey("hit_flying")).Returns(true); incomingMoveData.HasFlag(new StringKey("effective_against_fly")).Returns(false); incomingMove.UseMove.Returns(incomingMoveData); uint damage = 100; // Act effect!.ChangeIncomingMoveDamage(incomingMove, user, 0, ref damage); // Assert await Assert.That(damage).IsEqualTo(100u); } /// /// Bulbapedia: "If the user of Fly is hit by Smack Down or Thousand Arrows during its /// semi-invulnerable turn, it will be knocked down and Fly's execution will be cancelled" — more /// generally, when the user's executed choice was not the Fly charge (the move was disrupted or /// replaced), the semi-invulnerable state is removed. /// [Test] public async Task OnAfterMoveChoice_ChoiceIsNotFlyCharge_RemovesChargeFlyEffect() { // Arrange var user = Substitute.For(); var userVolatile = new ScriptSet(user); user.GetScripts().Returns(_ => new ScriptIterator(new List>())); var effect = new ChargeFlyEffect(user); userVolatile.Add(effect); var choice = Substitute.For(); choice.AdditionalData.Returns((Dictionary?)null); // Act effect.OnAfterMoveChoice(choice); // Assert await Assert.That(userVolatile.Contains(ScriptUtils.ResolveName())).IsFalse(); } /// /// The charging turn itself carries the "fly_charge" marker; the must /// survive that turn so the user stays airborne until the attack turn. /// [Test] public async Task OnAfterMoveChoice_ChoiceIsFlyCharge_KeepsChargeFlyEffect() { // Arrange var user = Substitute.For(); var userVolatile = new ScriptSet(user); user.GetScripts().Returns(_ => new ScriptIterator(new List>())); var effect = new ChargeFlyEffect(user); userVolatile.Add(effect); var choice = Substitute.For(); choice.AdditionalData.Returns(new Dictionary { { "fly_charge", true } }); // Act effect.OnAfterMoveChoice(choice); // Assert await Assert.That(userVolatile.Contains(ScriptUtils.ResolveName())).IsTrue(); } }