mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-06-21 09:02:47 +02:00
677 lines
25 KiB
TypeScript
677 lines
25 KiB
TypeScript
import { BattlerIndex } from "#enums/battler-index";
|
|
import { globalScene } from "#app/global-scene";
|
|
import { applyAbAttrs, applyPostMoveUsedAbAttrs, applyPreAttackAbAttrs } from "#app/data/abilities/apply-ab-attrs";
|
|
import type { DelayedAttackTag } from "#app/data/arena-tag";
|
|
import { CommonAnim } from "#enums/move-anims-common";
|
|
import { CenterOfAttentionTag } from "#app/data/battler-tags";
|
|
import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type";
|
|
import { applyMoveAttrs } from "#app/data/moves/apply-attrs";
|
|
import { MoveFlags } from "#enums/MoveFlags";
|
|
import { SpeciesFormChangePreMoveTrigger } from "#app/data/pokemon-forms/form-change-triggers";
|
|
import { getStatusEffectActivationText, getStatusEffectHealText } from "#app/data/status-effect";
|
|
import { PokemonType } from "#enums/pokemon-type";
|
|
import { getTerrainBlockMessage, getWeatherBlockMessage } from "#app/data/weather";
|
|
import { MoveUsedEvent } from "#app/events/battle-scene";
|
|
import type { PokemonMove } from "#app/data/moves/pokemon-move";
|
|
import type Pokemon from "#app/field/pokemon";
|
|
import { MoveResult } from "#enums/move-result";
|
|
import { getPokemonNameWithAffix } from "#app/messages";
|
|
import Overrides from "#app/overrides";
|
|
import { BattlePhase } from "#app/phases/battle-phase";
|
|
import { enumValueToKey, NumberHolder } from "#app/utils/common";
|
|
import { AbilityId } from "#enums/ability-id";
|
|
import { ArenaTagType } from "#enums/arena-tag-type";
|
|
import { BattlerTagType } from "#enums/battler-tag-type";
|
|
import { MoveId } from "#enums/move-id";
|
|
import { StatusEffect } from "#enums/status-effect";
|
|
import i18next from "i18next";
|
|
import { isVirtual, isIgnorePP, isReflected, MoveUseMode, isIgnoreStatus } from "#enums/move-use-mode";
|
|
import { frenzyMissFunc } from "#app/data/moves/move-utils";
|
|
|
|
export class MovePhase extends BattlePhase {
|
|
public readonly phaseName = "MovePhase";
|
|
protected _pokemon: Pokemon;
|
|
protected _move: PokemonMove;
|
|
protected _targets: BattlerIndex[];
|
|
public readonly useMode: MoveUseMode; // Made public for quash
|
|
/** Whether the current move is forced last (used for Quash). */
|
|
protected forcedLast: boolean;
|
|
/** 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;
|
|
|
|
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 move(): PokemonMove {
|
|
return this._move;
|
|
}
|
|
|
|
protected set move(move: PokemonMove) {
|
|
this._move = move;
|
|
}
|
|
|
|
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 forcedLast - Whether to force this phase to occur last in order (for {@linkcode MoveId.QUASH}); default `false`
|
|
*/
|
|
constructor(pokemon: Pokemon, targets: BattlerIndex[], move: PokemonMove, useMode: MoveUseMode, forcedLast = false) {
|
|
super();
|
|
|
|
this.pokemon = pokemon;
|
|
this.targets = targets;
|
|
this.move = move;
|
|
this.useMode = useMode;
|
|
this.forcedLast = forcedLast;
|
|
}
|
|
|
|
/**
|
|
* Checks if the pokemon is active, if the move is usable, and that the move is targeting something.
|
|
* @param ignoreDisableTags `true` to not check if the move is disabled
|
|
* @returns `true` if all the checks pass
|
|
*/
|
|
public canMove(ignoreDisableTags = false): boolean {
|
|
return (
|
|
this.pokemon.isActive(true) &&
|
|
this.move.isUsable(this.pokemon, isIgnorePP(this.useMode), ignoreDisableTags) &&
|
|
this.targets.length > 0
|
|
);
|
|
}
|
|
|
|
/** Signifies the current move should fail but still use PP */
|
|
public fail(): void {
|
|
this.failed = true;
|
|
}
|
|
|
|
/** Signifies the current move should cancel and retain PP */
|
|
public cancel(): void {
|
|
this.cancelled = true;
|
|
}
|
|
|
|
/**
|
|
* Shows whether the current move has been forced to the end of the turn
|
|
* Needed for speed order, see {@linkcode MoveId.QUASH}
|
|
*/
|
|
public isForcedLast(): boolean {
|
|
return this.forcedLast;
|
|
}
|
|
|
|
public start(): void {
|
|
super.start();
|
|
|
|
console.log(MoveId[this.move.moveId], enumValueToKey(MoveUseMode, this.useMode));
|
|
|
|
// 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), ending the phase early if not.
|
|
if (!this.canMove(true)) {
|
|
if (this.pokemon.isActive(true)) {
|
|
this.fail();
|
|
this.showMoveText();
|
|
this.showFailedText();
|
|
}
|
|
this.end();
|
|
return;
|
|
}
|
|
|
|
this.pokemon.turnData.acted = true;
|
|
|
|
// Reset hit-related turn data when starting follow-up moves (e.g. Metronomed moves, Dancer repeats)
|
|
if (isVirtual(this.useMode)) {
|
|
this.pokemon.turnData.hitsLeft = -1;
|
|
this.pokemon.turnData.hitCount = 0;
|
|
}
|
|
|
|
// Check move to see if arena.ignoreAbilities should be true.
|
|
if (
|
|
this.move.getMove().doesFlagEffectApply({
|
|
flag: MoveFlags.IGNORE_ABILITIES,
|
|
user: this.pokemon,
|
|
isFollowUp: isVirtual(this.useMode), // Sunsteel strike and co. don't work when called indirectly
|
|
})
|
|
) {
|
|
globalScene.arena.setIgnoreAbilities(true, this.pokemon.getBattlerIndex());
|
|
}
|
|
|
|
this.resolveRedirectTarget();
|
|
|
|
this.resolveCounterAttackTarget();
|
|
|
|
this.resolvePreMoveStatusEffects();
|
|
|
|
this.lapsePreMoveAndMoveTags();
|
|
|
|
if (!(this.failed || this.cancelled)) {
|
|
this.resolveFinalPreMoveCancellationChecks();
|
|
}
|
|
|
|
// Cancel, charge or use the move as applicable.
|
|
if (this.cancelled || this.failed) {
|
|
this.handlePreMoveFailures();
|
|
} else if (this.move.getMove().isChargingMove() && !this.pokemon.getTag(BattlerTagType.CHARGING)) {
|
|
this.chargeMove();
|
|
} else {
|
|
this.useMove();
|
|
}
|
|
|
|
this.end();
|
|
}
|
|
|
|
/** Check for cancellation edge cases - no targets remaining, or {@linkcode MoveId.NONE} is in the queue */
|
|
protected resolveFinalPreMoveCancellationChecks(): void {
|
|
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.showMoveText();
|
|
this.showFailedText();
|
|
this.cancel();
|
|
}
|
|
}
|
|
|
|
public getActiveTargetPokemon(): Pokemon[] {
|
|
return globalScene.getField(true).filter(p => this.targets.includes(p.getBattlerIndex()));
|
|
}
|
|
|
|
/**
|
|
* Handles {@link StatusEffect.SLEEP | Sleep}/{@link StatusEffect.PARALYSIS | Paralysis}/{@link StatusEffect.FREEZE | Freeze} rolls and side effects.
|
|
*/
|
|
protected resolvePreMoveStatusEffects(): void {
|
|
// Skip for follow ups/reflected moves, no status condition or post turn statuses (e.g. Poison/Toxic)
|
|
if (!this.pokemon.status?.effect || this.pokemon.status.isPostTurn() || isIgnoreStatus(this.useMode)) {
|
|
return;
|
|
}
|
|
|
|
if (
|
|
this.useMode === MoveUseMode.INDIRECT &&
|
|
[StatusEffect.SLEEP, StatusEffect.FREEZE].includes(this.pokemon.status.effect)
|
|
) {
|
|
// Dancer thaws out or wakes up a frozen/sleeping user prior to use
|
|
this.pokemon.resetStatus(false);
|
|
return;
|
|
}
|
|
|
|
this.pokemon.status.incrementTurn();
|
|
|
|
/** Whether to prevent us from using the move */
|
|
let activated = false;
|
|
/** Whether to cure the status */
|
|
let healed = false;
|
|
|
|
switch (this.pokemon.status.effect) {
|
|
case StatusEffect.PARALYSIS:
|
|
activated =
|
|
(this.pokemon.randBattleSeedInt(4) === 0 || Overrides.STATUS_ACTIVATION_OVERRIDE === true) &&
|
|
Overrides.STATUS_ACTIVATION_OVERRIDE !== false;
|
|
break;
|
|
case StatusEffect.SLEEP: {
|
|
applyMoveAttrs("BypassSleepAttr", this.pokemon, null, this.move.getMove());
|
|
const turnsRemaining = new NumberHolder(this.pokemon.status.sleepTurnsRemaining ?? 0);
|
|
applyAbAttrs(
|
|
"ReduceStatusEffectDurationAbAttr",
|
|
this.pokemon,
|
|
null,
|
|
false,
|
|
this.pokemon.status.effect,
|
|
turnsRemaining,
|
|
);
|
|
this.pokemon.status.sleepTurnsRemaining = turnsRemaining.value;
|
|
healed = this.pokemon.status.sleepTurnsRemaining <= 0;
|
|
activated = !healed && !this.pokemon.getTag(BattlerTagType.BYPASS_SLEEP);
|
|
break;
|
|
}
|
|
case StatusEffect.FREEZE:
|
|
healed =
|
|
!!this.move
|
|
.getMove()
|
|
.findAttr(
|
|
attr => attr.is("HealStatusEffectAttr") && attr.selfTarget && attr.isOfEffect(StatusEffect.FREEZE),
|
|
) ||
|
|
(!this.pokemon.randBattleSeedInt(5) && Overrides.STATUS_ACTIVATION_OVERRIDE !== true) ||
|
|
Overrides.STATUS_ACTIVATION_OVERRIDE === false;
|
|
|
|
activated = !healed;
|
|
break;
|
|
}
|
|
|
|
if (activated) {
|
|
// Cancel move activation and play effect
|
|
this.cancel();
|
|
globalScene.phaseManager.queueMessage(
|
|
getStatusEffectActivationText(this.pokemon.status.effect, getPokemonNameWithAffix(this.pokemon)),
|
|
);
|
|
globalScene.phaseManager.unshiftNew(
|
|
"CommonAnimPhase",
|
|
this.pokemon.getBattlerIndex(),
|
|
undefined,
|
|
CommonAnim.POISON + (this.pokemon.status.effect - 1), // offset anim # by effect #
|
|
);
|
|
} else if (healed) {
|
|
// cure status and play effect
|
|
globalScene.phaseManager.queueMessage(
|
|
getStatusEffectHealText(this.pokemon.status.effect, getPokemonNameWithAffix(this.pokemon)),
|
|
);
|
|
this.pokemon.resetStatus();
|
|
this.pokemon.updateInfo();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Lapse {@linkcode BattlerTagLapseType.PRE_MOVE | PRE_MOVE} tags that trigger before a move is used, regardless of whether or not it failed.
|
|
* Also lapse {@linkcode BattlerTagLapseType.MOVE | MOVE} tags if the move is successful and not called indirectly.
|
|
*/
|
|
protected lapsePreMoveAndMoveTags(): void {
|
|
this.pokemon.lapseTags(BattlerTagLapseType.PRE_MOVE);
|
|
|
|
// This intentionally happens before moves without targets are cancelled (Truant takes priority over lack of targets)
|
|
if (!isIgnoreStatus(this.useMode) && this.canMove() && !this.cancelled) {
|
|
this.pokemon.lapseTags(BattlerTagLapseType.MOVE);
|
|
}
|
|
}
|
|
|
|
protected useMove(): void {
|
|
const targets = this.getActiveTargetPokemon();
|
|
const moveQueue = this.pokemon.getMoveQueue();
|
|
const move = this.move.getMove();
|
|
|
|
// form changes happen even before we know that the move wll execute.
|
|
globalScene.triggerPokemonFormChange(this.pokemon, SpeciesFormChangePreMoveTrigger);
|
|
|
|
// Check the player side arena if another delayed attack is active and hitting the same slot.
|
|
if (move.hasAttr("DelayedAttackAttr")) {
|
|
const currentTargetIndex = targets[0].getBattlerIndex();
|
|
const delayedAttackHittingSameSlot = globalScene.arena.tags.some(
|
|
tag =>
|
|
(tag.tagType === ArenaTagType.FUTURE_SIGHT || tag.tagType === ArenaTagType.DOOM_DESIRE) &&
|
|
(tag as DelayedAttackTag).targetIndex === currentTargetIndex,
|
|
);
|
|
|
|
if (delayedAttackHittingSameSlot) {
|
|
this.failMove(true);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Check if the move has any attributes that can interrupt its own use **before** displaying text.
|
|
applyMoveAttrs("PreUseInterruptAttr", this.pokemon, targets[0], move);
|
|
let failed = move.getAttrs("PreUseInterruptAttr").some(attr => attr.apply(this.pokemon, targets[0], move));
|
|
if (failed) {
|
|
this.failMove(false);
|
|
return;
|
|
}
|
|
|
|
// 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 = moveQueue.shift()?.useMode ?? this.useMode;
|
|
|
|
if (this.pokemon.getTag(BattlerTagType.CHARGING)?.sourceMove === this.move.moveId) {
|
|
this.pokemon.lapseTag(BattlerTagType.CHARGING);
|
|
}
|
|
|
|
if (!isIgnorePP(this.useMode)) {
|
|
// "commit" to using the move, deducting PP.
|
|
const ppUsed = 1 + this.getPpIncreaseFromPressure(targets);
|
|
this.move.usePp(ppUsed);
|
|
globalScene.eventTarget.dispatchEvent(new MoveUsedEvent(this.pokemon.id, move, this.move.ppUsed));
|
|
}
|
|
|
|
/**
|
|
* Determine if the move is successful (meaning that its damage/effects can be attempted)
|
|
* by checking that all of the following are true:
|
|
* - Conditional attributes of the move are all met
|
|
* - Weather does not block the move
|
|
* - Terrain does not block the move
|
|
*/
|
|
|
|
/**
|
|
* Move conditions assume the move has a single target
|
|
* TODO: is this sustainable?
|
|
*/
|
|
const failsConditions = !move.applyConditions(this.pokemon, targets[0], move);
|
|
const failedDueToWeather = globalScene.arena.isMoveWeatherCancelled(this.pokemon, move);
|
|
const failedDueToTerrain = globalScene.arena.isMoveTerrainCancelled(this.pokemon, this.targets, move);
|
|
failed ||= failsConditions || failedDueToWeather || failedDueToTerrain;
|
|
|
|
if (failed) {
|
|
this.failMove(true, failedDueToWeather, failedDueToTerrain);
|
|
return;
|
|
}
|
|
|
|
this.executeMove();
|
|
}
|
|
|
|
private executeMove() {
|
|
const move = this.move.getMove();
|
|
if (!move.hasAttr("CopyMoveAttr") && !isReflected(this.useMode)) {
|
|
// Update the battle's "last move" pointer unless we're currently mimicking a move or triggering Dancer.
|
|
// TODO: Research how Copycat interacts with the final attacking turn of Future Sight and co.
|
|
globalScene.currentBattle.lastMove = move.id;
|
|
}
|
|
|
|
// trigger ability-based user type changes, display move text and then execute move effects.
|
|
applyPreAttackAbAttrs("PokemonTypeChangeAbAttr", this.pokemon, null, move);
|
|
this.showMoveText();
|
|
globalScene.phaseManager.unshiftNew(
|
|
"MoveEffectPhase",
|
|
this.pokemon.getBattlerIndex(),
|
|
this.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.
|
|
const dancerModes: MoveUseMode[] = [MoveUseMode.INDIRECT, MoveUseMode.REFLECTED] as const;
|
|
if (this.move.getMove().hasFlag(MoveFlags.DANCE_MOVE) && !dancerModes.includes(this.useMode)) {
|
|
globalScene.getField(true).forEach(pokemon => {
|
|
applyPostMoveUsedAbAttrs("PostMoveUsedAbAttr", pokemon, this.move, this.pokemon, this.targets);
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fail the move currently being used.
|
|
* Handles failure messages, pushing to move history, etc.
|
|
* @param showText - Whether to show move text when failing the move.
|
|
* @param failedDueToWeather - Whether the move failed due to weather (default `false`)
|
|
* @param failedDueToTerrain - Whether the move failed due to terrain (default `false`)
|
|
*/
|
|
protected failMove(showText: boolean, failedDueToWeather = false, failedDueToTerrain = false) {
|
|
const move = this.move.getMove();
|
|
const targets = this.getActiveTargetPokemon();
|
|
|
|
// NOTE: 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)
|
|
) {
|
|
applyPreAttackAbAttrs("PokemonTypeChangeAbAttr", this.pokemon, null, move);
|
|
}
|
|
|
|
if (showText) {
|
|
this.showMoveText();
|
|
}
|
|
|
|
this.pokemon.pushMoveHistory({
|
|
move: this.move.moveId,
|
|
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(this.pokemon, targets[0], move) ??
|
|
(failedDueToTerrain
|
|
? getTerrainBlockMessage(targets[0], globalScene.arena.getTerrainType())
|
|
: failedDueToWeather
|
|
? getWeatherBlockMessage(globalScene.arena.getWeatherType())
|
|
: i18next.t("battle:attackFailed"));
|
|
|
|
this.showFailedText(failureMessage);
|
|
|
|
// Remove the user from its semi-invulnerable state (if applicable)
|
|
this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT);
|
|
}
|
|
|
|
/**
|
|
* Queue a {@linkcode MoveChargePhase} for this phase's invoked move.
|
|
* Does NOT consume PP (occurs on the 2nd strike of the move)
|
|
*/
|
|
protected chargeMove() {
|
|
const move = this.move.getMove();
|
|
const targets = this.getActiveTargetPokemon();
|
|
|
|
if (!move.applyConditions(this.pokemon, targets[0], move)) {
|
|
this.failMove(true);
|
|
return;
|
|
}
|
|
|
|
// Protean and Libero apply on the charging turn of charge moves, even before showing usage text
|
|
applyPreAttackAbAttrs("PokemonTypeChangeAbAttr", this.pokemon, null, this.move.getMove());
|
|
|
|
this.showMoveText();
|
|
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()
|
|
.filter(o => targets.includes(o) && o.isActive(true) && o.hasAbilityWithAttr("IncreasePpAbAttr"));
|
|
return foesWithPressure.length;
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
const currentTarget = this.targets[0];
|
|
const redirectTarget = new NumberHolder(currentTarget);
|
|
|
|
// check move redirection abilities of every pokemon *except* the user.
|
|
globalScene
|
|
.getField(true)
|
|
.filter(p => p !== this.pokemon)
|
|
.forEach(p =>
|
|
applyAbAttrs("RedirectMoveAbAttr", p, null, false, this.move.moveId, redirectTarget, 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().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;
|
|
}
|
|
});
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 {
|
|
if (this.targets.length !== 1 || this.targets[0] !== BattlerIndex.ATTACKER) {
|
|
return;
|
|
}
|
|
|
|
if (this.pokemon.turnData.attacksReceived.length) {
|
|
this.targets[0] = this.pokemon.turnData.attacksReceived[0].sourceBattlerIndex;
|
|
|
|
// account for metal burst and comeuppance hitting remaining targets in double battles
|
|
// counterattack will redirect to remaining ally if original attacker faints
|
|
if (
|
|
globalScene.currentBattle.double &&
|
|
this.move.getMove().hasFlag(MoveFlags.REDIRECT_COUNTER) &&
|
|
globalScene.getField()[this.targets[0]].hp === 0
|
|
) {
|
|
const opposingField = this.pokemon.isPlayer() ? globalScene.getEnemyField() : globalScene.getPlayerField();
|
|
this.targets[0] = opposingField.find(p => p.hp > 0)?.getBattlerIndex() ?? BattlerIndex.ATTACKER;
|
|
}
|
|
}
|
|
|
|
if (this.targets[0] === BattlerIndex.ATTACKER) {
|
|
this.fail();
|
|
this.showMoveText();
|
|
this.showFailedText();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
if (this.failed) {
|
|
const ppUsed = isIgnorePP(this.useMode) ? 0 : 1;
|
|
this.move.usePp(ppUsed);
|
|
globalScene.eventTarget.dispatchEvent(new MoveUsedEvent(this.pokemon?.id, this.move.getMove(), ppUsed));
|
|
}
|
|
|
|
if (this.cancelled && this.pokemon.summonData.tags?.find(t => t.tagType === BattlerTagType.FRENZY)) {
|
|
frenzyMissFunc(this.pokemon, this.move.getMove());
|
|
}
|
|
|
|
this.pokemon.pushMoveHistory({
|
|
move: MoveId.NONE,
|
|
result: MoveResult.FAIL,
|
|
targets: this.targets,
|
|
useMode: this.useMode,
|
|
});
|
|
|
|
this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT);
|
|
this.pokemon.lapseTags(BattlerTagLapseType.AFTER_MOVE);
|
|
|
|
// This clears out 2 turn moves after they've been used
|
|
// TODO: Remove post move queue refactor
|
|
this.pokemon.getMoveQueue().shift();
|
|
}
|
|
|
|
/**
|
|
* Displays the move's usage text to the player as applicable for the move being used.
|
|
*/
|
|
public showMoveText(): void {
|
|
// No text for Moves.NONE, recharging/2-turn moves or interrupted moves
|
|
if (
|
|
this.move.moveId === MoveId.NONE ||
|
|
this.pokemon.getTag(BattlerTagType.RECHARGING) ||
|
|
this.pokemon.getTag(BattlerTagType.INTERRUPTED)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
// Play message for magic coat reflection
|
|
// TODO: This should be done by the move...
|
|
globalScene.phaseManager.queueMessage(
|
|
i18next.t(isReflected(this.useMode) ? "battle:magicCoatActivated" : "battle:useMove", {
|
|
pokemonNameWithAffix: getPokemonNameWithAffix(this.pokemon),
|
|
moveName: this.move.getName(),
|
|
}),
|
|
500,
|
|
);
|
|
applyMoveAttrs("PreMoveMessageAttr", this.pokemon, this.pokemon.getOpponents(false)[0], this.move.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);
|
|
}
|
|
}
|