From d30995f0402d61ab712662ccbc79f4739f3d4fa0 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Mon, 19 May 2025 22:44:17 -0400 Subject: [PATCH] Added helper functions complete with markdown tables --- src/data/moves/move.ts | 9 ++- src/enums/move-use-type.ts | 70 ++++++++++++++++++- src/field/pokemon.ts | 10 +-- src/phases/command-phase.ts | 12 ++-- src/phases/move-effect-phase.ts | 4 +- src/phases/move-phase.ts | 27 +++---- .../version_migration/versions/v1_10_0.ts | 3 +- 7 files changed, 101 insertions(+), 34 deletions(-) diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index fdaf32a74cc..9fbbda194ec 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -123,7 +123,7 @@ import { MoveEffectTrigger } from "#enums/MoveEffectTrigger"; import { MultiHitType } from "#enums/MultiHitType"; import { invalidAssistMoves, invalidCopycatMoves, invalidMetronomeMoves, invalidMirrorMoveMoves, invalidSleepTalkMoves, invalidSketchMoves } from "./invalid-moves"; import { SelectBiomePhase } from "#app/phases/select-biome-phase"; -import { MoveUseType } from "#enums/move-use-type"; +import { isFollowUp, MoveUseType } from "#enums/move-use-type"; type MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => boolean; type UserMoveConditionFunc = (user: Pokemon, move: Move) => boolean; @@ -7038,7 +7038,7 @@ export class RepeatMoveAttr extends MoveEffectAttr { const lastMove = target.getLastNonVirtualMove(); const movesetMove = target.getMoveset().find(m => m.moveId === lastMove?.move) - // never happens due to condition func, but makes TS compiler not sad + // never happens due to condition func, but makes TS compiler not sad about nullishness if (!lastMove || !movesetMove) { return false; } @@ -7046,7 +7046,7 @@ export class RepeatMoveAttr extends MoveEffectAttr { // If the last move used can hit more than one target or has variable targets, // re-compute the targets for the attack (mainly for alternating double/single battles) // Rampaging moves (e.g. Outrage) are not included due to being incompatible with Instruct, - // nor is Dragon Darts (due to handling its smart targeting entirely within `MoveEffectPhase`) + // nor is Dragon Darts (due to its smart targeting bypassing normal target selection) let moveTargets = movesetMove.getMove().isMultiTarget() ? getMoveTargets(target, lastMove.move).targets : lastMove.targets; // In the event the instructed move's only target is a fainted opponent, redirect it to an alive ally if possible. @@ -7816,12 +7816,11 @@ export class LastResortAttr extends MoveAttr { const movesInHistory = new Set( user.getMoveHistory() - .filter(m => m.useType < MoveUseType.INDIRECT) // Last resort ignores virtual moves + .filter(m => !isFollowUp(m.useType)) // Last resort ignores virtual moves .map(m => m.move) ); // Since `Set.intersection()` is only present in ESNext, we have to do this to check inclusion - // grumble mumble grumble mumble... return [...otherMovesInMoveset].every(m => movesInHistory.has(m)) }; } diff --git a/src/enums/move-use-type.ts b/src/enums/move-use-type.ts index 97ef60f3161..12d7eb8bff9 100644 --- a/src/enums/move-use-type.ts +++ b/src/enums/move-use-type.ts @@ -6,6 +6,10 @@ import type { PostDancingMoveAbAttr } from "#app/data/abilities/ability"; * Each one inherits the properties (or exclusions) of all types preceding it. * Properties newly found on a given use type will be **bolded**, * while oddities breaking a previous trend will be listed in _italics_. + + * Callers should refrain from performing non-equality checks on `MoveUseTypes` directly, + * instead using the available helper functions + * ({@linkcode isFollowUp}, {@linkcode isIgnorePP} and {@linkcode isReflected}). */ export enum MoveUseType { /** @@ -62,4 +66,68 @@ export enum MoveUseType { * Comment block to prevent IDE auto-import removal. * {@linkcode BattlerTagLapseType} * {@linkcode PostDancingMoveAbAttr} - */ \ No newline at end of file + */ + +// # HELPER FUNCTIONS +// Please update the markdown tables if any new `MoveUseType`s get added. + +/** + * Check if a given {@linkcode MoveUseType} is follow-up (i.e. called by another move). + * Follow-up moves are ignored by most moveset-related effects and pre-move cancellation checks. + * @param useType - The {@linkcode MoveUseType} to check. + * @returns Whether {@linkcode useType} is follow-up. + * @remarks + * This function is equivalent to the following truth table: + + * | Use Type | Returns | + * |------------------------------------|---------| + * | {@linkcode MoveUseType.NORMAL} | `false` | + * | {@linkcode MoveUseType.IGNORE_PP} | `false` | + * | {@linkcode MoveUseType.INDIRECT} | `true` | + * | {@linkcode MoveUseType.FOLLOW_UP} | `true` | + * | {@linkcode MoveUseType.REFLECTED} | `true` | + */ +export function isFollowUp(useType: MoveUseType): boolean { + return useType >= MoveUseType.INDIRECT +} + +/** + * Check if a given {@linkcode MoveUseType} should ignore PP. + * PP-ignoring moves will ignore normal PP consumption as well as associated failure checks. + * @param useType - The {@linkcode MoveUseType} to check. + * @returns Whether {@linkcode useType} ignores PP. + * @remarks + * This function is equivalent to the following truth table: + + * | Use Type | Returns | + * |------------------------------------|---------| + * | {@linkcode MoveUseType.NORMAL} | `false` | + * | {@linkcode MoveUseType.IGNORE_PP} | `true` | + * | {@linkcode MoveUseType.INDIRECT} | `true` | + * | {@linkcode MoveUseType.FOLLOW_UP} | `true` | + * | {@linkcode MoveUseType.REFLECTED} | `true` | + */ +export function isIgnorePP(useType: MoveUseType): boolean { + return useType >= MoveUseType.IGNORE_PP; +} + +/** + * Check if a given {@linkcode MoveUseType} is reflected. + * Reflected moves cannot be reflected, copied, or cancelled by status effects, + * nor will they trigger {@linkcode PostDancingMoveAbAttr | Dancer}. + * @param useType - The {@linkcode MoveUseType} to check. + * @returns Whether {@linkcode useType} is reflected. + * @remarks + * This function is equivalent to the following truth table: + * + * | Use Type | Returns | + * |------------------------------------|---------| + * | {@linkcode MoveUseType.NORMAL} | `false` | + * | {@linkcode MoveUseType.IGNORE_PP} | `false` | + * | {@linkcode MoveUseType.INDIRECT} | `false` | + * | {@linkcode MoveUseType.FOLLOW_UP} | `false` | + * | {@linkcode MoveUseType.REFLECTED} | `true` | + */ +export function isReflected(useType: MoveUseType): boolean { + return useType === MoveUseType.REFLECTED; +} diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 58f1f5a6c76..15c23e96500 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -260,7 +260,7 @@ import { MoveFlags } from "#enums/MoveFlags"; import { timedEventManager } from "#app/global-event-manager"; import { loadMoveAnimations } from "#app/sprites/pokemon-asset-loader"; import { ResetStatusPhase } from "#app/phases/reset-status-phase"; -import { MoveUseType } from "#enums/move-use-type"; +import { isFollowUp, isIgnorePP, MoveUseType } from "#enums/move-use-type"; export enum LearnMoveSituation { MISC, @@ -5164,16 +5164,16 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * - Non-virtual ({@linkcode MoveUseType | useType} < {@linkcode MoveUseType.INDIRECT}) * @param ignoreStruggle - Whether to additionally ignore {@linkcode Moves.STRUGGLE}; default `false` * @param ignoreFollowUp - Whether to ignore moves with a use type of {@linkcode MoveUseType.FOLLOW_UP} - * (Copycat, Mirror Move, etc.); default `true`. + * (e.g. ones called by Copycat/Mirror Move); default `true`. * @returns The last move this Pokemon has used satisfying the aforementioned conditions, * or `undefined` if no applicable moves have been used since switching in. */ getLastNonVirtualMove(ignoreStruggle = false, ignoreFollowUp = true): TurnMove | undefined { return this.getLastXMoves(-1).find(m => m.move !== Moves.NONE + && (!ignoreStruggle || m.move !== Moves.STRUGGLE) && (m.useType < MoveUseType.INDIRECT || (!ignoreFollowUp && m.useType === MoveUseType.FOLLOW_UP)) - && (!ignoreStruggle || m.move !== Moves.STRUGGLE) ); } @@ -7270,11 +7270,11 @@ export class EnemyPokemon extends Pokemon { // Otherwise, ensure that the move being used is actually usable // TODO: Virtual moves shouldn't use the move queue if ( - queuedMove.useType >= MoveUseType.INDIRECT || + isFollowUp(queuedMove.useType) || (moveIndex > -1 && this.getMoveset()[moveIndex].isUsable( this, - queuedMove.useType >= MoveUseType.IGNORE_PP) + isIgnorePP(queuedMove.useType)) ) ) { return queuedMove; diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index 4b7d66d1a29..d03e2b84afb 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -23,7 +23,7 @@ import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; import { isNullOrUndefined } from "#app/utils/common"; import { ArenaTagSide } from "#app/data/arena-tag"; import { ArenaTagType } from "#app/enums/arena-tag-type"; -import { MoveUseType } from "#enums/move-use-type"; +import { isFollowUp, isIgnorePP, MoveUseType } from "#enums/move-use-type"; export class CommandPhase extends FieldPhase { protected fieldIndex: number; @@ -104,13 +104,13 @@ export class CommandPhase extends FieldPhase { moveQueue.length && moveQueue[0] && moveQueue[0].move && - moveQueue[0].useType < MoveUseType.INDIRECT && + !isFollowUp(moveQueue[0].useType) && (!playerPokemon.getMoveset().find(m => m.moveId === moveQueue[0].move) || !playerPokemon .getMoveset() [playerPokemon.getMoveset().findIndex(m => m.moveId === moveQueue[0].move)].isUsable( playerPokemon, - moveQueue[0].useType >= MoveUseType.IGNORE_PP, + isIgnorePP(moveQueue[0].useType), )) ) { moveQueue.shift(); @@ -125,10 +125,8 @@ export class CommandPhase extends FieldPhase { const moveIndex = playerPokemon.getMoveset().findIndex(m => m.moveId === queuedMove.move); if ( (moveIndex > -1 && - playerPokemon - .getMoveset() - [moveIndex].isUsable(playerPokemon, queuedMove.useType >= MoveUseType.IGNORE_PP)) || - queuedMove.useType >= MoveUseType.INDIRECT + playerPokemon.getMoveset()[moveIndex].isUsable(playerPokemon, isIgnorePP(queuedMove.useType))) || + isFollowUp(queuedMove.useType) ) { this.handleCommand(Command.FIGHT, moveIndex, queuedMove.useType, queuedMove); } else { diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index 2ccb9e739d3..af522405a69 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -78,7 +78,7 @@ import type Move from "#app/data/moves/move"; import { isFieldTargeted } from "#app/data/moves/move-utils"; import { FaintPhase } from "./faint-phase"; import { DamageAchv } from "#app/system/achv"; -import { MoveUseType } from "#enums/move-use-type"; +import { isFollowUp, MoveUseType } from "#enums/move-use-type"; export type HitCheckEntry = [HitCheckResult, TypeDamageMultiplier]; @@ -302,7 +302,7 @@ export class MoveEffectPhase extends PokemonPhase { this.getFirstTarget() ?? null, move, overridden, - this.useType >= MoveUseType.INDIRECT, + isFollowUp(this.useType), ); // If other effects were overriden, stop this phase before they can be applied diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index eab47a6a722..8b547a260e1 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -50,7 +50,7 @@ import { BattlerTagType } from "#enums/battler-tag-type"; import { Moves } from "#enums/moves"; import { StatusEffect } from "#enums/status-effect"; import i18next from "i18next"; -import { MoveUseType } from "#enums/move-use-type"; +import { isFollowUp, isIgnorePP, isReflected, MoveUseType } from "#enums/move-use-type"; export class MovePhase extends BattlePhase { protected _pokemon: Pokemon; @@ -123,7 +123,7 @@ export class MovePhase extends BattlePhase { public canMove(ignoreDisableTags = false): boolean { return ( this.pokemon.isActive(true) && - this.move.isUsable(this.pokemon, this.useType >= MoveUseType.IGNORE_PP, ignoreDisableTags) && + this.move.isUsable(this.pokemon, isIgnorePP(this.useType), ignoreDisableTags) && this.targets.length > 0 ); } @@ -151,9 +151,14 @@ export class MovePhase extends BattlePhase { console.log(Moves[this.move.moveId], MoveUseType[this.useType]); - // Check if move is unusable (e.g. running out of PP due to a mid-turn Spite - // or the user no longer being on field). + if (!this.useType) { + console.warn(`Unexpected MoveUseType of ${this.useType} during move phase!`); + this.useType = MoveUseType.NORMAL; + } + if (!this.canMove(true)) { + // Check if move is unusable (e.g. running out of PP due to a mid-turn Spite + // or the user no longer being on field). if (this.pokemon.isActive(true)) { this.fail(); this.showMoveText(); @@ -166,7 +171,7 @@ export class MovePhase extends BattlePhase { this.pokemon.turnData.acted = true; // Reset hit-related turn data when starting follow-up moves (e.g. Metronomed moves, Dancer repeats) - if (this.useType >= MoveUseType.INDIRECT) { + if (isFollowUp(this.useType)) { this.pokemon.turnData.hitsLeft = -1; this.pokemon.turnData.hitCount = 0; } @@ -176,7 +181,7 @@ export class MovePhase extends BattlePhase { this.move.getMove().doesFlagEffectApply({ flag: MoveFlags.IGNORE_ABILITIES, user: this.pokemon, - isFollowUp: this.useType >= MoveUseType.INDIRECT, // Sunsteel strike and co. don't work when called indirectly + isFollowUp: isFollowUp(this.useType), // Sunsteel strike and co. don't work when called indirectly }) ) { globalScene.arena.setIgnoreAbilities(true, this.pokemon.getBattlerIndex()); @@ -376,7 +381,7 @@ export class MovePhase extends BattlePhase { this.pokemon.lapseTag(BattlerTagType.CHARGING); } - if (this.useType < MoveUseType.IGNORE_PP) { + if (!isIgnorePP(this.useType)) { // "commit" to using the move, deducting PP. const ppUsed = 1 + this.getPpIncreaseFromPressure(targets); @@ -506,11 +511,7 @@ export class MovePhase extends BattlePhase { */ public end(): void { globalScene.unshiftPhase( - new MoveEndPhase( - this.pokemon.getBattlerIndex(), - this.getActiveTargetPokemon(), - this.useType >= MoveUseType.INDIRECT, - ), + new MoveEndPhase(this.pokemon.getBattlerIndex(), this.getActiveTargetPokemon(), isFollowUp(this.useType)), ); super.end(); @@ -681,7 +682,7 @@ export class MovePhase extends BattlePhase { } globalScene.queueMessage( - i18next.t(this.useType === MoveUseType.REFLECTED ? "battle:magicCoatActivated" : "battle:useMove", { + i18next.t(isReflected(this.useType) ? "battle:magicCoatActivated" : "battle:useMove", { pokemonNameWithAffix: getPokemonNameWithAffix(this.pokemon), moveName: this.move.getName(), }), diff --git a/src/system/version_migration/versions/v1_10_0.ts b/src/system/version_migration/versions/v1_10_0.ts index 9fb95f003a6..ba6b8fa0355 100644 --- a/src/system/version_migration/versions/v1_10_0.ts +++ b/src/system/version_migration/versions/v1_10_0.ts @@ -29,7 +29,8 @@ const fixMoveHistory: SessionSaveMigrator = { targets: tm.targets, result: tm.result, turn: tm.turn, - // NOTE: This currently mis-classifies Dancer and Magic Bounce-induced moves, but not much we can do about it tbh + // NOTE: This unfortuately has to mis-classify Dancer and Magic Bounce-induced moves as `FOLLOW_UP`, + // given we previously had _no way_ of distinguishing them from follow-up moves post hoc. useType: tm.virtual ? MoveUseType.FOLLOW_UP : tm.ignorePP ? MoveUseType.IGNORE_PP : MoveUseType.NORMAL, }); data.party.forEach(pkmn => {