diff --git a/src/battle-scene.ts b/src/battle-scene.ts
index 7f17a666280..cc6934f20d1 100644
--- a/src/battle-scene.ts
+++ b/src/battle-scene.ts
@@ -1,57 +1,57 @@
 import Phaser from "phaser";
-import UI from "./ui/ui";
-import Pokemon, { EnemyPokemon, PlayerPokemon } from "./field/pokemon";
-import PokemonSpecies, { allSpecies, getPokemonSpecies, PokemonSpeciesFilter } from "./data/pokemon-species";
+import UI from "#app/ui/ui";
+import Pokemon, { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon";
+import PokemonSpecies, { allSpecies, getPokemonSpecies, PokemonSpeciesFilter } from "#app/data/pokemon-species";
 import { Constructor, isNullOrUndefined, randSeedInt } from "#app/utils";
-import * as Utils from "./utils";
+import * as Utils from "#app/utils";
 import { ConsumableModifier, ConsumablePokemonModifier, DoubleBattleChanceBoosterModifier, ExpBalanceModifier, ExpShareModifier, FusePokemonModifier, HealingBoosterModifier, Modifier, ModifierBar, ModifierPredicate, MultipleParticipantExpBonusModifier, overrideHeldItems, overrideModifiers, PersistentModifier, PokemonExpBoosterModifier, PokemonFormChangeItemModifier, PokemonHeldItemModifier, PokemonHpRestoreModifier, PokemonIncrementingStatModifier, TerastallizeModifier, TurnHeldItemTransferModifier } from "./modifier/modifier";
-import { PokeballType } from "./data/pokeball";
-import { initCommonAnims, initMoveAnim, loadCommonAnimAssets, loadMoveAnimAssets, populateAnims } from "./data/battle-anims";
-import { Phase } from "./phase";
-import { initGameSpeed } from "./system/game-speed";
-import { Arena, ArenaBase } from "./field/arena";
-import { GameData } from "./system/game-data";
-import { addTextObject, getTextColor, TextStyle } from "./ui/text";
-import { allMoves } from "./data/move";
-import { getDefaultModifierTypeForTier, getEnemyModifierTypesForWave, getLuckString, getLuckTextTint, getModifierPoolForType, getModifierType, getPartyLuckValue, ModifierPoolType, modifierTypes, PokemonHeldItemModifierType } from "./modifier/modifier-type";
-import AbilityBar from "./ui/ability-bar";
-import { allAbilities, applyAbAttrs, applyPostBattleInitAbAttrs, BlockItemTheftAbAttr, ChangeMovePriorityAbAttr, DoubleBattleChanceAbAttr, PostBattleInitAbAttr } from "./data/ability";
-import Battle, { BattleType, FixedBattleConfig } from "./battle";
-import { GameMode, GameModes, getGameMode } from "./game-mode";
-import FieldSpritePipeline from "./pipelines/field-sprite";
-import SpritePipeline from "./pipelines/sprite";
-import PartyExpBar from "./ui/party-exp-bar";
-import { trainerConfigs, TrainerSlot } from "./data/trainer-config";
-import Trainer, { TrainerVariant } from "./field/trainer";
-import TrainerData from "./system/trainer-data";
+import { PokeballType } from "#app/data/pokeball";
+import { initCommonAnims, initMoveAnim, loadCommonAnimAssets, loadMoveAnimAssets, populateAnims } from "#app/data/battle-anims";
+import { Phase } from "#app/phase";
+import { initGameSpeed } from "#app/system/game-speed";
+import { Arena, ArenaBase } from "#app/field/arena";
+import { GameData } from "#app/system/game-data";
+import { addTextObject, getTextColor, TextStyle } from "#app/ui/text";
+import { allMoves } from "#app/data/move";
+import { getDefaultModifierTypeForTier, getEnemyModifierTypesForWave, getLuckString, getLuckTextTint, getModifierPoolForType, getModifierType, getPartyLuckValue, ModifierPoolType, modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type";
+import AbilityBar from "#app/ui/ability-bar";
+import { allAbilities, applyAbAttrs, applyPostBattleInitAbAttrs, BlockItemTheftAbAttr, DoubleBattleChanceAbAttr, PostBattleInitAbAttr } from "#app/data/ability";
+import Battle, { BattleType, FixedBattleConfig } from "#app/battle";
+import { GameMode, GameModes, getGameMode } from "#app/game-mode";
+import FieldSpritePipeline from "#app/pipelines/field-sprite";
+import SpritePipeline from "#app/pipelines/sprite";
+import PartyExpBar from "#app/ui/party-exp-bar";
+import { trainerConfigs, TrainerSlot } from "#app/data/trainer-config";
+import Trainer, { TrainerVariant } from "#app/field/trainer";
+import TrainerData from "#app/system/trainer-data";
 import SoundFade from "phaser3-rex-plugins/plugins/soundfade";
-import { pokemonPrevolutions } from "./data/balance/pokemon-evolutions";
-import PokeballTray from "./ui/pokeball-tray";
-import InvertPostFX from "./pipelines/invert";
-import { Achv, achvs, ModifierAchv, MoneyAchv } from "./system/achv";
-import { Voucher, vouchers } from "./system/voucher";
-import { Gender } from "./data/gender";
+import { pokemonPrevolutions } from "#app/data/balance/pokemon-evolutions";
+import PokeballTray from "#app/ui/pokeball-tray";
+import InvertPostFX from "#app/pipelines/invert";
+import { Achv, achvs, ModifierAchv, MoneyAchv } from "#app/system/achv";
+import { Voucher, vouchers } from "#app/system/voucher";
+import { Gender } from "#app/data/gender";
 import UIPlugin from "phaser3-rex-plugins/templates/ui/ui-plugin";
-import { addUiThemeOverrides } from "./ui/ui-theme";
-import PokemonData from "./system/pokemon-data";
-import { Nature } from "./data/nature";
-import { FormChangeItem, pokemonFormChanges, SpeciesFormChange, SpeciesFormChangeManualTrigger, SpeciesFormChangeTimeOfDayTrigger, SpeciesFormChangeTrigger } from "./data/pokemon-forms";
-import { FormChangePhase } from "./phases/form-change-phase";
-import { getTypeRgb } from "./data/type";
-import PokemonSpriteSparkleHandler from "./field/pokemon-sprite-sparkle-handler";
-import CharSprite from "./ui/char-sprite";
-import DamageNumberHandler from "./field/damage-number-handler";
-import PokemonInfoContainer from "./ui/pokemon-info-container";
-import { biomeDepths, getBiomeName } from "./data/balance/biomes";
-import { SceneBase } from "./scene-base";
-import CandyBar from "./ui/candy-bar";
-import { Variant, variantData } from "./data/variant";
+import { addUiThemeOverrides } from "#app/ui/ui-theme";
+import PokemonData from "#app/system/pokemon-data";
+import { Nature } from "#app/data/nature";
+import { FormChangeItem, pokemonFormChanges, SpeciesFormChange, SpeciesFormChangeManualTrigger, SpeciesFormChangeTimeOfDayTrigger, SpeciesFormChangeTrigger } from "#app/data/pokemon-forms";
+import { FormChangePhase } from "#app/phases/form-change-phase";
+import { getTypeRgb } from "#app/data/type";
+import PokemonSpriteSparkleHandler from "#app/field/pokemon-sprite-sparkle-handler";
+import CharSprite from "#app/ui/char-sprite";
+import DamageNumberHandler from "#app/field/damage-number-handler";
+import PokemonInfoContainer from "#app/ui/pokemon-info-container";
+import { biomeDepths, getBiomeName } from "#app/data/balance/biomes";
+import { SceneBase } from "#app/scene-base";
+import CandyBar from "#app/ui/candy-bar";
+import { Variant, variantData } from "#app/data/variant";
 import { Localizable } from "#app/interfaces/locales";
 import Overrides from "#app/overrides";
-import { InputsController } from "./inputs-controller";
-import { UiInputs } from "./ui-inputs";
-import { NewArenaEvent } from "./events/battle-scene";
-import { ArenaFlyout } from "./ui/arena-flyout";
+import { InputsController } from "#app/inputs-controller";
+import { UiInputs } from "#app/ui-inputs";
+import { NewArenaEvent } from "#app/events/battle-scene";
+import { ArenaFlyout } from "#app/ui/arena-flyout";
 import { EaseType } from "#enums/ease-type";
 import { BattleSpec } from "#enums/battle-spec";
 import { BattleStyle } from "#enums/battle-style";
@@ -66,27 +66,27 @@ import { TimedEventManager } from "#app/timed-event-manager";
 import { PokemonAnimType } from "#enums/pokemon-anim-type";
 import i18next from "i18next";
 import { TrainerType } from "#enums/trainer-type";
-import { battleSpecDialogue } from "./data/dialogue";
-import { LoadingScene } from "./loading-scene";
-import { LevelCapPhase } from "./phases/level-cap-phase";
-import { LoginPhase } from "./phases/login-phase";
-import { MessagePhase } from "./phases/message-phase";
-import { MovePhase } from "./phases/move-phase";
-import { NewBiomeEncounterPhase } from "./phases/new-biome-encounter-phase";
-import { NextEncounterPhase } from "./phases/next-encounter-phase";
-import { PokemonAnimPhase } from "./phases/pokemon-anim-phase";
-import { QuietFormChangePhase } from "./phases/quiet-form-change-phase";
-import { ReturnPhase } from "./phases/return-phase";
-import { SelectBiomePhase } from "./phases/select-biome-phase";
-import { ShowTrainerPhase } from "./phases/show-trainer-phase";
-import { SummonPhase } from "./phases/summon-phase";
-import { SwitchPhase } from "./phases/switch-phase";
-import { TitlePhase } from "./phases/title-phase";
-import { ToggleDoublePositionPhase } from "./phases/toggle-double-position-phase";
-import { TurnInitPhase } from "./phases/turn-init-phase";
-import { ShopCursorTarget } from "./enums/shop-cursor-target";
-import MysteryEncounter from "./data/mystery-encounters/mystery-encounter";
-import { allMysteryEncounters, ANTI_VARIANCE_WEIGHT_MODIFIER, AVERAGE_ENCOUNTERS_PER_RUN_TARGET, BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT, MYSTERY_ENCOUNTER_SPAWN_MAX_WEIGHT, mysteryEncountersByBiome, WEIGHT_INCREMENT_ON_SPAWN_MISS } from "./data/mystery-encounters/mystery-encounters";
+import { battleSpecDialogue } from "#app/data/dialogue";
+import { LoadingScene } from "#app/loading-scene";
+import { LevelCapPhase } from "#app/phases/level-cap-phase";
+import { LoginPhase } from "#app/phases/login-phase";
+import { MessagePhase } from "#app/phases/message-phase";
+import { MovePhase } from "#app/phases/move-phase";
+import { NewBiomeEncounterPhase } from "#app/phases/new-biome-encounter-phase";
+import { NextEncounterPhase } from "#app/phases/next-encounter-phase";
+import { PokemonAnimPhase } from "#app/phases/pokemon-anim-phase";
+import { QuietFormChangePhase } from "#app/phases/quiet-form-change-phase";
+import { ReturnPhase } from "#app/phases/return-phase";
+import { SelectBiomePhase } from "#app/phases/select-biome-phase";
+import { ShowTrainerPhase } from "#app/phases/show-trainer-phase";
+import { SummonPhase } from "#app/phases/summon-phase";
+import { SwitchPhase } from "#app/phases/switch-phase";
+import { TitlePhase } from "#app/phases/title-phase";
+import { ToggleDoublePositionPhase } from "#app/phases/toggle-double-position-phase";
+import { TurnInitPhase } from "#app/phases/turn-init-phase";
+import { ShopCursorTarget } from "#app/enums/shop-cursor-target";
+import MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter";
+import { allMysteryEncounters, ANTI_VARIANCE_WEIGHT_MODIFIER, AVERAGE_ENCOUNTERS_PER_RUN_TARGET, BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT, MYSTERY_ENCOUNTER_SPAWN_MAX_WEIGHT, mysteryEncountersByBiome, WEIGHT_INCREMENT_ON_SPAWN_MISS } from "#app/data/mystery-encounters/mystery-encounters";
 import { MysteryEncounterSaveData } from "#app/data/mystery-encounters/mystery-encounter-save-data";
 import { MysteryEncounterType } from "#enums/mystery-encounter-type";
 import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
@@ -94,7 +94,7 @@ import HeldModifierConfig from "#app/interfaces/held-modifier-config";
 import { ExpPhase } from "#app/phases/exp-phase";
 import { ShowPartyExpBarPhase } from "#app/phases/show-party-exp-bar-phase";
 import { MysteryEncounterMode } from "#enums/mystery-encounter-mode";
-import { ExpGainsSpeed } from "./enums/exp-gains-speed";
+import { ExpGainsSpeed } from "#enums/exp-gains-speed";
 
 export const bypassLogin = import.meta.env.VITE_BYPASS_LOGIN === "1";
 
@@ -2359,17 +2359,6 @@ export default class BattleScene extends SceneBase {
     return false;
   }
 
-  pushMovePhase(movePhase: MovePhase, priorityOverride?: integer): void {
-    const movePriority = new Utils.IntegerHolder(priorityOverride !== undefined ? priorityOverride : movePhase.move.getMove().priority);
-    applyAbAttrs(ChangeMovePriorityAbAttr, movePhase.pokemon, null, false, movePhase.move.getMove(), movePriority);
-    const lowerPriorityPhase = this.phaseQueue.find(p => p instanceof MovePhase && p.move.getMove().priority < movePriority.value);
-    if (lowerPriorityPhase) {
-      this.phaseQueue.splice(this.phaseQueue.indexOf(lowerPriorityPhase), 0, movePhase);
-    } else {
-      this.pushPhase(movePhase);
-    }
-  }
-
   /**
    * Tries to add the input phase to index before target phase in the phaseQueue, else simply calls unshiftPhase()
    * @param phase {@linkcode Phase} the phase to be added
diff --git a/src/field/arena.ts b/src/field/arena.ts
index 9d5f1eb0a4e..1e164903e9d 100644
--- a/src/field/arena.ts
+++ b/src/field/arena.ts
@@ -392,16 +392,16 @@ export class Arena {
     return true;
   }
 
-  isMoveWeatherCancelled(user: Pokemon, move: Move) {
-    return this.weather && !this.weather.isEffectSuppressed(this.scene) && this.weather.isMoveWeatherCancelled(user, move);
+  public isMoveWeatherCancelled(user: Pokemon, move: Move): boolean {
+    return !!this.weather && !this.weather.isEffectSuppressed(this.scene) && this.weather.isMoveWeatherCancelled(user, move);
   }
 
-  isMoveTerrainCancelled(user: Pokemon, targets: BattlerIndex[], move: Move) {
-    return this.terrain && this.terrain.isMoveTerrainCancelled(user, targets, move);
+  public isMoveTerrainCancelled(user: Pokemon, targets: BattlerIndex[], move: Move): boolean {
+    return !!this.terrain && this.terrain.isMoveTerrainCancelled(user, targets, move);
   }
 
-  getTerrainType() : TerrainType {
-    return this.terrain?.terrainType || TerrainType.NONE;
+  public getTerrainType(): TerrainType {
+    return this.terrain?.terrainType ?? TerrainType.NONE;
   }
 
   getAttackTypeMultiplier(attackType: Type, grounded: boolean): number {
diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts
index fed91d05fd5..05567491a1a 100644
--- a/src/field/pokemon.ts
+++ b/src/field/pokemon.ts
@@ -5027,8 +5027,12 @@ export class PokemonBattleSummonData {
 export class PokemonTurnData {
   public flinched: boolean = false;
   public acted: boolean = false;
-  public hitCount: number;
-  public hitsLeft: number;
+  public hitCount: number = 0;
+  /**
+   * - `-1` = Calculate how many hits are left
+   * - `0` = Move is finished
+   */
+  public hitsLeft: number = -1;
   public damageDealt: number = 0;
   public currDamageDealt: number = 0;
   public damageTaken: number = 0;
@@ -5114,7 +5118,7 @@ export class PokemonMove {
    * @param {boolean} ignoreRestrictionTags If `true`, skips the check for move restriction tags (see {@link MoveRestrictionBattlerTag})
    * @returns `true` if the move can be selected and used by the Pokemon, otherwise `false`.
    */
-  isUsable(pokemon: Pokemon, ignorePp?: boolean, ignoreRestrictionTags?: boolean): boolean {
+  isUsable(pokemon: Pokemon, ignorePp: boolean = false, ignoreRestrictionTags: boolean = false): boolean {
     if (this.moveId && !ignoreRestrictionTags && pokemon.isMoveRestricted(this.moveId)) {
       return false;
     }
diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts
index 93466babb77..b2d429a4313 100644
--- a/src/phases/move-effect-phase.ts
+++ b/src/phases/move-effect-phase.ts
@@ -70,7 +70,7 @@ export class MoveEffectPhase extends PokemonPhase {
          * resolve the move's total hit count. This block combines the
          * effects of the move itself, Parental Bond, and Multi-Lens to do so.
          */
-      if (user.turnData.hitsLeft === undefined) {
+      if (user.turnData.hitsLeft === -1) {
         const hitCount = new Utils.IntegerHolder(1);
         // Assume single target for multi hit
         applyMoveAttrs(MultiHitAttr, user, this.getTarget() ?? null, move, hitCount);
diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts
index 154fbbe410d..807f194bad5 100644
--- a/src/phases/move-phase.ts
+++ b/src/phases/move-phase.ts
@@ -1,5 +1,5 @@
-import BattleScene from "#app/battle-scene";
 import { BattlerIndex } from "#app/battle";
+import BattleScene from "#app/battle-scene";
 import { applyAbAttrs, applyPostMoveUsedAbAttrs, applyPreAttackAbAttrs, BlockRedirectAbAttr, IncreasePpAbAttr, PokemonTypeChangeAbAttr, PostMoveUsedAbAttr, RedirectMoveAbAttr } from "#app/data/ability";
 import { CommonAnim } from "#app/data/battle-anims";
 import { BattlerTagLapseType, CenterOfAttentionTag } from "#app/data/battler-tags";
@@ -15,236 +15,149 @@ import { StatusEffect } from "#app/enums/status-effect";
 import { MoveUsedEvent } from "#app/events/battle-scene";
 import Pokemon, { MoveResult, PokemonMove, TurnMove } from "#app/field/pokemon";
 import { getPokemonNameWithAffix } from "#app/messages";
+import { BattlePhase } from "#app/phases/battle-phase";
+import { CommonAnimPhase } from "#app/phases/common-anim-phase";
+import { MoveEffectPhase } from "#app/phases/move-effect-phase";
+import { MoveEndPhase } from "#app/phases/move-end-phase";
+import { ShowAbilityPhase } from "#app/phases/show-ability-phase";
 import * as Utils from "#app/utils";
 import i18next from "i18next";
-import { BattlePhase } from "./battle-phase";
-import { CommonAnimPhase } from "./common-anim-phase";
-import { MoveEffectPhase } from "./move-effect-phase";
-import { MoveEndPhase } from "./move-end-phase";
-import { ShowAbilityPhase } from "./show-ability-phase";
 
 export class MovePhase extends BattlePhase {
-  public pokemon: Pokemon;
-  public move: PokemonMove;
-  public targets: BattlerIndex[];
+  protected _pokemon: Pokemon;
+  protected _move: PokemonMove;
+  protected _targets: BattlerIndex[];
   protected followUp: boolean;
   protected ignorePp: boolean;
-  protected failed: boolean;
-  protected cancelled: boolean;
+  protected failed: boolean = false;
+  protected cancelled: boolean = false;
 
-  constructor(scene: BattleScene, pokemon: Pokemon, targets: BattlerIndex[], move: PokemonMove, followUp?: boolean, ignorePp?: boolean) {
+  public get pokemon(): Pokemon {
+    return this._pokemon;
+  }
+
+  protected set pokemon(pokemon: Pokemon) {
+    this._pokemon = pokemon;
+  }
+
+  public get move(): PokemonMove {
+    return this._move;
+  }
+
+  protected set move(move: PokemonMove) {
+    this._move = move;
+  }
+
+  public get targets(): BattlerIndex[] {
+    return this._targets;
+  }
+
+  protected set targets(targets: BattlerIndex[]) {
+    this._targets = targets;
+  }
+
+  /**
+   * @param followUp Indicates that the move being uses is a "follow-up" - for example, a move being used by Metronome or Dancer.
+   *                 Follow-ups bypass a few failure conditions, including flinches, sleep/paralysis/freeze and volatile status checks, etc.
+   */
+  constructor(scene: BattleScene, pokemon: Pokemon, targets: BattlerIndex[], move: PokemonMove, followUp: boolean = false, ignorePp: boolean = false) {
     super(scene);
 
     this.pokemon = pokemon;
     this.targets = targets;
     this.move = move;
-    this.followUp = followUp ?? false;
-    this.ignorePp = ignorePp ?? false;
-    this.failed = false;
-    this.cancelled = false;
+    this.followUp = followUp;
+    this.ignorePp = ignorePp;
   }
 
-  canMove(ignoreDisableTags?: boolean): boolean {
+  /**
+   * Checks if the pokemon is active, if the move is usable, and that the move is targetting something.
+   * @param ignoreDisableTags `true` to not check if the move is disabled
+   * @returns `true` if all the checks pass
+   */
+  public canMove(ignoreDisableTags: boolean = false): boolean {
     return this.pokemon.isActive(true) && this.move.isUsable(this.pokemon, this.ignorePp, ignoreDisableTags) && !!this.targets.length;
   }
 
   /**Signifies the current move should fail but still use PP */
-  fail(): void {
+  public fail(): void {
     this.failed = true;
   }
 
   /**Signifies the current move should cancel and retain PP */
-  cancel(): void {
+  public cancel(): void {
     this.cancelled = true;
   }
 
-  start() {
+  public start() {
     super.start();
 
     console.log(Moves[this.move.moveId]);
 
+    // Check if move is unusable (e.g. because it's out of PP due to a mid-turn Spite).
     if (!this.canMove(true)) {
-      if (this.pokemon.isActive(true) && this.move.ppUsed >= this.move.getMovePp()) { // if the move PP was reduced from Spite or otherwise, the move fails
+      if (this.pokemon.isActive(true) && this.move.ppUsed >= this.move.getMovePp()) {
         this.fail();
         this.showMoveText();
         this.showFailedText();
       }
+
       return this.end();
     }
 
+    this.pokemon.turnData.acted = true;
+
+    // Reset hit-related turn data when starting follow-up moves (e.g. Metronomed moves, Dancer repeats)
+    if (this.followUp) {
+      this.pokemon.turnData.hitsLeft = -1;
+      this.pokemon.turnData.hitCount = 0;
+    }
+
+    // Check move to see if arena.ignoreAbilities should be true.
     if (!this.followUp) {
       if (this.move.getMove().checkFlag(MoveFlags.IGNORE_ABILITIES, this.pokemon, null)) {
         this.scene.arena.setIgnoreAbilities(true, this.pokemon.getBattlerIndex());
       }
+    }
+
+    this.resolveRedirectTarget();
+
+    this.resolveCounterAttackTarget();
+
+    this.resolvePreMoveStatusEffects();
+
+    this.lapsePreMoveAndMoveTags();
+
+    this.resolveFinalPreMoveCancellationChecks();
+
+    if (this.cancelled || this.failed) {
+      this.handlePreMoveFailures();
     } else {
-      this.pokemon.turnData.hitsLeft = 0; // TODO: is `0` correct?
-      this.pokemon.turnData.hitCount = 0; // TODO: is `0` correct?
+      this.useMove();
     }
 
-    // Move redirection abilities (ie. Storm Drain) only support single target moves
-    const moveTarget = this.targets.length === 1
-      ? new Utils.IntegerHolder(this.targets[0])
-      : null;
-    if (moveTarget) {
-      const oldTarget = moveTarget.value;
-      this.scene.getField(true).filter(p => p !== this.pokemon).forEach(p => applyAbAttrs(RedirectMoveAbAttr, p, null, false, this.move.moveId, moveTarget));
-      this.pokemon.getOpponents().forEach(p => {
-        const redirectTag = p.getTag(CenterOfAttentionTag) as CenterOfAttentionTag;
-        if (redirectTag && (!redirectTag.powder || (!this.pokemon.isOfType(Type.GRASS) && !this.pokemon.hasAbility(Abilities.OVERCOAT)))) {
-          moveTarget.value = p.getBattlerIndex();
-        }
-      });
-      //Check if this move is immune to being redirected, and restore its target to the intended target if it is.
-      if ((this.pokemon.hasAbilityWithAttr(BlockRedirectAbAttr) || this.move.getMove().hasAttr(BypassRedirectAttr))) {
-        //If an ability prevented this move from being redirected, display its ability pop up.
-        if ((this.pokemon.hasAbilityWithAttr(BlockRedirectAbAttr) && !this.move.getMove().hasAttr(BypassRedirectAttr)) && oldTarget !== moveTarget.value) {
-          this.scene.unshiftPhase(new ShowAbilityPhase(this.scene, this.pokemon.getBattlerIndex(), this.pokemon.getPassiveAbility().hasAttr(BlockRedirectAbAttr)));
-        }
-        moveTarget.value = oldTarget;
-      }
-      this.targets[0] = moveTarget.value;
+    this.end();
+  }
+
+  /** Check for cancellation edge cases - no targets remaining, or {@linkcode Moves.NONE} is in the queue */
+  protected resolveFinalPreMoveCancellationChecks() {
+    const targets = this.getActiveTargetPokemon();
+    const moveQueue = this.pokemon.getMoveQueue();
+
+    if (targets.length === 0 || (moveQueue.length && moveQueue[0].move === Moves.NONE)) {
+      this.showFailedText();
+      this.cancelled = true;
     }
+  }
 
-    // Check for counterattack moves to switch target
-    if (this.targets.length === 1 && this.targets[0] === BattlerIndex.ATTACKER) {
-      if (this.pokemon.turnData.attacksReceived.length) {
-        const attack = this.pokemon.turnData.attacksReceived[0];
-        this.targets[0] = attack.sourceBattlerIndex;
-
-        // account for metal burst and comeuppance hitting remaining targets in double battles
-        // counterattack will redirect to remaining ally if original attacker faints
-        if (this.scene.currentBattle.double && this.move.getMove().hasFlag(MoveFlags.REDIRECT_COUNTER)) {
-          if (this.scene.getField()[this.targets[0]].hp === 0) {
-            const opposingField = this.pokemon.isPlayer() ? this.scene.getEnemyField() : this.scene.getPlayerField();
-            //@ts-ignore
-            this.targets[0] = opposingField.find(p => p.hp > 0)?.getBattlerIndex(); //TODO: fix ts-ignore
-          }
-        }
-      }
-      if (this.targets[0] === BattlerIndex.ATTACKER) {
-        this.fail(); // Marks the move as failed for later in doMove
-        this.showMoveText();
-        this.showFailedText();
-      }
-    }
-
-    const targets = this.scene.getField(true).filter(p => {
-      if (this.targets.indexOf(p.getBattlerIndex()) > -1) {
-        return true;
-      }
-      return false;
-    });
-
-    const doMove = () => {
-      this.pokemon.turnData.acted = true; // Record that the move was attempted, even if it fails
-
-      this.pokemon.lapseTags(BattlerTagLapseType.PRE_MOVE);
-
-      let ppUsed = 1;
-      // Filter all opponents to include only those this move is targeting
-      const targetedOpponents = this.pokemon.getOpponents().filter(o => this.targets.includes(o.getBattlerIndex()));
-      for (const opponent of targetedOpponents) {
-        if (this.move.ppUsed + ppUsed >= this.move.getMovePp()) { // If we're already at max PP usage, stop checking
-          break;
-        }
-        if (opponent.hasAbilityWithAttr(IncreasePpAbAttr)) { // Accounting for abilities like Pressure
-          ppUsed++;
-        }
-      }
-
-      if (!this.followUp && this.canMove() && !this.cancelled) {
-        this.pokemon.lapseTags(BattlerTagLapseType.MOVE);
-      }
-
-      const moveQueue = this.pokemon.getMoveQueue();
-      if (this.cancelled || this.failed) {
-        if (this.failed) {
-          this.move.usePp(ppUsed); // Only use PP if the move failed
-          this.scene.eventTarget.dispatchEvent(new MoveUsedEvent(this.pokemon?.id, this.move.getMove(), this.move.ppUsed));
-        }
-
-        // Record a failed move so Abilities like Truant don't trigger next turn and soft-lock
-        this.pokemon.pushMoveHistory({ move: Moves.NONE, result: MoveResult.FAIL });
-
-        this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT); // Remove any tags from moves like Fly/Dive/etc.
-        this.pokemon.lapseTags(BattlerTagLapseType.AFTER_MOVE);
-        moveQueue.shift(); // Remove the second turn of charge moves
-        return this.end();
-      }
-
-      this.scene.triggerPokemonFormChange(this.pokemon, SpeciesFormChangePreMoveTrigger);
-
-      if (this.move.moveId) {
-        this.showMoveText();
-      }
-
-      // This should only happen when there are no valid targets left on the field
-      if ((moveQueue.length && moveQueue[0].move === Moves.NONE) || !targets.length) {
-        this.showFailedText();
-        this.cancel();
-
-        // Record a failed move so Abilities like Truant don't trigger next turn and soft-lock
-        this.pokemon.pushMoveHistory({ move: Moves.NONE, result: MoveResult.FAIL });
-
-        this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT); // Remove any tags from moves like Fly/Dive/etc.
-        this.pokemon.lapseTags(BattlerTagLapseType.AFTER_MOVE);
-
-        moveQueue.shift();
-        return this.end();
-      }
-
-      if ((!moveQueue.length || !moveQueue.shift()?.ignorePP) && !this.ignorePp) { // using .shift here clears out two turn moves once they've been used
-        this.move.usePp(ppUsed);
-        this.scene.eventTarget.dispatchEvent(new MoveUsedEvent(this.pokemon?.id, this.move.getMove(), this.move.ppUsed));
-      }
-
-      if (!allMoves[this.move.moveId].hasAttr(CopyMoveAttr)) {
-        this.scene.currentBattle.lastMove = this.move.moveId;
-      }
-
-      // Assume conditions affecting targets only apply to moves with a single target
-      let success = this.move.getMove().applyConditions(this.pokemon, targets[0], this.move.getMove());
-      const cancelled = new Utils.BooleanHolder(false);
-      let failedText = this.move.getMove().getFailedText(this.pokemon, targets[0], this.move.getMove(), cancelled);
-      if (success && this.scene.arena.isMoveWeatherCancelled(this.pokemon, this.move.getMove())) {
-        success = false;
-      } else if (success && this.scene.arena.isMoveTerrainCancelled(this.pokemon, this.targets, this.move.getMove())) {
-        success = false;
-        if (failedText === null) {
-          failedText = getTerrainBlockMessage(targets[0], this.scene.arena.terrain?.terrainType!); // TODO: is this bang correct?
-        }
-      }
-
-      /**
-         * Trigger pokemon type change before playing the move animation
-         * Will still change the user's type when using Roar, Whirlwind, Trick-or-Treat, and Forest's Curse,
-         * regardless of whether the move successfully executes or not.
-         */
-      if (success || [ Moves.ROAR, Moves.WHIRLWIND, Moves.TRICK_OR_TREAT, Moves.FORESTS_CURSE ].includes(this.move.moveId)) {
-        applyPreAttackAbAttrs(PokemonTypeChangeAbAttr, this.pokemon, null, this.move.getMove());
-      }
-
-      if (success) {
-        this.scene.unshiftPhase(this.getEffectPhase());
-      } else {
-        this.pokemon.pushMoveHistory({ move: this.move.moveId, targets: this.targets, result: MoveResult.FAIL, virtual: this.move.virtual });
-        if (!cancelled.value) {
-          this.showFailedText(failedText);
-        }
-      }
-      // Checks if Dancer ability is triggered
-      if (this.move.getMove().hasFlag(MoveFlags.DANCE_MOVE) && !this.followUp) {
-        // Pokemon with Dancer can be on either side of the battle so we check in both cases
-        this.scene.getPlayerField().forEach(pokemon => {
-          applyPostMoveUsedAbAttrs(PostMoveUsedAbAttr, pokemon, this.move, this.pokemon, this.targets);
-        });
-        this.scene.getEnemyField().forEach(pokemon => {
-          applyPostMoveUsedAbAttrs(PostMoveUsedAbAttr, pokemon, this.move, this.pokemon, this.targets);
-        });
-      }
-      this.end();
-    };
+  public getActiveTargetPokemon() {
+    return this.scene.getField(true).filter(p => this.targets.includes(p.getBattlerIndex()));
+  }
 
+  /**
+   * Handles {@link StatusEffect.SLEEP Sleep}/{@link StatusEffect.PARALYSIS Paralysis}/{@link StatusEffect.FREEZE Freeze} rolls and side effects.
+   */
+  protected resolvePreMoveStatusEffects() {
     if (!this.followUp && this.pokemon.status && !this.pokemon.status.isPostTurn()) {
       this.pokemon.status.incrementTurn();
       let activated = false;
@@ -273,25 +186,257 @@ export class MovePhase extends BattlePhase {
       if (activated) {
         this.scene.queueMessage(getStatusEffectActivationText(this.pokemon.status.effect, getPokemonNameWithAffix(this.pokemon)));
         this.scene.unshiftPhase(new CommonAnimPhase(this.scene, this.pokemon.getBattlerIndex(), undefined, CommonAnim.POISON + (this.pokemon.status.effect - 1)));
-        doMove();
-      } else {
-        if (healed) {
-          this.scene.queueMessage(getStatusEffectHealText(this.pokemon.status.effect, getPokemonNameWithAffix(this.pokemon)));
-          this.pokemon.resetStatus();
-          this.pokemon.updateInfo();
-        }
-        doMove();
+      } else if (healed) {
+        this.scene.queueMessage(getStatusEffectHealText(this.pokemon.status.effect, getPokemonNameWithAffix(this.pokemon)));
+        this.pokemon.resetStatus();
+        this.pokemon.updateInfo();
       }
-    } else {
-      doMove();
     }
   }
 
-  getEffectPhase(): MoveEffectPhase {
-    return new MoveEffectPhase(this.scene, this.pokemon.getBattlerIndex(), this.targets, this.move);
+  /**
+   * Lapse {@linkcode BattlerTagLapseType.PRE_MOVE PRE_MOVE} tags that trigger before a move is used, regardless of whether or not it failed.
+   * Also lapse {@linkcode BattlerTagLapseType.MOVE MOVE} tags if the move should be successful.
+   */
+  protected lapsePreMoveAndMoveTags() {
+    this.pokemon.lapseTags(BattlerTagLapseType.PRE_MOVE);
+
+    // TODO: does this intentionally happen before the no targets/Moves.NONE on queue cancellation case is checked?
+    if (!this.followUp && this.canMove() && !this.cancelled) {
+      this.pokemon.lapseTags(BattlerTagLapseType.MOVE);
+    }
   }
 
-  showMoveText(): void {
+  protected useMove() {
+    const targets = this.getActiveTargetPokemon();
+    const moveQueue = this.pokemon.getMoveQueue();
+
+    // form changes happen even before we know that the move wll execute.
+    this.scene.triggerPokemonFormChange(this.pokemon, SpeciesFormChangePreMoveTrigger);
+
+    this.showMoveText();
+
+    // TODO: Clean up implementation of two-turn moves.
+    if (moveQueue.length > 0) { // Using .shift here clears out two turn moves once they've been used
+      this.ignorePp = moveQueue.shift()?.ignorePP ?? false;
+    }
+
+    // "commit" to using the move, deducting PP.
+    if (!this.ignorePp) {
+      const ppUsed = 1 + this.getPpIncreaseFromPressure(targets);
+
+      this.move.usePp(ppUsed);
+      this.scene.eventTarget.dispatchEvent(new MoveUsedEvent(this.pokemon?.id, this.move.getMove(), ppUsed));
+    }
+
+    // Update the battle's "last move" pointer, unless we're currently mimicking a move.
+    if (!allMoves[this.move.moveId].hasAttr(CopyMoveAttr)) {
+      this.scene.currentBattle.lastMove = this.move.moveId;
+    }
+
+    /**
+     * Determine if the move is successful (meaning that its damage/effects can be attempted)
+     * by checking that all of the following are true:
+     * - Conditional attributes of the move are all met
+     * - The target's `ForceSwitchOutImmunityAbAttr` is not triggered (see {@linkcode Move.prototype.applyConditions})
+     * - Weather does not block the move
+     * - Terrain does not block the move
+     *
+     * TODO: These steps are straightforward, but the implementation below is extremely convoluted.
+     */
+
+    const move = this.move.getMove();
+
+    /**
+     * Move conditions assume the move has a single target
+     * TODO: is this sustainable?
+     */
+    const passesConditions = move.applyConditions(this.pokemon, targets[0], move);
+    const failedDueToWeather: boolean = this.scene.arena.isMoveWeatherCancelled(this.pokemon, move);
+    const failedDueToTerrain: boolean = this.scene.arena.isMoveTerrainCancelled(this.pokemon, this.targets, move);
+
+    const success = passesConditions && !failedDueToWeather && !failedDueToTerrain;
+
+    /**
+     * If the move has not failed, trigger ability-based user type changes and then execute it.
+     *
+     * Notably, Roar, Whirlwind, Trick-or-Treat, and Forest's Curse will trigger these type changes even
+     * if the move fails.
+     */
+    if (success) {
+      applyPreAttackAbAttrs(PokemonTypeChangeAbAttr, this.pokemon, null, this.move.getMove());
+      this.scene.unshiftPhase(new MoveEffectPhase(this.scene, this.pokemon.getBattlerIndex(), this.targets, this.move));
+
+    } else {
+      if ([ Moves.ROAR, Moves.WHIRLWIND, Moves.TRICK_OR_TREAT, Moves.FORESTS_CURSE ].includes(this.move.moveId)) {
+        applyPreAttackAbAttrs(PokemonTypeChangeAbAttr, this.pokemon, null, this.move.getMove());
+      }
+
+      this.pokemon.pushMoveHistory({ move: this.move.moveId, targets: this.targets, result: MoveResult.FAIL, virtual: this.move.virtual });
+
+      let failedText: string | undefined;
+      const failureMessage = move.getFailedText(this.pokemon, targets[0], move, new Utils.BooleanHolder(false));
+
+      if (failureMessage) {
+        failedText = failureMessage;
+      } else if (failedDueToTerrain) {
+        failedText = getTerrainBlockMessage(this.pokemon, this.scene.arena.getTerrainType());
+      }
+
+      this.showFailedText(failedText);
+    }
+
+    // Handle Dancer, which triggers immediately after a move is used (rather than waiting on `this.end()`).
+    // Note that the `!this.followUp` check here prevents an infinite Dancer loop.
+    if (this.move.getMove().hasFlag(MoveFlags.DANCE_MOVE) && !this.followUp) {
+      this.scene.getField(true).forEach(pokemon => {
+        applyPostMoveUsedAbAttrs(PostMoveUsedAbAttr, pokemon, this.move, this.pokemon, this.targets);
+      });
+    }
+  }
+
+  /**
+   * Queues a {@linkcode MoveEndPhase} if the move wasn't a {@linkcode followUp} and {@linkcode canMove()} returns `true`,
+   * then ends the phase.
+   */
+  public end() {
+    if (!this.followUp && this.canMove()) {
+      this.scene.unshiftPhase(new MoveEndPhase(this.scene, this.pokemon.getBattlerIndex()));
+    }
+
+    super.end();
+  }
+
+  /**
+   * Applies PP increasing abilities (currently only {@link Abilities.PRESSURE Pressure}) if they exist on the target pokemon.
+   * Note that targets must include only active pokemon.
+   *
+   * TODO: This hardcodes the PP increase at 1 per opponent, rather than deferring to the ability.
+   */
+  public getPpIncreaseFromPressure(targets: Pokemon[]) {
+    const foesWithPressure = this.pokemon.getOpponents().filter(o => targets.includes(o) && o.isActive(true) && o.hasAbilityWithAttr(IncreasePpAbAttr));
+    return foesWithPressure.length;
+  }
+
+  /**
+   * Modifies `this.targets` in place, based upon:
+   * - Move redirection abilities, effects, etc.
+   * - Counterattacks, which pass a special value into the `targets` constructor param (`[`{@linkcode BattlerIndex.ATTACKER}`]`).
+   */
+  protected resolveRedirectTarget() {
+    if (this.targets.length === 1) {
+      const currentTarget = this.targets[0];
+      const redirectTarget = new Utils.NumberHolder(currentTarget);
+
+      // check move redirection abilities of every pokemon *except* the user.
+      this.scene.getField(true).filter(p => p !== this.pokemon).forEach(p => applyAbAttrs(RedirectMoveAbAttr, p, null, false, this.move.moveId, redirectTarget));
+
+      // check for center-of-attention tags (note that this will override redirect abilities)
+      this.pokemon.getOpponents().forEach(p => {
+        const redirectTag = p.getTag(CenterOfAttentionTag) as CenterOfAttentionTag;
+
+        // TODO: don't hardcode this interaction.
+        // Handle interaction between the rage powder center-of-attention tag and moves used by grass types/overcoat-havers (which are immune to RP's redirect)
+        if (redirectTag && (!redirectTag.powder || (!this.pokemon.isOfType(Type.GRASS) && !this.pokemon.hasAbility(Abilities.OVERCOAT)))) {
+          redirectTarget.value = p.getBattlerIndex();
+        }
+      });
+
+      if (currentTarget !== redirectTarget.value) {
+        if (this.move.getMove().hasAttr(BypassRedirectAttr)) {
+          redirectTarget.value = currentTarget;
+
+        } else if (this.pokemon.hasAbilityWithAttr(BlockRedirectAbAttr)) {
+          redirectTarget.value = currentTarget;
+          this.scene.unshiftPhase(new ShowAbilityPhase(this.scene, this.pokemon.getBattlerIndex(), this.pokemon.getPassiveAbility().hasAttr(BlockRedirectAbAttr)));
+        }
+
+        this.targets[0] = redirectTarget.value;
+      }
+    }
+  }
+
+  /**
+   * Counter-attacking moves pass in `[`{@linkcode BattlerIndex.ATTACKER}`]` into the constructor's `targets` param.
+   * This function modifies `this.targets` to reflect the actual battler index of the user's last
+   * attacker.
+   *
+   * If there is no last attacker, or they are no longer on the field, a message is displayed and the
+   * move is marked for failure.
+   */
+  protected resolveCounterAttackTarget() {
+    if (this.targets.length === 1 && this.targets[0] === BattlerIndex.ATTACKER) {
+      if (this.pokemon.turnData.attacksReceived.length) {
+        const attacker = this.pokemon.scene.getPokemonById(this.pokemon.turnData.attacksReceived[0].sourceId);
+
+        if (attacker?.isActive(true)) {
+          this.targets[0] = attacker.getBattlerIndex();
+        }
+
+        // account for metal burst and comeuppance hitting remaining targets in double battles
+        // counterattack will redirect to remaining ally if original attacker faints
+        if (this.scene.currentBattle.double && this.move.getMove().hasFlag(MoveFlags.REDIRECT_COUNTER)) {
+          if (this.scene.getField()[this.targets[0]].hp === 0) {
+            const opposingField = this.pokemon.isPlayer() ? this.scene.getEnemyField() : this.scene.getPlayerField();
+            this.targets[0] = opposingField.find(p => p.hp > 0)?.getBattlerIndex() ?? BattlerIndex.ATTACKER;
+          }
+        }
+      }
+
+      if (this.targets[0] === BattlerIndex.ATTACKER) {
+        this.fail();
+        this.showMoveText();
+        this.showFailedText();
+      }
+    }
+  }
+
+  /**
+   * Handles the case where the move was cancelled or failed:
+   * - Uses PP if the move failed (not cancelled) and should use PP (failed moves are not affected by {@link Abilities.PRESSURE Pressure})
+   * - Records a cancelled OR failed move in move history, so abilities like {@link Abilities.TRUANT Truant} don't trigger on the
+   *   next turn and soft-lock.
+   * - Lapses `MOVE_EFFECT` tags:
+   *   - Semi-invulnerable battler tags (Fly/Dive/etc.) are intended to lapse on move effects, but also need
+   *     to lapse on move failure/cancellation.
+   *
+   *     TODO: ...this seems weird.
+   * - Lapses `AFTER_MOVE` tags:
+   *   - This handles the effects of {@link Moves.SUBSTITUTE Substitute}
+   * - Removes the second turn of charge moves
+   *
+   *   TODO: handle charge moves more gracefully
+   */
+  protected handlePreMoveFailures() {
+    if (this.cancelled || this.failed) {
+      if (this.failed) {
+        const ppUsed = this.ignorePp ? 0 : 1;
+
+        if (ppUsed) {
+          this.move.usePp();
+        }
+
+        this.scene.eventTarget.dispatchEvent(new MoveUsedEvent(this.pokemon?.id, this.move.getMove(), ppUsed));
+      }
+
+      this.pokemon.pushMoveHistory({ move: Moves.NONE, result: MoveResult.FAIL });
+
+      this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT);
+      this.pokemon.lapseTags(BattlerTagLapseType.AFTER_MOVE);
+
+      this.pokemon.getMoveQueue().shift();
+    }
+  }
+
+  /**
+   * Displays the move's usage text to the player, unless it's a charge turn (ie: {@link Moves.SOLAR_BEAM Solar Beam}),
+   * the pokemon is on a recharge turn (ie: {@link Moves.HYPER_BEAM Hyper Beam}), or a 2-turn move was interrupted (ie: {@link Moves.FLY Fly}).
+   */
+  protected showMoveText(): void {
+    if (this.move.moveId === Moves.NONE) {
+      return;
+    }
+
     if (this.move.getMove().hasAttr(ChargeAttr)) {
       const lastMove = this.pokemon.getLastXMoves() as TurnMove[];
       if (!lastMove.length || lastMove[0].move !== this.move.getMove().id || lastMove[0].result !== MoveResult.OTHER) {
@@ -311,18 +456,10 @@ export class MovePhase extends BattlePhase {
       pokemonNameWithAffix: getPokemonNameWithAffix(this.pokemon),
       moveName: this.move.getName()
     }), 500);
-    applyMoveAttrs(PreMoveMessageAttr, this.pokemon, this.pokemon.getOpponents().find(() => true)!, this.move.getMove()); //TODO: is the bang correct here?
+    applyMoveAttrs(PreMoveMessageAttr, this.pokemon, this.pokemon.getOpponents()[0], this.move.getMove());
   }
 
-  showFailedText(failedText: string | null = null): void {
-    this.scene.queueMessage(failedText || i18next.t("battle:attackFailed"));
-  }
-
-  end() {
-    if (!this.followUp && this.canMove()) {
-      this.scene.unshiftPhase(new MoveEndPhase(this.scene, this.pokemon.getBattlerIndex()));
-    }
-
-    super.end();
+  protected showFailedText(failedText?: string): void {
+    this.scene.queueMessage(failedText ?? i18next.t("battle:attackFailed"));
   }
 }
diff --git a/src/test/moves/whirlwind.test.ts b/src/test/moves/whirlwind.test.ts
index c8ad29a23d7..cc31b2591a2 100644
--- a/src/test/moves/whirlwind.test.ts
+++ b/src/test/moves/whirlwind.test.ts
@@ -1,12 +1,11 @@
-import { BattlerIndex } from "#app/battle";
-import { allMoves } from "#app/data/move";
 import { BattlerTagType } from "#app/enums/battler-tag-type";
+import { MoveResult } from "#app/field/pokemon";
 import { Abilities } from "#enums/abilities";
 import { Moves } from "#enums/moves";
 import { Species } from "#enums/species";
 import GameManager from "#test/utils/gameManager";
 import Phaser from "phaser";
-import { afterEach, beforeAll, beforeEach, describe, it, expect, vi } from "vitest";
+import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
 
 describe("Moves - Whirlwind", () => {
   let phaserGame: Phaser.Game;
@@ -40,14 +39,11 @@ describe("Moves - Whirlwind", () => {
     await game.classicMode.startBattle([ Species.STARAPTOR ]);
 
     const staraptor = game.scene.getPlayerPokemon()!;
-    const whirlwind = allMoves[Moves.WHIRLWIND];
-    vi.spyOn(whirlwind, "getFailedText");
 
     game.move.select(move);
-    await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
     await game.toNextTurn();
 
     expect(staraptor.findTag((t) => t.tagType === BattlerTagType.FLYING)).toBeDefined();
-    expect(whirlwind.getFailedText).toHaveBeenCalledOnce();
+    expect(game.scene.getEnemyPokemon()!.getLastXMoves(1)[0].result).toBe(MoveResult.MISS);
   });
 });