From 0935d817e9ab9261a7a736be249e7d5f168ec4e8 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Sun, 25 May 2025 17:26:37 -0500 Subject: [PATCH 01/12] breakup fight and ball commands into their own methods --- src/phases/command-phase.ts | 418 +++++++++++++++++++----------------- 1 file changed, 222 insertions(+), 196 deletions(-) diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index 14674037fbe..92c01aa4b85 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -148,206 +148,232 @@ export class CommandPhase extends FieldPhase { } /** - * TODO: Remove `args` and clean this thing up - * Code will need to be copied over from pkty except replacing the `virtual` and `ignorePP` args with a corresponding `MoveUseMode`. + * + * @param user - The pokemon using the move + * @param cursor - */ - handleCommand(command: Command, cursor: number, ...args: any[]): boolean { + private queueFightErrorMessage(user: PlayerPokemon, cursor: number) { + const move = user.getMoveset()[cursor]; + globalScene.ui.setMode(UiMode.MESSAGE); + + // Decides between a Disabled, Not Implemented, or No PP translation message + const errorMessage = user.isMoveRestricted(move.moveId, user) + ? user.getRestrictingTag(move.moveId, user)!.selectionDeniedText(user, move.moveId) + : move.getName().endsWith(" (N)") + ? "battle:moveNotImplemented" + : "battle:moveNoPP"; + const moveName = move.getName().replace(" (N)", ""); // Trims off the indicator + + globalScene.ui.showText( + i18next.t(errorMessage, { moveName: moveName }), + null, + () => { + globalScene.ui.clearText(); + globalScene.ui.setMode(UiMode.FIGHT, this.fieldIndex); + }, + null, + true, + ); + } + + /** Helper method for {@linkcode handleFightCommand} that returns the moveID for the phase + * based on the move passed in or the cursor. + * + * Does not check if the move is usable or not, that should be handled by the caller. + */ + private comptueMoveId(playerPokemon: PlayerPokemon, cursor: number, move?: TurnMove): MoveId { + return move?.move ?? (cursor > -1 ? playerPokemon.getMoveset()[cursor]?.moveId : MoveId.NONE); + } + + /** + * Handle fight logic + * @param command - The command to handle (FIGHT or TERA) + * @param cursor - The index that the cursor is placed on, or -1 if no move can be selected. + * @param args - Any additional arguments to pass to the command + */ + private handleFightCommand( + command: Command.FIGHT | Command.TERA, + cursor: number, + useMode: MoveUseMode = MoveUseMode.NORMAL, + move?: TurnMove, + ): boolean { + const playerPokemon = globalScene.getPlayerField()[this.fieldIndex]; + const ignorePP = isIgnorePP(useMode); + + /** Whether or not to display an error message instead of attempting to initiate the command selection process */ + let canUse = cursor !== -1 || !playerPokemon.trySelectMove(cursor, ignorePP); + + const useStruggle = canUse + ? false + : cursor > -1 && !playerPokemon.getMoveset().some(m => m.isUsable(playerPokemon)); + + canUse = canUse || useStruggle; + + if (!canUse) { + this.queueFightErrorMessage(playerPokemon, cursor); + return false; + } + + const moveId = useStruggle ? MoveId.STRUGGLE : this.comptueMoveId(playerPokemon, cursor, move); + + const turnCommand: TurnCommand = { + command: Command.FIGHT, + cursor: cursor, + move: { move: moveId, targets: [], useMode }, + args: [useMode, move], + }; + const preTurnCommand: TurnCommand = { + command: command, + targets: [this.fieldIndex], + skip: command === Command.FIGHT, + }; + + const moveTargets: MoveTargetSet = + move === undefined + ? getMoveTargets(playerPokemon, moveId) + : { + targets: move.targets, + multiple: move.targets.length > 1, + }; + + if (!moveId) { + turnCommand.targets = [this.fieldIndex]; + } + + console.log(moveTargets, getPokemonNameWithAffix(playerPokemon)); + + if (moveTargets.multiple) { + globalScene.phaseManager.unshiftNew("SelectTargetPhase", this.fieldIndex); + } + + if (turnCommand.move && (moveTargets.targets.length <= 1 || moveTargets.multiple)) { + turnCommand.move.targets = moveTargets.targets; + } else if ( + turnCommand.move && + playerPokemon.getTag(BattlerTagType.CHARGING) && + playerPokemon.getMoveQueue().length >= 1 + ) { + turnCommand.move.targets = playerPokemon.getMoveQueue()[0].targets; + } else { + globalScene.phaseManager.unshiftNew("SelectTargetPhase", this.fieldIndex); + } + + globalScene.currentBattle.preTurnCommands[this.fieldIndex] = preTurnCommand; + globalScene.currentBattle.turnCommands[this.fieldIndex] = turnCommand; + + return true; + } + + private queueShowText(key: string) { + globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); + globalScene.ui.setMode(UiMode.MESSAGE); + + globalScene.ui.showText( + i18next.t(key), + null, + () => { + globalScene.ui.showText("", 0); + globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); + }, + null, + true, + ); + } + + /** + * Helper method for {@linkcode handleBallCommand} that checks if the pokeball can be thrown. + * + * The pokeball may not be thrown if: + * - It is a trainer battle + * - The biome is {@linkcode Biome.END} and it is not classic mode + * - The biome is {@linkcode Biome.END} and the fresh start challenge is active + * - The biome is {@linkcode Biome.END} and the player has not either caught the target before or caught all but one starter + * - The player is in a mystery encounter that disallows catching the pokemon + * @returns Whether a pokeball can be thrown + * + */ + private checkCanUseBall(): boolean { + if ( + globalScene.arena.biomeType === BiomeId.END && + (!globalScene.gameMode.isClassic || + globalScene.gameMode.isFreshStartChallenge() || + (globalScene + .getEnemyField() + .some(p => p.isActive() && !globalScene.gameData.dexData[p.species.speciesId].caughtAttr) && + globalScene.gameData.getStarterCount(d => !!d.caughtAttr) < Object.keys(speciesStarterCosts).length - 1)) + ) { + this.queueShowText("battle:noPokeballForce"); + } else if (globalScene.currentBattle.battleType === BattleType.TRAINER) { + this.queueShowText("battle:noPokeballTrainer"); + } else if ( + globalScene.currentBattle.isBattleMysteryEncounter() && + !globalScene.currentBattle.mysteryEncounter!.catchAllowed + ) { + this.queueShowText("battle:noPokeballMysteryEncounter"); + } else { + return true; + } + + return false; + } + + /** + * Helper method for {@linkcode handleCommand} that handles the logic when the selected command is to use a pokeball. + * + * @param cursor - The index of the pokeball to use + * @returns Whether the command was successfully initiated + */ + handleBallCommand(cursor: number): boolean { + const targets = globalScene + .getEnemyField() + .filter(p => p.isActive(true)) + .map(p => p.getBattlerIndex()); + if (targets.length > 1) { + this.queueShowText("battle:noPokeballMulti"); + return false; + } + + if (!this.checkCanUseBall()) { + return false; + } + + if (cursor < 5) { + const targetPokemon = globalScene.getEnemyPokemon(); + if ( + targetPokemon?.isBoss() && + targetPokemon?.bossSegmentIndex >= 1 && + // TODO: Decouple this hardcoded exception for wonder guard and just check the target... + !targetPokemon?.hasAbility(AbilityId.WONDER_GUARD, false, true) && + cursor < PokeballType.MASTER_BALL + ) { + this.queueShowText("battle:noPokeballStrong"); + return false; + } + + globalScene.currentBattle.turnCommands[this.fieldIndex] = { + command: Command.BALL, + cursor: cursor, + }; + globalScene.currentBattle.turnCommands[this.fieldIndex]!.targets = targets; + if (this.fieldIndex) { + globalScene.currentBattle.turnCommands[this.fieldIndex - 1]!.skip = true; + } + return true; + } + + return false; + } + + handleCommand(command: Command, cursor: number, useMode: MoveUseMode = MoveUseMode.NORMAL, move?: TurnMove): boolean { const playerPokemon = globalScene.getPlayerField()[this.fieldIndex]; let success = false; switch (command) { - // TODO: We don't need 2 args for this - moveUseMode is carried over from queuedMove case Command.TERA: - case Command.FIGHT: { - let useStruggle = false; - const turnMove: TurnMove | undefined = args.length === 2 ? (args[1] as TurnMove) : undefined; - if ( - cursor === -1 || - playerPokemon.trySelectMove(cursor, isIgnorePP(args[0] as MoveUseMode)) || - (useStruggle = cursor > -1 && !playerPokemon.getMoveset().filter(m => m.isUsable(playerPokemon)).length) - ) { - let moveId: MoveId; - if (useStruggle) { - moveId = MoveId.STRUGGLE; - } else if (turnMove !== undefined) { - moveId = turnMove.move; - } else if (cursor > -1) { - moveId = playerPokemon.getMoveset()[cursor].moveId; - } else { - moveId = MoveId.NONE; - } - - const turnCommand: TurnCommand = { - command: Command.FIGHT, - cursor: cursor, - move: { move: moveId, targets: [], useMode: args[0] }, - args: args, - }; - const preTurnCommand: TurnCommand = { - command: command, - targets: [this.fieldIndex], - skip: command === Command.FIGHT, - }; - const moveTargets: MoveTargetSet = - turnMove === undefined - ? getMoveTargets(playerPokemon, moveId) - : { - targets: turnMove.targets, - multiple: turnMove.targets.length > 1, - }; - if (!moveId) { - turnCommand.targets = [this.fieldIndex]; - } - console.log(moveTargets, getPokemonNameWithAffix(playerPokemon)); - if (moveTargets.targets.length > 1 && moveTargets.multiple) { - globalScene.phaseManager.unshiftNew("SelectTargetPhase", this.fieldIndex); - } - if (turnCommand.move && (moveTargets.targets.length <= 1 || moveTargets.multiple)) { - turnCommand.move.targets = moveTargets.targets; - } else if ( - turnCommand.move && - playerPokemon.getTag(BattlerTagType.CHARGING) && - playerPokemon.getMoveQueue().length >= 1 - ) { - turnCommand.move.targets = playerPokemon.getMoveQueue()[0].targets; - } else { - globalScene.phaseManager.unshiftNew("SelectTargetPhase", this.fieldIndex); - } - globalScene.currentBattle.preTurnCommands[this.fieldIndex] = preTurnCommand; - globalScene.currentBattle.turnCommands[this.fieldIndex] = turnCommand; - success = true; - } else if (cursor < playerPokemon.getMoveset().length) { - const move = playerPokemon.getMoveset()[cursor]; - globalScene.ui.setMode(UiMode.MESSAGE); - - // Decides between a Disabled, Not Implemented, or No PP translation message - const errorMessage = playerPokemon.isMoveRestricted(move.moveId, playerPokemon) - ? playerPokemon - .getRestrictingTag(move.moveId, playerPokemon)! - .selectionDeniedText(playerPokemon, move.moveId) - : move.getName().endsWith(" (N)") - ? "battle:moveNotImplemented" - : "battle:moveNoPP"; - const moveName = move.getName().replace(" (N)", ""); // Trims off the indicator - - globalScene.ui.showText( - i18next.t(errorMessage, { moveName: moveName }), - null, - () => { - globalScene.ui.clearText(); - globalScene.ui.setMode(UiMode.FIGHT, this.fieldIndex); - }, - null, - true, - ); - } - break; - } - case Command.BALL: { - const notInDex = - globalScene - .getEnemyField() - .filter(p => p.isActive(true)) - .some(p => !globalScene.gameData.dexData[p.species.speciesId].caughtAttr) && - globalScene.gameData.getStarterCount(d => !!d.caughtAttr) < Object.keys(speciesStarterCosts).length - 1; - if ( - globalScene.arena.biomeType === BiomeId.END && - (!globalScene.gameMode.isClassic || globalScene.gameMode.isFreshStartChallenge() || notInDex) - ) { - globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); - globalScene.ui.setMode(UiMode.MESSAGE); - globalScene.ui.showText( - i18next.t("battle:noPokeballForce"), - null, - () => { - globalScene.ui.showText("", 0); - globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); - }, - null, - true, - ); - } else if (globalScene.currentBattle.battleType === BattleType.TRAINER) { - globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); - globalScene.ui.setMode(UiMode.MESSAGE); - globalScene.ui.showText( - i18next.t("battle:noPokeballTrainer"), - null, - () => { - globalScene.ui.showText("", 0); - globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); - }, - null, - true, - ); - } else if ( - globalScene.currentBattle.isBattleMysteryEncounter() && - !globalScene.currentBattle.mysteryEncounter!.catchAllowed - ) { - globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); - globalScene.ui.setMode(UiMode.MESSAGE); - globalScene.ui.showText( - i18next.t("battle:noPokeballMysteryEncounter"), - null, - () => { - globalScene.ui.showText("", 0); - globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); - }, - null, - true, - ); - } else { - const targets = globalScene - .getEnemyField() - .filter(p => p.isActive(true)) - .map(p => p.getBattlerIndex()); - if (targets.length > 1) { - globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); - globalScene.ui.setMode(UiMode.MESSAGE); - globalScene.ui.showText( - i18next.t("battle:noPokeballMulti"), - null, - () => { - globalScene.ui.showText("", 0); - globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); - }, - null, - true, - ); - } else if (cursor < 5) { - const targetPokemon = globalScene.getEnemyField().find(p => p.isActive(true)); - if ( - targetPokemon?.isBoss() && - targetPokemon?.bossSegmentIndex >= 1 && - !targetPokemon?.hasAbility(AbilityId.WONDER_GUARD, false, true) && - cursor < PokeballType.MASTER_BALL - ) { - globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); - globalScene.ui.setMode(UiMode.MESSAGE); - globalScene.ui.showText( - i18next.t("battle:noPokeballStrong"), - null, - () => { - globalScene.ui.showText("", 0); - globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); - }, - null, - true, - ); - } else { - globalScene.currentBattle.turnCommands[this.fieldIndex] = { - command: Command.BALL, - cursor: cursor, - }; - globalScene.currentBattle.turnCommands[this.fieldIndex]!.targets = targets; - if (this.fieldIndex) { - globalScene.currentBattle.turnCommands[this.fieldIndex - 1]!.skip = true; - } - success = true; - } - } - } - break; - } + case Command.FIGHT: + return this.handleFightCommand(command, cursor, useMode, move); + case Command.BALL: + return this.handleBallCommand(cursor); case Command.POKEMON: case Command.RUN: { const isSwitch = command === Command.POKEMON; @@ -388,11 +414,11 @@ export class CommandPhase extends FieldPhase { true, ); } else { - const batonPass = isSwitch && (args[0] as boolean); + const batonPass = isSwitch && useMode; const trappedAbMessages: string[] = []; if (batonPass || !playerPokemon.isTrapped(trappedAbMessages)) { currentBattle.turnCommands[this.fieldIndex] = isSwitch - ? { command: Command.POKEMON, cursor: cursor, args: args } + ? { command: Command.POKEMON, cursor: cursor } : { command: Command.RUN }; success = true; if (!isSwitch && this.fieldIndex) { From 22b9a19ba65950c909328b057a8d002007cdc462 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Mon, 26 May 2025 11:46:53 -0500 Subject: [PATCH 02/12] Breakup run and pokemon commands --- src/phases/command-phase.ts | 274 +++++++++++++++++++++--------------- 1 file changed, 158 insertions(+), 116 deletions(-) diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index 92c01aa4b85..a37721ac912 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -20,15 +20,19 @@ import { UiMode } from "#enums/ui-mode"; import type { PlayerPokemon } from "#field/pokemon"; import type { MoveTargetSet } from "#moves/move"; import { getMoveTargets } from "#moves/move-utils"; -import { FieldPhase } from "#phases/field-phase"; import type { TurnMove } from "#types/turn-move"; -import { isNullOrUndefined } from "#utils/common"; import i18next from "i18next"; +import { FieldPhase } from "./field-phase"; export class CommandPhase extends FieldPhase { public readonly phaseName = "CommandPhase"; protected fieldIndex: number; + /** + * Whether the command phase is handling a switch command + */ + private isSwitch = false; + constructor(fieldIndex: number) { super(); @@ -150,7 +154,7 @@ export class CommandPhase extends FieldPhase { /** * * @param user - The pokemon using the move - * @param cursor - + * @param cursor - The index of the move in the moveset */ private queueFightErrorMessage(user: PlayerPokemon, cursor: number) { const move = user.getMoveset()[cursor]; @@ -264,6 +268,11 @@ export class CommandPhase extends FieldPhase { return true; } + /** + * Set the mode in preparation to show the text, and then show the text. + * Only works for parameterless i18next keys. + * @param key - The i18next key for the text to show + */ private queueShowText(key: string) { globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); globalScene.ui.setMode(UiMode.MESSAGE); @@ -323,7 +332,7 @@ export class CommandPhase extends FieldPhase { * @param cursor - The index of the pokeball to use * @returns Whether the command was successfully initiated */ - handleBallCommand(cursor: number): boolean { + private handleBallCommand(cursor: number): boolean { const targets = globalScene .getEnemyField() .filter(p => p.isActive(true)) @@ -364,125 +373,158 @@ export class CommandPhase extends FieldPhase { return false; } - handleCommand(command: Command, cursor: number, useMode: MoveUseMode = MoveUseMode.NORMAL, move?: TurnMove): boolean { + /** + * Common helper method to handle the logic for effects that prevent the pokemon from leaving the field + * due to trapping abilities or effects. + * + * This method queues the proper messages in the case of trapping abilities or effects + * + * @returns Whether the pokemon is currently trapped + */ + private handleTrap(): boolean { const playerPokemon = globalScene.getPlayerField()[this.fieldIndex]; + const trappedAbMessages: string[] = []; + const isSwitch = this.isSwitch; + if (!playerPokemon.isTrapped(trappedAbMessages)) { + return false; + } + if (trappedAbMessages.length > 0) { + if (isSwitch) { + globalScene.ui.setMode(UiMode.MESSAGE); + } + globalScene.ui.showText( + trappedAbMessages[0], + null, + () => { + globalScene.ui.showText("", 0); + if (isSwitch) { + globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); + } + }, + null, + true, + ); + } else { + const trapTag = playerPokemon.getTag(TrappedTag); + const fairyLockTag = globalScene.arena.getTagOnSide(ArenaTagType.FAIRY_LOCK, ArenaTagSide.PLAYER); + + if (!isSwitch) { + globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); + globalScene.ui.setMode(UiMode.MESSAGE); + } + if (trapTag) { + this.showNoEscapeText(trapTag, false); + } else if (fairyLockTag) { + this.showNoEscapeText(fairyLockTag, false); + } + } + + return true; + } + + /** + * Common helper method that attempts to have the pokemon leave the field. + * Checks for trapping abilities and effects. + * + * @param cursor - The index of the option that the cursor is on + * @param isBatonSwitch - Whether the switch command is switching via the Baton item + * @returns whether the pokemon is able to leave the field, indicating the command phase should end + */ + private tryLeaveField(cursor?: number, isBatonSwitch = false): boolean { + const currentBattle = globalScene.currentBattle; + + if (isBatonSwitch && !this.handleTrap()) { + currentBattle.turnCommands[this.fieldIndex] = { + command: this.isSwitch ? Command.POKEMON : Command.RUN, + cursor: cursor, + }; + if (!this.isSwitch && this.fieldIndex) { + currentBattle.turnCommands[this.fieldIndex - 1]!.skip = true; + } + return true; + } + + return false; + } + + private handleRunCommand(): boolean { + const { currentBattle, arena } = globalScene; + const mysteryEncounterFleeAllowed = currentBattle.mysteryEncounter?.fleeAllowed ?? true; + if (arena.biomeType === BiomeId.END || !mysteryEncounterFleeAllowed) { + this.queueShowText("battle:noEscapeForce"); + return false; + } + if ( + currentBattle.battleType === BattleType.TRAINER || + currentBattle.mysteryEncounter?.encounterMode === MysteryEncounterMode.TRAINER_BATTLE + ) { + this.queueShowText("battle:noEscapeTrainer"); + return false; + } + + const success = this.tryLeaveField(); + + return success; + } + + /** + * Show a message indicating that the pokemon cannot escape, and then return to the command phase. + */ + private showNoEscapeText(tag: any, isSwitch: boolean): void { + globalScene.ui.showText( + i18next.t("battle:noEscapePokemon", { + pokemonName: + tag.sourceId && globalScene.getPokemonById(tag.sourceId) + ? getPokemonNameWithAffix(globalScene.getPokemonById(tag.sourceId)!) + : "", + moveName: tag.getMoveName(), + escapeVerb: i18next.t(isSwitch ? "battle:escapeVerbSwitch" : "battle:escapeVerbFlee"), + }), + null, + () => { + globalScene.ui.showText("", 0); + if (!isSwitch) { + globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); + } + }, + null, + true, + ); + } + + // Overloads for handleCommand to provide a more specific type signature for the different options + handleCommand(command: Command.FIGHT | Command.TERA, cursor: number, useMode?: MoveUseMode, move?: TurnMove): boolean; + handleCommand(command: Command.BALL, cursor: number): boolean; + handleCommand(command: Command.POKEMON, cursor: number, useBaton: boolean): boolean; + handleCommand(command: Command.RUN, cursor: number): boolean; + handleCommand(command: Command, cursor: number, useMode?: boolean | MoveUseMode, move?: TurnMove): boolean; + + /** + * Process the command phase logic based on the selected command + * + * @param command - The kind of command to handle + * @param cursor - The index of option that the cursor is on, or -1 if no option is selected + * @param useMode - The mode to use for the move, if applicable. For switches, a boolean that specifies whether the switch is a Baton switch. + * @param move - For {@linkcode Command.FIGHT}, the move to use + */ + handleCommand(command: Command, cursor: number, useMode: boolean | MoveUseMode = false, move?: TurnMove): boolean { let success = false; switch (command) { case Command.TERA: case Command.FIGHT: - return this.handleFightCommand(command, cursor, useMode, move); - case Command.BALL: - return this.handleBallCommand(cursor); - case Command.POKEMON: - case Command.RUN: { - const isSwitch = command === Command.POKEMON; - const { currentBattle, arena } = globalScene; - const mysteryEncounterFleeAllowed = currentBattle.mysteryEncounter?.fleeAllowed; - if ( - !isSwitch && - (arena.biomeType === BiomeId.END || - (!isNullOrUndefined(mysteryEncounterFleeAllowed) && !mysteryEncounterFleeAllowed)) - ) { - globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); - globalScene.ui.setMode(UiMode.MESSAGE); - globalScene.ui.showText( - i18next.t("battle:noEscapeForce"), - null, - () => { - globalScene.ui.showText("", 0); - globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); - }, - null, - true, - ); - } else if ( - !isSwitch && - (currentBattle.battleType === BattleType.TRAINER || - currentBattle.mysteryEncounter?.encounterMode === MysteryEncounterMode.TRAINER_BATTLE) - ) { - globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); - globalScene.ui.setMode(UiMode.MESSAGE); - globalScene.ui.showText( - i18next.t("battle:noEscapeTrainer"), - null, - () => { - globalScene.ui.showText("", 0); - globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); - }, - null, - true, - ); - } else { - const batonPass = isSwitch && useMode; - const trappedAbMessages: string[] = []; - if (batonPass || !playerPokemon.isTrapped(trappedAbMessages)) { - currentBattle.turnCommands[this.fieldIndex] = isSwitch - ? { command: Command.POKEMON, cursor: cursor } - : { command: Command.RUN }; - success = true; - if (!isSwitch && this.fieldIndex) { - currentBattle.turnCommands[this.fieldIndex - 1]!.skip = true; - } - } else if (trappedAbMessages.length > 0) { - if (!isSwitch) { - globalScene.ui.setMode(UiMode.MESSAGE); - } - globalScene.ui.showText( - trappedAbMessages[0], - null, - () => { - globalScene.ui.showText("", 0); - if (!isSwitch) { - globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); - } - }, - null, - true, - ); - } else { - const trapTag = playerPokemon.getTag(TrappedTag); - const fairyLockTag = globalScene.arena.getTagOnSide(ArenaTagType.FAIRY_LOCK, ArenaTagSide.PLAYER); - - if (!trapTag && !fairyLockTag) { - i18next.t(`battle:noEscape${isSwitch ? "Switch" : "Flee"}`); - break; - } - if (!isSwitch) { - globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); - globalScene.ui.setMode(UiMode.MESSAGE); - } - const showNoEscapeText = (tag: any) => { - globalScene.ui.showText( - i18next.t("battle:noEscapePokemon", { - pokemonName: - tag.sourceId && globalScene.getPokemonById(tag.sourceId) - ? getPokemonNameWithAffix(globalScene.getPokemonById(tag.sourceId)!) - : "", - moveName: tag.getMoveName(), - escapeVerb: isSwitch ? i18next.t("battle:escapeVerbSwitch") : i18next.t("battle:escapeVerbFlee"), - }), - null, - () => { - globalScene.ui.showText("", 0); - if (!isSwitch) { - globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); - } - }, - null, - true, - ); - }; - - if (trapTag) { - showNoEscapeText(trapTag); - } else if (fairyLockTag) { - showNoEscapeText(fairyLockTag); - } - } - } + success = this.handleFightCommand(command, cursor, typeof useMode === "boolean" ? undefined : useMode, move); break; - } + case Command.BALL: + success = this.handleBallCommand(cursor); + break; + case Command.POKEMON: + this.isSwitch = true; + success = this.tryLeaveField(cursor, typeof useMode === "boolean" ? useMode : undefined); + this.isSwitch = false; + break; + case Command.RUN: + success = this.handleRunCommand(); } if (success) { From 958eb005665637223b22a41371a95b09d379dce8 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Mon, 26 May 2025 15:56:14 -0500 Subject: [PATCH 03/12] Breakup commandPhase#start Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com> --- src/phases/command-phase.ts | 205 ++++++++++++++++++++++-------------- 1 file changed, 125 insertions(+), 80 deletions(-) diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index a37721ac912..943eca87e5a 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -1,14 +1,13 @@ import type { TurnCommand } from "#app/battle"; +import { BattlerTagType } from "#app/enums/battler-tag-type"; import { globalScene } from "#app/global-scene"; import { getPokemonNameWithAffix } from "#app/messages"; import { speciesStarterCosts } from "#balance/starters"; -import type { EncoreTag } from "#data/battler-tags"; import { TrappedTag } from "#data/battler-tags"; import { AbilityId } from "#enums/ability-id"; import { ArenaTagSide } from "#enums/arena-tag-side"; import { ArenaTagType } from "#enums/arena-tag-type"; import { BattleType } from "#enums/battle-type"; -import { BattlerTagType } from "#enums/battler-tag-type"; import { BiomeId } from "#enums/biome-id"; import { Command } from "#enums/command"; import { FieldPosition } from "#enums/field-position"; @@ -39,11 +38,13 @@ export class CommandPhase extends FieldPhase { this.fieldIndex = fieldIndex; } - start() { - super.start(); - - globalScene.updateGameInfo(); - + /** + * Resets the cursor to the position of {@linkcode Command.FIGHT} if any of the following are true + * - The setting to remember the last action is not enabled + * - This is the first turn of a mystery encounter, trainer battle, or the END biome + * - The cursor is currently on the POKEMON command + */ + private resetCursorIfNeeded() { const commandUiHandler = globalScene.ui.handlers[UiMode.COMMAND]; // If one of these conditions is true, we always reset the cursor to Command.FIGHT @@ -52,33 +53,42 @@ export class CommandPhase extends FieldPhase { globalScene.currentBattle.battleType === BattleType.TRAINER || globalScene.arena.biomeType === BiomeId.END; - if (commandUiHandler) { - if ( - (globalScene.currentBattle.turn === 1 && (!globalScene.commandCursorMemory || cursorResetEvent)) || - commandUiHandler.getCursor() === Command.POKEMON - ) { - commandUiHandler.setCursor(Command.FIGHT); - } else { - commandUiHandler.setCursor(commandUiHandler.getCursor()); - } + if ( + (commandUiHandler && + globalScene.currentBattle.turn === 1 && + (!globalScene.commandCursorMemory || cursorResetEvent)) || + commandUiHandler.getCursor() === Command.POKEMON + ) { + commandUiHandler.setCursor(Command.FIGHT); + } + } + + /** + * Submethod of {@linkcode start} that validates field index logic for nonzero field indices. + * Must only be called if the field index is nonzero. + */ + private handleFieldIndexLogic() { + // If we somehow are attempting to check the right pokemon but there's only one pokemon out + // Switch back to the center pokemon. This can happen rarely in double battles with mid turn switching + // TODO: Prevent this from happening in the first place + if (globalScene.getPlayerField().filter(p => p.isActive()).length === 1) { + this.fieldIndex = FieldPosition.CENTER; + return; } - if (this.fieldIndex) { - // If we somehow are attempting to check the right pokemon but there's only one pokemon out - // Switch back to the center pokemon. This can happen rarely in double battles with mid turn switching - if (globalScene.getPlayerField().filter(p => p.isActive()).length === 1) { - this.fieldIndex = FieldPosition.CENTER; - } else { - const allyCommand = globalScene.currentBattle.turnCommands[this.fieldIndex - 1]; - if (allyCommand?.command === Command.BALL || allyCommand?.command === Command.RUN) { - globalScene.currentBattle.turnCommands[this.fieldIndex] = { - command: allyCommand?.command, - skip: true, - }; - } - } + const allyCommand = globalScene.currentBattle.turnCommands[this.fieldIndex - 1]; + if (allyCommand?.command === Command.BALL || allyCommand?.command === Command.RUN) { + globalScene.currentBattle.turnCommands[this.fieldIndex] = { + command: allyCommand?.command, + skip: true, + }; } + } + /** Submethod of {@linkcode start} that sets the turn command to skip if this pokemon is commanding its ally + * via {@linkcode Abilities.COMMANDER}. + */ + private checkCommander() { // If the Pokemon has applied Commander's effects to its ally, skip this command if ( globalScene.currentBattle?.double && @@ -90,64 +100,99 @@ export class CommandPhase extends FieldPhase { skip: true, }; } + } - // Checks if the Pokemon is under the effects of Encore. If so, Encore can end early if the encored move has no more PP. - const encoreTag = this.getPokemon().getTag(BattlerTagType.ENCORE) as EncoreTag | undefined; - if (encoreTag) { - this.getPokemon().lapseTag(BattlerTagType.ENCORE); + /** + * Clear out all unusable moves in front of the currently acting pokemon's move queue. + * TODO: Refactor move queue handling to ensure that this method is not necessary. + */ + private clearUnusuableMoves() { + const playerPokemon = this.getPokemon(); + const moveQueue = playerPokemon.getMoveQueue(); + if (moveQueue.length === 0) { + return; } + let entriesToDelete = 0; + const moveset = playerPokemon.getMoveset(); + for (const queuedMove of moveQueue) { + const movesetQueuedMove = moveset.find(m => m.moveId === queuedMove.move); + if ( + queuedMove.move !== MoveId.NONE && + !isVirtual(queuedMove.useMode) && + !movesetQueuedMove?.isUsable(playerPokemon, isIgnorePP(queuedMove.useMode)) + ) { + entriesToDelete++; + } else { + break; + } + } + if (entriesToDelete) { + moveQueue.splice(0, entriesToDelete); + } + } + + /** + * Attempt to execute the first usable move in this Pokemon's move queue + * @returns Whether a queued move was successfully set to be executed. + */ + private tryExecuteQueuedMove(): boolean { + this.clearUnusuableMoves(); + const playerPokemon = globalScene.getPlayerField()[this.fieldIndex]; + const moveQueue = playerPokemon.getMoveQueue(); + + if (moveQueue.length === 0) { + return false; + } + + const queuedMove = moveQueue[0]; + if (queuedMove.move === MoveId.NONE) { + this.handleCommand(Command.FIGHT, -1); + return true; + } + const moveIndex = playerPokemon.getMoveset().findIndex(m => m.moveId === queuedMove.move); + if (!isVirtual(queuedMove.useMode) && moveIndex === -1) { + globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); + } else { + this.handleCommand(Command.FIGHT, moveIndex, queuedMove.useMode, queuedMove); + } + + return true; + } + + start() { + super.start(); + + globalScene.updateGameInfo(); + this.resetCursorIfNeeded(); + + if (this.fieldIndex) { + this.handleFieldIndexLogic(); + } + + this.checkCommander(); + + const playerPokemon = this.getPokemon(); + + // Note: It is OK to call this if the target is not under the effect of encore; it will simply do nothing. + playerPokemon.lapseTag(BattlerTagType.ENCORE); + if (globalScene.currentBattle.turnCommands[this.fieldIndex]?.skip) { return this.end(); } - const playerPokemon = globalScene.getPlayerField()[this.fieldIndex]; - - const moveQueue = playerPokemon.getMoveQueue(); - - while ( - moveQueue.length && - moveQueue[0] && - moveQueue[0].move && - !isVirtual(moveQueue[0].useMode) && - (!playerPokemon.getMoveset().find(m => m.moveId === moveQueue[0].move) || - !playerPokemon - .getMoveset() - [playerPokemon.getMoveset().findIndex(m => m.moveId === moveQueue[0].move)].isUsable( - playerPokemon, - isIgnorePP(moveQueue[0].useMode), - )) - ) { - moveQueue.shift(); + if (this.tryExecuteQueuedMove()) { + return; } - // TODO: Refactor this. I did a few simple find/replace matches but this is just ABHORRENTLY structured - if (moveQueue.length > 0) { - const queuedMove = moveQueue[0]; - if (!queuedMove.move) { - this.handleCommand(Command.FIGHT, -1, MoveUseMode.NORMAL); - } else { - const moveIndex = playerPokemon.getMoveset().findIndex(m => m.moveId === queuedMove.move); - if ( - (moveIndex > -1 && - playerPokemon.getMoveset()[moveIndex].isUsable(playerPokemon, isIgnorePP(queuedMove.useMode))) || - isVirtual(queuedMove.useMode) - ) { - this.handleCommand(Command.FIGHT, moveIndex, queuedMove.useMode, queuedMove); - } else { - globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); - } - } + if ( + globalScene.currentBattle.isBattleMysteryEncounter() && + globalScene.currentBattle.mysteryEncounter?.skipToFightInput + ) { + globalScene.ui.clearText(); + globalScene.ui.setMode(UiMode.FIGHT, this.fieldIndex); } else { - if ( - globalScene.currentBattle.isBattleMysteryEncounter() && - globalScene.currentBattle.mysteryEncounter?.skipToFightInput - ) { - globalScene.ui.clearText(); - globalScene.ui.setMode(UiMode.FIGHT, this.fieldIndex); - } else { - globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); - } + globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); } } @@ -201,7 +246,7 @@ export class CommandPhase extends FieldPhase { useMode: MoveUseMode = MoveUseMode.NORMAL, move?: TurnMove, ): boolean { - const playerPokemon = globalScene.getPlayerField()[this.fieldIndex]; + const playerPokemon = this.getPokemon(); const ignorePP = isIgnorePP(useMode); /** Whether or not to display an error message instead of attempting to initiate the command selection process */ @@ -382,7 +427,7 @@ export class CommandPhase extends FieldPhase { * @returns Whether the pokemon is currently trapped */ private handleTrap(): boolean { - const playerPokemon = globalScene.getPlayerField()[this.fieldIndex]; + const playerPokemon = this.getPokemon(); const trappedAbMessages: string[] = []; const isSwitch = this.isSwitch; if (!playerPokemon.isTrapped(trappedAbMessages)) { From 22d2b4a43665543ae386a3ca88df2834482912eb Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Mon, 26 May 2025 16:41:58 -0500 Subject: [PATCH 04/12] Minor touchups --- src/phases/command-phase.ts | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index 943eca87e5a..34ca7bb71e5 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -249,8 +249,7 @@ export class CommandPhase extends FieldPhase { const playerPokemon = this.getPokemon(); const ignorePP = isIgnorePP(useMode); - /** Whether or not to display an error message instead of attempting to initiate the command selection process */ - let canUse = cursor !== -1 || !playerPokemon.trySelectMove(cursor, ignorePP); + let canUse = cursor === -1 || playerPokemon.trySelectMove(cursor, ignorePP); const useStruggle = canUse ? false @@ -291,7 +290,7 @@ export class CommandPhase extends FieldPhase { console.log(moveTargets, getPokemonNameWithAffix(playerPokemon)); - if (moveTargets.multiple) { + if (moveTargets.targets.length > 1 && moveTargets.multiple) { globalScene.phaseManager.unshiftNew("SelectTargetPhase", this.fieldIndex); } @@ -419,7 +418,7 @@ export class CommandPhase extends FieldPhase { } /** - * Common helper method to handle the logic for effects that prevent the pokemon from leaving the field + * Helper method to handle the logic for effects that prevent the pokemon from leaving the field * due to trapping abilities or effects. * * This method queues the proper messages in the case of trapping abilities or effects @@ -478,11 +477,16 @@ export class CommandPhase extends FieldPhase { private tryLeaveField(cursor?: number, isBatonSwitch = false): boolean { const currentBattle = globalScene.currentBattle; - if (isBatonSwitch && !this.handleTrap()) { - currentBattle.turnCommands[this.fieldIndex] = { - command: this.isSwitch ? Command.POKEMON : Command.RUN, - cursor: cursor, - }; + if (isBatonSwitch || !this.handleTrap()) { + currentBattle.turnCommands[this.fieldIndex] = this.isSwitch + ? { + command: Command.POKEMON, + cursor: cursor, + args: [isBatonSwitch], + } + : { + command: Command.RUN, + }; if (!this.isSwitch && this.fieldIndex) { currentBattle.turnCommands[this.fieldIndex - 1]!.skip = true; } @@ -537,7 +541,7 @@ export class CommandPhase extends FieldPhase { ); } - // Overloads for handleCommand to provide a more specific type signature for the different options + // Overloads for handleCommand to provide a more specific signature for the different options handleCommand(command: Command.FIGHT | Command.TERA, cursor: number, useMode?: MoveUseMode, move?: TurnMove): boolean; handleCommand(command: Command.BALL, cursor: number): boolean; handleCommand(command: Command.POKEMON, cursor: number, useBaton: boolean): boolean; From 9218d35938d11813d53a44c48a1fe181cce0a105 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Mon, 26 May 2025 16:49:00 -0500 Subject: [PATCH 05/12] Add overload for handle command --- src/phases/command-phase.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index 34ca7bb71e5..c9a83096359 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -249,6 +249,7 @@ export class CommandPhase extends FieldPhase { const playerPokemon = this.getPokemon(); const ignorePP = isIgnorePP(useMode); + let canUse = cursor === -1 || playerPokemon.trySelectMove(cursor, ignorePP); let canUse = cursor === -1 || playerPokemon.trySelectMove(cursor, ignorePP); const useStruggle = canUse From adb5900497fcc1a6d6afeb892e84e508c6b899f8 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Mon, 26 May 2025 20:40:48 -0500 Subject: [PATCH 06/12] Fix improperly named computeMoveId method --- src/phases/command-phase.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index c9a83096359..024e54f8065 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -230,7 +230,7 @@ export class CommandPhase extends FieldPhase { * * Does not check if the move is usable or not, that should be handled by the caller. */ - private comptueMoveId(playerPokemon: PlayerPokemon, cursor: number, move?: TurnMove): MoveId { + private computeMoveId(playerPokemon: PlayerPokemon, cursor: number, move?: TurnMove): MoveId { return move?.move ?? (cursor > -1 ? playerPokemon.getMoveset()[cursor]?.moveId : MoveId.NONE); } @@ -263,7 +263,7 @@ export class CommandPhase extends FieldPhase { return false; } - const moveId = useStruggle ? MoveId.STRUGGLE : this.comptueMoveId(playerPokemon, cursor, move); + const moveId = useStruggle ? MoveId.STRUGGLE : this.computeMoveId(playerPokemon, cursor, move); const turnCommand: TurnCommand = { command: Command.FIGHT, From e1e7287919513d9017b8e17f142e34d3cd103ef5 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Mon, 26 May 2025 23:13:46 -0500 Subject: [PATCH 07/12] Improve `canUse` computation --- src/phases/command-phase.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index 024e54f8065..85b919148e3 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -252,13 +252,12 @@ export class CommandPhase extends FieldPhase { let canUse = cursor === -1 || playerPokemon.trySelectMove(cursor, ignorePP); let canUse = cursor === -1 || playerPokemon.trySelectMove(cursor, ignorePP); + // Ternary here ensures we don't compute struggle conditions unless necessary const useStruggle = canUse ? false : cursor > -1 && !playerPokemon.getMoveset().some(m => m.isUsable(playerPokemon)); - canUse = canUse || useStruggle; - - if (!canUse) { + if (!canUse && !useStruggle) { this.queueFightErrorMessage(playerPokemon, cursor); return false; } From 1df3ac096254d0dd3587d9c26a986212d24dcf60 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Mon, 26 May 2025 23:18:04 -0500 Subject: [PATCH 08/12] Explicitly check against Moves.NONE Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com> --- src/phases/command-phase.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index 85b919148e3..a8ea0abe5c2 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -284,7 +284,7 @@ export class CommandPhase extends FieldPhase { multiple: move.targets.length > 1, }; - if (!moveId) { + if (moveId === Moves.NONE) { turnCommand.targets = [this.fieldIndex]; } From 26cbf2db8c1d6a3f1485ad53f2b007ba40c39187 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Fri, 6 Jun 2025 10:41:34 -0500 Subject: [PATCH 09/12] Update with Bertie's comments Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com> --- src/phases/command-phase.ts | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index a8ea0abe5c2..ce83c25d314 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -225,7 +225,8 @@ export class CommandPhase extends FieldPhase { ); } - /** Helper method for {@linkcode handleFightCommand} that returns the moveID for the phase + /** + * Helper method for {@linkcode handleFightCommand} that returns the moveID for the phase * based on the move passed in or the cursor. * * Does not check if the move is usable or not, that should be handled by the caller. @@ -235,10 +236,16 @@ export class CommandPhase extends FieldPhase { } /** - * Handle fight logic + * Process the logic for executing a fight-related command + * + * @remarks + * - Validates whether the move can be used, using struggle if not + * - Constructs the turn command and inserts it into the battle's turn commands + * * @param command - The command to handle (FIGHT or TERA) * @param cursor - The index that the cursor is placed on, or -1 if no move can be selected. - * @param args - Any additional arguments to pass to the command + * @param ignorePP - Whether to ignore PP when checking if the move can be used. + * @param move - The move to force the command to use, if any. */ private handleFightCommand( command: Command.FIGHT | Command.TERA, @@ -249,7 +256,6 @@ export class CommandPhase extends FieldPhase { const playerPokemon = this.getPokemon(); const ignorePP = isIgnorePP(useMode); - let canUse = cursor === -1 || playerPokemon.trySelectMove(cursor, ignorePP); let canUse = cursor === -1 || playerPokemon.trySelectMove(cursor, ignorePP); // Ternary here ensures we don't compute struggle conditions unless necessary @@ -257,7 +263,9 @@ export class CommandPhase extends FieldPhase { ? false : cursor > -1 && !playerPokemon.getMoveset().some(m => m.isUsable(playerPokemon)); - if (!canUse && !useStruggle) { + canUse ||= useStruggle; + + if (!canUse) { this.queueFightErrorMessage(playerPokemon, cursor); return false; } @@ -284,7 +292,7 @@ export class CommandPhase extends FieldPhase { multiple: move.targets.length > 1, }; - if (moveId === Moves.NONE) { + if (moveId === MoveId.NONE) { turnCommand.targets = [this.fieldIndex]; } From c0a42985bffbab5369bf797b3adb9246fa653819 Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Tue, 15 Jul 2025 22:12:19 -0700 Subject: [PATCH 10/12] Fix imports --- src/phases/command-phase.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index ce83c25d314..d44681ab12c 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -1,5 +1,4 @@ import type { TurnCommand } from "#app/battle"; -import { BattlerTagType } from "#app/enums/battler-tag-type"; import { globalScene } from "#app/global-scene"; import { getPokemonNameWithAffix } from "#app/messages"; import { speciesStarterCosts } from "#balance/starters"; @@ -8,6 +7,7 @@ import { AbilityId } from "#enums/ability-id"; import { ArenaTagSide } from "#enums/arena-tag-side"; import { ArenaTagType } from "#enums/arena-tag-type"; import { BattleType } from "#enums/battle-type"; +import { BattlerTagType } from "#enums/battler-tag-type"; import { BiomeId } from "#enums/biome-id"; import { Command } from "#enums/command"; import { FieldPosition } from "#enums/field-position"; @@ -19,9 +19,9 @@ import { UiMode } from "#enums/ui-mode"; import type { PlayerPokemon } from "#field/pokemon"; import type { MoveTargetSet } from "#moves/move"; import { getMoveTargets } from "#moves/move-utils"; +import { FieldPhase } from "#phases/field-phase"; import type { TurnMove } from "#types/turn-move"; import i18next from "i18next"; -import { FieldPhase } from "./field-phase"; export class CommandPhase extends FieldPhase { public readonly phaseName = "CommandPhase"; From ed915dcb58c85a9c511b80dba329b65bdde609be Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Wed, 23 Jul 2025 21:15:23 -0600 Subject: [PATCH 11/12] Apply kev's suggestions from code review Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/phases/command-phase.ts | 115 +++++++++++++++++++++--------------- 1 file changed, 69 insertions(+), 46 deletions(-) diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index d44681ab12c..44b365146d9 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -44,19 +44,21 @@ export class CommandPhase extends FieldPhase { * - This is the first turn of a mystery encounter, trainer battle, or the END biome * - The cursor is currently on the POKEMON command */ - private resetCursorIfNeeded() { + private resetCursorIfNeeded(): void { const commandUiHandler = globalScene.ui.handlers[UiMode.COMMAND]; + const { arena, commandCursorMemory, currentBattle } = globalScene; + const { battleType, turn } = currentBattle; + const { biomeType } = arena; // If one of these conditions is true, we always reset the cursor to Command.FIGHT const cursorResetEvent = - globalScene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER || - globalScene.currentBattle.battleType === BattleType.TRAINER || - globalScene.arena.biomeType === BiomeId.END; + battleType === BattleType.MYSTERY_ENCOUNTER || battleType === BattleType.TRAINER || biomeType === BiomeId.END; + if (!commandUiHandler) { + return; + } if ( - (commandUiHandler && - globalScene.currentBattle.turn === 1 && - (!globalScene.commandCursorMemory || cursorResetEvent)) || + (turn === 1 && (!commandCursorMemory || cursorResetEvent)) || commandUiHandler.getCursor() === Command.POKEMON ) { commandUiHandler.setCursor(Command.FIGHT); @@ -67,7 +69,7 @@ export class CommandPhase extends FieldPhase { * Submethod of {@linkcode start} that validates field index logic for nonzero field indices. * Must only be called if the field index is nonzero. */ - private handleFieldIndexLogic() { + private handleFieldIndexLogic(): void { // If we somehow are attempting to check the right pokemon but there's only one pokemon out // Switch back to the center pokemon. This can happen rarely in double battles with mid turn switching // TODO: Prevent this from happening in the first place @@ -85,10 +87,11 @@ export class CommandPhase extends FieldPhase { } } - /** Submethod of {@linkcode start} that sets the turn command to skip if this pokemon is commanding its ally - * via {@linkcode Abilities.COMMANDER}. + /** + * Submethod of {@linkcode start} that sets the turn command to skip if this pokemon + * is commanding its ally via {@linkcode AbilityId.COMMANDER}. */ - private checkCommander() { + private checkCommander(): void { // If the Pokemon has applied Commander's effects to its ally, skip this command if ( globalScene.currentBattle?.double && @@ -104,9 +107,10 @@ export class CommandPhase extends FieldPhase { /** * Clear out all unusable moves in front of the currently acting pokemon's move queue. + * * TODO: Refactor move queue handling to ensure that this method is not necessary. */ - private clearUnusuableMoves() { + private clearUnusuableMoves(): void { const playerPokemon = this.getPokemon(); const moveQueue = playerPokemon.getMoveQueue(); if (moveQueue.length === 0) { @@ -160,7 +164,7 @@ export class CommandPhase extends FieldPhase { return true; } - start() { + public override start(): void { super.start(); globalScene.updateGameInfo(); @@ -178,7 +182,8 @@ export class CommandPhase extends FieldPhase { playerPokemon.lapseTag(BattlerTagType.ENCORE); if (globalScene.currentBattle.turnCommands[this.fieldIndex]?.skip) { - return this.end(); + this.end(); + return; } if (this.tryExecuteQueuedMove()) { @@ -197,7 +202,8 @@ export class CommandPhase extends FieldPhase { } /** - * + * Submethod of {@linkcode handleFightCommand} responsible for queuing the appropriate + * error message when a move cannot be used. * @param user - The pokemon using the move * @param cursor - The index of the move in the moveset */ @@ -231,7 +237,7 @@ export class CommandPhase extends FieldPhase { * * Does not check if the move is usable or not, that should be handled by the caller. */ - private computeMoveId(playerPokemon: PlayerPokemon, cursor: number, move?: TurnMove): MoveId { + private computeMoveId(playerPokemon: PlayerPokemon, cursor: number, move: TurnMove | undefined): MoveId { return move?.move ?? (cursor > -1 ? playerPokemon.getMoveset()[cursor]?.moveId : MoveId.NONE); } @@ -274,12 +280,12 @@ export class CommandPhase extends FieldPhase { const turnCommand: TurnCommand = { command: Command.FIGHT, - cursor: cursor, + cursor, move: { move: moveId, targets: [], useMode }, args: [useMode, move], }; const preTurnCommand: TurnCommand = { - command: command, + command, targets: [this.fieldIndex], skip: command === Command.FIGHT, }; @@ -296,7 +302,14 @@ export class CommandPhase extends FieldPhase { turnCommand.targets = [this.fieldIndex]; } - console.log(moveTargets, getPokemonNameWithAffix(playerPokemon)); + console.log( + "Move:", + MoveId[moveId], + "Move targets:", + moveTargets, + "\nPlayer Pokemon:", + getPokemonNameWithAffix(playerPokemon), + ); if (moveTargets.targets.length > 1 && moveTargets.multiple) { globalScene.phaseManager.unshiftNew("SelectTargetPhase", this.fieldIndex); @@ -325,7 +338,7 @@ export class CommandPhase extends FieldPhase { * Only works for parameterless i18next keys. * @param key - The i18next key for the text to show */ - private queueShowText(key: string) { + private queueShowText(key: string): void { globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); globalScene.ui.setMode(UiMode.MESSAGE); @@ -344,32 +357,35 @@ export class CommandPhase extends FieldPhase { /** * Helper method for {@linkcode handleBallCommand} that checks if the pokeball can be thrown. * - * The pokeball may not be thrown if: + * The pokeball may not be thrown if any of the following are true: * - It is a trainer battle - * - The biome is {@linkcode Biome.END} and it is not classic mode - * - The biome is {@linkcode Biome.END} and the fresh start challenge is active - * - The biome is {@linkcode Biome.END} and the player has not either caught the target before or caught all but one starter + * - The player is in the {@linkcode BiomeId.END | End} biome and + * - it is not classic mode; or + * - the fresh start challenge is active; or + * - the player has not caught the target before and the player is still missing more than one starter * - The player is in a mystery encounter that disallows catching the pokemon * @returns Whether a pokeball can be thrown - * */ private checkCanUseBall(): boolean { + const { arena, currentBattle, gameData, gameMode } = globalScene; + const { battleType } = currentBattle; + const { biomeType } = arena; + const { isClassic } = gameMode; + const { dexData } = gameData; + + const someUncaughtSpeciesOnField = globalScene + .getEnemyField() + .some(p => p.isActive() && !dexData[p.species.speciesId].caughtAttr); + const missingMultipleStarters = + gameData.getStarterCount(d => !!d.caughtAttr) < Object.keys(speciesStarterCosts).length - 1; if ( - globalScene.arena.biomeType === BiomeId.END && - (!globalScene.gameMode.isClassic || - globalScene.gameMode.isFreshStartChallenge() || - (globalScene - .getEnemyField() - .some(p => p.isActive() && !globalScene.gameData.dexData[p.species.speciesId].caughtAttr) && - globalScene.gameData.getStarterCount(d => !!d.caughtAttr) < Object.keys(speciesStarterCosts).length - 1)) + biomeType === BiomeId.END && + (!isClassic || gameMode.isFreshStartChallenge() || (someUncaughtSpeciesOnField && missingMultipleStarters)) ) { this.queueShowText("battle:noPokeballForce"); - } else if (globalScene.currentBattle.battleType === BattleType.TRAINER) { + } else if (battleType === BattleType.TRAINER) { this.queueShowText("battle:noPokeballTrainer"); - } else if ( - globalScene.currentBattle.isBattleMysteryEncounter() && - !globalScene.currentBattle.mysteryEncounter!.catchAllowed - ) { + } else if (currentBattle.isBattleMysteryEncounter() && !currentBattle.mysteryEncounter!.catchAllowed) { this.queueShowText("battle:noPokeballMysteryEncounter"); } else { return true; @@ -398,7 +414,8 @@ export class CommandPhase extends FieldPhase { return false; } - if (cursor < 5) { + const numBallTypes = 5; + if (cursor < numBallTypes) { const targetPokemon = globalScene.getEnemyPokemon(); if ( targetPokemon?.isBoss() && @@ -489,7 +506,7 @@ export class CommandPhase extends FieldPhase { currentBattle.turnCommands[this.fieldIndex] = this.isSwitch ? { command: Command.POKEMON, - cursor: cursor, + cursor, args: [isBatonSwitch], } : { @@ -550,12 +567,6 @@ export class CommandPhase extends FieldPhase { } // Overloads for handleCommand to provide a more specific signature for the different options - handleCommand(command: Command.FIGHT | Command.TERA, cursor: number, useMode?: MoveUseMode, move?: TurnMove): boolean; - handleCommand(command: Command.BALL, cursor: number): boolean; - handleCommand(command: Command.POKEMON, cursor: number, useBaton: boolean): boolean; - handleCommand(command: Command.RUN, cursor: number): boolean; - handleCommand(command: Command, cursor: number, useMode?: boolean | MoveUseMode, move?: TurnMove): boolean; - /** * Process the command phase logic based on the selected command * @@ -563,8 +574,20 @@ export class CommandPhase extends FieldPhase { * @param cursor - The index of option that the cursor is on, or -1 if no option is selected * @param useMode - The mode to use for the move, if applicable. For switches, a boolean that specifies whether the switch is a Baton switch. * @param move - For {@linkcode Command.FIGHT}, the move to use + * @returns Whether the command was successful */ - handleCommand(command: Command, cursor: number, useMode: boolean | MoveUseMode = false, move?: TurnMove): boolean { + handleCommand(command: Command.FIGHT | Command.TERA, cursor: number, useMode?: MoveUseMode, move?: TurnMove): boolean; + handleCommand(command: Command.BALL, cursor: number): boolean; + handleCommand(command: Command.POKEMON, cursor: number, useBaton: boolean): boolean; + handleCommand(command: Command.RUN, cursor: number): boolean; + handleCommand(command: Command, cursor: number, useMode?: boolean | MoveUseMode, move?: TurnMove): boolean; + + public handleCommand( + command: Command, + cursor: number, + useMode: boolean | MoveUseMode = false, + move?: TurnMove, + ): boolean { let success = false; switch (command) { From 0c82b3f514d2624ea1d36e39b5a749f385fa3013 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Thu, 24 Jul 2025 15:14:24 -0600 Subject: [PATCH 12/12] Improve documentation Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com> --- src/phases/command-phase.ts | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index 44b365146d9..016d4ff5d3b 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -107,9 +107,8 @@ export class CommandPhase extends FieldPhase { /** * Clear out all unusable moves in front of the currently acting pokemon's move queue. - * - * TODO: Refactor move queue handling to ensure that this method is not necessary. */ + // TODO: Refactor move queue handling to ensure that this method is not necessary. private clearUnusuableMoves(): void { const playerPokemon = this.getPokemon(); const moveQueue = playerPokemon.getMoveQueue(); @@ -355,8 +354,10 @@ export class CommandPhase extends FieldPhase { } /** - * Helper method for {@linkcode handleBallCommand} that checks if the pokeball can be thrown. + * Helper method for {@linkcode handleBallCommand} that checks if a pokeball can be thrown + * and displays the appropriate error message. * + * @remarks * The pokeball may not be thrown if any of the following are true: * - It is a trainer battle * - The player is in the {@linkcode BiomeId.END | End} biome and @@ -443,10 +444,10 @@ export class CommandPhase extends FieldPhase { } /** - * Helper method to handle the logic for effects that prevent the pokemon from leaving the field + * Submethod of {@linkcode tryLeaveField} to handle the logic for effects that prevent the pokemon from leaving the field * due to trapping abilities or effects. * - * This method queues the proper messages in the case of trapping abilities or effects + * This method queues the proper messages in the case of trapping abilities or effects. * * @returns Whether the pokemon is currently trapped */ @@ -496,8 +497,7 @@ export class CommandPhase extends FieldPhase { * Checks for trapping abilities and effects. * * @param cursor - The index of the option that the cursor is on - * @param isBatonSwitch - Whether the switch command is switching via the Baton item - * @returns whether the pokemon is able to leave the field, indicating the command phase should end + * @returns Whether the pokemon is able to leave the field, indicating the command phase should end */ private tryLeaveField(cursor?: number, isBatonSwitch = false): boolean { const currentBattle = globalScene.currentBattle; @@ -521,6 +521,19 @@ export class CommandPhase extends FieldPhase { return false; } + /** + * Helper method for {@linkcode handleCommand} that handles the logic when the selected command is RUN. + * + * @remarks + * Checks if the player is allowed to flee, and if not, queues the appropriate message. + * + * The player cannot flee if: + * - The player is in the {@linkcode BiomeId.END | End} biome + * - The player is in a trainer battle + * - The player is in a mystery encounter that disallows fleeing + * - The player's pokemon is trapped by an ability or effect + * @returns Whether the pokemon is able to leave the field, indicating the command phase should end + */ private handleRunCommand(): boolean { const { currentBattle, arena } = globalScene; const mysteryEncounterFleeAllowed = currentBattle.mysteryEncounter?.fleeAllowed ?? true;