Added helper functions complete with markdown tables

This commit is contained in:
Bertie690 2025-05-19 22:44:17 -04:00
parent 4a6b392c3f
commit d30995f040
7 changed files with 101 additions and 34 deletions

View File

@ -123,7 +123,7 @@ import { MoveEffectTrigger } from "#enums/MoveEffectTrigger";
import { MultiHitType } from "#enums/MultiHitType"; import { MultiHitType } from "#enums/MultiHitType";
import { invalidAssistMoves, invalidCopycatMoves, invalidMetronomeMoves, invalidMirrorMoveMoves, invalidSleepTalkMoves, invalidSketchMoves } from "./invalid-moves"; import { invalidAssistMoves, invalidCopycatMoves, invalidMetronomeMoves, invalidMirrorMoveMoves, invalidSleepTalkMoves, invalidSketchMoves } from "./invalid-moves";
import { SelectBiomePhase } from "#app/phases/select-biome-phase"; 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 MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => boolean;
type UserMoveConditionFunc = (user: 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 lastMove = target.getLastNonVirtualMove();
const movesetMove = target.getMoveset().find(m => m.moveId === lastMove?.move) 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) { if (!lastMove || !movesetMove) {
return false; 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, // 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) // 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, // 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; 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. // 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<Moves>( const movesInHistory = new Set<Moves>(
user.getMoveHistory() 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) .map(m => m.move)
); );
// Since `Set.intersection()` is only present in ESNext, we have to do this to check inclusion // 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)) return [...otherMovesInMoveset].every(m => movesInHistory.has(m))
}; };
} }

View File

