import BattleScene from "#app/battle-scene"; import { applyAbAttrs, BypassSpeedChanceAbAttr, PreventBypassSpeedChanceAbAttr, ChangeMovePriorityAbAttr } from "#app/data/ability"; import { allMoves, applyMoveAttrs, IncrementMovePriorityAttr, MoveHeaderAttr } from "#app/data/move"; import { Abilities } from "#app/enums/abilities"; import { Stat } from "#app/enums/stat"; import Pokemon, { PokemonMove } from "#app/field/pokemon"; import { BypassSpeedChanceModifier } from "#app/modifier/modifier"; import { Command } from "#app/ui/command-ui-handler"; import * as Utils from "#app/utils"; import { AttemptCapturePhase } from "./attempt-capture-phase"; import { AttemptRunPhase } from "./attempt-run-phase"; import { BerryPhase } from "./berry-phase"; import { FieldPhase } from "./field-phase"; import { MoveHeaderPhase } from "./move-header-phase"; import { MovePhase } from "./move-phase"; import { PostTurnStatusEffectPhase } from "./post-turn-status-effect-phase"; import { SwitchSummonPhase } from "./switch-summon-phase"; import { TurnEndPhase } from "./turn-end-phase"; import { WeatherEffectPhase } from "./weather-effect-phase"; import { BattlerIndex } from "#app/battle"; import { TrickRoomTag } from "#app/data/arena-tag"; export class TurnStartPhase extends FieldPhase { constructor(scene: BattleScene) { super(scene); } /** * This orders the active Pokemon on the field by speed into an BattlerIndex array and returns that array. * It also checks for Trick Room and reverses the array if it is present. * @returns {@linkcode BattlerIndex[]} the battle indices of all pokemon on the field ordered by speed */ getSpeedOrder(): BattlerIndex[] { const playerField = this.scene.getPlayerField().filter(p => p.isActive()) as Pokemon[]; const enemyField = this.scene.getEnemyField().filter(p => p.isActive()) as Pokemon[]; // We shuffle the list before sorting so speed ties produce random results let orderedTargets: Pokemon[] = playerField.concat(enemyField); // We seed it with the current turn to prevent an inconsistency where it // was varying based on how long since you last reloaded this.scene.executeWithSeedOffset(() => { orderedTargets = Utils.randSeedShuffle(orderedTargets); }, this.scene.currentBattle.turn, this.scene.waveSeed); orderedTargets.sort((a: Pokemon, b: Pokemon) => { const aSpeed = a?.getEffectiveStat(Stat.SPD) || 0; const bSpeed = b?.getEffectiveStat(Stat.SPD) || 0; return bSpeed - aSpeed; }); // Next, a check for Trick Room is applied. If Trick Room is present, the order is reversed. const speedReversed = new Utils.BooleanHolder(false); this.scene.arena.applyTags(TrickRoomTag, speedReversed); if (speedReversed.value) { orderedTargets = orderedTargets.reverse(); } return orderedTargets.map(t => t.getFieldIndex() + (!t.isPlayer() ? BattlerIndex.ENEMY : BattlerIndex.PLAYER)); } /** * This takes the result of getSpeedOrder and applies priority / bypass speed attributes to it. * This also considers the priority levels of various commands and changes the result of getSpeedOrder based on such. * @returns {@linkcode BattlerIndex[]} the final sequence of commands for this turn */ getCommandOrder(): BattlerIndex[] { let moveOrder = this.getSpeedOrder(); // The creation of the battlerBypassSpeed object contains checks for the ability Quick Draw and the held item Quick Claw // The ability Mycelium Might disables Quick Claw's activation when using a status move // This occurs before the main loop because of battles with more than two Pokemon const battlerBypassSpeed = {}; this.scene.getField(true).filter(p => p.summonData).map(p => { const bypassSpeed = new Utils.BooleanHolder(false); const canCheckHeldItems = new Utils.BooleanHolder(true); applyAbAttrs(BypassSpeedChanceAbAttr, p, null, false, bypassSpeed); applyAbAttrs(PreventBypassSpeedChanceAbAttr, p, null, false, bypassSpeed, canCheckHeldItems); if (canCheckHeldItems.value) { this.scene.applyModifiers(BypassSpeedChanceModifier, p.isPlayer(), p, bypassSpeed); } battlerBypassSpeed[p.getBattlerIndex()] = bypassSpeed; }); // The function begins sorting orderedTargets based on command priority, move priority, and possible speed bypasses. // Non-FIGHT commands (SWITCH, BALL, RUN) have a higher command priority and will always occur before any FIGHT commands. moveOrder = moveOrder.slice(0); moveOrder.sort((a, b) => { const aCommand = this.scene.currentBattle.turnCommands[a]; const bCommand = this.scene.currentBattle.turnCommands[b]; if (aCommand?.command !== bCommand?.command) { if (aCommand?.command === Command.FIGHT) { return 1; } else if (bCommand?.command === Command.FIGHT) { return -1; } } else if (aCommand?.command === Command.FIGHT) { const aMove = allMoves[aCommand.move!.move]; const bMove = allMoves[bCommand!.move!.move]; // The game now considers priority and applies the relevant move and ability attributes const aPriority = new Utils.IntegerHolder(aMove.priority); const bPriority = new Utils.IntegerHolder(bMove.priority); applyMoveAttrs(IncrementMovePriorityAttr, this.scene.getField().find(p => p?.isActive() && p.getBattlerIndex() === a)!, null, aMove, aPriority); applyMoveAttrs(IncrementMovePriorityAttr, this.scene.getField().find(p => p?.isActive() && p.getBattlerIndex() === b)!, null, bMove, bPriority); applyAbAttrs(ChangeMovePriorityAbAttr, this.scene.getField().find(p => p?.isActive() && p.getBattlerIndex() === a)!, null, false, aMove, aPriority); applyAbAttrs(ChangeMovePriorityAbAttr, this.scene.getField().find(p => p?.isActive() && p.getBattlerIndex() === b)!, null, false, bMove, bPriority); // The game now checks for differences in priority levels. // If the moves share the same original priority bracket, it can check for differences in battlerBypassSpeed and return the result. // This conditional is used to ensure that Quick Claw can still activate with abilities like Stall and Mycelium Might (attack moves only) // Otherwise, the game returns the user of the move with the highest priority. const isSameBracket = Math.ceil(aPriority.value) - Math.ceil(bPriority.value) === 0; if (aPriority.value !== bPriority.value) { if (isSameBracket && battlerBypassSpeed[a].value !== battlerBypassSpeed[b].value) { return battlerBypassSpeed[a].value ? -1 : 1; } return aPriority.value < bPriority.value ? 1 : -1; } } // If there is no difference between the move's calculated priorities, the game checks for differences in battlerBypassSpeed and returns the result. if (battlerBypassSpeed[a].value !== battlerBypassSpeed[b].value) { return battlerBypassSpeed[a].value ? -1 : 1; } const aIndex = moveOrder.indexOf(a); const bIndex = moveOrder.indexOf(b); return aIndex < bIndex ? -1 : aIndex > bIndex ? 1 : 0; }); return moveOrder; } start() { super.start(); const field = this.scene.getField(); const moveOrder = this.getCommandOrder(); let orderIndex = 0; for (const o of moveOrder) { const pokemon = field[o]; const turnCommand = this.scene.currentBattle.turnCommands[o]; if (turnCommand?.skip) { continue; } switch (turnCommand?.command) { case Command.FIGHT: const queuedMove = turnCommand.move; pokemon.turnData.order = orderIndex++; if (!queuedMove) { continue; } const move = pokemon.getMoveset().find(m => m?.moveId === queuedMove.move) || new PokemonMove(queuedMove.move); if (move.getMove().hasAttr(MoveHeaderAttr)) { this.scene.unshiftPhase(new MoveHeaderPhase(this.scene, pokemon, move)); } if (pokemon.isPlayer()) { if (turnCommand.cursor === -1) { this.scene.pushPhase(new MovePhase(this.scene, pokemon, turnCommand.targets || turnCommand.move!.targets, move));//TODO: is the bang correct here? } else { const playerPhase = new MovePhase(this.scene, pokemon, turnCommand.targets || turnCommand.move!.targets, move, false, queuedMove.ignorePP);//TODO: is the bang correct here? this.scene.pushPhase(playerPhase); } } else { this.scene.pushPhase(new MovePhase(this.scene, pokemon, turnCommand.targets || turnCommand.move!.targets, move, false, queuedMove.ignorePP));//TODO: is the bang correct here? } break; case Command.BALL: this.scene.unshiftPhase(new AttemptCapturePhase(this.scene, turnCommand.targets![0] % 2, turnCommand.cursor!));//TODO: is the bang correct here? break; case Command.POKEMON: this.scene.unshiftPhase(new SwitchSummonPhase(this.scene, pokemon.getFieldIndex(), turnCommand.cursor!, true, turnCommand.args![0] as boolean, pokemon.isPlayer()));//TODO: is the bang correct here? break; case Command.RUN: let runningPokemon = pokemon; if (this.scene.currentBattle.double) { const playerActivePokemon = field.filter(pokemon => { if (!!pokemon) { return pokemon.isPlayer() && pokemon.isActive(); } else { return; } }); // if only one pokemon is alive, use that one if (playerActivePokemon.length > 1) { // find which active pokemon has faster speed const fasterPokemon = playerActivePokemon[0].getStat(Stat.SPD) > playerActivePokemon[1].getStat(Stat.SPD) ? playerActivePokemon[0] : playerActivePokemon[1]; // check if either active pokemon has the ability "Run Away" const hasRunAway = playerActivePokemon.find(p => p.hasAbility(Abilities.RUN_AWAY)); runningPokemon = hasRunAway !== undefined ? hasRunAway : fasterPokemon; } } this.scene.unshiftPhase(new AttemptRunPhase(this.scene, runningPokemon.getFieldIndex())); break; } } this.scene.pushPhase(new WeatherEffectPhase(this.scene)); for (const o of moveOrder) { if (field[o].status && field[o].status.isPostTurn()) { this.scene.pushPhase(new PostTurnStatusEffectPhase(this.scene, o)); } } this.scene.pushPhase(new BerryPhase(this.scene)); this.scene.pushPhase(new TurnEndPhase(this.scene)); /** * this.end() will call shiftPhase(), which dumps everything from PrependQueue (aka everything that is unshifted()) to the front * of the queue and dequeues to start the next phase * this is important since stuff like SwitchSummon, AttemptRun, AttemptCapture Phases break the "flow" and should take precedence */ this.end(); } }