pokerogue/src/phases/move-phase.ts

1049 lines
38 KiB
TypeScript

// biome-ignore-start lint/correctness/noUnusedImports: Used in a tsdoc comment
import type { Move, PreUseInterruptAttr } from "#types/move-types";
// biome-ignore-end lint/correctness/noUnusedImports: Used in a tsdoc comment
import { applyAbAttrs } from "#abilities/apply-ab-attrs";
import { globalScene } from "#app/global-scene";
import { getPokemonNameWithAffix } from "#app/messages";
import Overrides from "#app/overrides";
import { PokemonPhase } from "#app/phases/pokemon-phase";
import { CenterOfAttentionTag } from "#data/battler-tags";
import { SpeciesFormChangePreMoveTrigger } from "#data/form-change-triggers";
import { getStatusEffectActivationText } from "#data/status-effect";
import { getTerrainBlockMessage } from "#data/terrain";
import { getWeatherBlockMessage } from "#data/weather";
import { AbilityId } from "#enums/ability-id";
import { ArenaTagSide } from "#enums/arena-tag-side";
import { ArenaTagType } from "#enums/arena-tag-type";
import { BattlerIndex } from "#enums/battler-index";
import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type";
import { BattlerTagType } from "#enums/battler-tag-type";
import { ChallengeType } from "#enums/challenge-type";
import { CommonAnim } from "#enums/move-anims-common";
import { MoveFlags } from "#enums/move-flags";
import { MoveId } from "#enums/move-id";
import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier";
import { MoveResult } from "#enums/move-result";
import { isIgnorePP, isIgnoreStatus, isReflected, isVirtual, MoveUseMode } from "#enums/move-use-mode";
import { PokemonType } from "#enums/pokemon-type";
import { StatusEffect } from "#enums/status-effect";
import { MoveUsedEvent } from "#events/battle-scene";
import type { Pokemon } from "#field/pokemon";
import { applyMoveAttrs } from "#moves/apply-attrs";
import { frenzyMissFunc } from "#moves/move-utils";
import type { PokemonMove } from "#moves/pokemon-move";
import type { TurnMove } from "#types/turn-move";
import { applyChallenges } from "#utils/challenge-utils";
import { BooleanHolder, NumberHolder } from "#utils/common";
import { enumValueToKey } from "#utils/enums";
import { inSpeedOrder } from "#utils/speed-order-generator";
import i18next from "i18next";
export class MovePhase extends PokemonPhase {
public readonly phaseName = "MovePhase";
protected _pokemon: Pokemon;
public move: PokemonMove;
protected _targets: BattlerIndex[];
public readonly useMode: MoveUseMode; // Made public for quash
/** The timing modifier of the move (used by Quash and to force called moves to the front of their queue) */
public timingModifier: MovePhaseTimingModifier;
/** Whether the current move should fail but still use PP. */
protected failed = false;
/** Whether the current move should fail and retain PP. */
protected cancelled = false;
/** Flag set to `true` during {@linkcode checkFreeze} that indicates that the pokemon will thaw if it passes the failure conditions */
private declare thaw?: boolean;
/** The move history entry object that is pushed to the pokemon's move history
*
* @remarks
* Can be edited _after_ being pushed to the history to adjust the result, targets, etc, for this move phase.
*/
protected readonly moveHistoryEntry: TurnMove;
public get pokemon(): Pokemon {
return this._pokemon;
}
// TODO: Do we need public getters but only protected setters?
protected set pokemon(pokemon: Pokemon) {
this._pokemon = pokemon;
}
public get targets(): BattlerIndex[] {
return this._targets;
}
protected set targets(targets: BattlerIndex[]) {
this._targets = targets;
}
/**
* Create a new MovePhase for using moves.
* @param pokemon - The {@linkcode Pokemon} using the move
* @param move - The {@linkcode PokemonMove} to use
* @param useMode - The {@linkcode MoveUseMode} corresponding to this move's means of execution (usually `MoveUseMode.NORMAL`).
* Not marked optional to ensure callers correctly pass on `useModes`.
* @param timingModifier - The {@linkcode MovePhaseTimingModifier} for the move; Default {@linkcode MovePhaseTimingModifier.NORMAL}
*/
constructor(
pokemon: Pokemon,
targets: BattlerIndex[],
move: PokemonMove,
useMode: MoveUseMode,
timingModifier: MovePhaseTimingModifier = MovePhaseTimingModifier.NORMAL,
) {
super(pokemon.getBattlerIndex());
this.pokemon = pokemon;
this.targets = targets;
this.move = move;
this.useMode = useMode;
this.timingModifier = timingModifier;
this.moveHistoryEntry = {
move: MoveId.NONE,
targets,
useMode,
};
}
/** Signifies the current move should fail but still use PP */
public fail(): void {
this.moveHistoryEntry.result = MoveResult.FAIL;
this.failed = true;
}
/** Signifies the current move should cancel and retain PP */
public cancel(): void {
this.cancelled = true;
this.moveHistoryEntry.result = MoveResult.FAIL;
}
/**
* Check the first round of failure checks
*
* @returns Whether the move failed
*
* @remarks
* Based on battle mechanics research conducted primarily by Smogon, checks happen in the following order (as of Gen 9):
* 1. Sleep/Freeze
* 2. Disobedience due to overleveled (not implemented in Pokerogue)
* 3. Insufficient PP after being selected
* 4. (Pokerogue specific) Moves disabled because they are not implemented / prevented from a challenge / somehow have no targets
* 5. Sky battle (see {@linkcode https://github.com/pagefaultgames/pokerogue/pull/5983 | PR#5983})
* 6. Truant
* 7. Focus Punch's loss of focus
* 8. Flinch
* 9. Move was disabled after being selected
* 10. Healing move with heal block
* 11. Sound move with throat chop
* 12. Failure due to gravity
* 13. Move lock from choice items / gorilla tactics
* 14. Failure from taunt
* 15. Failure from imprison
* 16. Failure from confusion
* 17. Failure from paralysis
* 18. Failure from infatuation
*/
protected firstFailureCheck(): boolean {
// A big if statement will handle the checks (that each have side effects!) in the correct order
return (
this.checkSleep()
|| this.checkFreeze()
|| this.checkPP()
|| this.checkValidity()
|| this.checkTagCancel(BattlerTagType.TRUANT)
|| this.checkPreUseInterrupt()
|| this.checkTagCancel(BattlerTagType.FLINCHED)
|| this.checkTagCancel(BattlerTagType.DISABLED)
|| this.checkTagCancel(BattlerTagType.HEAL_BLOCK)
|| this.checkTagCancel(BattlerTagType.THROAT_CHOPPED)
|| this.checkGravity()
|| this.checkTagCancel(BattlerTagType.TAUNT)
|| this.checkTagCancel(BattlerTagType.IMPRISON)
|| this.checkTagCancel(BattlerTagType.CONFUSED)
|| this.checkPara()
|| this.checkTagCancel(BattlerTagType.INFATUATED)
);
}
/**
* Follow up moves need to check a subset of the first failure checks
*
* @remarks
*
* Based on smogon battle mechanics research, checks happen in the following order:
* 1. Invalid move (skipped in pokerogue)
* 2. Move prevented by heal block
* 3. Move prevented by throat chop
* 4. Gravity
* 5. Sky Battle (See {@link https://github.com/pagefaultgames/pokerogue/pull/5983 | PR#5983})
*/
protected followUpMoveFirstFailureCheck(): boolean {
return (
this.checkTagCancel(BattlerTagType.HEAL_BLOCK)
|| this.checkTagCancel(BattlerTagType.THROAT_CHOPPED)
|| this.checkGravity()
);
}
/**
* Handle the status interactions for sleep and freeze that happen after passing the first failure check
*
* @remarks
* - If the user is asleep but can use the move, the sleep animation and message is still shown
* - If the user is frozen but is thawed from its move, the user's status is cured and the thaw message is shown
*/
private doThawCheck(): void {
const user = this.pokemon;
if (isIgnoreStatus(this.useMode)) {
return;
}
if (this.thaw) {
user.cureStatus(
StatusEffect.FREEZE,
i18next.t("statusEffect:freeze.healByMove", {
pokemonNameWithAffix: getPokemonNameWithAffix(user),
moveName: this.move.getMove().name,
}),
);
}
}
/**
* Second failure check that occurs after the "Pokemon used move" text is shown but BEFORE the move has been registered
* as being the last move used (for the purposes of something like Copycat)
*
* @remarks
* Other than powder, each failure condition is mutually exclusive (as they are tied to specific moves), so order does not matter.
* Notably, this failure check only includes failure conditions intrinsic to the move itself, other than Powder (which marks the end of this failure check)
*
*
* - Pollen puff used on an ally that is under effect of heal block
* - Burn up / Double shock when the user does not have the required type
* - No Retreat while already under its effects
* - Failure due to primal weather
* - (on cart, not applicable to Pokerogue) Moves that fail if used ON a raid / special boss: selfdestruct/explosion/imprision/power split / guard split
* - (on cart, not applicable to Pokerogue) Moves that fail during a "co-op" battle (like when Arven helps during raid boss): ally switch / teatime
*
* After all checks, Powder causing the user to explode
*/
protected secondFailureCheck(): boolean {
const move = this.move.getMove();
const user = this.pokemon;
let failedText: string | undefined;
const arena = globalScene.arena;
if (!move.applyConditions(user, this.getActiveTargetPokemon()[0], 2)) {
// TODO: Make pollen puff failing from heal block use its own message
this.failed = true;
} else if (arena.isMoveWeatherCancelled(user, move)) {
failedText = getWeatherBlockMessage(globalScene.arena.getWeatherType());
this.failed = true;
} else {
// Powder *always* happens last
// Note: Powder's lapse method handles everything: messages, damage, animation, primal weather interaction,
// determining type of type changing moves, etc.
// It will set this phase's `failed` flag to true if it procs
user.lapseTag(BattlerTagType.POWDER, BattlerTagLapseType.PRE_MOVE);
return this.failed;
}
if (this.failed) {
this.showFailedText(failedText);
}
return this.failed;
}
/**
* Third failure check is from moves and abilities themselves
*
* @returns Whether the move failed
*
* @remarks
* - Anything in {@linkcode Move.conditionsSeq3}
* - Weather blocking the move
* - Terrain blocking the move
* - Queenly Majesty / Dazzling
* - Damp (which is handled by move conditions in pokerogue rather than the ability, like queenly majesty / dazzling)
*
* The rest of the failure conditions are marked as sequence 4 and *should* happen in the move effect phase (though happen here for now)
*/
protected thirdFailureCheck(): boolean {
/**
* Move conditions assume the move has a single target
* TODO: is this sustainable?
*/
const move = this.move.getMove();
const targets = this.getActiveTargetPokemon();
const arena = globalScene.arena;
const user = this.pokemon;
const failsConditions = !move.applyConditions(user, targets[0], 3);
const failedDueToTerrain = arena.isMoveTerrainCancelled(user, this.targets, move);
let failed = failsConditions || failedDueToTerrain;
// Apply queenly majesty / dazzling
if (!failed) {
const defendingSidePlayField = user.isPlayer() ? globalScene.getEnemyField() : globalScene.getPlayerField();
const cancelled = new BooleanHolder(false);
defendingSidePlayField.forEach((pokemon: Pokemon) => {
applyAbAttrs("FieldPriorityMoveImmunityAbAttr", {
pokemon,
opponent: user,
move,
cancelled,
});
});
failed = cancelled.value;
}
if (failed) {
this.failMove(failedDueToTerrain);
return true;
}
return false;
}
/**
* Modifies `this.targets` in place, based upon:
* - Move redirection abilities, effects, etc.
* - Counterattacks, which pass a special value into the `targets` constructor param (`[`{@linkcode BattlerIndex.ATTACKER}`]`).
*/
protected resolveRedirectTarget(): void {
if (this.targets.length !== 1) {
// Spread moves cannot be redirected
return;
}
const currentTarget = this.targets[0];
const redirectTarget = new NumberHolder(currentTarget);
// check move redirection abilities of every pokemon *except* the user.
// TODO: Make storm drain, lightning rod, etc, redirect at this point for type changing moves
for (const pokemon of inSpeedOrder(ArenaTagSide.BOTH)) {
if (pokemon !== this.pokemon) {
applyAbAttrs("RedirectMoveAbAttr", {
pokemon,
moveId: this.move.moveId,
targetIndex: redirectTarget,
sourcePokemon: this.pokemon,
});
}
}
/** `true` if an Ability is responsible for redirecting the move to another target; `false` otherwise */
let redirectedByAbility = currentTarget !== redirectTarget.value;
// check for center-of-attention tags (note that this will override redirect abilities)
this.pokemon.getOpponents(true).forEach(p => {
const redirectTag = p.getTag(CenterOfAttentionTag);
// TODO: don't hardcode this interaction.
// Handle interaction between the rage powder center-of-attention tag and moves used by grass types/overcoat-havers (which are immune to RP's redirect)
if (
redirectTag
&& (!redirectTag.powder
|| (!this.pokemon.isOfType(PokemonType.GRASS) && !this.pokemon.hasAbility(AbilityId.OVERCOAT)))
) {
redirectTarget.value = p.getBattlerIndex();
redirectedByAbility = false;
}
});
// TODO: Don't hardcode these ability interactions
if (currentTarget !== redirectTarget.value) {
const bypassRedirectAttrs = this.move.getMove().getAttrs("BypassRedirectAttr");
bypassRedirectAttrs.forEach(attr => {
if (!attr.abilitiesOnly || redirectedByAbility) {
redirectTarget.value = currentTarget;
}
});
if (this.pokemon.hasAbilityWithAttr("BlockRedirectAbAttr")) {
redirectTarget.value = currentTarget;
// TODO: Ability displays should be handled by the ability
globalScene.phaseManager.queueAbilityDisplay(
this.pokemon,
this.pokemon.getPassiveAbility().hasAttr("BlockRedirectAbAttr"),
true,
);
globalScene.phaseManager.queueAbilityDisplay(
this.pokemon,
this.pokemon.getPassiveAbility().hasAttr("BlockRedirectAbAttr"),
false,
);
}
this.targets[0] = redirectTarget.value;
}
}
public start(): void {
super.start();
if (!this.pokemon.isActive(true)) {
this.end();
return;
}
const user = this.pokemon;
// Removing Glaive Rush's two flags *always* happens first
user.removeTag(BattlerTagType.ALWAYS_GET_HIT);
user.removeTag(BattlerTagType.RECEIVE_DOUBLE_DAMAGE);
console.log(MoveId[this.move.moveId], enumValueToKey(MoveUseMode, this.useMode));
// For the purposes of payback and kin, the pokemon is considered to have acted
// if it attempted to move at all.
user.turnData.acted = true;
const useMode = this.useMode;
const ignoreStatus = isIgnoreStatus(useMode);
const isFollowUp = useMode === MoveUseMode.FOLLOW_UP;
if (!ignoreStatus) {
this.firstFailureCheck();
user.lapseTags(BattlerTagLapseType.PRE_MOVE);
// At this point, called moves should be decided.
// For now, this comment works as a placeholder until called moves are reworked
// For correct alignment with mainline, this SHOULD go here, and this phase SHOULD rewrite its own move
} else if (isFollowUp) {
// Follow up moves need to make sure the called move passes a few of the conditions to continue
this.followUpMoveFirstFailureCheck();
}
// If the first failure check did not pass, then the move is cancelled
// Note: This only checks `cancelled`, as `failed` should NEVER be set by anything in the first failure check
if (this.cancelled) {
this.handlePreMoveFailures();
this.end();
return;
}
// If the first failure check passes (and this is not a sub-move) then thaw the user if its move will thaw it.
if (!isFollowUp) {
this.doThawCheck();
}
// Reset hit-related turn data when starting follow-up moves (e.g. Metronomed moves, Dancer repeats)
if (isVirtual(useMode)) {
const turnData = user.turnData;
turnData.hitsLeft = -1;
turnData.hitCount = 0;
}
const pokemonMove = this.move;
// Check move to see if arena.ignoreAbilities should be true.
if (
pokemonMove.getMove().doesFlagEffectApply({
flag: MoveFlags.IGNORE_ABILITIES,
user,
isFollowUp: isVirtual(useMode), // Sunsteel strike and co. don't work when called indirectly
})
) {
globalScene.arena.setIgnoreAbilities(true, user.getBattlerIndex());
}
// At this point, move's type changing and multi-target effects *should* be applied
// Pokerogue's current implementation applies these effects during the move effect phase
// as there is not (yet) a notion of a move-in-flight for determinations to occur
this.resolveRedirectTarget();
this.resolveCounterAttackTarget();
// If this is the *release* turn of the charge move, PP is not deducted
const move = this.move.getMove();
const isChargingMove = move.isChargingMove();
/** Indicates this is the charging turn of the move */
const charging = isChargingMove && !user.getTag(BattlerTagType.CHARGING);
/** Indicates this is the release turn of the move */
const releasing = isChargingMove && !charging;
// Update the battle's "last move" pointer unless we're currently mimicking a move or triggering Dancer.
if (!move.hasAttr("CopyMoveAttr") && !isReflected(useMode)) {
globalScene.currentBattle.lastMove = move.id;
}
// Charging moves consume PP when they begin charging, *not* when they release
if (!releasing) {
this.usePP();
}
if (!isFollowUp) {
// Gorilla tactics lock in (and choice items if they are ever added)
// Stance Change form change
// Struggle's "There are no more moves it can use" message
globalScene.triggerPokemonFormChange(user, SpeciesFormChangePreMoveTrigger);
// TODO: apply gorilla tactics here instead of in the move effect phase
}
this.showMoveText();
if (this.secondFailureCheck()) {
this.handlePreMoveFailures();
this.end();
return;
}
if (!this.resolveFinalPreMoveCancellationChecks()) {
this.useMove(charging);
}
this.end();
}
/**
* Check for cancellation edge cases - no targets remaining
* @returns Whether the move fails
*/
protected resolveFinalPreMoveCancellationChecks(): boolean {
const targets = this.getActiveTargetPokemon();
const moveQueue = this.pokemon.getMoveQueue();
if (
(targets.length === 0 && !this.move.getMove().hasAttr("AddArenaTrapTagAttr"))
|| (moveQueue.length > 0 && moveQueue[0].move === MoveId.NONE)
) {
this.showFailedText();
this.fail();
// clear out 2 turn moves
// TODO: Make a helper for this atp
this.pokemon.getMoveQueue().shift();
this.pokemon.pushMoveHistory(this.moveHistoryEntry);
return true;
}
this.pokemon.lapseTags(BattlerTagLapseType.MOVE);
return false;
}
public getActiveTargetPokemon(): Pokemon[] {
return globalScene.getField(true).filter(p => this.targets.includes(p.getBattlerIndex()));
}
/**
* Queue the status activation message, play its animation, and cancel the move
*
* @param effect - The effect being triggered
* @param cancel - Whether to cancel the move after triggering the status
* effect animation message; default `true`. Set to `false` for
* sleep-bypassing moves to avoid cancelling attack.
*/
private triggerStatus(effect: StatusEffect, cancel = true): void {
const pokemon = this.pokemon;
globalScene.phaseManager.queueMessage(getStatusEffectActivationText(effect, getPokemonNameWithAffix(pokemon)));
globalScene.phaseManager.unshiftNew(
"CommonAnimPhase",
pokemon.getBattlerIndex(),
undefined,
CommonAnim.POISON + (effect - 1), // offset anim # by effect #
);
if (cancel) {
this.cancelled = true;
}
}
/**
* Handle the sleep check
* @returns Whether the move was cancelled due to sleep
*/
protected checkSleep(): boolean {
const user = this.pokemon;
if (user.status?.effect !== StatusEffect.SLEEP) {
return false;
}
// For some reason, dancer will immediately wake its user from sleep when triggering
if (this.useMode === MoveUseMode.INDIRECT) {
user.resetStatus(false);
return false;
}
user.status.incrementTurn();
const turnsRemaining = new NumberHolder(user.status.sleepTurnsRemaining ?? 0);
applyAbAttrs("ReduceStatusEffectDurationAbAttr", {
pokemon: user,
statusEffect: user.status.effect,
duration: turnsRemaining,
});
user.status.sleepTurnsRemaining = turnsRemaining.value;
if (user.status.sleepTurnsRemaining <= 0) {
user.cureStatus(StatusEffect.SLEEP);
return false;
}
const bypassSleepHolder = new BooleanHolder(false);
applyMoveAttrs("BypassSleepAttr", this.pokemon, null, this.move.getMove(), bypassSleepHolder);
const cancel = !bypassSleepHolder.value;
this.triggerStatus(StatusEffect.SLEEP, cancel);
return cancel;
}
/**
* Handle the freeze status effect check
*
* @remarks
* Responsible for the following
* - Checking if the pokemon is frozen
* - Checking if the pokemon will thaw from random chance, OR from a thawing move.
* Thawing from a freeze move is not applied until AFTER all other failure checks.
* - Activating the freeze status effect (cancelling the move, playing the message, and displaying the animation)
* @returns Whether the move was cancelled due to the pokemon being frozen
*/
protected checkFreeze(): boolean {
const pokemon = this.pokemon;
if (pokemon.status?.effect !== StatusEffect.FREEZE) {
return false;
}
// For some reason, dancer will immediately thaw its user
if (this.useMode === MoveUseMode.INDIRECT) {
pokemon.resetStatus(false);
return false;
}
if (Overrides.STATUS_ACTIVATION_OVERRIDE) {
return false;
}
// Check if the move will heal
const move = this.move.getMove();
if (
move.findAttr(attr => attr.selfTarget && attr.is("HealStatusEffectAttr") && attr.isOfEffect(StatusEffect.FREEZE))
&& (move.id !== MoveId.BURN_UP || pokemon.isOfType(PokemonType.FIRE, true, true))
) {
this.thaw = true;
return false;
}
if (
Overrides.STATUS_ACTIVATION_OVERRIDE === false
|| this.move
.getMove()
.findAttr(attr => attr.selfTarget && attr.is("HealStatusEffectAttr") && attr.isOfEffect(StatusEffect.FREEZE))
|| (!pokemon.randBattleSeedInt(5) && Overrides.STATUS_ACTIVATION_OVERRIDE !== true)
) {
pokemon.cureStatus(StatusEffect.FREEZE);
return false;
}
this.triggerStatus(StatusEffect.FREEZE);
return true;
}
/**
* Check if the move is usable based on PP
* @returns Whether the move was cancelled due to insufficient PP
*/
protected checkPP(): boolean {
const move = this.move;
if (move.getMove().pp !== -1 && !isIgnorePP(this.useMode) && move.ppUsed >= move.getMovePp()) {
this.cancel();
this.showFailedText();
return true;
}
return false;
}
/**
* Check if the move is valid and not in an error state
*
* @remarks
* Checks occur in the following order
* 1. Move is not implemented
* 2. Move is somehow invalid (it is {@linkcode MoveId.NONE} or {@linkcode targets} is somehow empty)
* 3. Move cannot be used by the player due to a challenge
*
* @returns Whether the move was cancelled due to being invalid
*/
protected checkValidity(): boolean {
const move = this.move;
const moveId = move.moveId;
const moveName = move.getName();
let failedText: string | undefined;
const usability = new BooleanHolder(false);
if (moveName.endsWith(" (N)")) {
failedText = i18next.t("battle:moveNotImplemented", { moveName: moveName.replace(" (N)", "") });
} else if (moveId === MoveId.NONE || this.targets.length === 0) {
this.cancel();
const pokemonName = this.pokemon.name;
const warningText =
moveId === MoveId.NONE
? `${pokemonName} is attempting to use MoveId.NONE`
: `${pokemonName} is attempting to use a move with no targets`;
console.warn(warningText);
return true;
} else if (
this.pokemon.isPlayer()
&& applyChallenges(ChallengeType.POKEMON_MOVE, moveId, usability) // check the value inside of usability after calling applyChallenges
&& !usability.value
) {
failedText = i18next.t("battle:moveCannotUseChallenge", { moveName });
} else {
return false;
}
this.cancel();
this.showFailedText(failedText);
return true;
}
/**
* Cancel the move if its pre use condition fails
*
* @remarks
* The only official move with a pre-use condition is Focus Punch
*
* @returns Whether the move was cancelled due to a pre-use interruption
* @see {@linkcode PreUseInterruptAttr}
*/
private checkPreUseInterrupt(): boolean {
const move = this.move.getMove();
const user = this.pokemon;
const target = this.getActiveTargetPokemon()[0];
return move.getAttrs("PreUseInterruptAttr").some(attr => {
attr.apply(user, target, move);
if (this.cancelled) {
return true;
}
return false;
});
}
/**
* Lapse the tag type and check if the move is cancelled from it. Meant to be used during the first failure check
* @param tag - The tag type whose lapse method will be called with {@linkcode BattlerTagLapseType.PRE_MOVE}
* @param checkIgnoreStatus - Whether to check {@link isIgnoreStatus} for the current {@linkcode MoveUseMode} to skip this check
* @returns Whether the move was cancelled due to a `BattlerTag` effect
*/
private checkTagCancel(tag: BattlerTagType): boolean {
this.pokemon.lapseTag(tag, BattlerTagLapseType.PRE_MOVE);
return this.cancelled;
}
/**
* Handle move failures due to Gravity, cancelling the move and showing the failure text
* @returns Whether the move was cancelled due to Gravity
*/
private checkGravity(): boolean {
const move = this.move.getMove();
if (!globalScene.arena.hasTag(ArenaTagType.GRAVITY) || !move.hasFlag(MoveFlags.GRAVITY)) {
return false;
}
this.showFailedText(
i18next.t("battle:moveDisabledGravity", {
pokemonNameWithAffix: getPokemonNameWithAffix(this.pokemon),
moveName: move.name,
}),
);
return true;
}
/**
* Handle the paralysis status effect check, cancelling the move and queueing the activation message and animation
*
* @returns Whether the move was cancelled due to paralysis
*/
private checkPara(): boolean {
if (this.pokemon.status?.effect !== StatusEffect.PARALYSIS) {
return false;
}
const proc = Overrides.STATUS_ACTIVATION_OVERRIDE ?? this.pokemon.randBattleSeedInt(4) === 0;
if (!proc) {
return false;
}
this.triggerStatus(StatusEffect.PARALYSIS);
return true;
}
/**
* Deduct PP from the move being used, accounting for Pressure and other effects
*/
protected usePP(): void {
if (!isIgnorePP(this.useMode)) {
const move = this.move;
// "commit" to using the move, deducting PP.
const ppUsed = 1 + this.getPpIncreaseFromPressure(this.getActiveTargetPokemon());
move.usePp(ppUsed);
globalScene.eventTarget.dispatchEvent(new MoveUsedEvent(this.pokemon.id, move.getMove(), move.ppUsed));
}
}
/**
* Clear out two turn moves, then schedule the move to be used if it passes
* the third failure check.
*/
protected useMove(charging = false): void {
const user = this.pokemon;
/* Clear out any two turn moves once they've been used.
TODO: Refactor move queues and remove this assignment;
Move queues should be handled by the calling `CommandPhase` or a manager for it */
// @ts-expect-error - useMode is readonly and shouldn't normally be assigned to
this.useMode = user.getMoveQueue().shift()?.useMode ?? this.useMode;
if (!charging && user.getTag(BattlerTagType.CHARGING)?.sourceMove === this.move.moveId) {
user.lapseTag(BattlerTagType.CHARGING);
}
if (this.thirdFailureCheck()) {
console.log("Move failed during third failure check");
return;
}
/*
At this point, delayed moves (future sight, wish, doom desire) are issued, and, if they occur, the move animations are played.
Then, combined pledge moves are checked for. Interestingly, the "wasMoveEffective" flag is set to false if the combined technique
In either case, the phase should end here without proceeding
*/
const move = this.move.getMove();
const opponent = this.getActiveTargetPokemon()[0];
/*
After the third failure check, the move is "locked in"
The following things now occur on cartridge
- Heal Bell / Aromatherapy's custom message is queued (but displayed after the move text)
- The message for combined pledge moves is queued
- The custom message for fickle beam is queued
- Gulp missile's form change is triggered IF the user is using dive (surf happens later)
- Protean / Libero trigger the type change and flyout
*/
// Currently, we only do the libero/protean type change here
applyAbAttrs("PokemonTypeChangeAbAttr", { pokemon: user, move, opponent });
// TODO: Move this to the Move effect phase where it belongs.
// Fourth failure check happens _after_ protean
if (!move.applyConditions(user, opponent, 4)) {
console.log("Move failed during fourth failure check");
this.failMove();
return;
}
if (charging) {
this.chargeMove();
} else {
this.executeMove();
}
}
/** Execute the current move and apply its effects. */
private executeMove() {
const user = this.pokemon;
const move = this.move.getMove();
const targets = this.targets;
// Trigger ability-based user type changes, display move text and then execute move effects.
// TODO: Investigate whether PokemonTypeChangeAbAttr can drop the "opponent" parameter
globalScene.phaseManager.unshiftNew("MoveEffectPhase", user.getBattlerIndex(), targets, move, this.useMode);
// Handle Dancer, which triggers immediately after a move is used (rather than waiting on `this.end()`).
// Note the MoveUseMode check here prevents an infinite Dancer loop.
// TODO: This needs to go at the end of `MoveEffectPhase` to check move results
const dancerModes: MoveUseMode[] = [MoveUseMode.INDIRECT, MoveUseMode.REFLECTED] as const;
if (this.move.getMove().hasFlag(MoveFlags.DANCE_MOVE) && !dancerModes.includes(this.useMode)) {
for (const pokemon of inSpeedOrder(ArenaTagSide.BOTH)) {
applyAbAttrs("PostMoveUsedAbAttr", { pokemon, move: this.move, source: user, targets });
}
}
}
/**
* Fail the move currently being used.
* Handles failure messages, pushing to move history, etc.
* @param failedDueToTerrain - Whether the move failed due to terrain (default `false`)
*/
protected failMove(failedDueToTerrain = false) {
const move = this.move.getMove();
const targets = this.getActiveTargetPokemon();
const pokemon = this.pokemon;
// DO NOT CHANGE THE ORDER OF OPERATIONS HERE!
// Protean is supposed to trigger its effects first, _then_ move text is displayed,
// _then_ any blockage messages are shown.
// Roar, Whirlwind, Trick-or-Treat, and Forest's Curse will trigger Protean/Libero
// even on failure, as will all moves blocked by terrain.
// TODO: Verify if this also applies to primal weather failures
if (
failedDueToTerrain
|| [MoveId.ROAR, MoveId.WHIRLWIND, MoveId.TRICK_OR_TREAT, MoveId.FORESTS_CURSE].includes(this.move.moveId)
) {
applyAbAttrs("PokemonTypeChangeAbAttr", {
pokemon,
move,
opponent: targets[0],
});
}
pokemon.pushMoveHistory({
move: move.id,
targets: this.targets,
result: MoveResult.FAIL,
useMode: this.useMode,
});
// Use move-specific failure messages if present before checking terrain/weather blockage
// and falling back to the classic "But it failed!".
const failureMessage =
move.getFailedText(pokemon, targets[0], move)
|| (failedDueToTerrain
? getTerrainBlockMessage(targets[0], globalScene.arena.getTerrainType())
: i18next.t("battle:attackFailed"));
this.showFailedText(failureMessage);
// Remove the user from its semi-invulnerable state (if applicable)
pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT);
}
/**
* Queue a {@linkcode MoveChargePhase} for this phase's invoked move.
*/
protected chargeMove() {
globalScene.phaseManager.unshiftNew(
"MoveChargePhase",
this.pokemon.getBattlerIndex(),
this.targets[0],
this.move,
this.useMode,
);
}
/**
* Queue a {@linkcode MoveEndPhase} and then end this phase.
*/
public end(): void {
globalScene.phaseManager.unshiftNew(
"MoveEndPhase",
this.pokemon.getBattlerIndex(),
this.getActiveTargetPokemon(),
isVirtual(this.useMode),
);
super.end();
}
/**
* Applies PP increasing abilities (currently only {@linkcode AbilityId.PRESSURE | Pressure}) if they exist on the target pokemon.
* Note that targets must include only active pokemon.
*
* TODO: This hardcodes the PP increase at 1 per opponent, rather than deferring to the ability.
*/
public getPpIncreaseFromPressure(targets: Pokemon[]): number {
const foesWithPressure = this.pokemon
.getOpponents(true)
.filter(opponent => targets.includes(opponent) && opponent.hasAbilityWithAttr("IncreasePpAbAttr"));
return foesWithPressure.length;
}
/**
* Update the targets of any counter-attacking moves with `[`{@linkcode BattlerIndex.ATTACKER}`]` set
* to reflect the actual battler index of the user's last attacker.
*
* If there is no last attacker or they are no longer on the field, a message is displayed and the
* move is marked for failure
*/
protected resolveCounterAttackTarget(): void {
const targets = this.targets;
if (targets.length !== 1 || targets[0] !== BattlerIndex.ATTACKER) {
return;
}
const targetHolder = new NumberHolder(BattlerIndex.ATTACKER);
applyMoveAttrs("CounterRedirectAttr", this.pokemon, null, this.move.getMove(), targetHolder);
targets[0] = targetHolder.value;
if (targetHolder.value === BattlerIndex.ATTACKER) {
this.fail();
}
}
/**
* Handles the case where the move was cancelled or failed:
* - Uses PP if the move failed (not cancelled) and should use PP (failed moves are not affected by {@linkcode AbilityId.PRESSURE | Pressure})
* - Records a cancelled OR failed move in move history, so abilities like {@linkcode AbilityId.TRUANT | Truant} don't trigger on the
* next turn and soft-lock.
* - Lapses `MOVE_EFFECT` tags:
* - Semi-invulnerable battler tags (Fly/Dive/etc.) are intended to lapse on move effects, but also need
* to lapse on move failure/cancellation.
*
* TODO: ...this seems weird.
* - Lapses `AFTER_MOVE` tags:
* - This handles the effects of {@linkcode MoveId.SUBSTITUTE | Substitute}
* - Removes the second turn of charge moves
*/
protected handlePreMoveFailures(): void {
if (!this.cancelled && !this.failed) {
return;
}
const pokemon = this.pokemon;
if (this.cancelled && pokemon.summonData.tags.some(t => t.tagType === BattlerTagType.FRENZY)) {
frenzyMissFunc(pokemon, this.move.getMove());
}
const moveHistoryEntry = this.moveHistoryEntry;
moveHistoryEntry.result = MoveResult.FAIL;
pokemon.pushMoveHistory(moveHistoryEntry);
pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT);
pokemon.lapseTags(BattlerTagLapseType.AFTER_MOVE);
// This clears out 2 turn moves after they've been used
// TODO: Remove post move queue refactor
pokemon.getMoveQueue().shift();
}
/**
* Displays the move's usage text to the player as applicable for the move being used.
*/
public showMoveText(): void {
const pokemonMove = this.move;
const moveId = pokemonMove.moveId;
const pokemon = this.pokemon;
if (
moveId === MoveId.NONE
|| pokemon.getTag(BattlerTagType.RECHARGING)
|| pokemon.getTag(BattlerTagType.INTERRUPTED)
) {
return;
}
// Showing move text always adjusts the move history entry's move id
this.moveHistoryEntry.move = moveId;
// TODO: This should be done by the move...
globalScene.phaseManager.queueMessage(
i18next.t(isReflected(this.useMode) ? "battle:magicCoatActivated" : "battle:useMove", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
moveName: pokemonMove.getName(),
}),
500,
);
// Moves with pre-use messages (Magnitude, Chilly Reception, Fickle Beam, etc.) always display their messages even on failure
// TODO: This assumes single target for message funcs - is this sustainable?
applyMoveAttrs("PreMoveMessageAttr", pokemon, this.getActiveTargetPokemon()[0], pokemonMove.getMove());
}
/**
* Display the text for a move failing to execute.
* @param failedText - The failure text to display; defaults to `"battle:attackFailed"` locale key
* ("But it failed!" in english)
*/
public showFailedText(failedText = i18next.t("battle:attackFailed")): void {
globalScene.phaseManager.queueMessage(failedText);
}
}