@ -6,6 +6,10 @@ import type { PostDancingMoveAbAttr } from "#app/data/abilities/ability";
* Each one inherits the properties (or exclusions) of all types preceding it. * Each one inherits the properties (or exclusions) of all types preceding it.
* Properties newly found on a given use type will be **bolded**, * Properties newly found on a given use type will be **bolded**,
* while oddities breaking a previous trend will be listed in _italics_. * 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 { export enum MoveUseType {
/** /**
@ -62,4 +66,68 @@ export enum MoveUseType {
* Comment block to prevent IDE auto-import removal. * Comment block to prevent IDE auto-import removal.
* {@linkcode BattlerTagLapseType} * {@linkcode BattlerTagLapseType}
* {@linkcode PostDancingMoveAbAttr} * {@linkcode PostDancingMoveAbAttr}
*/ */
// # 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;
}

View File

@ -260,7 +260,7 @@ import { MoveFlags } from "#enums/MoveFlags";
import { timedEventManager } from "#app/global-event-manager"; import { timedEventManager } from "#app/global-event-manager";
import { loadMoveAnimations } from "#app/sprites/pokemon-asset-loader"; import { loadMoveAnimations } from "#app/sprites/pokemon-asset-loader";
import { ResetStatusPhase } from "#app/phases/reset-status-phase"; 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 { export enum LearnMoveSituation {
MISC, MISC,
@ -5164,16 +5164,16 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* - Non-virtual ({@linkcode MoveUseType | useType} < {@linkcode MoveUseType.INDIRECT}) * - Non-virtual ({@linkcode MoveUseType | useType} < {@linkcode MoveUseType.INDIRECT})
* @param ignoreStruggle - Whether to additionally ignore {@linkcode Moves.STRUGGLE}; default `false` * @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} * @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, * @returns The last move this Pokemon has used satisfying the aforementioned conditions,
* or `undefined` if no applicable moves have been used since switching in. * or `undefined` if no applicable moves have been used since switching in.
*/ */
getLastNonVirtualMove(ignoreStruggle = false, ignoreFollowUp = true): TurnMove | undefined { getLastNonVirtualMove(ignoreStruggle = false, ignoreFollowUp = true): TurnMove | undefined {
return this.getLastXMoves(-1).find(m => return this.getLastXMoves(-1).find(m =>
m.move !== Moves.NONE m.move !== Moves.NONE
&& (!ignoreStruggle || m.move !== Moves.STRUGGLE)
&& (m.useType < MoveUseType.INDIRECT || && (m.useType < MoveUseType.INDIRECT ||
(!ignoreFollowUp && m.useType === MoveUseType.FOLLOW_UP)) (!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 // Otherwise, ensure that the move being used is actually usable
// TODO: Virtual moves shouldn't use the move queue // TODO: Virtual moves shouldn't use the move queue
if ( if (
queuedMove.useType >= MoveUseType.INDIRECT || isFollowUp(queuedMove.useType) ||
(moveIndex > -1 && (moveIndex > -1 &&
this.getMoveset()[moveIndex].isUsable( this.getMoveset()[moveIndex].isUsable(
this, this,
queuedMove.useType >= MoveUseType.IGNORE_PP) isIgnorePP(queuedMove.useType))
) )
) { ) {
return queuedMove; return queuedMove;

View File

@ -23,7 +23,7 @@ import { MysteryEncounterMode } from "#enums/mystery-encounter-mode";
import { isNullOrUndefined } from "#app/utils/common"; import { isNullOrUndefined } from "#app/utils/common";
import { ArenaTagSide } from "#app/data/arena-tag"; import { ArenaTagSide } from "#app/data/arena-tag";
import { ArenaTagType } from "#app/enums/arena-tag-type"; 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 { export class CommandPhase extends FieldPhase {
protected fieldIndex: number; protected fieldIndex: number;
@ -104,13 +104,13 @@ export class CommandPhase extends FieldPhase {
moveQueue.length && moveQueue.length &&
moveQueue[0] && moveQueue[0] &&
moveQueue[0].move && moveQueue[0].move &&
moveQueue[0].useType < MoveUseType.INDIRECT && !isFollowUp(moveQueue[0].useType) &&
(!playerPokemon.getMoveset().find(m => m.moveId === moveQueue[0].move) || (!playerPokemon.getMoveset().find(m => m.moveId === moveQueue[0].move) ||
!playerPokemon !playerPokemon
.getMoveset() .getMoveset()
[playerPokemon.getMoveset().findIndex(m => m.moveId === moveQueue[0].move)].isUsable( [playerPokemon.getMoveset().findIndex(m => m.moveId === moveQueue[0].move)].isUsable(
playerPokemon, playerPokemon,
moveQueue[0].useType >= MoveUseType.IGNORE_PP, isIgnorePP(moveQueue[0].useType),
)) ))
) { ) {
moveQueue.shift(); moveQueue.shift();
@ -125,10 +125,8 @@ export class CommandPhase extends FieldPhase {
const moveIndex = playerPokemon.getMoveset().findIndex(m => m.moveId === queuedMove.move); const moveIndex = playerPokemon.getMoveset().findIndex(m => m.moveId === queuedMove.move);
if ( if (
(moveIndex > -1 && (moveIndex > -1 &&
playerPokemon playerPokemon.getMoveset()[moveIndex].isUsable(playerPokemon, isIgnorePP(queuedMove.useType))) ||
.getMoveset() isFollowUp(queuedMove.useType)
[moveIndex].isUsable(playerPokemon, queuedMove.useType >= MoveUseType.IGNORE_PP)) ||
queuedMove.useType >= MoveUseType.INDIRECT
) { ) {
this.handleCommand(Command.FIGHT, moveIndex, queuedMove.useType, queuedMove); this.handleCommand(Command.FIGHT, moveIndex, queuedMove.useType, queuedMove);
} else { } else {

View File

@ -78,7 +78,7 @@ import type Move from "#app/data/moves/move";
import { isFieldTargeted } from "#app/data/moves/move-utils"; import { isFieldTargeted } from "#app/data/moves/move-utils";
import { FaintPhase } from "./faint-phase"; import { FaintPhase } from "./faint-phase";
import { DamageAchv } from "#app/system/achv"; 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]; export type HitCheckEntry = [HitCheckResult, TypeDamageMultiplier];
@ -302,7 +302,7 @@ export class MoveEffectPhase extends PokemonPhase {
this.getFirstTarget() ?? null, this.getFirstTarget() ?? null,
move, move,
overridden, overridden,
this.useType >= MoveUseType.INDIRECT, isFollowUp(this.useType),
); );
// If other effects were overriden, stop this phase before they can be applied // If other effects were overriden, stop this phase before they can be applied

View File

@ -50,7 +50,7 @@ import { BattlerTagType } from "#enums/battler-tag-type";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { StatusEffect } from "#enums/status-effect"; import { StatusEffect } from "#enums/status-effect";
import i18next from "i18next"; 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 { export class MovePhase extends BattlePhase {
protected _pokemon: Pokemon; protected _pokemon: Pokemon;
@ -123,7 +123,7 @@ export class MovePhase extends BattlePhase {
public canMove(ignoreDisableTags = false): boolean { public canMove(ignoreDisableTags = false): boolean {
return ( return (
this.pokemon.isActive(true) && 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 this.targets.length > 0
); );
} }
@ -151,9 +151,14 @@ export class MovePhase extends BattlePhase {
console.log(Moves[this.move.moveId], MoveUseType[this.useType]); 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 if (!this.useType) {
// or the user no longer being on field). console.warn(`Unexpected MoveUseType of ${this.useType} during move phase!`);
this.useType = MoveUseType.NORMAL;
}
if (!this.canMove(true)) { 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)) { if (this.pokemon.isActive(true)) {
this.fail(); this.fail();
this.showMoveText(); this.showMoveText();
@ -166,7 +171,7 @@ export class MovePhase extends BattlePhase {
this.pokemon.turnData.acted = true; this.pokemon.turnData.acted = true;
// Reset hit-related turn data when starting follow-up moves (e.g. Metronomed moves, Dancer repeats) // 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.hitsLeft = -1;
this.pokemon.turnData.hitCount = 0; this.pokemon.turnData.hitCount = 0;
} }
@ -176,7 +181,7 @@ export class MovePhase extends BattlePhase {
this.move.getMove().doesFlagEffectApply({ this.move.getMove().doesFlagEffectApply({
flag: MoveFlags.IGNORE_ABILITIES, flag: MoveFlags.IGNORE_ABILITIES,
user: this.pokemon, 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()); globalScene.arena.setIgnoreAbilities(true, this.pokemon.getBattlerIndex());
@ -376,7 +381,7 @@ export class MovePhase extends BattlePhase {
this.pokemon.lapseTag(BattlerTagType.CHARGING); this.pokemon.lapseTag(BattlerTagType.CHARGING);
} }
if (this.useType < MoveUseType.IGNORE_PP) { if (!isIgnorePP(this.useType)) {
// "commit" to using the move, deducting PP. // "commit" to using the move, deducting PP.
const ppUsed = 1 + this.getPpIncreaseFromPressure(targets); const ppUsed = 1 + this.getPpIncreaseFromPressure(targets);
@ -506,11 +511,7 @@ export class MovePhase extends BattlePhase {
*/ */
public end(): void { public end(): void {
globalScene.unshiftPhase( globalScene.unshiftPhase(
new MoveEndPhase( new MoveEndPhase(this.pokemon.getBattlerIndex(), this.getActiveTargetPokemon(), isFollowUp(this.useType)),
this.pokemon.getBattlerIndex(),
this.getActiveTargetPokemon(),
this.useType >= MoveUseType.INDIRECT,
),
); );
super.end(); super.end();
@ -681,7 +682,7 @@ export class MovePhase extends BattlePhase {
} }
globalScene.queueMessage( 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), pokemonNameWithAffix: getPokemonNameWithAffix(this.pokemon),
moveName: this.move.getName(), moveName: this.move.getName(),
}), }),

View File

@ -29,7 +29,8 @@ const fixMoveHistory: SessionSaveMigrator = {
targets: tm.targets, targets: tm.targets,
result: tm.result, result: tm.result,
turn: tm.turn, 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, useType: tm.virtual ? MoveUseType.FOLLOW_UP : tm.ignorePP ? MoveUseType.IGNORE_PP : MoveUseType.NORMAL,
}); });
data.party.forEach(pkmn => { data.party.forEach(pkmn => {