mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-08-19 13:59:27 +02:00
Merge 0112244de2
into dd03887d05
This commit is contained in:
commit
93665b0240
@ -9,7 +9,7 @@ import { getStatusEffectHealText } from "#data/status-effect";
|
||||
import { TerrainType } from "#data/terrain";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import type { BattlerIndex } from "#enums/battler-index";
|
||||
import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type";
|
||||
import { BattlerTagLapseType, type NonCustomBattlerTagLapseType } from "#enums/battler-tag-lapse-type";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { HitResult } from "#enums/hit-result";
|
||||
import { ChargeAnim, CommonAnim } from "#enums/move-anims-common";
|
||||
@ -121,6 +121,11 @@ export class BattlerTag implements BaseBattlerTag {
|
||||
}
|
||||
|
||||
#lapseTypes: readonly [BattlerTagLapseType, ...BattlerTagLapseType[]];
|
||||
/**
|
||||
* The set of lapse types that this tag can be automatically lapsed with.
|
||||
* If this is exclusively {@linkcode BattlerTagLapseType.CUSTOM}, then the tag can only ever be lapsed
|
||||
* manually via {@linkcode Pokemon.lapseTag} (or calling the tag's lapse method directly)
|
||||
*/
|
||||
public get lapseTypes(): readonly BattlerTagLapseType[] {
|
||||
return this.#lapseTypes;
|
||||
}
|
||||
@ -128,7 +133,7 @@ export class BattlerTag implements BaseBattlerTag {
|
||||
|
||||
constructor(
|
||||
tagType: BattlerTagType,
|
||||
lapseType: BattlerTagLapseType | [BattlerTagLapseType, ...BattlerTagLapseType[]],
|
||||
lapseType: BattlerTagLapseType | [NonCustomBattlerTagLapseType, ...NonCustomBattlerTagLapseType[]],
|
||||
turnCount: number,
|
||||
sourceMove?: MoveId,
|
||||
sourceId?: number,
|
||||
@ -160,7 +165,10 @@ export class BattlerTag implements BaseBattlerTag {
|
||||
onOverlap(_pokemon: Pokemon): void {}
|
||||
|
||||
/**
|
||||
* Tick down this {@linkcode BattlerTag}'s duration.
|
||||
* Apply the battler tag's effects based on the lapse type
|
||||
*
|
||||
* @remarks
|
||||
* Generally, this involves ticking down the tag's duration. The tag also initiates the effects it is responsbile for
|
||||
* @param _pokemon - The {@linkcode Pokemon} whom this tag belongs to.
|
||||
* Unused by default but can be used by subclasses.
|
||||
* @param _lapseType - The {@linkcode BattlerTagLapseType} being lapsed.
|
||||
@ -302,12 +310,7 @@ export abstract class MoveRestrictionBattlerTag extends SerializableBattlerTag {
|
||||
export class ThroatChoppedTag extends MoveRestrictionBattlerTag {
|
||||
public override readonly tagType = BattlerTagType.THROAT_CHOPPED;
|
||||
constructor() {
|
||||
super(
|
||||
BattlerTagType.THROAT_CHOPPED,
|
||||
[BattlerTagLapseType.TURN_END, BattlerTagLapseType.PRE_MOVE],
|
||||
2,
|
||||
MoveId.THROAT_CHOP,
|
||||
);
|
||||
super(BattlerTagType.THROAT_CHOPPED, BattlerTagLapseType.TURN_END, 2, MoveId.THROAT_CHOP);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -356,13 +359,7 @@ export class DisabledTag extends MoveRestrictionBattlerTag {
|
||||
public readonly moveId: MoveId = MoveId.NONE;
|
||||
|
||||
constructor(sourceId: number) {
|
||||
super(
|
||||
BattlerTagType.DISABLED,
|
||||
[BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.TURN_END],
|
||||
4,
|
||||
MoveId.DISABLE,
|
||||
sourceId,
|
||||
);
|
||||
super(BattlerTagType.DISABLED, BattlerTagLapseType.TURN_END, 4, MoveId.DISABLE, sourceId);
|
||||
}
|
||||
|
||||
override isMoveRestricted(move: MoveId): boolean {
|
||||
@ -511,7 +508,10 @@ export class RechargingTag extends SerializableBattlerTag {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||
}),
|
||||
);
|
||||
(globalScene.phaseManager.getCurrentPhase() as MovePhase).cancel();
|
||||
const currentPhase = globalScene.phaseManager.getCurrentPhase();
|
||||
if (currentPhase?.is("MovePhase")) {
|
||||
currentPhase.cancel();
|
||||
}
|
||||
pokemon.getMoveQueue().shift();
|
||||
}
|
||||
return super.lapse(pokemon, lapseType);
|
||||
@ -708,18 +708,21 @@ class NoRetreatTag extends TrappedTag {
|
||||
export class FlinchedTag extends BattlerTag {
|
||||
public override readonly tagType = BattlerTagType.FLINCHED;
|
||||
constructor(sourceMove: MoveId) {
|
||||
super(BattlerTagType.FLINCHED, [BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.TURN_END], 1, sourceMove);
|
||||
super(BattlerTagType.FLINCHED, BattlerTagLapseType.TURN_END, 1, sourceMove);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the flinched Pokemon's currently used move this turn if called mid-execution, or removes the tag at end of turn.
|
||||
* @param pokemon - The {@linkcode Pokemon} with this tag.
|
||||
* @param lapseType - The {@linkcode BattlerTagLapseType | lapse type} used for this function call.
|
||||
* @param lapseType - The {@linkcode BattlerTagLapseType | lapse type} used for this function call. Must be {@linkcode BattlerTagLapseType.PRE_MOVE} in order to apply the flinch effect.
|
||||
* @returns Whether the tag should remain active.
|
||||
*/
|
||||
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
||||
if (lapseType === BattlerTagLapseType.PRE_MOVE) {
|
||||
(globalScene.phaseManager.getCurrentPhase() as MovePhase).cancel();
|
||||
const currentPhase = globalScene.phaseManager.getCurrentPhase();
|
||||
if (currentPhase?.is("MovePhase")) {
|
||||
currentPhase.cancel();
|
||||
}
|
||||
globalScene.phaseManager.queueMessage(
|
||||
i18next.t("battlerTags:flinchedLapse", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||
@ -729,7 +732,7 @@ export class FlinchedTag extends BattlerTag {
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.lapse(pokemon, lapseType);
|
||||
return lapseType === BattlerTagLapseType.TURN_END && super.lapse(pokemon, lapseType);
|
||||
}
|
||||
|
||||
getDescriptor(): string {
|
||||
@ -760,7 +763,10 @@ export class InterruptedTag extends BattlerTag {
|
||||
}
|
||||
|
||||
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
||||
(globalScene.phaseManager.getCurrentPhase() as MovePhase).cancel();
|
||||
const currentPhase = globalScene.phaseManager.getCurrentPhase();
|
||||
if (currentPhase?.is("MovePhase")) {
|
||||
currentPhase.cancel();
|
||||
}
|
||||
return super.lapse(pokemon, lapseType);
|
||||
}
|
||||
}
|
||||
@ -771,7 +777,7 @@ export class InterruptedTag extends BattlerTag {
|
||||
export class ConfusedTag extends SerializableBattlerTag {
|
||||
public override readonly tagType = BattlerTagType.CONFUSED;
|
||||
constructor(turnCount: number, sourceMove: MoveId) {
|
||||
super(BattlerTagType.CONFUSED, BattlerTagLapseType.MOVE, turnCount, sourceMove, undefined, true);
|
||||
super(BattlerTagType.CONFUSED, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, undefined, true);
|
||||
}
|
||||
|
||||
canAdd(pokemon: Pokemon): boolean {
|
||||
@ -814,8 +820,18 @@ export class ConfusedTag extends SerializableBattlerTag {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tick down the confusion duration and, if there are remaining turns, activate the confusion effect
|
||||
*
|
||||
* @remarks
|
||||
* Rolls the
|
||||
* @param pokemon - The pokemon with this tag
|
||||
* @param lapseType - The lapse type
|
||||
* @returns
|
||||
*/
|
||||
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
||||
const shouldLapse = lapseType !== BattlerTagLapseType.CUSTOM && super.lapse(pokemon, lapseType);
|
||||
// Duration is only ticked down for PRE_MOVE lapse type
|
||||
const shouldLapse = lapseType === BattlerTagLapseType.PRE_MOVE && super.lapse(pokemon, lapseType);
|
||||
|
||||
if (!shouldLapse) {
|
||||
return false;
|
||||
@ -840,7 +856,10 @@ export class ConfusedTag extends SerializableBattlerTag {
|
||||
// Intentionally don't increment rage fist's hitCount
|
||||
phaseManager.queueMessage(i18next.t("battlerTags:confusedLapseHurtItself"));
|
||||
pokemon.damageAndUpdate(damage, { result: HitResult.CONFUSION });
|
||||
(phaseManager.getCurrentPhase() as MovePhase).cancel();
|
||||
const currentPhase = phaseManager.getCurrentPhase();
|
||||
if (currentPhase?.is("MovePhase") && currentPhase.pokemon === pokemon) {
|
||||
currentPhase.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
@ -916,7 +935,7 @@ export class DestinyBondTag extends SerializableBattlerTag {
|
||||
export class InfatuatedTag extends SerializableBattlerTag {
|
||||
public override readonly tagType = BattlerTagType.INFATUATED;
|
||||
constructor(sourceMove: number, sourceId: number) {
|
||||
super(BattlerTagType.INFATUATED, BattlerTagLapseType.MOVE, 1, sourceMove, sourceId);
|
||||
super(BattlerTagType.INFATUATED, BattlerTagLapseType.CUSTOM, 1, sourceMove, sourceId);
|
||||
}
|
||||
|
||||
canAdd(pokemon: Pokemon): boolean {
|
||||
@ -979,7 +998,10 @@ export class InfatuatedTag extends SerializableBattlerTag {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||
}),
|
||||
);
|
||||
(phaseManager.getCurrentPhase() as MovePhase).cancel();
|
||||
const currentPhase = phaseManager.getCurrentPhase();
|
||||
if (currentPhase?.is("MovePhase")) {
|
||||
currentPhase.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
@ -1105,7 +1127,7 @@ export class SeedTag extends SerializableBattlerTag {
|
||||
export class PowderTag extends BattlerTag {
|
||||
public override readonly tagType = BattlerTagType.POWDER;
|
||||
constructor() {
|
||||
super(BattlerTagType.POWDER, [BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.TURN_END], 1);
|
||||
super(BattlerTagType.POWDER, BattlerTagLapseType.TURN_END, 1);
|
||||
}
|
||||
|
||||
onAdd(pokemon: Pokemon): void {
|
||||
@ -1127,23 +1149,26 @@ export class PowderTag extends BattlerTag {
|
||||
* @returns `true` if the tag should remain active.
|
||||
*/
|
||||
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
||||
const movePhase = globalScene.phaseManager.getCurrentPhase();
|
||||
if (lapseType !== BattlerTagLapseType.PRE_MOVE || !movePhase?.is("MovePhase")) {
|
||||
if (lapseType === BattlerTagLapseType.TURN_END) {
|
||||
return false;
|
||||
}
|
||||
const currentPhase = globalScene.phaseManager.getCurrentPhase();
|
||||
|
||||
const move = movePhase.move.getMove();
|
||||
if (!currentPhase?.is("MovePhase")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const move = currentPhase.move.getMove();
|
||||
const weather = globalScene.arena.weather;
|
||||
if (
|
||||
pokemon.getMoveType(move) !== PokemonType.FIRE ||
|
||||
(weather?.weatherType === WeatherType.HEAVY_RAIN && !weather.isEffectSuppressed()) // Heavy rain takes priority over powder
|
||||
(weather?.weatherType === WeatherType.HEAVY_RAIN && !weather.isEffectSuppressed()) // Since gen 7, Heavy rain takes priority over powder
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Disable the target's fire type move and damage it (subject to Magic Guard)
|
||||
movePhase.showMoveText();
|
||||
movePhase.fail();
|
||||
currentPhase.fail();
|
||||
|
||||
const idx = pokemon.getBattlerIndex();
|
||||
|
||||
@ -1243,13 +1268,7 @@ export class EncoreTag extends MoveRestrictionBattlerTag {
|
||||
public moveId: MoveId;
|
||||
|
||||
constructor(sourceId: number) {
|
||||
super(
|
||||
BattlerTagType.ENCORE,
|
||||
[BattlerTagLapseType.CUSTOM, BattlerTagLapseType.AFTER_MOVE],
|
||||
3,
|
||||
MoveId.ENCORE,
|
||||
sourceId,
|
||||
);
|
||||
super(BattlerTagType.ENCORE, BattlerTagLapseType.AFTER_MOVE, 3, MoveId.ENCORE, sourceId);
|
||||
}
|
||||
|
||||
public override loadTag(source: BaseBattlerTag & Pick<EncoreTag, "tagType" | "moveId">): void {
|
||||
@ -2044,7 +2063,7 @@ export class UnburdenTag extends AbilityBattlerTag {
|
||||
export class TruantTag extends AbilityBattlerTag {
|
||||
public override readonly tagType = BattlerTagType.TRUANT;
|
||||
constructor() {
|
||||
super(BattlerTagType.TRUANT, AbilityId.TRUANT, BattlerTagLapseType.MOVE, 1);
|
||||
super(BattlerTagType.TRUANT, AbilityId.TRUANT, BattlerTagLapseType.CUSTOM, 1);
|
||||
}
|
||||
|
||||
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
||||
@ -2880,12 +2899,7 @@ export class ExposedTag extends SerializableBattlerTag {
|
||||
export class HealBlockTag extends MoveRestrictionBattlerTag {
|
||||
public override readonly tagType = BattlerTagType.HEAL_BLOCK;
|
||||
constructor(turnCount: number, sourceMove: MoveId) {
|
||||
super(
|
||||
BattlerTagType.HEAL_BLOCK,
|
||||
[BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.TURN_END],
|
||||
turnCount,
|
||||
sourceMove,
|
||||
);
|
||||
super(BattlerTagType.HEAL_BLOCK, BattlerTagLapseType.TURN_END, turnCount, sourceMove);
|
||||
}
|
||||
|
||||
onActivation(pokemon: Pokemon): string {
|
||||
@ -3076,7 +3090,7 @@ export class SubstituteTag extends SerializableBattlerTag {
|
||||
constructor(sourceMove: MoveId, sourceId: number) {
|
||||
super(
|
||||
BattlerTagType.SUBSTITUTE,
|
||||
[BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.AFTER_MOVE, BattlerTagLapseType.HIT],
|
||||
[BattlerTagLapseType.MOVE, BattlerTagLapseType.AFTER_MOVE, BattlerTagLapseType.HIT],
|
||||
0,
|
||||
sourceMove,
|
||||
sourceId,
|
||||
@ -3134,7 +3148,7 @@ export class SubstituteTag extends SerializableBattlerTag {
|
||||
|
||||
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
||||
switch (lapseType) {
|
||||
case BattlerTagLapseType.PRE_MOVE:
|
||||
case BattlerTagLapseType.MOVE:
|
||||
this.onPreMove(pokemon);
|
||||
break;
|
||||
case BattlerTagLapseType.AFTER_MOVE:
|
||||
@ -3292,7 +3306,7 @@ export class TormentTag extends MoveRestrictionBattlerTag {
|
||||
export class TauntTag extends MoveRestrictionBattlerTag {
|
||||
public override readonly tagType = BattlerTagType.TAUNT;
|
||||
constructor() {
|
||||
super(BattlerTagType.TAUNT, [BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.AFTER_MOVE], 4, MoveId.TAUNT);
|
||||
super(BattlerTagType.TAUNT, BattlerTagLapseType.AFTER_MOVE, 4, MoveId.TAUNT);
|
||||
}
|
||||
|
||||
override onAdd(pokemon: Pokemon) {
|
||||
@ -3347,13 +3361,7 @@ export class TauntTag extends MoveRestrictionBattlerTag {
|
||||
export class ImprisonTag extends MoveRestrictionBattlerTag {
|
||||
public override readonly tagType = BattlerTagType.IMPRISON;
|
||||
constructor(sourceId: number) {
|
||||
super(
|
||||
BattlerTagType.IMPRISON,
|
||||
[BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.AFTER_MOVE],
|
||||
1,
|
||||
MoveId.IMPRISON,
|
||||
sourceId,
|
||||
);
|
||||
super(BattlerTagType.IMPRISON, BattlerTagLapseType.AFTER_MOVE, 1, MoveId.IMPRISON, sourceId);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -3546,7 +3554,7 @@ export class PowerTrickTag extends SerializableBattlerTag {
|
||||
export class GrudgeTag extends SerializableBattlerTag {
|
||||
public override readonly tagType = BattlerTagType.GRUDGE;
|
||||
constructor() {
|
||||
super(BattlerTagType.GRUDGE, [BattlerTagLapseType.CUSTOM, BattlerTagLapseType.PRE_MOVE], 1, MoveId.GRUDGE);
|
||||
super(BattlerTagType.GRUDGE, BattlerTagLapseType.PRE_MOVE, 1, MoveId.GRUDGE);
|
||||
}
|
||||
|
||||
onAdd(pokemon: Pokemon) {
|
||||
@ -3567,23 +3575,23 @@ export class GrudgeTag extends SerializableBattlerTag {
|
||||
*/
|
||||
// TODO: Confirm whether this should interact with copying moves
|
||||
override lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType, sourcePokemon?: Pokemon): boolean {
|
||||
if (lapseType === BattlerTagLapseType.CUSTOM && sourcePokemon) {
|
||||
if (sourcePokemon.isActive() && pokemon.isOpponent(sourcePokemon)) {
|
||||
const lastMove = pokemon.turnData.attacksReceived[0];
|
||||
const lastMoveData = sourcePokemon.getMoveset().find(m => m.moveId === lastMove.move);
|
||||
if (lastMoveData && lastMove.move !== MoveId.STRUGGLE) {
|
||||
lastMoveData.ppUsed = lastMoveData.getMovePp();
|
||||
globalScene.phaseManager.queueMessage(
|
||||
i18next.t("battlerTags:grudgeLapse", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||
moveName: lastMoveData.getName(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
if (!sourcePokemon || lapseType !== BattlerTagLapseType.CUSTOM) {
|
||||
return super.lapse(pokemon, lapseType);
|
||||
}
|
||||
return super.lapse(pokemon, lapseType);
|
||||
if (sourcePokemon.isActive() && pokemon.isOpponent(sourcePokemon)) {
|
||||
const lastMove = pokemon.turnData.attacksReceived[0];
|
||||
const lastMoveData = sourcePokemon.getMoveset().find(m => m.moveId === lastMove.move);
|
||||
if (lastMoveData && lastMove.move !== MoveId.STRUGGLE) {
|
||||
lastMoveData.ppUsed = lastMoveData.getMovePp();
|
||||
globalScene.phaseManager.queueMessage(
|
||||
i18next.t("battlerTags:grudgeLapse", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||
moveName: lastMoveData.getName(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
259
src/data/moves/move-condition.ts
Normal file
259
src/data/moves/move-condition.ts
Normal file
@ -0,0 +1,259 @@
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
import { TrappedTag } from "#data/battler-tags";
|
||||
import { allMoves } from "#data/data-lists";
|
||||
import { ArenaTagType } from "#enums/arena-tag-type";
|
||||
import { Command } from "#enums/command";
|
||||
import { MoveCategory, type MoveDamageCategory } from "#enums/move-category";
|
||||
import type { MoveId } from "#enums/move-id";
|
||||
import { isVirtual } from "#enums/move-use-mode";
|
||||
import { PokemonType } from "#enums/pokemon-type";
|
||||
import type { Pokemon } from "#field/pokemon";
|
||||
import type { Move, MoveConditionFunc, UserMoveConditionFunc } from "#moves/move";
|
||||
import { getCounterAttackTarget } from "#moves/move-utils";
|
||||
import i18next from "i18next";
|
||||
|
||||
/**
|
||||
* A condition that determines whether a move can be used successfully.
|
||||
*
|
||||
* @remarks
|
||||
* This is only checked when the move is attempted to be invoked. To prevent a move from being selected,
|
||||
* use a {@linkcode MoveRestriction} instead.
|
||||
*/
|
||||
export class MoveCondition {
|
||||
public declare readonly func: MoveConditionFunc;
|
||||
|
||||
/**
|
||||
* @param func - A condition function that determines if the move can be used successfully
|
||||
*/
|
||||
constructor(func: MoveConditionFunc) {
|
||||
this.func = func;
|
||||
}
|
||||
|
||||
apply(user: Pokemon, target: Pokemon, move: Move): boolean {
|
||||
return this.func(user, target, move);
|
||||
}
|
||||
|
||||
getUserBenefitScore(_user: Pokemon, _target: Pokemon, _move: Move): number {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Condition to allow a move's use only on the first turn this Pokemon is sent into battle
|
||||
* (or the start of a new wave, whichever comes first).
|
||||
*/
|
||||
|
||||
export class FirstMoveCondition extends MoveCondition {
|
||||
constructor() {
|
||||
super(user => user.tempSummonData.waveTurnCount === 0 && user.tempSummonData.turnCount === 0);
|
||||
}
|
||||
|
||||
// TODO: Update AI move selection logic to not require this method at all
|
||||
// Currently, it is used to avoid having the AI select the move if its condition will fail
|
||||
getUserBenefitScore(user: Pokemon, _target: Pokemon, _move: Move): number {
|
||||
return this.apply(user, _target, _move) ? 10 : -20;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Condition that fails the move if the user has less than 1/x of their max HP.
|
||||
* @remarks
|
||||
* Used by Clangorous Soul and Fillet Away
|
||||
*/
|
||||
export class FailIfInsufficientHpCondition extends MoveCondition {
|
||||
/**
|
||||
* Condition that fails the move if the user has less than 1/x of their max HP.
|
||||
* @param ratio - The required HP ratio (the `x` in `1/x`)
|
||||
*/
|
||||
constructor(cutRatio: number) {
|
||||
super(user => user.getHpRatio() > 1 / cutRatio);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Teleport condition checks
|
||||
*
|
||||
* @remarks
|
||||
* For trainer pokemon, just checks if there are any benched pokemon allowed in battle
|
||||
*
|
||||
* Wild pokemon cannot teleport if either:
|
||||
* - The current battle is a double battle
|
||||
* - They are under the effects of a *move-based* trapping effect like and are neither a ghost type nor have an active run away ability
|
||||
*/
|
||||
export const failTeleportCondition = new MoveCondition(user => {
|
||||
if (user.hasTrainer()) {
|
||||
const party = user.isPlayer() ? globalScene.getPlayerParty() : globalScene.getEnemyParty();
|
||||
for (const pokemon of party) {
|
||||
if (!pokemon.isOnField() && pokemon.isAllowedInBattle()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Wild pokemon
|
||||
|
||||
// Cannot teleport in double battles (even if last remaining)
|
||||
if (globalScene.currentBattle.double) {
|
||||
return false;
|
||||
}
|
||||
// If smoke ball / shed tail items are ever added, checks for them should be placed here
|
||||
// If a conditional "run away" ability is ever added, then we should use the apply method instead of the `hasAbility`
|
||||
if (user.isOfType(PokemonType.GHOST, true, true) || user.hasAbilityWithAttr("RunSuccessAbAttr")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Wild pokemon are prevented from fleeing if they are trapped *specifically*
|
||||
if (globalScene.arena.hasTag(ArenaTagType.FAIRY_LOCK) || user.getTag(TrappedTag) !== undefined) {
|
||||
// Fairy Lock prevents teleporting
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
/**
|
||||
* Condition that forces moves to fail if the target's selected move is not an attacking move
|
||||
*
|
||||
* @remarks
|
||||
* Used by Sucker Punch and Thunderclap
|
||||
*/
|
||||
export const failIfTargetNotAttackingCondition = new MoveCondition((_user, target) => {
|
||||
const turnCommand = globalScene.currentBattle.turnCommands[target.getBattlerIndex()];
|
||||
if (!turnCommand || !turnCommand.move) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
turnCommand.command === Command.FIGHT &&
|
||||
!target.turnData.acted &&
|
||||
allMoves[turnCommand.move.move].category !== MoveCategory.STATUS
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Condition that forces moves to fail against the final boss in classic and the major boss in endless
|
||||
* @remarks
|
||||
* ⚠️ Only works reliably for single-target moves as only one target is provided; should not be used for multi-target moves
|
||||
* @see {@linkcode GameMode.isBattleClassicFinalBoss}
|
||||
* @see {@linkcode GameMode.isEndlessMinorBoss}
|
||||
*/
|
||||
export const failAgainstFinalBossCondition = new MoveCondition((_user, target) => {
|
||||
const gameMode = globalScene.gameMode;
|
||||
const currentWave = globalScene.currentBattle.waveIndex;
|
||||
return (
|
||||
target.isEnemy() && (gameMode.isBattleClassicFinalBoss(currentWave) || gameMode.isEndlessMinorBoss(currentWave))
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Condition used by the move {@link https://bulbapedia.bulbagarden.net/wiki/Upper_Hand_(move) | Upper Hand}.
|
||||
* Moves with this condition are only successful when the target has selected
|
||||
* a high-priority attack (after factoring in priority-boosting effects) and
|
||||
* hasn't moved yet this turn.
|
||||
*/
|
||||
export const UpperHandCondition = new MoveCondition((_user, target) => {
|
||||
const targetCommand = globalScene.currentBattle.turnCommands[target.getBattlerIndex()];
|
||||
return (
|
||||
targetCommand?.command === Command.FIGHT &&
|
||||
!target.turnData.acted &&
|
||||
!!targetCommand.move?.move &&
|
||||
allMoves[targetCommand.move.move].category !== MoveCategory.STATUS &&
|
||||
allMoves[targetCommand.move.move].getPriority(target) > 0
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Condition used by the move {@link https://bulbapedia.bulbagarden.net/wiki/Last_Resort_(move) | Last Resort}
|
||||
*
|
||||
* @remarks
|
||||
* Last resort fails if
|
||||
* - It is not in the user's moveset
|
||||
* - The user does not know at least one other move
|
||||
* - The user has not directly used each other move in its moveset since it was sent into battle
|
||||
* - A move is considered *used* for this purpose if it passed the first failure check sequence in the move phase
|
||||
* (i.e. its usage message was displayed)
|
||||
*/
|
||||
export const lastResortCondition = new MoveCondition((user, _target, move) => {
|
||||
const otherMovesInMoveset = new Set<MoveId>(user.getMoveset().map(m => m.moveId));
|
||||
if (!otherMovesInMoveset.delete(move.id) || !otherMovesInMoveset.size) {
|
||||
return false; // Last resort fails if used when not in user's moveset or no other moves exist
|
||||
}
|
||||
|
||||
const movesInHistory = new Set<MoveId>(
|
||||
user
|
||||
.getMoveHistory()
|
||||
.filter(m => !isVirtual(m.useMode)) // 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
|
||||
return [...otherMovesInMoveset].every(m => movesInHistory.has(m));
|
||||
});
|
||||
|
||||
/**
|
||||
* Condition used by counter-like moves if the user was hit by at least one qualifying attack this turn.
|
||||
* Qualifying attacks are those that match the specified category (physical, special or either)
|
||||
* that did not come from an ally.
|
||||
*/
|
||||
class CounterAttackConditon extends MoveCondition {
|
||||
/**
|
||||
* @param damageCategory - The category of move to counter (physical or special), or `undefined` to counter both
|
||||
*/
|
||||
constructor(damageCategory?: MoveDamageCategory) {
|
||||
super(user => getCounterAttackTarget(user, damageCategory) !== null);
|
||||
}
|
||||
}
|
||||
|
||||
/** Condition check for counterattacks that proc againt physical moves */
|
||||
export const counterAttackCondition_Physical = new CounterAttackConditon(MoveCategory.PHYSICAL);
|
||||
/** Condition check for counterattacks that proc against special moves*/
|
||||
export const counterAttackCondition_Special = new CounterAttackConditon(MoveCategory.SPECIAL);
|
||||
/** Condition check for counterattacks that proc against moves regardless of damage type */
|
||||
export const counterAttackCondition_Both = new CounterAttackConditon();
|
||||
|
||||
/**
|
||||
* A restriction that prevents a move from being selected
|
||||
*
|
||||
* @remarks
|
||||
* Only checked when the move is selected, but not when it is attempted to be invoked. To prevent a move from being used,
|
||||
* use a {@linkcode MoveCondition} instead.
|
||||
*/
|
||||
export class MoveRestriction {
|
||||
public declare readonly func: UserMoveConditionFunc;
|
||||
public declare readonly i18nkey: string;
|
||||
constructor(func: UserMoveConditionFunc, i18nkey = "battle:moveRestricted") {
|
||||
this.func = func;
|
||||
this.i18nkey = i18nkey;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param user - The Pokemon attempting to select the move
|
||||
* @param move - The move being selected
|
||||
* @returns Whether the move is restricted for the user.
|
||||
*/
|
||||
apply(user: Pokemon, move: Move): boolean {
|
||||
return this.func(user, move);
|
||||
}
|
||||
|
||||
public getSelectionDeniedText(user: Pokemon, move: Move): string {
|
||||
// While not all restriction texts use all the parameters, passing extra ones is harmless
|
||||
return i18next.t(this.i18nkey, { pokemonNameWithAffix: getPokemonNameWithAffix(user), moveName: move.name });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevents a Pokemon from using the move if it was the last move it used
|
||||
*
|
||||
* @remarks
|
||||
* Used by {@link https://bulbapedia.bulbagarden.net/wiki/Blood_Moon_(move) | Blood Moon} and {@link https://bulbapedia.bulbagarden.net/wiki/Gigaton_Hammer_(move) | Gigaton Hammer}
|
||||
*/
|
||||
export const ConsecutiveUseRestriction = new MoveRestriction(
|
||||
(user, move) => user.getLastXMoves(1)[0]?.move === move.id,
|
||||
"battle:moveDisabledConsecutive",
|
||||
);
|
||||
|
||||
/** Prevents a move from being selected if Gravity is in effect */
|
||||
export const GravityUseRestriction = new MoveRestriction(
|
||||
() => globalScene.arena.hasTag(ArenaTagType.GRAVITY),
|
||||
"battle:moveDisabledGravity",
|
||||
);
|
@ -1,6 +1,7 @@
|
||||
import { allMoves } from "#data/data-lists";
|
||||
import type { BattlerIndex } from "#enums/battler-index";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { MoveCategory, type MoveDamageCategory } from "#enums/move-category";
|
||||
import type { MoveId } from "#enums/move-id";
|
||||
import { MoveTarget } from "#enums/move-target";
|
||||
import { PokemonType } from "#enums/pokemon-type";
|
||||
@ -8,6 +9,7 @@ import type { Pokemon } from "#field/pokemon";
|
||||
import { applyMoveAttrs } from "#moves/apply-attrs";
|
||||
import type { Move, MoveTargetSet, UserMoveConditionFunc } from "#moves/move";
|
||||
import { isNullOrUndefined, NumberHolder } from "#utils/common";
|
||||
import { areAllies } from "#utils/pokemon-utils";
|
||||
|
||||
/**
|
||||
* Return whether the move targets the field
|
||||
@ -133,3 +135,25 @@ export const frenzyMissFunc: UserMoveConditionFunc = (user: Pokemon, move: Move)
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine the target for the `user`'s counter-attack move
|
||||
* @param user - The pokemon using the counter-like move
|
||||
* @param damageCategory - The category of move to counter (physical or special), or `undefined` to counter both
|
||||
* @returns - The battler index of the most recent, non-ally attacker using a move that matches the specified category, or `null` if no such attacker exists
|
||||
*/
|
||||
export function getCounterAttackTarget(user: Pokemon, damageCategory?: MoveDamageCategory): BattlerIndex | null {
|
||||
for (const attackRecord of user.turnData.attacksReceived) {
|
||||
// check if the attacker was an ally
|
||||
const moveCategory = allMoves[attackRecord.move].category;
|
||||
const sourceBattlerIndex = attackRecord.sourceBattlerIndex;
|
||||
if (
|
||||
moveCategory !== MoveCategory.STATUS &&
|
||||
!areAllies(sourceBattlerIndex, user.getBattlerIndex()) &&
|
||||
(damageCategory === undefined || moveCategory === damageCategory)
|
||||
) {
|
||||
return sourceBattlerIndex;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -5,6 +5,7 @@ import type { Pokemon } from "#field/pokemon";
|
||||
import type { Move } from "#moves/move";
|
||||
import { applyChallenges } from "#utils/challenge-utils";
|
||||
import { BooleanHolder, toDmgValue } from "#utils/common";
|
||||
import i18next from "i18next";
|
||||
|
||||
/**
|
||||
* Wrapper class for the {@linkcode Move} class for Pokemon to interact with.
|
||||
@ -38,33 +39,58 @@ export class PokemonMove {
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether this move can be selected/performed by a Pokemon, without consideration for the move's targets.
|
||||
* Checks whether this move can be performed by a Pokemon, without consideration for the move's targets.
|
||||
* The move is unusable if it is out of PP, restricted by an effect, or unimplemented.
|
||||
*
|
||||
* Should not be confused with {@linkcode isSelectable}, which only checks if the move can be selected by a Pokemon.
|
||||
*
|
||||
* @param pokemon - The {@linkcode Pokemon} attempting to use this move
|
||||
* @param ignorePp - Whether to ignore checking if the move is out of PP; default `false`
|
||||
* @param ignoreRestrictionTags - Whether to skip checks for {@linkcode MoveRestrictionBattlerTag}s; default `false`
|
||||
* @param forSelection - Whether this is being checked for move selection; default `false`
|
||||
* @returns Whether this {@linkcode PokemonMove} can be selected by this Pokemon.
|
||||
*/
|
||||
isUsable(pokemon: Pokemon, ignorePp = false, ignoreRestrictionTags = false): boolean {
|
||||
public isUsable(pokemon: Pokemon, ignorePp = false, forSelection = false): [boolean, string] {
|
||||
const move = this.getMove();
|
||||
const moveName = move.name;
|
||||
|
||||
// TODO: Add Sky Drop's 1 turn stall
|
||||
const usability = new BooleanHolder(
|
||||
!move.name.endsWith(" (N)") &&
|
||||
(ignorePp || this.ppUsed < this.getMovePp() || move.pp === -1) &&
|
||||
// TODO: Review if the `MoveId.NONE` check is even necessary anymore
|
||||
!(this.moveId !== MoveId.NONE && !ignoreRestrictionTags && pokemon.isMoveRestricted(this.moveId, pokemon)),
|
||||
);
|
||||
if (pokemon.isPlayer()) {
|
||||
applyChallenges(ChallengeType.POKEMON_MOVE, move.id, usability);
|
||||
if (this.moveId === MoveId.NONE || move.name.endsWith(" (N)")) {
|
||||
return [false, i18next.t("battle:moveNotImplemented", moveName.replace(" (N)", ""))];
|
||||
}
|
||||
return usability.value;
|
||||
|
||||
if (!ignorePp && move.pp !== -1 && this.ppUsed >= this.getMovePp()) {
|
||||
return [false, i18next.t("battle:moveNoPP", { moveName: move.name })];
|
||||
}
|
||||
|
||||
if (forSelection) {
|
||||
const result = pokemon.isMoveSelectable(this.moveId);
|
||||
if (!result[0]) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
const usability = new BooleanHolder(true);
|
||||
if (applyChallenges(ChallengeType.POKEMON_MOVE, this.moveId, usability) && !usability.value) {
|
||||
return [false, i18next.t("battle:moveCannotUseChallenge", { moveName: move.name })];
|
||||
}
|
||||
|
||||
return [true, ""];
|
||||
}
|
||||
|
||||
getMove(): Move {
|
||||
return allMoves[this.moveId];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the move can be selected by the pokemon
|
||||
*
|
||||
* @param pokemon - The Pokemon under consideration
|
||||
* @returns An array containing a boolean indicating whether the move can be selected, and a string with the reason if it cannot
|
||||
*/
|
||||
public isSelectable(pokemon: Pokemon): [boolean, string] {
|
||||
return pokemon.isMoveSelectable(this.moveId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets {@link ppUsed} for this move and ensures the value does not exceed {@link getMovePp}
|
||||
* @param count Amount of PP to use
|
||||
|
@ -1,3 +1,5 @@
|
||||
import type { MoveUseMode } from "#enums/move-use-mode";
|
||||
|
||||
/**
|
||||
* Enum representing the possible ways a given BattlerTag can activate and/or tick down.
|
||||
* Each tag can have multiple different behaviors attached to different lapse types.
|
||||
@ -6,11 +8,23 @@ export enum BattlerTagLapseType {
|
||||
// TODO: This is unused...
|
||||
FAINT,
|
||||
/**
|
||||
* Tag activate before the holder uses a non-virtual move, possibly interrupting its action.
|
||||
* Tag activate before the holder uses a non-virtual move, after passing the first failure check sequence during the
|
||||
* move phase.
|
||||
* @see MoveUseMode for more information
|
||||
*/
|
||||
MOVE,
|
||||
/** Tag activates before the holder uses **any** move, triggering effects or interrupting its action. */
|
||||
/**
|
||||
* Tag activates during (or just after) the first failure check sequence in the move phase
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* Note tags with this lapse type will lapse immediately after the first failure check sequence,
|
||||
* regardless of whether the move was successful or not, but is skipped if the move is a
|
||||
* {@linkcode MoveUseMode.FOLLOW_UP | follow-up} move.
|
||||
*
|
||||
* To only lapse the tag between the first and second failure check sequences, use
|
||||
* {@linkcode BattlerTagLapseType.MOVE} instead.
|
||||
*/
|
||||
PRE_MOVE,
|
||||
/** Tag activates immediately after the holder's move finishes triggering (successful or not). */
|
||||
AFTER_MOVE,
|
||||
@ -32,6 +46,16 @@ export enum BattlerTagLapseType {
|
||||
* but still triggers on being KO'd.
|
||||
*/
|
||||
AFTER_HIT,
|
||||
/** The tag has some other custom activation or removal condition. */
|
||||
/**
|
||||
* The tag has some other custom activation or removal condition.
|
||||
* @remarks
|
||||
* Tags can use this lapse type to prevent them from being automatically lapsed during automatic lapse instances,
|
||||
* such as before a move is used or at the end of a turn. Note that a tag's lapse method can still make use of
|
||||
* these lapse types, which can be invoked via the `lapseTag` method on {@linkcode Pokemon} with the tag and
|
||||
* lapse type.
|
||||
* */
|
||||
CUSTOM,
|
||||
}
|
||||
|
||||
/** Same type as {@linkcode BattlerTagLapseType}, but excludes the {@linkcode BattlerTagLapseType.CUSTOM} type */
|
||||
export type NonCustomBattlerTagLapseType = Exclude<BattlerTagLapseType, BattlerTagLapseType.CUSTOM>;
|
@ -3,3 +3,6 @@ export enum MoveCategory {
|
||||
SPECIAL,
|
||||
STATUS
|
||||
}
|
||||
|
||||
/** Type of damage categories */
|
||||
export type MoveDamageCategory = Exclude<MoveCategory, MoveCategory.STATUS>;
|
@ -47,8 +47,8 @@ export enum MoveFlags {
|
||||
CHECK_ALL_HITS = 1 << 16,
|
||||
/** Indicates a move is able to bypass its target's Substitute (if the target has one) */
|
||||
IGNORE_SUBSTITUTE = 1 << 17,
|
||||
/** Indicates a move is able to be redirected to allies in a double battle if the attacker faints */
|
||||
REDIRECT_COUNTER = 1 << 18,
|
||||
/** Indicates a move is able to be reflected by {@linkcode AbilityId.MAGIC_BOUNCE} and {@linkcode MoveId.MAGIC_COAT} */
|
||||
REFLECTABLE = 1 << 19
|
||||
REFLECTABLE = 1 << 18,
|
||||
/** Indicates a move that fails when {@link https://bulbapedia.bulbagarden.net/wiki/Gravity_(move) | Gravity} is in effect */
|
||||
GRAVITY = 1 << 19,
|
||||
}
|
||||
|
@ -724,7 +724,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
|
||||
abstract getFieldIndex(): number;
|
||||
|
||||
abstract getBattlerIndex(): BattlerIndex;
|
||||
abstract getBattlerIndex(): Exclude<BattlerIndex, BattlerIndex.ATTACKER>;
|
||||
|
||||
/**
|
||||
* Load all assets needed for this Pokemon's use in battle
|
||||
@ -2375,7 +2375,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
* @param source {@linkcode Pokemon} The attacking Pokémon.
|
||||
* @param move {@linkcode Move} The move being used by the attacking Pokémon.
|
||||
* @param ignoreAbility Whether to ignore abilities that might affect type effectiveness or immunity (defaults to `false`).
|
||||
* @param simulated Whether to apply abilities via simulated calls (defaults to `true`)
|
||||
* @param simulated - Whether to apply abilities via simulated calls (defaults to `true`). This should only be false during the move effect phase
|
||||
* @param cancelled {@linkcode BooleanHolder} Stores whether the move was cancelled by a non-type-based immunity.
|
||||
* @param useIllusion - Whether we want the attack move effectiveness on the illusion or not
|
||||
* @returns The type damage multiplier, indicating the effectiveness of the move
|
||||
@ -2429,7 +2429,9 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
applyAbAttrs("MoveImmunityAbAttr", commonAbAttrParams);
|
||||
}
|
||||
|
||||
if (!cancelledHolder.value) {
|
||||
// Do not check queenly majesty unless this is being simulated
|
||||
// This is because the move effect phase should not check queenly majesty, as that is handled by the move phase
|
||||
if (simulated && !cancelledHolder.value) {
|
||||
const defendingSidePlayField = this.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField();
|
||||
defendingSidePlayField.forEach(p =>
|
||||
applyAbAttrs("FieldPriorityMoveImmunityAbAttr", {
|
||||
@ -3295,9 +3297,15 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
}
|
||||
}
|
||||
|
||||
public trySelectMove(moveIndex: number, ignorePp?: boolean): boolean {
|
||||
/**
|
||||
* Attempt to select the move at the move index.
|
||||
* @param moveIndex
|
||||
* @param ignorePp
|
||||
* @returns
|
||||
*/
|
||||
public trySelectMove(moveIndex: number, ignorePp?: boolean): [boolean, string] {
|
||||
const move = this.getMoveset().length > moveIndex ? this.getMoveset()[moveIndex] : null;
|
||||
return move?.isUsable(this, ignorePp) ?? false;
|
||||
return move?.isUsable(this, ignorePp, true) ?? [false, ""];
|
||||
}
|
||||
|
||||
showInfo(): void {
|
||||
@ -4267,19 +4275,28 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
}
|
||||
|
||||
/**
|
||||
* Tick down the first {@linkcode BattlerTag} found matching the given {@linkcode BattlerTagType},
|
||||
* removing it if its duration goes below 0.
|
||||
* @param tagType the {@linkcode BattlerTagType} to check against
|
||||
* @returns `true` if the tag was present
|
||||
* Lapse the first {@linkcode BattlerTag} matching `tagType`
|
||||
*
|
||||
* @remarks
|
||||
* Also responsible for removing the tag when the lapse method returns `false`.
|
||||
*
|
||||
*
|
||||
* ⚠️ Lapse types other than `CUSTOM` are generally lapsed automatically. However, some tags
|
||||
* support manually lapsing
|
||||
*
|
||||
* @param tagType - The {@linkcode BattlerTagType} to search for
|
||||
* @param lapseType - The lapse type to use for the lapse method; defaults to {@linkcode BattlerTagLapseType.CUSTOM}
|
||||
* @returns Whether a tag matching the given type was found
|
||||
* @see {@linkcode BattlerTag.lapse}
|
||||
*/
|
||||
lapseTag(tagType: BattlerTagType): boolean {
|
||||
lapseTag(tagType: BattlerTagType, lapseType = BattlerTagLapseType.CUSTOM): boolean {
|
||||
const tags = this.summonData.tags;
|
||||
const tag = tags.find(t => t.tagType === tagType);
|
||||
if (!tag) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!tag.lapse(this, BattlerTagLapseType.CUSTOM)) {
|
||||
if (!tag.lapse(this, lapseType)) {
|
||||
tag.onRemove(this);
|
||||
tags.splice(tags.indexOf(tag), 1);
|
||||
}
|
||||
@ -4291,7 +4308,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
* removing any whose durations fall below 0.
|
||||
* @param tagType the {@linkcode BattlerTagLapseType} to tick down
|
||||
*/
|
||||
lapseTags(lapseType: BattlerTagLapseType): void {
|
||||
lapseTags(lapseType: Exclude<BattlerTagLapseType, BattlerTagLapseType.CUSTOM>): void {
|
||||
const tags = this.summonData.tags;
|
||||
tags
|
||||
.filter(
|
||||
@ -4381,13 +4398,37 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
/**
|
||||
* Gets whether the given move is currently disabled for this Pokemon.
|
||||
*
|
||||
* @remarks
|
||||
* ⚠️ Only checks for restrictions due to a battler tag, not due to the move's own attributes.
|
||||
* (for that behavior, see {@linkcode isMoveSelectable}).
|
||||
*
|
||||
* @param moveId - The {@linkcode MoveId} ID of the move to check
|
||||
* @returns `true` if the move is disabled for this Pokemon, otherwise `false`
|
||||
*
|
||||
* @see {@linkcode MoveRestrictionBattlerTag}
|
||||
*/
|
||||
public isMoveRestricted(moveId: MoveId, pokemon?: Pokemon): boolean {
|
||||
return this.getRestrictingTag(moveId, pokemon) !== null;
|
||||
// TODO: rename this method as it can be easily confused with a move being restricted
|
||||
public isMoveRestricted(moveId: MoveId): boolean {
|
||||
return this.getRestrictingTag(moveId, this) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the given move is selectable by this Pokemon and the message to display if it is not.
|
||||
*
|
||||
* @remarks
|
||||
* Checks both the move's own restrictions and any restrictions imposed by battler tags like disable or throat chop.
|
||||
*
|
||||
* @param moveId - The move ID to check
|
||||
* @returns A tuple of the form [response, msg], where msg contains the text to display if `response` is false.
|
||||
*
|
||||
* @see {@linkcode isMoveRestricted}
|
||||
*/
|
||||
public isMoveSelectable(moveId: MoveId): [boolean, string] {
|
||||
const restrictedTag = this.getRestrictingTag(moveId, this);
|
||||
if (restrictedTag) {
|
||||
return [false, restrictedTag.selectionDeniedText(this, moveId)];
|
||||
}
|
||||
return allMoves[moveId].checkRestrictions(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -5746,7 +5787,7 @@ export class PlayerPokemon extends Pokemon {
|
||||
return globalScene.getPlayerField().indexOf(this);
|
||||
}
|
||||
|
||||
getBattlerIndex(): BattlerIndex {
|
||||
getBattlerIndex(): Exclude<BattlerIndex, BattlerIndex.ATTACKER> {
|
||||
return this.getFieldIndex();
|
||||
}
|
||||
|
||||
@ -6398,7 +6439,7 @@ export class EnemyPokemon extends Pokemon {
|
||||
// If the queued move was called indirectly, ignore all PP and usability checks.
|
||||
// Otherwise, ensure that the move being used is actually usable & in our moveset.
|
||||
// TODO: What should happen if a pokemon forgets a charging move mid-use?
|
||||
if (isVirtual(queuedMove.useMode) || movesetMove?.isUsable(this, isIgnorePP(queuedMove.useMode))) {
|
||||
if (isVirtual(queuedMove.useMode) || movesetMove?.isUsable(this, isIgnorePP(queuedMove.useMode), true)) {
|
||||
moveQueue.splice(0, i); // TODO: This should not be done here
|
||||
return queuedMove;
|
||||
}
|
||||
@ -6408,7 +6449,7 @@ export class EnemyPokemon extends Pokemon {
|
||||
this.summonData.moveQueue = [];
|
||||
|
||||
// Filter out any moves this Pokemon cannot use
|
||||
let movePool = this.getMoveset().filter(m => m.isUsable(this));
|
||||
let movePool = this.getMoveset().filter(m => m.isUsable(this, false, true)[0]);
|
||||
// If no moves are left, use Struggle. Otherwise, continue with move selection
|
||||
if (movePool.length) {
|
||||
// If there's only 1 move in the move pool, use it.
|
||||
@ -6466,7 +6507,7 @@ export class EnemyPokemon extends Pokemon {
|
||||
move.category !== MoveCategory.STATUS &&
|
||||
moveTargets.some(p => {
|
||||
const doesNotFail =
|
||||
move.applyConditions(this, p, move) ||
|
||||
move.applyConditions(this, p) ||
|
||||
[MoveId.SUCKER_PUNCH, MoveId.UPPER_HAND, MoveId.THUNDERCLAP].includes(move.id);
|
||||
return (
|
||||
doesNotFail &&
|
||||
@ -6525,7 +6566,7 @@ export class EnemyPokemon extends Pokemon {
|
||||
* target score to -20
|
||||
*/
|
||||
if (
|
||||
(move.name.endsWith(" (N)") || !move.applyConditions(this, target, move)) &&
|
||||
(move.name.endsWith(" (N)") || !move.applyConditions(this, target)) &&
|
||||
![MoveId.SUCKER_PUNCH, MoveId.UPPER_HAND, MoveId.THUNDERCLAP].includes(move.id)
|
||||
) {
|
||||
targetScore = -20;
|
||||
@ -6862,7 +6903,7 @@ export class EnemyPokemon extends Pokemon {
|
||||
return globalScene.getEnemyField().indexOf(this);
|
||||
}
|
||||
|
||||
getBattlerIndex(): BattlerIndex {
|
||||
getBattlerIndex(): Exclude<BattlerIndex, BattlerIndex.ATTACKER> {
|
||||
return BattlerIndex.ENEMY + this.getFieldIndex();
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,6 @@ import { ArenaTagType } from "#enums/arena-tag-type";
|
||||
import { BattleType } from "#enums/battle-type";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { BiomeId } from "#enums/biome-id";
|
||||
import { ChallengeType } from "#enums/challenge-type";
|
||||
import { Command } from "#enums/command";
|
||||
import { FieldPosition } from "#enums/field-position";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
@ -22,8 +21,6 @@ import type { MoveTargetSet } from "#moves/move";
|
||||
import { getMoveTargets } from "#moves/move-utils";
|
||||
import { FieldPhase } from "#phases/field-phase";
|
||||
import type { TurnMove } from "#types/turn-move";
|
||||
import { applyChallenges } from "#utils/challenge-utils";
|
||||
import { BooleanHolder } from "#utils/common";
|
||||
import i18next from "i18next";
|
||||
|
||||
export class CommandPhase extends FieldPhase {
|
||||
@ -126,7 +123,7 @@ export class CommandPhase extends FieldPhase {
|
||||
if (
|
||||
queuedMove.move !== MoveId.NONE &&
|
||||
!isVirtual(queuedMove.useMode) &&
|
||||
!movesetQueuedMove?.isUsable(playerPokemon, isIgnorePP(queuedMove.useMode))
|
||||
!movesetQueuedMove?.isUsable(playerPokemon, isIgnorePP(queuedMove.useMode), true)
|
||||
) {
|
||||
entriesToDelete++;
|
||||
} else {
|
||||
@ -204,40 +201,18 @@ export class CommandPhase extends FieldPhase {
|
||||
}
|
||||
|
||||
/**
|
||||
* Submethod of {@linkcode handleFightCommand} responsible for queuing the appropriate
|
||||
* error message when a move cannot be used.
|
||||
* @param user - The pokemon using the move
|
||||
* @param cursor - The index of the move in the moveset
|
||||
* Submethod of {@linkcode handleFightCommand} responsible for queuing the provided error message when the move cannot be used
|
||||
* @param msg - The reason why the move cannot be used
|
||||
*/
|
||||
private queueFightErrorMessage(user: PlayerPokemon, cursor: number) {
|
||||
const move = user.getMoveset()[cursor];
|
||||
globalScene.ui.setMode(UiMode.MESSAGE);
|
||||
|
||||
// Set the translation key for why the move cannot be selected
|
||||
let cannotSelectKey: string;
|
||||
const moveStatus = new BooleanHolder(true);
|
||||
applyChallenges(ChallengeType.POKEMON_MOVE, move.moveId, moveStatus);
|
||||
if (!moveStatus.value) {
|
||||
cannotSelectKey = "battle:moveCannotUseChallenge";
|
||||
} else if (move.getPpRatio() === 0) {
|
||||
cannotSelectKey = "battle:moveNoPP";
|
||||
} else if (move.getName().endsWith(" (N)")) {
|
||||
cannotSelectKey = "battle:moveNotImplemented";
|
||||
} else if (user.isMoveRestricted(move.moveId, user)) {
|
||||
cannotSelectKey = user.getRestrictingTag(move.moveId, user)!.selectionDeniedText(user, move.moveId);
|
||||
} else {
|
||||
// TODO: Consider a message that signals a being unusable for an unknown reason
|
||||
cannotSelectKey = "";
|
||||
}
|
||||
|
||||
const moveName = move.getName().replace(" (N)", ""); // Trims off the indicator
|
||||
|
||||
globalScene.ui.showText(
|
||||
i18next.t(cannotSelectKey, { moveName: moveName }),
|
||||
private queueFightErrorMessage(msg: string) {
|
||||
const ui = globalScene.ui;
|
||||
ui.setMode(UiMode.MESSAGE);
|
||||
ui.showText(
|
||||
msg,
|
||||
null,
|
||||
() => {
|
||||
globalScene.ui.clearText();
|
||||
globalScene.ui.setMode(UiMode.FIGHT, this.fieldIndex);
|
||||
ui.clearText();
|
||||
ui.setMode(UiMode.FIGHT, this.fieldIndex);
|
||||
},
|
||||
null,
|
||||
true,
|
||||
@ -274,18 +249,16 @@ export class CommandPhase extends FieldPhase {
|
||||
): boolean {
|
||||
const playerPokemon = this.getPokemon();
|
||||
const ignorePP = isIgnorePP(useMode);
|
||||
|
||||
let canUse = cursor === -1 || playerPokemon.trySelectMove(cursor, ignorePP);
|
||||
const [canUse, reason] = cursor === -1 ? [true, ""] : playerPokemon.trySelectMove(cursor, ignorePP);
|
||||
|
||||
// Ternary here ensures we don't compute struggle conditions unless necessary
|
||||
const useStruggle = canUse
|
||||
? false
|
||||
: cursor > -1 && !playerPokemon.getMoveset().some(m => m.isUsable(playerPokemon));
|
||||
: cursor > -1 && !playerPokemon.getMoveset().some(m => m.isUsable(playerPokemon, ignorePP, true)[0]);
|
||||
|
||||
canUse ||= useStruggle;
|
||||
|
||||
if (!canUse) {
|
||||
this.queueFightErrorMessage(playerPokemon, cursor);
|
||||
if (!canUse && !useStruggle) {
|
||||
console.error("Cannot use move:", reason);
|
||||
this.queueFightErrorMessage(reason);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -16,7 +16,7 @@ export class MoveHeaderPhase extends BattlePhase {
|
||||
}
|
||||
|
||||
canMove(): boolean {
|
||||
return this.pokemon.isActive(true) && this.move.isUsable(this.pokemon);
|
||||
return this.pokemon.isActive(true) && this.move.isUsable(this.pokemon)[0];
|
||||
}
|
||||
|
||||
start() {
|
||||
|
@ -8,9 +8,11 @@ import { getStatusEffectActivationText, getStatusEffectHealText } from "#data/st
|
||||
import { getTerrainBlockMessage } from "#data/terrain";
|
||||
import { getWeatherBlockMessage } from "#data/weather";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
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";
|
||||
@ -24,7 +26,10 @@ import { applyMoveAttrs } from "#moves/apply-attrs";
|
||||
import { frenzyMissFunc } from "#moves/move-utils";
|
||||
import type { PokemonMove } from "#moves/pokemon-move";
|
||||
import { BattlePhase } from "#phases/battle-phase";
|
||||
import { NumberHolder } from "#utils/common";
|
||||
// biome-ignore lint/correctness/noUnusedImports: Used in a tsdoc comment
|
||||
import type { Move, PreUseInterruptAttr } from "#types/move-types";
|
||||
import { applyChallenges } from "#utils/challenge-utils";
|
||||
import { BooleanHolder, NumberHolder } from "#utils/common";
|
||||
import { enumValueToKey } from "#utils/enums";
|
||||
import i18next from "i18next";
|
||||
|
||||
@ -41,6 +46,9 @@ export class MovePhase extends BattlePhase {
|
||||
/** 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;
|
||||
|
||||
public get pokemon(): Pokemon {
|
||||
return this._pokemon;
|
||||
}
|
||||
@ -84,19 +92,6 @@ export class MovePhase extends BattlePhase {
|
||||
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;
|
||||
@ -115,27 +110,240 @@ export class MovePhase extends BattlePhase {
|
||||
return this.forcedLast;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (not implemented in Pokerogue)
|
||||
* 6. Truant
|
||||
* 7. 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 (not implemented in Pokerogue, can't occur here from 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 (unused in Pokerogue)
|
||||
*/
|
||||
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 post1stFailSleepOrThaw(): void {
|
||||
const user = this.pokemon;
|
||||
// If the move was successful, then... play the "sleeping" animation if the user is asleep but uses something like rest / snore
|
||||
// Cure the user's freeze and queue the thaw message from unfreezing due to move use
|
||||
if (!isIgnoreStatus(this.useMode)) {
|
||||
if (user.status?.effect === StatusEffect.SLEEP) {
|
||||
// Commence the sleeping animation and message, which happens anyway
|
||||
// TODO...
|
||||
} else if (this.thaw) {
|
||||
this.cureStatus(
|
||||
StatusEffect.FREEZE,
|
||||
i18next.t("statusEffect:freeze.healByMove", {
|
||||
pokemonName: 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, ther 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 happen in the move effect phase.
|
||||
*/
|
||||
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]);
|
||||
const failedDueToTerrain = arena.isMoveTerrainCancelled(user, this.targets, move);
|
||||
let failed = failsConditions || failedDueToTerrain;
|
||||
|
||||
// Apply queenly majesty / dazzling
|
||||
if (!failed) {
|
||||
const defendingSidePlayField = user.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField();
|
||||
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;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
if (!this.pokemon.isActive(true)) {
|
||||
this.end();
|
||||
return;
|
||||
}
|
||||
|
||||
this.pokemon.turnData.acted = true;
|
||||
const user = this.pokemon;
|
||||
|
||||
// Removing gigaton hammer always happens first
|
||||
user.removeTag(BattlerTagType.ALWAYS_GET_HIT);
|
||||
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);
|
||||
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 (useMode === MoveUseMode.FOLLOW_UP) {
|
||||
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 this is a follow-up move , at this point, we need to re-check a few conditions
|
||||
|
||||
// If the first failure check passes (and this is not a sub-move) then thaw the user if its move will thaw it.
|
||||
// The sleep message and animation should also play if the user is asleep but using a move anyway (snore, sleep talk, etc)
|
||||
if (useMode !== MoveUseMode.FOLLOW_UP) {
|
||||
this.post1stFailSleepOrThaw();
|
||||
}
|
||||
|
||||
// Reset hit-related turn data when starting follow-up moves (e.g. Metronomed moves, Dancer repeats)
|
||||
if (isVirtual(this.useMode)) {
|
||||
if (isVirtual(useMode)) {
|
||||
this.pokemon.turnData.hitsLeft = -1;
|
||||
this.pokemon.turnData.hitCount = 0;
|
||||
}
|
||||
@ -151,22 +359,41 @@ export class MovePhase extends BattlePhase {
|
||||
globalScene.arena.setIgnoreAbilities(true, this.pokemon.getBattlerIndex());
|
||||
}
|
||||
|
||||
this.resolveRedirectTarget();
|
||||
// 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();
|
||||
|
||||
this.resolvePreMoveStatusEffects();
|
||||
// Move is announced
|
||||
this.showMoveText();
|
||||
// Stance change happens
|
||||
const charging = this.move.getMove().isChargingMove() && !this.pokemon.getTag(BattlerTagType.CHARGING);
|
||||
const move = this.move.getMove();
|
||||
|
||||
this.lapsePreMoveAndMoveTags();
|
||||
|
||||
if (!(this.failed || this.cancelled)) {
|
||||
this.resolveFinalPreMoveCancellationChecks();
|
||||
// Update the battle's "last move" pointer unless we're currently mimicking a move or triggering Dancer.
|
||||
if (!move.hasAttr("CopyMoveAttr") && !isReflected(this.useMode)) {
|
||||
globalScene.currentBattle.lastMove = move.id;
|
||||
}
|
||||
|
||||
// Cancel, charge or use the move as applicable.
|
||||
if (this.cancelled || this.failed) {
|
||||
// Stance change happens now if the move is about to be executed and is not a charging move
|
||||
if (!charging) {
|
||||
this.usePP();
|
||||
globalScene.triggerPokemonFormChange(this.pokemon, SpeciesFormChangePreMoveTrigger);
|
||||
}
|
||||
|
||||
if (this.secondFailureCheck()) {
|
||||
this.handlePreMoveFailures();
|
||||
} else if (this.move.getMove().isChargingMove() && !this.pokemon.getTag(BattlerTagType.CHARGING)) {
|
||||
this.end();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.resolveFinalPreMoveCancellationChecks()) {
|
||||
this.end();
|
||||
}
|
||||
|
||||
if (charging) {
|
||||
this.chargeMove();
|
||||
} else {
|
||||
this.useMove();
|
||||
@ -175,8 +402,11 @@ export class MovePhase extends BattlePhase {
|
||||
this.end();
|
||||
}
|
||||
|
||||
/** Check for cancellation edge cases - no targets remaining, or {@linkcode MoveId.NONE} is in the queue */
|
||||
protected resolveFinalPreMoveCancellationChecks(): void {
|
||||
/**
|
||||
* 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();
|
||||
|
||||
@ -184,10 +414,12 @@ export class MovePhase extends BattlePhase {
|
||||
(targets.length === 0 && !this.move.getMove().hasAttr("AddArenaTrapTagAttr")) ||
|
||||
(moveQueue.length > 0 && moveQueue[0].move === MoveId.NONE)
|
||||
) {
|
||||
this.showMoveText();
|
||||
this.showFailedText();
|
||||
this.cancel();
|
||||
this.fail();
|
||||
return true;
|
||||
}
|
||||
this.pokemon.lapseTags(BattlerTagLapseType.MOVE);
|
||||
return false;
|
||||
}
|
||||
|
||||
public getActiveTargetPokemon(): Pokemon[] {
|
||||
@ -195,177 +427,290 @@ export class MovePhase extends BattlePhase {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles {@link StatusEffect.SLEEP | Sleep}/{@link StatusEffect.PARALYSIS | Paralysis}/{@link StatusEffect.FREEZE | Freeze} rolls and side effects.
|
||||
* Queue the status cure message, reset the status, and update the Pokemon info display
|
||||
* @param effect - The effect being cured
|
||||
* @param msg - A custom message to display when curing the status effect (used for curing freeze due to move use)
|
||||
*/
|
||||
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", {
|
||||
pokemon: this.pokemon,
|
||||
statusEffect: this.pokemon.status.effect,
|
||||
duration: 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();
|
||||
}
|
||||
private cureStatus(effect: StatusEffect, msg?: string): void {
|
||||
const pokemon = this.pokemon;
|
||||
// Freeze healed by move uses its own msg
|
||||
globalScene.phaseManager.queueMessage(msg ?? getStatusEffectHealText(effect, getPokemonNameWithAffix(pokemon)));
|
||||
pokemon.resetStatus();
|
||||
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.
|
||||
* Queue the status activation message, play its animation, and cancel the move
|
||||
* @param effect - The effect being triggered
|
||||
*/
|
||||
protected lapsePreMoveAndMoveTags(): void {
|
||||
this.pokemon.lapseTags(BattlerTagLapseType.PRE_MOVE);
|
||||
private triggerStatus(effect: StatusEffect): void {
|
||||
const pokemon = this.pokemon;
|
||||
this.showFailedText(getStatusEffectActivationText(effect, getPokemonNameWithAffix(pokemon)));
|
||||
globalScene.phaseManager.unshiftNew(
|
||||
"CommonAnimPhase",
|
||||
pokemon.getBattlerIndex(),
|
||||
undefined,
|
||||
CommonAnim.POISON + (effect - 1), // offset anim # by effect #
|
||||
);
|
||||
this.cancelled = true;
|
||||
}
|
||||
|
||||
// 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);
|
||||
/**
|
||||
* Handle the sleep check
|
||||
* @returns Whether the move was cancelled due to sleep
|
||||
*/
|
||||
protected checkSleep(): boolean {
|
||||
if (this.pokemon.status?.effect !== StatusEffect.SLEEP) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// For some reason, dancer will immediately wake its user from sleep when triggering
|
||||
if (this.useMode === MoveUseMode.INDIRECT) {
|
||||
this.pokemon.resetStatus(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
this.pokemon.status.incrementTurn();
|
||||
applyMoveAttrs("BypassSleepAttr", this.pokemon, null, this.move.getMove());
|
||||
const turnsRemaining = new NumberHolder(this.pokemon.status.sleepTurnsRemaining ?? 0);
|
||||
applyAbAttrs("ReduceStatusEffectDurationAbAttr", {
|
||||
pokemon: this.pokemon,
|
||||
statusEffect: this.pokemon.status.effect,
|
||||
duration: turnsRemaining,
|
||||
});
|
||||
this.pokemon.status.sleepTurnsRemaining = turnsRemaining.value;
|
||||
if (this.pokemon.status.sleepTurnsRemaining <= 0) {
|
||||
this.cureStatus(StatusEffect.SLEEP);
|
||||
return false;
|
||||
}
|
||||
|
||||
this.triggerStatus(StatusEffect.SLEEP);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
*/
|
||||
protected checkFreeze(): boolean {
|
||||
if (this.pokemon.status?.effect !== StatusEffect.FREEZE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// For some reason, dancer will immediately its user
|
||||
if (this.useMode === MoveUseMode.INDIRECT) {
|
||||
this.pokemon.resetStatus(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if move use would heal the user
|
||||
|
||||
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))
|
||||
) {
|
||||
// On cartridge, burn up will not cure if it would fail
|
||||
if (move.id === MoveId.BURN_UP && !this.pokemon.isOfType(PokemonType.FIRE)) {
|
||||
}
|
||||
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)) ||
|
||||
(!this.pokemon.randBattleSeedInt(5) && Overrides.STATUS_ACTIVATION_OVERRIDE !== true)
|
||||
) {
|
||||
this.cureStatus(StatusEffect.FREEZE);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Otherwise, trigger the freeze status effect
|
||||
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
|
||||
*/
|
||||
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)) {
|
||||
// Play the failure message
|
||||
this.showFailedText(
|
||||
i18next.t("battle:moveDisabledGravity", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(this.pokemon),
|
||||
moveName: move.name,
|
||||
}),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 =
|
||||
(this.pokemon.randBattleSeedInt(4) === 0 || Overrides.STATUS_ACTIVATION_OVERRIDE === true) &&
|
||||
Overrides.STATUS_ACTIVATION_OVERRIDE !== false;
|
||||
if (!proc) {
|
||||
return false;
|
||||
}
|
||||
this.triggerStatus(StatusEffect.PARALYSIS);
|
||||
return true;
|
||||
}
|
||||
|
||||
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, this.move.getMove(), this.move.ppUsed));
|
||||
}
|
||||
}
|
||||
|
||||
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 if the move has any attributes that can interrupt its own use **before** displaying text.
|
||||
// TODO: This should not rely on direct return values
|
||||
let failed = move.getAttrs("PreUseInterruptAttr").some(attr => attr.apply(this.pokemon, targets[0], move));
|
||||
if (failed) {
|
||||
this.failMove(false);
|
||||
return;
|
||||
}
|
||||
|
||||
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 = moveQueue.shift()?.useMode ?? this.useMode;
|
||||
this.useMode = user.getMoveQueue().shift()?.useMode ?? this.useMode;
|
||||
|
||||
if (this.pokemon.getTag(BattlerTagType.CHARGING)?.sourceMove === this.move.moveId) {
|
||||
this.pokemon.lapseTag(BattlerTagType.CHARGING);
|
||||
if (user.getTag(BattlerTagType.CHARGING)?.sourceMove === this.move.moveId) {
|
||||
user.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));
|
||||
if (!this.thirdFailureCheck()) {
|
||||
this.executeMove();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
|
||||
/** Execute the current move and apply its effects. */
|
||||
private executeMove() {
|
||||
const user = this.pokemon;
|
||||
const move = this.move.getMove();
|
||||
const targets = this.getActiveTargetPokemon();
|
||||
|
||||
// Update the battle's "last move" pointer unless we're currently mimicking a move or triggering Dancer.
|
||||
if (!move.hasAttr("CopyMoveAttr") && !isReflected(this.useMode)) {
|
||||
globalScene.currentBattle.lastMove = move.id;
|
||||
}
|
||||
const opponent = this.getActiveTargetPokemon()[0];
|
||||
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
|
||||
applyAbAttrs("PokemonTypeChangeAbAttr", { pokemon: this.pokemon, move, opponent: targets[0] });
|
||||
this.showMoveText();
|
||||
globalScene.phaseManager.unshiftNew(
|
||||
"MoveEffectPhase",
|
||||
this.pokemon.getBattlerIndex(),
|
||||
this.targets,
|
||||
move,
|
||||
this.useMode,
|
||||
);
|
||||
applyAbAttrs("PokemonTypeChangeAbAttr", { pokemon: user, move, opponent });
|
||||
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.
|
||||
@ -373,7 +718,7 @@ export class MovePhase extends BattlePhase {
|
||||
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 => {
|
||||
applyAbAttrs("PostMoveUsedAbAttr", { pokemon, move: this.move, source: this.pokemon, targets: this.targets });
|
||||
applyAbAttrs("PostMoveUsedAbAttr", { pokemon, move: this.move, source: user, targets: targets });
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -381,11 +726,9 @@ export class MovePhase extends BattlePhase {
|
||||
/**
|
||||
* 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) {
|
||||
protected failMove(failedDueToTerrain = false) {
|
||||
const move = this.move.getMove();
|
||||
const targets = this.getActiveTargetPokemon();
|
||||
|
||||
@ -407,10 +750,6 @@ export class MovePhase extends BattlePhase {
|
||||
});
|
||||
}
|
||||
|
||||
if (showText) {
|
||||
this.showMoveText();
|
||||
}
|
||||
|
||||
this.pokemon.pushMoveHistory({
|
||||
move: this.move.moveId,
|
||||
targets: this.targets,
|
||||
@ -424,9 +763,7 @@ export class MovePhase extends BattlePhase {
|
||||
move.getFailedText(this.pokemon, targets[0], move) ||
|
||||
(failedDueToTerrain
|
||||
? getTerrainBlockMessage(targets[0], globalScene.arena.getTerrainType())
|
||||
: failedDueToWeather
|
||||
? getWeatherBlockMessage(globalScene.arena.getWeatherType())
|
||||
: i18next.t("battle:attackFailed"));
|
||||
: i18next.t("battle:attackFailed"));
|
||||
|
||||
this.showFailedText(failureMessage);
|
||||
|
||||
@ -442,8 +779,8 @@ export class MovePhase extends BattlePhase {
|
||||
const move = this.move.getMove();
|
||||
const targets = this.getActiveTargetPokemon();
|
||||
|
||||
if (!move.applyConditions(this.pokemon, targets[0], move)) {
|
||||
this.failMove(true);
|
||||
if (!move.applyConditions(this.pokemon, targets[0])) {
|
||||
this.failMove();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -506,6 +843,7 @@ export class MovePhase extends BattlePhase {
|
||||
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
|
||||
globalScene
|
||||
.getField(true)
|
||||
.filter(p => p !== this.pokemon)
|
||||
@ -570,33 +908,21 @@ export class MovePhase extends BattlePhase {
|
||||
* 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.
|
||||
* @todo Make this a feature of the move rather than basing logic on {@linkcode BattlerIndex.ATTACKER}
|
||||
* move is marked for failure
|
||||
*/
|
||||
protected resolveCounterAttackTarget(): void {
|
||||
if (this.targets.length !== 1 || this.targets[0] !== BattlerIndex.ATTACKER) {
|
||||
console.log("%cSkipping counter attack target resolution", "color: blue");
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: This should be covered in move conditions
|
||||
if (this.pokemon.turnData.attacksReceived.length === 0) {
|
||||
const targetHolder = new NumberHolder(BattlerIndex.ATTACKER);
|
||||
|
||||
applyMoveAttrs("CounterRedirectAttr", this.pokemon, null, this.move.getMove(), targetHolder);
|
||||
this.targets[0] = targetHolder.value;
|
||||
if (targetHolder.value === BattlerIndex.ATTACKER) {
|
||||
console.log("%cSkipping counter attack target resolution", "color: red");
|
||||
this.fail();
|
||||
this.showMoveText();
|
||||
this.showFailedText();
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -619,13 +945,6 @@ export class MovePhase extends BattlePhase {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.failed) {
|
||||
// TODO: should this consider struggle?
|
||||
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.some(t => t.tagType === BattlerTagType.FRENZY)) {
|
||||
frenzyMissFunc(this.pokemon, this.move.getMove());
|
||||
}
|
||||
|
@ -2,7 +2,10 @@ import { globalScene } from "#app/global-scene";
|
||||
import { POKERUS_STARTER_COUNT, speciesStarterCosts } from "#balance/starters";
|
||||
import { allSpecies } from "#data/data-lists";
|
||||
import type { PokemonSpecies, PokemonSpeciesForm } from "#data/pokemon-species";
|
||||
import { BattlerIndex } from "#enums/battler-index";
|
||||
import type { SpeciesId } from "#enums/species-id";
|
||||
// biome-ignore lint/correctness/noUnusedImports: Used in a TSDoc comment
|
||||
import type { Pokemon } from "#field/pokemon";
|
||||
import { randSeedItem } from "./common";
|
||||
|
||||
/**
|
||||
@ -123,3 +126,20 @@ export function getPokemonSpeciesForm(species: SpeciesId, formIndex: number): Po
|
||||
}
|
||||
return retSpecies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether two battler indices are considered allies.
|
||||
* To instead check with {@linkcode Pokemon} objects, use {@linkcode Pokemon.isOpponent}.
|
||||
* @param a - First battler index
|
||||
* @param b - Second battler index
|
||||
* @returns Whether the two battler indices are allies. Always `false` if either index is `ATTACKER`.
|
||||
*/
|
||||
export function areAllies(a: BattlerIndex, b: BattlerIndex): boolean {
|
||||
if (a === BattlerIndex.ATTACKER || b === BattlerIndex.ATTACKER) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
(a === BattlerIndex.PLAYER || a === BattlerIndex.PLAYER_2) ===
|
||||
(b === BattlerIndex.PLAYER || b === BattlerIndex.PLAYER_2)
|
||||
);
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { CudChewConsumeBerryAbAttr } from "#abilities/ability";
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
@ -6,7 +5,6 @@ import { BerryType } from "#enums/berry-type";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { Stat } from "#enums/stat";
|
||||
import { Pokemon } from "#field/pokemon";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import i18next from "i18next";
|
||||
import Phaser from "phaser";
|
||||
@ -111,7 +109,6 @@ describe("Abilities - Cud Chew", () => {
|
||||
|
||||
it("can store multiple berries across 2 turns with teatime", async () => {
|
||||
// always eat first berry for stuff cheeks & company
|
||||
vi.spyOn(Pokemon.prototype, "randBattleSeedInt").mockReturnValue(0);
|
||||
game.override
|
||||
.startingHeldItems([
|
||||
{ name: "BERRY", type: BerryType.PETAYA, count: 3 },
|
||||
@ -121,7 +118,10 @@ describe("Abilities - Cud Chew", () => {
|
||||
await game.classicMode.startBattle([SpeciesId.FARIGIRAF]);
|
||||
|
||||
const farigiraf = game.field.getPlayerPokemon();
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
farigiraf.hp = 1; // needed to allow berry procs
|
||||
vi.spyOn(farigiraf, "randBattleSeedInt").mockReturnValue(0);
|
||||
vi.spyOn(enemy, "randBattleSeedInt").mockReturnValue(0);
|
||||
|
||||
game.move.select(MoveId.STUFF_CHEEKS);
|
||||
await game.toNextTurn();
|
||||
@ -196,10 +196,10 @@ describe("Abilities - Cud Chew", () => {
|
||||
|
||||
describe("regurgiates berries", () => {
|
||||
it("re-triggers effects on eater without pushing to array", async () => {
|
||||
const apply = vi.spyOn(CudChewConsumeBerryAbAttr.prototype, "apply");
|
||||
await game.classicMode.startBattle([SpeciesId.FARIGIRAF]);
|
||||
|
||||
const farigiraf = game.field.getPlayerPokemon();
|
||||
const apply = vi.spyOn(farigiraf.getAbilityAttrs("CudChewConsumeBerryAbAttr")[0], "apply");
|
||||
farigiraf.hp = 1;
|
||||
|
||||
game.move.select(MoveId.SPLASH);
|
||||
|
@ -142,7 +142,7 @@ describe("BattlerTag - SubstituteTag", () => {
|
||||
vi.spyOn(messages, "getPokemonNameWithAffix").mockReturnValue("");
|
||||
});
|
||||
|
||||
it("PRE_MOVE lapse triggers pre-move animation", async () => {
|
||||
it("MOVE lapse triggers pre-move animation", async () => {
|
||||
const subject = new SubstituteTag(MoveId.SUBSTITUTE, mockPokemon.id);
|
||||
|
||||
vi.spyOn(mockPokemon.scene as BattleScene, "triggerPokemonBattleAnim").mockImplementation(
|
||||
@ -154,7 +154,7 @@ describe("BattlerTag - SubstituteTag", () => {
|
||||
|
||||
vi.spyOn((mockPokemon.scene as BattleScene).phaseManager, "queueMessage").mockReturnValue();
|
||||
|
||||
expect(subject.lapse(mockPokemon, BattlerTagLapseType.PRE_MOVE)).toBeTruthy();
|
||||
expect(subject.lapse(mockPokemon, BattlerTagLapseType.MOVE)).toBeTruthy();
|
||||
|
||||
expect(subject.sourceInFocus).toBeTruthy();
|
||||
expect((mockPokemon.scene as BattleScene).triggerPokemonBattleAnim).toHaveBeenCalledTimes(1);
|
||||
|
49
test/moves/belch.test.ts
Normal file
49
test/moves/belch.test.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
describe("Move - Belch", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.ability(AbilityId.BALL_FETCH)
|
||||
.battleStyle("single")
|
||||
.criticalHits(false)
|
||||
.moveset(MoveId.BELCH)
|
||||
.enemySpecies(SpeciesId.MAGIKARP)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
});
|
||||
|
||||
it("should only be selectable if the user has previously eaten a berry", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
const player = game.field.getPlayerPokemon();
|
||||
expect(
|
||||
!game.field.getPlayerPokemon().isMoveSelectable(MoveId.BELCH),
|
||||
"Belch should not be selectable without a berry",
|
||||
);
|
||||
|
||||
player.battleData.hasEatenBerry = true;
|
||||
|
||||
expect(
|
||||
game.field.getPlayerPokemon().isMoveSelectable(MoveId.BELCH),
|
||||
"Belch should be selectable after eating a berry",
|
||||
);
|
||||
});
|
||||
});
|
@ -33,7 +33,7 @@ describe("Moves - Gigaton Hammer", () => {
|
||||
});
|
||||
|
||||
it("can't be used two turns in a row", async () => {
|
||||
await game.classicMode.startBattle();
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
|
||||
const enemy1 = game.field.getEnemyPokemon();
|
||||
|
||||
@ -46,17 +46,17 @@ describe("Moves - Gigaton Hammer", () => {
|
||||
await game.doKillOpponents();
|
||||
await game.toNextWave();
|
||||
|
||||
// Attempting to use Gigaton Hammer again should result in struggle
|
||||
game.move.select(MoveId.GIGATON_HAMMER);
|
||||
await game.toNextTurn();
|
||||
|
||||
const enemy2 = game.field.getEnemyPokemon();
|
||||
|
||||
expect(enemy2.hp).toBe(enemy2.getMaxHp());
|
||||
const player = game.field.getPlayerPokemon();
|
||||
expect(player.getLastXMoves()[0]?.move).toBe(MoveId.STRUGGLE);
|
||||
});
|
||||
|
||||
it("can be used again if recalled and sent back out", async () => {
|
||||
game.override.startingWave(4);
|
||||
await game.classicMode.startBattle();
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
|
||||
const enemy1 = game.field.getEnemyPokemon();
|
||||
|
||||
|
@ -35,6 +35,7 @@ describe("Moves - Throat Chop", () => {
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
const player = game.field.getPlayerPokemon();
|
||||
|
||||
game.move.select(MoveId.GROWL);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
@ -46,6 +47,8 @@ describe("Moves - Throat Chop", () => {
|
||||
// Second turn, struggle if no valid moves
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(player.trySelectMove(MoveId.GROWL)[0]).toBe(false);
|
||||
|
||||
game.move.select(MoveId.GROWL);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user