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 { 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<Moves>(
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))
};
}

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.
* 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}
*/
*/
// # 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 { 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;

View File

@ -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 {

View File

@ -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

View File

@ -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(),
}),

View File

@ -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 => {