mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-08-19 13:59:27 +02:00
Add failure conditions and move failures part 1
This commit is contained in:
parent
dd03887d05
commit
6b34ea3c46
@ -9,7 +9,7 @@ import { getStatusEffectHealText } from "#data/status-effect";
|
|||||||
import { TerrainType } from "#data/terrain";
|
import { TerrainType } from "#data/terrain";
|
||||||
import { AbilityId } from "#enums/ability-id";
|
import { AbilityId } from "#enums/ability-id";
|
||||||
import type { BattlerIndex } from "#enums/battler-index";
|
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 { BattlerTagType } from "#enums/battler-tag-type";
|
||||||
import { HitResult } from "#enums/hit-result";
|
import { HitResult } from "#enums/hit-result";
|
||||||
import { ChargeAnim, CommonAnim } from "#enums/move-anims-common";
|
import { ChargeAnim, CommonAnim } from "#enums/move-anims-common";
|
||||||
@ -121,6 +121,11 @@ export class BattlerTag implements BaseBattlerTag {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#lapseTypes: readonly [BattlerTagLapseType, ...BattlerTagLapseType[]];
|
#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[] {
|
public get lapseTypes(): readonly BattlerTagLapseType[] {
|
||||||
return this.#lapseTypes;
|
return this.#lapseTypes;
|
||||||
}
|
}
|
||||||
@ -128,7 +133,7 @@ export class BattlerTag implements BaseBattlerTag {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
tagType: BattlerTagType,
|
tagType: BattlerTagType,
|
||||||
lapseType: BattlerTagLapseType | [BattlerTagLapseType, ...BattlerTagLapseType[]],
|
lapseType: BattlerTagLapseType | [NonCustomBattlerTagLapseType, ...NonCustomBattlerTagLapseType[]],
|
||||||
turnCount: number,
|
turnCount: number,
|
||||||
sourceMove?: MoveId,
|
sourceMove?: MoveId,
|
||||||
sourceId?: number,
|
sourceId?: number,
|
||||||
@ -160,7 +165,10 @@ export class BattlerTag implements BaseBattlerTag {
|
|||||||
onOverlap(_pokemon: Pokemon): void {}
|
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.
|
* @param _pokemon - The {@linkcode Pokemon} whom this tag belongs to.
|
||||||
* Unused by default but can be used by subclasses.
|
* Unused by default but can be used by subclasses.
|
||||||
* @param _lapseType - The {@linkcode BattlerTagLapseType} being lapsed.
|
* @param _lapseType - The {@linkcode BattlerTagLapseType} being lapsed.
|
||||||
@ -302,12 +310,7 @@ export abstract class MoveRestrictionBattlerTag extends SerializableBattlerTag {
|
|||||||
export class ThroatChoppedTag extends MoveRestrictionBattlerTag {
|
export class ThroatChoppedTag extends MoveRestrictionBattlerTag {
|
||||||
public override readonly tagType = BattlerTagType.THROAT_CHOPPED;
|
public override readonly tagType = BattlerTagType.THROAT_CHOPPED;
|
||||||
constructor() {
|
constructor() {
|
||||||
super(
|
super(BattlerTagType.THROAT_CHOPPED, BattlerTagLapseType.TURN_END, 2, MoveId.THROAT_CHOP);
|
||||||
BattlerTagType.THROAT_CHOPPED,
|
|
||||||
[BattlerTagLapseType.TURN_END, BattlerTagLapseType.PRE_MOVE],
|
|
||||||
2,
|
|
||||||
MoveId.THROAT_CHOP,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -356,13 +359,7 @@ export class DisabledTag extends MoveRestrictionBattlerTag {
|
|||||||
public readonly moveId: MoveId = MoveId.NONE;
|
public readonly moveId: MoveId = MoveId.NONE;
|
||||||
|
|
||||||
constructor(sourceId: number) {
|
constructor(sourceId: number) {
|
||||||
super(
|
super(BattlerTagType.DISABLED, BattlerTagLapseType.TURN_END, 4, MoveId.DISABLE, sourceId);
|
||||||
BattlerTagType.DISABLED,
|
|
||||||
[BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.TURN_END],
|
|
||||||
4,
|
|
||||||
MoveId.DISABLE,
|
|
||||||
sourceId,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override isMoveRestricted(move: MoveId): boolean {
|
override isMoveRestricted(move: MoveId): boolean {
|
||||||
@ -511,7 +508,10 @@ export class RechargingTag extends SerializableBattlerTag {
|
|||||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
(globalScene.phaseManager.getCurrentPhase() as MovePhase).cancel();
|
const currentPhase = globalScene.phaseManager.getCurrentPhase();
|
||||||
|
if (currentPhase?.is("MovePhase")) {
|
||||||
|
currentPhase.cancel();
|
||||||
|
}
|
||||||
pokemon.getMoveQueue().shift();
|
pokemon.getMoveQueue().shift();
|
||||||
}
|
}
|
||||||
return super.lapse(pokemon, lapseType);
|
return super.lapse(pokemon, lapseType);
|
||||||
@ -708,18 +708,21 @@ class NoRetreatTag extends TrappedTag {
|
|||||||
export class FlinchedTag extends BattlerTag {
|
export class FlinchedTag extends BattlerTag {
|
||||||
public override readonly tagType = BattlerTagType.FLINCHED;
|
public override readonly tagType = BattlerTagType.FLINCHED;
|
||||||
constructor(sourceMove: MoveId) {
|
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.
|
* 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 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.
|
* @returns Whether the tag should remain active.
|
||||||
*/
|
*/
|
||||||
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
||||||
if (lapseType === BattlerTagLapseType.PRE_MOVE) {
|
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(
|
globalScene.phaseManager.queueMessage(
|
||||||
i18next.t("battlerTags:flinchedLapse", {
|
i18next.t("battlerTags:flinchedLapse", {
|
||||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||||
@ -729,7 +732,7 @@ export class FlinchedTag extends BattlerTag {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.lapse(pokemon, lapseType);
|
return lapseType === BattlerTagLapseType.TURN_END && super.lapse(pokemon, lapseType);
|
||||||
}
|
}
|
||||||
|
|
||||||
getDescriptor(): string {
|
getDescriptor(): string {
|
||||||
@ -760,7 +763,10 @@ export class InterruptedTag extends BattlerTag {
|
|||||||
}
|
}
|
||||||
|
|
||||||
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
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);
|
return super.lapse(pokemon, lapseType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -771,7 +777,7 @@ export class InterruptedTag extends BattlerTag {
|
|||||||
export class ConfusedTag extends SerializableBattlerTag {
|
export class ConfusedTag extends SerializableBattlerTag {
|
||||||
public override readonly tagType = BattlerTagType.CONFUSED;
|
public override readonly tagType = BattlerTagType.CONFUSED;
|
||||||
constructor(turnCount: number, sourceMove: MoveId) {
|
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 {
|
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 {
|
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) {
|
if (!shouldLapse) {
|
||||||
return false;
|
return false;
|
||||||
@ -840,7 +856,10 @@ export class ConfusedTag extends SerializableBattlerTag {
|
|||||||
// Intentionally don't increment rage fist's hitCount
|
// Intentionally don't increment rage fist's hitCount
|
||||||
phaseManager.queueMessage(i18next.t("battlerTags:confusedLapseHurtItself"));
|
phaseManager.queueMessage(i18next.t("battlerTags:confusedLapseHurtItself"));
|
||||||
pokemon.damageAndUpdate(damage, { result: HitResult.CONFUSION });
|
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;
|
return true;
|
||||||
@ -916,7 +935,7 @@ export class DestinyBondTag extends SerializableBattlerTag {
|
|||||||
export class InfatuatedTag extends SerializableBattlerTag {
|
export class InfatuatedTag extends SerializableBattlerTag {
|
||||||
public override readonly tagType = BattlerTagType.INFATUATED;
|
public override readonly tagType = BattlerTagType.INFATUATED;
|
||||||
constructor(sourceMove: number, sourceId: number) {
|
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 {
|
canAdd(pokemon: Pokemon): boolean {
|
||||||
@ -979,7 +998,10 @@ export class InfatuatedTag extends SerializableBattlerTag {
|
|||||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
(phaseManager.getCurrentPhase() as MovePhase).cancel();
|
const currentPhase = phaseManager.getCurrentPhase();
|
||||||
|
if (currentPhase?.is("MovePhase")) {
|
||||||
|
currentPhase.cancel();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@ -1105,7 +1127,7 @@ export class SeedTag extends SerializableBattlerTag {
|
|||||||
export class PowderTag extends BattlerTag {
|
export class PowderTag extends BattlerTag {
|
||||||
public override readonly tagType = BattlerTagType.POWDER;
|
public override readonly tagType = BattlerTagType.POWDER;
|
||||||
constructor() {
|
constructor() {
|
||||||
super(BattlerTagType.POWDER, [BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.TURN_END], 1);
|
super(BattlerTagType.POWDER, BattlerTagLapseType.TURN_END, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
onAdd(pokemon: Pokemon): void {
|
onAdd(pokemon: Pokemon): void {
|
||||||
@ -1127,23 +1149,27 @@ export class PowderTag extends BattlerTag {
|
|||||||
* @returns `true` if the tag should remain active.
|
* @returns `true` if the tag should remain active.
|
||||||
*/
|
*/
|
||||||
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
||||||
const movePhase = globalScene.phaseManager.getCurrentPhase();
|
if (lapseType === BattlerTagLapseType.TURN_END) {
|
||||||
if (lapseType !== BattlerTagLapseType.PRE_MOVE || !movePhase?.is("MovePhase")) {
|
|
||||||
return false;
|
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;
|
const weather = globalScene.arena.weather;
|
||||||
if (
|
if (
|
||||||
pokemon.getMoveType(move) !== PokemonType.FIRE ||
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disable the target's fire type move and damage it (subject to Magic Guard)
|
// Disable the target's fire type move and damage it (subject to Magic Guard)
|
||||||
movePhase.showMoveText();
|
currentPhase.showMoveText();
|
||||||
movePhase.fail();
|
currentPhase.fail();
|
||||||
|
|
||||||
const idx = pokemon.getBattlerIndex();
|
const idx = pokemon.getBattlerIndex();
|
||||||
|
|
||||||
@ -1243,13 +1269,7 @@ export class EncoreTag extends MoveRestrictionBattlerTag {
|
|||||||
public moveId: MoveId;
|
public moveId: MoveId;
|
||||||
|
|
||||||
constructor(sourceId: number) {
|
constructor(sourceId: number) {
|
||||||
super(
|
super(BattlerTagType.ENCORE, BattlerTagLapseType.AFTER_MOVE, 3, MoveId.ENCORE, sourceId);
|
||||||
BattlerTagType.ENCORE,
|
|
||||||
[BattlerTagLapseType.CUSTOM, BattlerTagLapseType.AFTER_MOVE],
|
|
||||||
3,
|
|
||||||
MoveId.ENCORE,
|
|
||||||
sourceId,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override loadTag(source: BaseBattlerTag & Pick<EncoreTag, "tagType" | "moveId">): void {
|
public override loadTag(source: BaseBattlerTag & Pick<EncoreTag, "tagType" | "moveId">): void {
|
||||||
@ -2044,7 +2064,7 @@ export class UnburdenTag extends AbilityBattlerTag {
|
|||||||
export class TruantTag extends AbilityBattlerTag {
|
export class TruantTag extends AbilityBattlerTag {
|
||||||
public override readonly tagType = BattlerTagType.TRUANT;
|
public override readonly tagType = BattlerTagType.TRUANT;
|
||||||
constructor() {
|
constructor() {
|
||||||
super(BattlerTagType.TRUANT, AbilityId.TRUANT, BattlerTagLapseType.MOVE, 1);
|
super(BattlerTagType.TRUANT, AbilityId.TRUANT, BattlerTagLapseType.CUSTOM, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
||||||
@ -2880,12 +2900,7 @@ export class ExposedTag extends SerializableBattlerTag {
|
|||||||
export class HealBlockTag extends MoveRestrictionBattlerTag {
|
export class HealBlockTag extends MoveRestrictionBattlerTag {
|
||||||
public override readonly tagType = BattlerTagType.HEAL_BLOCK;
|
public override readonly tagType = BattlerTagType.HEAL_BLOCK;
|
||||||
constructor(turnCount: number, sourceMove: MoveId) {
|
constructor(turnCount: number, sourceMove: MoveId) {
|
||||||
super(
|
super(BattlerTagType.HEAL_BLOCK, BattlerTagLapseType.TURN_END, turnCount, sourceMove);
|
||||||
BattlerTagType.HEAL_BLOCK,
|
|
||||||
[BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.TURN_END],
|
|
||||||
turnCount,
|
|
||||||
sourceMove,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onActivation(pokemon: Pokemon): string {
|
onActivation(pokemon: Pokemon): string {
|
||||||
@ -3134,7 +3149,7 @@ export class SubstituteTag extends SerializableBattlerTag {
|
|||||||
|
|
||||||
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
||||||
switch (lapseType) {
|
switch (lapseType) {
|
||||||
case BattlerTagLapseType.PRE_MOVE:
|
case BattlerTagLapseType.MOVE:
|
||||||
this.onPreMove(pokemon);
|
this.onPreMove(pokemon);
|
||||||
break;
|
break;
|
||||||
case BattlerTagLapseType.AFTER_MOVE:
|
case BattlerTagLapseType.AFTER_MOVE:
|
||||||
@ -3292,7 +3307,7 @@ export class TormentTag extends MoveRestrictionBattlerTag {
|
|||||||
export class TauntTag extends MoveRestrictionBattlerTag {
|
export class TauntTag extends MoveRestrictionBattlerTag {
|
||||||
public override readonly tagType = BattlerTagType.TAUNT;
|
public override readonly tagType = BattlerTagType.TAUNT;
|
||||||
constructor() {
|
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) {
|
override onAdd(pokemon: Pokemon) {
|
||||||
@ -3347,13 +3362,7 @@ export class TauntTag extends MoveRestrictionBattlerTag {
|
|||||||
export class ImprisonTag extends MoveRestrictionBattlerTag {
|
export class ImprisonTag extends MoveRestrictionBattlerTag {
|
||||||
public override readonly tagType = BattlerTagType.IMPRISON;
|
public override readonly tagType = BattlerTagType.IMPRISON;
|
||||||
constructor(sourceId: number) {
|
constructor(sourceId: number) {
|
||||||
super(
|
super(BattlerTagType.IMPRISON, BattlerTagLapseType.AFTER_MOVE, 1, MoveId.IMPRISON, sourceId);
|
||||||
BattlerTagType.IMPRISON,
|
|
||||||
[BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.AFTER_MOVE],
|
|
||||||
1,
|
|
||||||
MoveId.IMPRISON,
|
|
||||||
sourceId,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -3546,7 +3555,7 @@ export class PowerTrickTag extends SerializableBattlerTag {
|
|||||||
export class GrudgeTag extends SerializableBattlerTag {
|
export class GrudgeTag extends SerializableBattlerTag {
|
||||||
public override readonly tagType = BattlerTagType.GRUDGE;
|
public override readonly tagType = BattlerTagType.GRUDGE;
|
||||||
constructor() {
|
constructor() {
|
||||||
super(BattlerTagType.GRUDGE, [BattlerTagLapseType.CUSTOM, BattlerTagLapseType.PRE_MOVE], 1, MoveId.GRUDGE);
|
super(BattlerTagType.GRUDGE, BattlerTagLapseType.PRE_MOVE, 1, MoveId.GRUDGE);
|
||||||
}
|
}
|
||||||
|
|
||||||
onAdd(pokemon: Pokemon) {
|
onAdd(pokemon: Pokemon) {
|
||||||
@ -3567,23 +3576,23 @@ export class GrudgeTag extends SerializableBattlerTag {
|
|||||||
*/
|
*/
|
||||||
// TODO: Confirm whether this should interact with copying moves
|
// TODO: Confirm whether this should interact with copying moves
|
||||||
override lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType, sourcePokemon?: Pokemon): boolean {
|
override lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType, sourcePokemon?: Pokemon): boolean {
|
||||||
if (lapseType === BattlerTagLapseType.CUSTOM && sourcePokemon) {
|
if (!sourcePokemon || lapseType !== BattlerTagLapseType.CUSTOM) {
|
||||||
if (sourcePokemon.isActive() && pokemon.isOpponent(sourcePokemon)) {
|
return super.lapse(pokemon, lapseType);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
117
src/data/moves/move-condition.ts
Normal file
117
src/data/moves/move-condition.ts
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import { globalScene } from "#app/global-scene";
|
||||||
|
import { getPokemonNameWithAffix } from "#app/messages";
|
||||||
|
import { allMoves } from "#data/data-lists";
|
||||||
|
import { ArenaTagType } from "#enums/arena-tag-type";
|
||||||
|
import { Command } from "#enums/command";
|
||||||
|
import { MoveCategory } from "#enums/move-category";
|
||||||
|
import type { Pokemon } from "#field/pokemon";
|
||||||
|
import type { Move, MoveConditionFunc, UserMoveConditionFunc } from "#moves/move";
|
||||||
|
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) {
|
||||||
|
if (func) {
|
||||||
|
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 {
|
||||||
|
public override readonly func: MoveConditionFunc = user => {
|
||||||
|
return user.tempSummonData.waveTurnCount === 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
getUserBenefitScore(user: Pokemon, _target: Pokemon, _move: Move): number {
|
||||||
|
return this.apply(user, _target, _move) ? 10 : -20;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 class UpperHandCondition extends MoveCondition {
|
||||||
|
public override readonly func: MoveConditionFunc = (_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
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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",
|
||||||
|
);
|
@ -93,12 +93,13 @@ import { getEnumValues } from "#utils/enums";
|
|||||||
import { toCamelCase, toTitleCase } from "#utils/strings";
|
import { toCamelCase, toTitleCase } from "#utils/strings";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import { applyChallenges } from "#utils/challenge-utils";
|
import { applyChallenges } from "#utils/challenge-utils";
|
||||||
|
import { ConsecutiveUseRestriction, FirstMoveCondition, GravityUseRestriction, MoveCondition, MoveRestriction, UpperHandCondition } from "#moves/move-condition";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A function used to conditionally determine execution of a given {@linkcode MoveAttr}.
|
* A function used to conditionally determine execution of a given {@linkcode MoveAttr}.
|
||||||
* Conventionally returns `true` for success and `false` for failure.
|
* Conventionally returns `true` for success and `false` for failure.
|
||||||
*/
|
*/
|
||||||
type MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => boolean;
|
export type MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => boolean;
|
||||||
export type UserMoveConditionFunc = (user: Pokemon, move: Move) => boolean;
|
export type UserMoveConditionFunc = (user: Pokemon, move: Move) => boolean;
|
||||||
|
|
||||||
export abstract class Move implements Localizable {
|
export abstract class Move implements Localizable {
|
||||||
@ -116,7 +117,16 @@ export abstract class Move implements Localizable {
|
|||||||
public priority: number;
|
public priority: number;
|
||||||
public generation: number;
|
public generation: number;
|
||||||
public attrs: MoveAttr[] = [];
|
public attrs: MoveAttr[] = [];
|
||||||
|
/** Conditions that must be met for the move to succeed when it is used.
|
||||||
|
*
|
||||||
|
* @remarks Different from {@linkcode restrictions}, which is checked when the move is selected
|
||||||
|
*/
|
||||||
private conditions: MoveCondition[] = [];
|
private conditions: MoveCondition[] = [];
|
||||||
|
/** Conditions that must be false for a move to be able to be selected.
|
||||||
|
*
|
||||||
|
* @remarks Different from {@linkcode conditions}, which is checked when the move is invoked
|
||||||
|
*/
|
||||||
|
private restrictions: MoveRestriction[] = [];
|
||||||
/** The move's {@linkcode MoveFlags} */
|
/** The move's {@linkcode MoveFlags} */
|
||||||
private flags: number = 0;
|
private flags: number = 0;
|
||||||
private nameAppend: string = "";
|
private nameAppend: string = "";
|
||||||
@ -370,7 +380,7 @@ export abstract class Move implements Localizable {
|
|||||||
* Adds a condition to this move (in addition to any provided by its prior {@linkcode MoveAttr}s).
|
* Adds a condition to this move (in addition to any provided by its prior {@linkcode MoveAttr}s).
|
||||||
* The move will fail upon use if at least 1 of its conditions is not met.
|
* The move will fail upon use if at least 1 of its conditions is not met.
|
||||||
* @param condition - The {@linkcode MoveCondition} or {@linkcode MoveConditionFunc} to add to the conditions array.
|
* @param condition - The {@linkcode MoveCondition} or {@linkcode MoveConditionFunc} to add to the conditions array.
|
||||||
* @returns `this`
|
* @returns `this` for method chaining
|
||||||
*/
|
*/
|
||||||
condition(condition: MoveCondition | MoveConditionFunc): this {
|
condition(condition: MoveCondition | MoveConditionFunc): this {
|
||||||
if (typeof condition === "function") {
|
if (typeof condition === "function") {
|
||||||
@ -381,6 +391,48 @@ export abstract class Move implements Localizable {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a restriction condition to this move.
|
||||||
|
* The move will not be selectable if at least 1 of its restrictions is met.
|
||||||
|
* @param restriction - A function that evaluates to `true` if the move is restricted from being selected
|
||||||
|
* @returns `this` for method chaining
|
||||||
|
*/
|
||||||
|
public restriction(restriction: MoveRestriction): this;
|
||||||
|
/**
|
||||||
|
* Adds a restriction condition to this move.
|
||||||
|
* The move will not be selectable if at least 1 of its restrictions is met.
|
||||||
|
* @param restriction - The function or `MoveRestriction` that evaluates to `true` if the move is restricted from
|
||||||
|
* being selected
|
||||||
|
* @param i18nkey - The i18n key for the restriction text
|
||||||
|
* @param alsoCondition - If `true`, also adds a {@linkcode MoveCondition} that checks the same condition when the
|
||||||
|
* move is used; default `false`
|
||||||
|
* @returns `this` for method chaining
|
||||||
|
*/
|
||||||
|
|
||||||
|
public restriction(restriction: UserMoveConditionFunc, i18nkey: string, alsoCondition?: boolean): this;
|
||||||
|
/**
|
||||||
|
* Adds a restriction condition to this move.
|
||||||
|
* The move will not be selectable if at least 1 of its restrictions is met.
|
||||||
|
* @param restriction - The function or `MoveRestriction` that evaluates to `true` if the move is restricted from
|
||||||
|
* being selected
|
||||||
|
* @param i18nkey - The i18n key for the restriction text, ignored if `restriction` is a `MoveRestriction`
|
||||||
|
* @param alsoCondition - If `true`, also adds a {@linkcode MoveCondition} that checks the same condition when the
|
||||||
|
* move is used; default `false`. Ignored if `restriction` is a `MoveRestriction`.
|
||||||
|
* @returns `this` for method chaining
|
||||||
|
*/
|
||||||
|
public restriction<T extends UserMoveConditionFunc | MoveRestriction>(restriction: T, i18nkey?: string, alsoCondition: typeof restriction extends MoveRestriction ? false : boolean = false): this {
|
||||||
|
if (typeof restriction === "function") {
|
||||||
|
this.restrictions.push(new MoveRestriction(restriction));
|
||||||
|
if (alsoCondition) {
|
||||||
|
this.conditions.push(new MoveCondition((user, _, move) => restriction(user, move)));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.restrictions.push(restriction);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mark a move as having one or more edge cases.
|
* Mark a move as having one or more edge cases.
|
||||||
* The move may lack certain niche interactions with other moves/abilities,
|
* The move may lack certain niche interactions with other moves/abilities,
|
||||||
@ -579,6 +631,19 @@ export abstract class Move implements Localizable {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@linkcode MoveFlags.GRAVITY} flag for the calling Move and adds {@linkcode GravityUseRestriction} to the
|
||||||
|
* move's restrictions.
|
||||||
|
*
|
||||||
|
* @see {@linkcode MoveId.GRAVITY}
|
||||||
|
* @returns The {@linkcode Move} that called this function
|
||||||
|
*/
|
||||||
|
affectedByGravity(): this {
|
||||||
|
this.setFlag(MoveFlags.GRAVITY, true);
|
||||||
|
this.restrictions.push(GravityUseRestriction);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the {@linkcode MoveFlags.IGNORE_ABILITIES} flag for the calling Move
|
* Sets the {@linkcode MoveFlags.IGNORE_ABILITIES} flag for the calling Move
|
||||||
* @see {@linkcode MoveId.SUNSTEEL_STRIKE}
|
* @see {@linkcode MoveId.SUNSTEEL_STRIKE}
|
||||||
@ -702,8 +767,29 @@ export abstract class Move implements Localizable {
|
|||||||
* @param move {@linkcode Move} to apply conditions to
|
* @param move {@linkcode Move} to apply conditions to
|
||||||
* @returns boolean: false if any of the apply()'s return false, else true
|
* @returns boolean: false if any of the apply()'s return false, else true
|
||||||
*/
|
*/
|
||||||
applyConditions(user: Pokemon, target: Pokemon, move: Move): boolean {
|
applyConditions(user: Pokemon, target: Pokemon): boolean {
|
||||||
return this.conditions.every(cond => cond.apply(user, target, move));
|
return this.conditions.every(cond => cond.apply(user, target, this));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the move is restricted from being selected due to its own requirements.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* Does not check for external factors that prohibit move selection, such as disable
|
||||||
|
*
|
||||||
|
* @param user - The Pokemon using the move
|
||||||
|
* @returns - An array whose first element is `false` if the move is restricted, and the second element is a string
|
||||||
|
* with the reason for the restriction, otherwise, `false` and the empty string.
|
||||||
|
*/
|
||||||
|
public checkRestrictions(user: Pokemon): [boolean, string] {
|
||||||
|
for (const restriction of this.restrictions) {
|
||||||
|
if (restriction.apply(user, this)) {
|
||||||
|
return [false, restriction.getSelectionDeniedText(user, this)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [true, ""];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1473,13 +1559,21 @@ export class PreUseInterruptAttr extends MoveAttr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Message to display when a move is interrupted.
|
* Cancel the current MovePhase and queue the interrupt message if the condition is met
|
||||||
* @param user {@linkcode Pokemon} using the move
|
* @param user - {@linkcode Pokemon} using the move
|
||||||
* @param target {@linkcode Pokemon} target of the move
|
* @param target - {@linkcode Pokemon} target of the move
|
||||||
* @param move {@linkcode Move} with this attribute
|
* @param move - {@linkcode Move} with this attribute
|
||||||
*/
|
*/
|
||||||
override apply(user: Pokemon, target: Pokemon, move: Move): boolean {
|
override apply(user: Pokemon, target: Pokemon, move: Move): boolean {
|
||||||
return this.conditionFunc(user, target, move);
|
const currentPhase = globalScene.phaseManager.getCurrentPhase();
|
||||||
|
if (!currentPhase?.is("MovePhase") || !this.conditionFunc(user, target, move)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
currentPhase.cancel();
|
||||||
|
globalScene.phaseManager.queueMessage(
|
||||||
|
typeof this.message === "string" ? this.message : this.message(user, target, move)
|
||||||
|
)
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -8040,8 +8134,6 @@ const phaseForcedSlower = (phase: MovePhase, target: Pokemon, trickRoom: boolean
|
|||||||
return phase.isForcedLast() && slower;
|
return phase.isForcedLast() && slower;
|
||||||
};
|
};
|
||||||
|
|
||||||
const failOnGravityCondition: MoveConditionFunc = (user, target, move) => !globalScene.arena.getTag(ArenaTagType.GRAVITY);
|
|
||||||
|
|
||||||
const failOnBossCondition: MoveConditionFunc = (user, target, move) => !target.isBossImmune();
|
const failOnBossCondition: MoveConditionFunc = (user, target, move) => !target.isBossImmune();
|
||||||
|
|
||||||
const failIfSingleBattle: MoveConditionFunc = (user, target, move) => globalScene.currentBattle.double;
|
const failIfSingleBattle: MoveConditionFunc = (user, target, move) => globalScene.currentBattle.double;
|
||||||
@ -8081,57 +8173,6 @@ const attackedByItemMessageFunc = (user: Pokemon, target: Pokemon, move: Move) =
|
|||||||
return message;
|
return message;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class MoveCondition {
|
|
||||||
protected func: MoveConditionFunc;
|
|
||||||
|
|
||||||
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, _target, _move) => user.tempSummonData.waveTurnCount === 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
getUserBenefitScore(user: Pokemon, _target: Pokemon, _move: Move): number {
|
|
||||||
return this.apply(user, _target, _move) ? 10 : -20;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 class UpperHandCondition extends MoveCondition {
|
|
||||||
constructor() {
|
|
||||||
super((user, target, move) => {
|
|
||||||
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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class HitsSameTypeAttr extends VariableMoveTypeMultiplierAttr {
|
export class HitsSameTypeAttr extends VariableMoveTypeMultiplierAttr {
|
||||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||||
const multiplier = args[0] as NumberHolder;
|
const multiplier = args[0] as NumberHolder;
|
||||||
@ -8538,7 +8579,7 @@ export function initMoves() {
|
|||||||
new ChargingAttackMove(MoveId.FLY, PokemonType.FLYING, MoveCategory.PHYSICAL, 90, 95, 15, -1, 0, 1)
|
new ChargingAttackMove(MoveId.FLY, PokemonType.FLYING, MoveCategory.PHYSICAL, 90, 95, 15, -1, 0, 1)
|
||||||
.chargeText(i18next.t("moveTriggers:flewUpHigh", { pokemonName: "{USER}" }))
|
.chargeText(i18next.t("moveTriggers:flewUpHigh", { pokemonName: "{USER}" }))
|
||||||
.chargeAttr(SemiInvulnerableAttr, BattlerTagType.FLYING)
|
.chargeAttr(SemiInvulnerableAttr, BattlerTagType.FLYING)
|
||||||
.condition(failOnGravityCondition),
|
.affectedByGravity(),
|
||||||
new AttackMove(MoveId.BIND, PokemonType.NORMAL, MoveCategory.PHYSICAL, 15, 85, 20, -1, 0, 1)
|
new AttackMove(MoveId.BIND, PokemonType.NORMAL, MoveCategory.PHYSICAL, 15, 85, 20, -1, 0, 1)
|
||||||
.attr(TrapAttr, BattlerTagType.BIND),
|
.attr(TrapAttr, BattlerTagType.BIND),
|
||||||
new AttackMove(MoveId.SLAM, PokemonType.NORMAL, MoveCategory.PHYSICAL, 80, 75, 20, -1, 0, 1),
|
new AttackMove(MoveId.SLAM, PokemonType.NORMAL, MoveCategory.PHYSICAL, 80, 75, 20, -1, 0, 1),
|
||||||
@ -8553,7 +8594,7 @@ export function initMoves() {
|
|||||||
new AttackMove(MoveId.JUMP_KICK, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 100, 95, 10, -1, 0, 1)
|
new AttackMove(MoveId.JUMP_KICK, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 100, 95, 10, -1, 0, 1)
|
||||||
.attr(MissEffectAttr, crashDamageFunc)
|
.attr(MissEffectAttr, crashDamageFunc)
|
||||||
.attr(NoEffectAttr, crashDamageFunc)
|
.attr(NoEffectAttr, crashDamageFunc)
|
||||||
.condition(failOnGravityCondition)
|
.affectedByGravity()
|
||||||
.recklessMove(),
|
.recklessMove(),
|
||||||
new AttackMove(MoveId.ROLLING_KICK, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 60, 85, 15, 30, 0, 1)
|
new AttackMove(MoveId.ROLLING_KICK, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 60, 85, 15, 30, 0, 1)
|
||||||
.attr(FlinchAttr),
|
.attr(FlinchAttr),
|
||||||
@ -8868,7 +8909,7 @@ export function initMoves() {
|
|||||||
new AttackMove(MoveId.HIGH_JUMP_KICK, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 130, 90, 10, -1, 0, 1)
|
new AttackMove(MoveId.HIGH_JUMP_KICK, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 130, 90, 10, -1, 0, 1)
|
||||||
.attr(MissEffectAttr, crashDamageFunc)
|
.attr(MissEffectAttr, crashDamageFunc)
|
||||||
.attr(NoEffectAttr, crashDamageFunc)
|
.attr(NoEffectAttr, crashDamageFunc)
|
||||||
.condition(failOnGravityCondition)
|
.affectedByGravity()
|
||||||
.recklessMove(),
|
.recklessMove(),
|
||||||
new StatusMove(MoveId.GLARE, PokemonType.NORMAL, 100, 30, -1, 0, 1)
|
new StatusMove(MoveId.GLARE, PokemonType.NORMAL, 100, 30, -1, 0, 1)
|
||||||
.attr(StatusEffectAttr, StatusEffect.PARALYSIS)
|
.attr(StatusEffectAttr, StatusEffect.PARALYSIS)
|
||||||
@ -8922,7 +8963,7 @@ export function initMoves() {
|
|||||||
.attr(RandomLevelDamageAttr),
|
.attr(RandomLevelDamageAttr),
|
||||||
new SelfStatusMove(MoveId.SPLASH, PokemonType.NORMAL, -1, 40, -1, 0, 1)
|
new SelfStatusMove(MoveId.SPLASH, PokemonType.NORMAL, -1, 40, -1, 0, 1)
|
||||||
.attr(MessageAttr, i18next.t("moveTriggers:splash"))
|
.attr(MessageAttr, i18next.t("moveTriggers:splash"))
|
||||||
.condition(failOnGravityCondition),
|
.affectedByGravity(),
|
||||||
new SelfStatusMove(MoveId.ACID_ARMOR, PokemonType.POISON, -1, 20, -1, 0, 1)
|
new SelfStatusMove(MoveId.ACID_ARMOR, PokemonType.POISON, -1, 20, -1, 0, 1)
|
||||||
.attr(StatStageChangeAttr, [ Stat.DEF ], 2, true),
|
.attr(StatStageChangeAttr, [ Stat.DEF ], 2, true),
|
||||||
new AttackMove(MoveId.CRABHAMMER, PokemonType.WATER, MoveCategory.PHYSICAL, 100, 90, 10, -1, 0, 1)
|
new AttackMove(MoveId.CRABHAMMER, PokemonType.WATER, MoveCategory.PHYSICAL, 100, 90, 10, -1, 0, 1)
|
||||||
@ -9563,7 +9604,7 @@ export function initMoves() {
|
|||||||
.chargeText(i18next.t("moveTriggers:sprangUp", { pokemonName: "{USER}" }))
|
.chargeText(i18next.t("moveTriggers:sprangUp", { pokemonName: "{USER}" }))
|
||||||
.chargeAttr(SemiInvulnerableAttr, BattlerTagType.FLYING)
|
.chargeAttr(SemiInvulnerableAttr, BattlerTagType.FLYING)
|
||||||
.attr(StatusEffectAttr, StatusEffect.PARALYSIS)
|
.attr(StatusEffectAttr, StatusEffect.PARALYSIS)
|
||||||
.condition(failOnGravityCondition),
|
.affectedByGravity(),
|
||||||
new AttackMove(MoveId.MUD_SHOT, PokemonType.GROUND, MoveCategory.SPECIAL, 55, 95, 15, 100, 0, 3)
|
new AttackMove(MoveId.MUD_SHOT, PokemonType.GROUND, MoveCategory.SPECIAL, 55, 95, 15, 100, 0, 3)
|
||||||
.attr(StatStageChangeAttr, [ Stat.SPD ], -1),
|
.attr(StatStageChangeAttr, [ Stat.SPD ], -1),
|
||||||
new AttackMove(MoveId.POISON_TAIL, PokemonType.POISON, MoveCategory.PHYSICAL, 50, 100, 25, 10, 0, 3)
|
new AttackMove(MoveId.POISON_TAIL, PokemonType.POISON, MoveCategory.PHYSICAL, 50, 100, 25, 10, 0, 3)
|
||||||
@ -9615,6 +9656,7 @@ export function initMoves() {
|
|||||||
new StatusMove(MoveId.GRAVITY, PokemonType.PSYCHIC, -1, 5, -1, 0, 4)
|
new StatusMove(MoveId.GRAVITY, PokemonType.PSYCHIC, -1, 5, -1, 0, 4)
|
||||||
.ignoresProtect()
|
.ignoresProtect()
|
||||||
.attr(AddArenaTagAttr, ArenaTagType.GRAVITY, 5)
|
.attr(AddArenaTagAttr, ArenaTagType.GRAVITY, 5)
|
||||||
|
.condition(() => !globalScene.arena.hasTag(ArenaTagType.GRAVITY))
|
||||||
.target(MoveTarget.BOTH_SIDES),
|
.target(MoveTarget.BOTH_SIDES),
|
||||||
new StatusMove(MoveId.MIRACLE_EYE, PokemonType.PSYCHIC, -1, 40, -1, 0, 4)
|
new StatusMove(MoveId.MIRACLE_EYE, PokemonType.PSYCHIC, -1, 40, -1, 0, 4)
|
||||||
.attr(ExposedMoveAttr, BattlerTagType.IGNORE_DARK)
|
.attr(ExposedMoveAttr, BattlerTagType.IGNORE_DARK)
|
||||||
@ -9745,7 +9787,8 @@ export function initMoves() {
|
|||||||
.attr(AddBattlerTagAttr, BattlerTagType.AQUA_RING, true, true),
|
.attr(AddBattlerTagAttr, BattlerTagType.AQUA_RING, true, true),
|
||||||
new SelfStatusMove(MoveId.MAGNET_RISE, PokemonType.ELECTRIC, -1, 10, -1, 0, 4)
|
new SelfStatusMove(MoveId.MAGNET_RISE, PokemonType.ELECTRIC, -1, 10, -1, 0, 4)
|
||||||
.attr(AddBattlerTagAttr, BattlerTagType.FLOATING, true, true, 5)
|
.attr(AddBattlerTagAttr, BattlerTagType.FLOATING, true, true, 5)
|
||||||
.condition((user, target, move) => !globalScene.arena.getTag(ArenaTagType.GRAVITY) && [ BattlerTagType.FLOATING, BattlerTagType.IGNORE_FLYING, BattlerTagType.INGRAIN ].every((tag) => !user.getTag(tag))),
|
.condition(user => [ BattlerTagType.FLOATING, BattlerTagType.IGNORE_FLYING, BattlerTagType.INGRAIN ].every((tag) => !user.getTag(tag)))
|
||||||
|
.affectedByGravity(),
|
||||||
new AttackMove(MoveId.FLARE_BLITZ, PokemonType.FIRE, MoveCategory.PHYSICAL, 120, 100, 15, 10, 0, 4)
|
new AttackMove(MoveId.FLARE_BLITZ, PokemonType.FIRE, MoveCategory.PHYSICAL, 120, 100, 15, 10, 0, 4)
|
||||||
.attr(RecoilAttr, false, 0.33)
|
.attr(RecoilAttr, false, 0.33)
|
||||||
.attr(HealStatusEffectAttr, true, StatusEffect.FREEZE)
|
.attr(HealStatusEffectAttr, true, StatusEffect.FREEZE)
|
||||||
@ -9974,7 +10017,7 @@ export function initMoves() {
|
|||||||
.powderMove()
|
.powderMove()
|
||||||
.attr(AddBattlerTagAttr, BattlerTagType.CENTER_OF_ATTENTION, true),
|
.attr(AddBattlerTagAttr, BattlerTagType.CENTER_OF_ATTENTION, true),
|
||||||
new StatusMove(MoveId.TELEKINESIS, PokemonType.PSYCHIC, -1, 15, -1, 0, 5)
|
new StatusMove(MoveId.TELEKINESIS, PokemonType.PSYCHIC, -1, 15, -1, 0, 5)
|
||||||
.condition(failOnGravityCondition)
|
.affectedByGravity()
|
||||||
.condition((_user, target, _move) => ![ SpeciesId.DIGLETT, SpeciesId.DUGTRIO, SpeciesId.ALOLA_DIGLETT, SpeciesId.ALOLA_DUGTRIO, SpeciesId.SANDYGAST, SpeciesId.PALOSSAND, SpeciesId.WIGLETT, SpeciesId.WUGTRIO ].includes(target.species.speciesId))
|
.condition((_user, target, _move) => ![ SpeciesId.DIGLETT, SpeciesId.DUGTRIO, SpeciesId.ALOLA_DIGLETT, SpeciesId.ALOLA_DUGTRIO, SpeciesId.SANDYGAST, SpeciesId.PALOSSAND, SpeciesId.WIGLETT, SpeciesId.WUGTRIO ].includes(target.species.speciesId))
|
||||||
.condition((_user, target, _move) => !(target.species.speciesId === SpeciesId.GENGAR && target.getFormKey() === "mega"))
|
.condition((_user, target, _move) => !(target.species.speciesId === SpeciesId.GENGAR && target.getFormKey() === "mega"))
|
||||||
.condition((_user, target, _move) => isNullOrUndefined(target.getTag(BattlerTagType.INGRAIN)) && isNullOrUndefined(target.getTag(BattlerTagType.IGNORE_FLYING)))
|
.condition((_user, target, _move) => isNullOrUndefined(target.getTag(BattlerTagType.INGRAIN)) && isNullOrUndefined(target.getTag(BattlerTagType.IGNORE_FLYING)))
|
||||||
@ -10078,7 +10121,7 @@ export function initMoves() {
|
|||||||
new ChargingAttackMove(MoveId.SKY_DROP, PokemonType.FLYING, MoveCategory.PHYSICAL, 60, 100, 10, -1, 0, 5)
|
new ChargingAttackMove(MoveId.SKY_DROP, PokemonType.FLYING, MoveCategory.PHYSICAL, 60, 100, 10, -1, 0, 5)
|
||||||
.chargeText(i18next.t("moveTriggers:tookTargetIntoSky", { pokemonName: "{USER}", targetName: "{TARGET}" }))
|
.chargeText(i18next.t("moveTriggers:tookTargetIntoSky", { pokemonName: "{USER}", targetName: "{TARGET}" }))
|
||||||
.chargeAttr(SemiInvulnerableAttr, BattlerTagType.FLYING)
|
.chargeAttr(SemiInvulnerableAttr, BattlerTagType.FLYING)
|
||||||
.condition(failOnGravityCondition)
|
.affectedByGravity()
|
||||||
.condition((user, target, move) => !target.getTag(BattlerTagType.SUBSTITUTE))
|
.condition((user, target, move) => !target.getTag(BattlerTagType.SUBSTITUTE))
|
||||||
/*
|
/*
|
||||||
* Cf https://bulbapedia.bulbagarden.net/wiki/Sky_Drop_(move) and https://www.smogon.com/dex/sv/moves/sky-drop/:
|
* Cf https://bulbapedia.bulbagarden.net/wiki/Sky_Drop_(move) and https://www.smogon.com/dex/sv/moves/sky-drop/:
|
||||||
@ -10266,14 +10309,14 @@ export function initMoves() {
|
|||||||
.attr(AlwaysHitMinimizeAttr)
|
.attr(AlwaysHitMinimizeAttr)
|
||||||
.attr(FlyingTypeMultiplierAttr)
|
.attr(FlyingTypeMultiplierAttr)
|
||||||
.attr(HitsTagForDoubleDamageAttr, BattlerTagType.MINIMIZED)
|
.attr(HitsTagForDoubleDamageAttr, BattlerTagType.MINIMIZED)
|
||||||
.condition(failOnGravityCondition),
|
.affectedByGravity(),
|
||||||
new StatusMove(MoveId.MAT_BLOCK, PokemonType.FIGHTING, -1, 10, -1, 0, 6)
|
new StatusMove(MoveId.MAT_BLOCK, PokemonType.FIGHTING, -1, 10, -1, 0, 6)
|
||||||
.target(MoveTarget.USER_SIDE)
|
.target(MoveTarget.USER_SIDE)
|
||||||
.attr(AddArenaTagAttr, ArenaTagType.MAT_BLOCK, 1, true, true)
|
.attr(AddArenaTagAttr, ArenaTagType.MAT_BLOCK, 1, true, true)
|
||||||
.condition(new FirstMoveCondition())
|
.condition(new FirstMoveCondition())
|
||||||
.condition(failIfLastCondition),
|
.condition(failIfLastCondition),
|
||||||
new AttackMove(MoveId.BELCH, PokemonType.POISON, MoveCategory.SPECIAL, 120, 90, 10, -1, 0, 6)
|
new AttackMove(MoveId.BELCH, PokemonType.POISON, MoveCategory.SPECIAL, 120, 90, 10, -1, 0, 6)
|
||||||
.condition((user, target, move) => user.battleData.hasEatenBerry),
|
.restriction(user => !user.battleData.hasEatenBerry, "battle:moveDisabledBelch", true),
|
||||||
new StatusMove(MoveId.ROTOTILLER, PokemonType.GROUND, -1, 10, -1, 0, 6)
|
new StatusMove(MoveId.ROTOTILLER, PokemonType.GROUND, -1, 10, -1, 0, 6)
|
||||||
.target(MoveTarget.ALL)
|
.target(MoveTarget.ALL)
|
||||||
.condition((user, target, move) => {
|
.condition((user, target, move) => {
|
||||||
@ -10804,7 +10847,8 @@ export function initMoves() {
|
|||||||
.attr(StatusEffectAttr, StatusEffect.PARALYSIS)
|
.attr(StatusEffectAttr, StatusEffect.PARALYSIS)
|
||||||
.target(MoveTarget.ALL_NEAR_ENEMIES),
|
.target(MoveTarget.ALL_NEAR_ENEMIES),
|
||||||
new AttackMove(MoveId.FLOATY_FALL, PokemonType.FLYING, MoveCategory.PHYSICAL, 90, 95, 15, 30, 0, 7)
|
new AttackMove(MoveId.FLOATY_FALL, PokemonType.FLYING, MoveCategory.PHYSICAL, 90, 95, 15, 30, 0, 7)
|
||||||
.attr(FlinchAttr),
|
.attr(FlinchAttr)
|
||||||
|
.affectedByGravity(),
|
||||||
new AttackMove(MoveId.PIKA_PAPOW, PokemonType.ELECTRIC, MoveCategory.SPECIAL, -1, -1, 20, -1, 0, 7)
|
new AttackMove(MoveId.PIKA_PAPOW, PokemonType.ELECTRIC, MoveCategory.SPECIAL, -1, -1, 20, -1, 0, 7)
|
||||||
.attr(FriendshipPowerAttr),
|
.attr(FriendshipPowerAttr),
|
||||||
new AttackMove(MoveId.BOUNCY_BUBBLE, PokemonType.WATER, MoveCategory.SPECIAL, 60, 100, 20, -1, 0, 7)
|
new AttackMove(MoveId.BOUNCY_BUBBLE, PokemonType.WATER, MoveCategory.SPECIAL, 60, 100, 20, -1, 0, 7)
|
||||||
@ -10858,11 +10902,10 @@ export function initMoves() {
|
|||||||
new SelfStatusMove(MoveId.STUFF_CHEEKS, PokemonType.NORMAL, -1, 10, -1, 0, 8)
|
new SelfStatusMove(MoveId.STUFF_CHEEKS, PokemonType.NORMAL, -1, 10, -1, 0, 8)
|
||||||
.attr(EatBerryAttr, true)
|
.attr(EatBerryAttr, true)
|
||||||
.attr(StatStageChangeAttr, [ Stat.DEF ], 2, true)
|
.attr(StatStageChangeAttr, [ Stat.DEF ], 2, true)
|
||||||
.condition((user) => {
|
.restriction(
|
||||||
const userBerries = globalScene.findModifiers(m => m instanceof BerryModifier, user.isPlayer());
|
user => globalScene.findModifiers(m => m instanceof BerryModifier, user.isPlayer()).length > 0,
|
||||||
return userBerries.length > 0;
|
"battle:moveDisabledNoBerry",
|
||||||
})
|
true),
|
||||||
.edgeCase(), // Stuff Cheeks should not be selectable when the user does not have a berry, see wiki
|
|
||||||
new SelfStatusMove(MoveId.NO_RETREAT, PokemonType.FIGHTING, -1, 5, -1, 0, 8)
|
new SelfStatusMove(MoveId.NO_RETREAT, PokemonType.FIGHTING, -1, 5, -1, 0, 8)
|
||||||
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true)
|
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true)
|
||||||
.attr(AddBattlerTagAttr, BattlerTagType.NO_RETREAT, true, false)
|
.attr(AddBattlerTagAttr, BattlerTagType.NO_RETREAT, true, false)
|
||||||
@ -11439,10 +11482,7 @@ export function initMoves() {
|
|||||||
}),
|
}),
|
||||||
new AttackMove(MoveId.GIGATON_HAMMER, PokemonType.STEEL, MoveCategory.PHYSICAL, 160, 100, 5, -1, 0, 9)
|
new AttackMove(MoveId.GIGATON_HAMMER, PokemonType.STEEL, MoveCategory.PHYSICAL, 160, 100, 5, -1, 0, 9)
|
||||||
.makesContact(false)
|
.makesContact(false)
|
||||||
.condition((user, target, move) => {
|
.restriction(ConsecutiveUseRestriction),
|
||||||
const turnMove = user.getLastXMoves(1);
|
|
||||||
return !turnMove.length || turnMove[0].move !== move.id || turnMove[0].result !== MoveResult.SUCCESS;
|
|
||||||
}), // TODO Add Instruct/Encore interaction
|
|
||||||
new AttackMove(MoveId.COMEUPPANCE, PokemonType.DARK, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 9)
|
new AttackMove(MoveId.COMEUPPANCE, PokemonType.DARK, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 9)
|
||||||
.attr(CounterDamageAttr, (move: Move) => (move.category === MoveCategory.PHYSICAL || move.category === MoveCategory.SPECIAL), 1.5)
|
.attr(CounterDamageAttr, (move: Move) => (move.category === MoveCategory.PHYSICAL || move.category === MoveCategory.SPECIAL), 1.5)
|
||||||
.redirectCounter()
|
.redirectCounter()
|
||||||
@ -11467,10 +11507,7 @@ export function initMoves() {
|
|||||||
.attr(ConfuseAttr)
|
.attr(ConfuseAttr)
|
||||||
.makesContact(false),
|
.makesContact(false),
|
||||||
new AttackMove(MoveId.BLOOD_MOON, PokemonType.NORMAL, MoveCategory.SPECIAL, 140, 100, 5, -1, 0, 9)
|
new AttackMove(MoveId.BLOOD_MOON, PokemonType.NORMAL, MoveCategory.SPECIAL, 140, 100, 5, -1, 0, 9)
|
||||||
.condition((user, target, move) => {
|
.restriction(ConsecutiveUseRestriction),
|
||||||
const turnMove = user.getLastXMoves(1);
|
|
||||||
return !turnMove.length || turnMove[0].move !== move.id || turnMove[0].result !== MoveResult.SUCCESS;
|
|
||||||
}), // TODO Add Instruct/Encore interaction
|
|
||||||
new AttackMove(MoveId.MATCHA_GOTCHA, PokemonType.GRASS, MoveCategory.SPECIAL, 80, 90, 15, 20, 0, 9)
|
new AttackMove(MoveId.MATCHA_GOTCHA, PokemonType.GRASS, MoveCategory.SPECIAL, 80, 90, 15, 20, 0, 9)
|
||||||
.attr(HitHealAttr)
|
.attr(HitHealAttr)
|
||||||
.attr(HealStatusEffectAttr, true, StatusEffect.FREEZE)
|
.attr(HealStatusEffectAttr, true, StatusEffect.FREEZE)
|
||||||
|
@ -5,6 +5,7 @@ import type { Pokemon } from "#field/pokemon";
|
|||||||
import type { Move } from "#moves/move";
|
import type { Move } from "#moves/move";
|
||||||
import { applyChallenges } from "#utils/challenge-utils";
|
import { applyChallenges } from "#utils/challenge-utils";
|
||||||
import { BooleanHolder, toDmgValue } from "#utils/common";
|
import { BooleanHolder, toDmgValue } from "#utils/common";
|
||||||
|
import i18next from "i18next";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wrapper class for the {@linkcode Move} class for Pokemon to interact with.
|
* 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.
|
* 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 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 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.
|
* @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 move = this.getMove();
|
||||||
|
const moveName = move.name;
|
||||||
|
|
||||||
// TODO: Add Sky Drop's 1 turn stall
|
// TODO: Add Sky Drop's 1 turn stall
|
||||||
const usability = new BooleanHolder(
|
if (this.moveId === MoveId.NONE || move.name.endsWith(" (N)")) {
|
||||||
!move.name.endsWith(" (N)") &&
|
return [false, i18next.t("battle:moveNotImplemented", moveName.replace(" (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);
|
|
||||||
}
|
}
|
||||||
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 {
|
getMove(): Move {
|
||||||
return allMoves[this.moveId];
|
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}
|
* Sets {@link ppUsed} for this move and ensures the value does not exceed {@link getMovePp}
|
||||||
* @param count Amount of PP to use
|
* @param count Amount of PP to use
|
||||||
|
@ -6,11 +6,12 @@ export enum BattlerTagLapseType {
|
|||||||
// TODO: This is unused...
|
// TODO: This is unused...
|
||||||
FAINT,
|
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
|
* @see MoveUseMode for more information
|
||||||
*/
|
*/
|
||||||
MOVE,
|
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. */
|
||||||
PRE_MOVE,
|
PRE_MOVE,
|
||||||
/** Tag activates immediately after the holder's move finishes triggering (successful or not). */
|
/** Tag activates immediately after the holder's move finishes triggering (successful or not). */
|
||||||
AFTER_MOVE,
|
AFTER_MOVE,
|
||||||
@ -32,6 +33,16 @@ export enum BattlerTagLapseType {
|
|||||||
* but still triggers on being KO'd.
|
* but still triggers on being KO'd.
|
||||||
*/
|
*/
|
||||||
AFTER_HIT,
|
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,
|
CUSTOM,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Same type as {@linkcode BattlerTagLapseType}, but excludes the {@linkcode BattlerTagLapseType.CUSTOM} type */
|
||||||
|
export type NonCustomBattlerTagLapseType = Exclude<BattlerTagLapseType, BattlerTagLapseType.CUSTOM>;
|
@ -50,5 +50,7 @@ export enum MoveFlags {
|
|||||||
/** Indicates a move is able to be redirected to allies in a double battle if the attacker faints */
|
/** Indicates a move is able to be redirected to allies in a double battle if the attacker faints */
|
||||||
REDIRECT_COUNTER = 1 << 18,
|
REDIRECT_COUNTER = 1 << 18,
|
||||||
/** Indicates a move is able to be reflected by {@linkcode AbilityId.MAGIC_BOUNCE} and {@linkcode MoveId.MAGIC_COAT} */
|
/** Indicates a move is able to be reflected by {@linkcode AbilityId.MAGIC_BOUNCE} and {@linkcode MoveId.MAGIC_COAT} */
|
||||||
REFLECTABLE = 1 << 19
|
REFLECTABLE = 1 << 19,
|
||||||
|
/** Indicates a move that fails when {@link https://bulbapedia.bulbagarden.net/wiki/Gravity_(move) | Gravity} is in effect */
|
||||||
|
GRAVITY = 1 << 20,
|
||||||
}
|
}
|
||||||
|
@ -3295,9 +3295,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;
|
const move = this.getMoveset().length > moveIndex ? this.getMoveset()[moveIndex] : null;
|
||||||
return move?.isUsable(this, ignorePp) ?? false;
|
return move?.isUsable(this, ignorePp) ?? [false, ""];
|
||||||
}
|
}
|
||||||
|
|
||||||
showInfo(): void {
|
showInfo(): void {
|
||||||
@ -4267,19 +4273,28 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tick down the first {@linkcode BattlerTag} found matching the given {@linkcode BattlerTagType},
|
* Lapse the first {@linkcode BattlerTag} matching `tagType`
|
||||||
* removing it if its duration goes below 0.
|
*
|
||||||
* @param tagType the {@linkcode BattlerTagType} to check against
|
* @remarks
|
||||||
* @returns `true` if the tag was present
|
* 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 tags = this.summonData.tags;
|
||||||
const tag = tags.find(t => t.tagType === tagType);
|
const tag = tags.find(t => t.tagType === tagType);
|
||||||
if (!tag) {
|
if (!tag) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!tag.lapse(this, BattlerTagLapseType.CUSTOM)) {
|
if (!tag.lapse(this, lapseType)) {
|
||||||
tag.onRemove(this);
|
tag.onRemove(this);
|
||||||
tags.splice(tags.indexOf(tag), 1);
|
tags.splice(tags.indexOf(tag), 1);
|
||||||
}
|
}
|
||||||
@ -4291,7 +4306,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
* removing any whose durations fall below 0.
|
* removing any whose durations fall below 0.
|
||||||
* @param tagType the {@linkcode BattlerTagLapseType} to tick down
|
* @param tagType the {@linkcode BattlerTagLapseType} to tick down
|
||||||
*/
|
*/
|
||||||
lapseTags(lapseType: BattlerTagLapseType): void {
|
lapseTags(lapseType: Exclude<BattlerTagLapseType, BattlerTagLapseType.CUSTOM>): void {
|
||||||
const tags = this.summonData.tags;
|
const tags = this.summonData.tags;
|
||||||
tags
|
tags
|
||||||
.filter(
|
.filter(
|
||||||
@ -4381,13 +4396,37 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
/**
|
/**
|
||||||
* Gets whether the given move is currently disabled for this Pokemon.
|
* 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
|
* @param moveId - The {@linkcode MoveId} ID of the move to check
|
||||||
* @returns `true` if the move is disabled for this Pokemon, otherwise `false`
|
* @returns `true` if the move is disabled for this Pokemon, otherwise `false`
|
||||||
*
|
*
|
||||||
* @see {@linkcode MoveRestrictionBattlerTag}
|
* @see {@linkcode MoveRestrictionBattlerTag}
|
||||||
*/
|
*/
|
||||||
public isMoveRestricted(moveId: MoveId, pokemon?: Pokemon): boolean {
|
// TODO: rename this method as it can be easily confused with a move being restricted
|
||||||
return this.getRestrictingTag(moveId, pokemon) !== null;
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -6398,7 +6437,7 @@ export class EnemyPokemon extends Pokemon {
|
|||||||
// If the queued move was called indirectly, ignore all PP and usability checks.
|
// 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.
|
// 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?
|
// 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
|
moveQueue.splice(0, i); // TODO: This should not be done here
|
||||||
return queuedMove;
|
return queuedMove;
|
||||||
}
|
}
|
||||||
@ -6408,7 +6447,7 @@ export class EnemyPokemon extends Pokemon {
|
|||||||
this.summonData.moveQueue = [];
|
this.summonData.moveQueue = [];
|
||||||
|
|
||||||
// Filter out any moves this Pokemon cannot use
|
// 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 no moves are left, use Struggle. Otherwise, continue with move selection
|
||||||
if (movePool.length) {
|
if (movePool.length) {
|
||||||
// If there's only 1 move in the move pool, use it.
|
// If there's only 1 move in the move pool, use it.
|
||||||
@ -6466,7 +6505,7 @@ export class EnemyPokemon extends Pokemon {
|
|||||||
move.category !== MoveCategory.STATUS &&
|
move.category !== MoveCategory.STATUS &&
|
||||||
moveTargets.some(p => {
|
moveTargets.some(p => {
|
||||||
const doesNotFail =
|
const doesNotFail =
|
||||||
move.applyConditions(this, p, move) ||
|
move.applyConditions(this, p) ||
|
||||||
[MoveId.SUCKER_PUNCH, MoveId.UPPER_HAND, MoveId.THUNDERCLAP].includes(move.id);
|
[MoveId.SUCKER_PUNCH, MoveId.UPPER_HAND, MoveId.THUNDERCLAP].includes(move.id);
|
||||||
return (
|
return (
|
||||||
doesNotFail &&
|
doesNotFail &&
|
||||||
@ -6525,7 +6564,7 @@ export class EnemyPokemon extends Pokemon {
|
|||||||
* target score to -20
|
* target score to -20
|
||||||
*/
|
*/
|
||||||
if (
|
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)
|
![MoveId.SUCKER_PUNCH, MoveId.UPPER_HAND, MoveId.THUNDERCLAP].includes(move.id)
|
||||||
) {
|
) {
|
||||||
targetScore = -20;
|
targetScore = -20;
|
||||||
|
@ -9,7 +9,6 @@ import { ArenaTagType } from "#enums/arena-tag-type";
|
|||||||
import { BattleType } from "#enums/battle-type";
|
import { BattleType } from "#enums/battle-type";
|
||||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||||
import { BiomeId } from "#enums/biome-id";
|
import { BiomeId } from "#enums/biome-id";
|
||||||
import { ChallengeType } from "#enums/challenge-type";
|
|
||||||
import { Command } from "#enums/command";
|
import { Command } from "#enums/command";
|
||||||
import { FieldPosition } from "#enums/field-position";
|
import { FieldPosition } from "#enums/field-position";
|
||||||
import { MoveId } from "#enums/move-id";
|
import { MoveId } from "#enums/move-id";
|
||||||
@ -22,8 +21,6 @@ import type { MoveTargetSet } from "#moves/move";
|
|||||||
import { getMoveTargets } from "#moves/move-utils";
|
import { getMoveTargets } from "#moves/move-utils";
|
||||||
import { FieldPhase } from "#phases/field-phase";
|
import { FieldPhase } from "#phases/field-phase";
|
||||||
import type { TurnMove } from "#types/turn-move";
|
import type { TurnMove } from "#types/turn-move";
|
||||||
import { applyChallenges } from "#utils/challenge-utils";
|
|
||||||
import { BooleanHolder } from "#utils/common";
|
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
|
||||||
export class CommandPhase extends FieldPhase {
|
export class CommandPhase extends FieldPhase {
|
||||||
@ -126,7 +123,7 @@ export class CommandPhase extends FieldPhase {
|
|||||||
if (
|
if (
|
||||||
queuedMove.move !== MoveId.NONE &&
|
queuedMove.move !== MoveId.NONE &&
|
||||||
!isVirtual(queuedMove.useMode) &&
|
!isVirtual(queuedMove.useMode) &&
|
||||||
!movesetQueuedMove?.isUsable(playerPokemon, isIgnorePP(queuedMove.useMode))
|
!movesetQueuedMove?.isUsable(playerPokemon, isIgnorePP(queuedMove.useMode), true)
|
||||||
) {
|
) {
|
||||||
entriesToDelete++;
|
entriesToDelete++;
|
||||||
} else {
|
} else {
|
||||||
@ -204,40 +201,18 @@ export class CommandPhase extends FieldPhase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Submethod of {@linkcode handleFightCommand} responsible for queuing the appropriate
|
* Submethod of {@linkcode handleFightCommand} responsible for queuing the provided error message when the move cannot be used
|
||||||
* error message when a move cannot be used.
|
* @param msg - The reason why the move cannot be used
|
||||||
* @param user - The pokemon using the move
|
|
||||||
* @param cursor - The index of the move in the moveset
|
|
||||||
*/
|
*/
|
||||||
private queueFightErrorMessage(user: PlayerPokemon, cursor: number) {
|
private queueFightErrorMessage(msg: string) {
|
||||||
const move = user.getMoveset()[cursor];
|
const ui = globalScene.ui;
|
||||||
globalScene.ui.setMode(UiMode.MESSAGE);
|
ui.setMode(UiMode.MESSAGE);
|
||||||
|
ui.showText(
|
||||||
// Set the translation key for why the move cannot be selected
|
msg,
|
||||||
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 }),
|
|
||||||
null,
|
null,
|
||||||
() => {
|
() => {
|
||||||
globalScene.ui.clearText();
|
ui.clearText();
|
||||||
globalScene.ui.setMode(UiMode.FIGHT, this.fieldIndex);
|
ui.setMode(UiMode.FIGHT, this.fieldIndex);
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
true,
|
true,
|
||||||
@ -274,18 +249,15 @@ export class CommandPhase extends FieldPhase {
|
|||||||
): boolean {
|
): boolean {
|
||||||
const playerPokemon = this.getPokemon();
|
const playerPokemon = this.getPokemon();
|
||||||
const ignorePP = isIgnorePP(useMode);
|
const ignorePP = isIgnorePP(useMode);
|
||||||
|
const [canUse, reason] = cursor === -1 ? [true, ""] : playerPokemon.trySelectMove(cursor, ignorePP);
|
||||||
let canUse = cursor === -1 || playerPokemon.trySelectMove(cursor, ignorePP);
|
|
||||||
|
|
||||||
// Ternary here ensures we don't compute struggle conditions unless necessary
|
// Ternary here ensures we don't compute struggle conditions unless necessary
|
||||||
const useStruggle = canUse
|
const useStruggle = canUse
|
||||||
? false
|
? false
|
||||||
: cursor > -1 && !playerPokemon.getMoveset().some(m => m.isUsable(playerPokemon));
|
: cursor > -1 && !playerPokemon.getMoveset().some(m => m.isUsable(playerPokemon)[0]);
|
||||||
|
|
||||||
canUse ||= useStruggle;
|
if (!canUse && !useStruggle) {
|
||||||
|
this.queueFightErrorMessage(reason);
|
||||||
if (!canUse) {
|
|
||||||
this.queueFightErrorMessage(playerPokemon, cursor);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ export class MoveHeaderPhase extends BattlePhase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
canMove(): boolean {
|
canMove(): boolean {
|
||||||
return this.pokemon.isActive(true) && this.move.isUsable(this.pokemon);
|
return this.pokemon.isActive(true) && this.move.isUsable(this.pokemon)[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
|
@ -2,15 +2,19 @@ import { applyAbAttrs } from "#abilities/apply-ab-attrs";
|
|||||||
import { globalScene } from "#app/global-scene";
|
import { globalScene } from "#app/global-scene";
|
||||||
import { getPokemonNameWithAffix } from "#app/messages";
|
import { getPokemonNameWithAffix } from "#app/messages";
|
||||||
import Overrides from "#app/overrides";
|
import Overrides from "#app/overrides";
|
||||||
|
// biome-ignore lint/correctness/noUnusedImports: Used in a tsdoc comment
|
||||||
|
import type { TauntTag, TruantTag } from "#data/battler-tags";
|
||||||
import { CenterOfAttentionTag } from "#data/battler-tags";
|
import { CenterOfAttentionTag } from "#data/battler-tags";
|
||||||
import { SpeciesFormChangePreMoveTrigger } from "#data/form-change-triggers";
|
import { SpeciesFormChangePreMoveTrigger } from "#data/form-change-triggers";
|
||||||
import { getStatusEffectActivationText, getStatusEffectHealText } from "#data/status-effect";
|
import { getStatusEffectActivationText, getStatusEffectHealText } from "#data/status-effect";
|
||||||
import { getTerrainBlockMessage } from "#data/terrain";
|
import { getTerrainBlockMessage } from "#data/terrain";
|
||||||
import { getWeatherBlockMessage } from "#data/weather";
|
import { getWeatherBlockMessage } from "#data/weather";
|
||||||
import { AbilityId } from "#enums/ability-id";
|
import { AbilityId } from "#enums/ability-id";
|
||||||
|
import { ArenaTagType } from "#enums/arena-tag-type";
|
||||||
import { BattlerIndex } from "#enums/battler-index";
|
import { BattlerIndex } from "#enums/battler-index";
|
||||||
import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type";
|
import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type";
|
||||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||||
|
import { ChallengeType } from "#enums/challenge-type";
|
||||||
import { CommonAnim } from "#enums/move-anims-common";
|
import { CommonAnim } from "#enums/move-anims-common";
|
||||||
import { MoveFlags } from "#enums/move-flags";
|
import { MoveFlags } from "#enums/move-flags";
|
||||||
import { MoveId } from "#enums/move-id";
|
import { MoveId } from "#enums/move-id";
|
||||||
@ -24,7 +28,10 @@ import { applyMoveAttrs } from "#moves/apply-attrs";
|
|||||||
import { frenzyMissFunc } from "#moves/move-utils";
|
import { frenzyMissFunc } from "#moves/move-utils";
|
||||||
import type { PokemonMove } from "#moves/pokemon-move";
|
import type { PokemonMove } from "#moves/pokemon-move";
|
||||||
import { BattlePhase } from "#phases/battle-phase";
|
import { BattlePhase } from "#phases/battle-phase";
|
||||||
import { NumberHolder } from "#utils/common";
|
// biome-ignore lint/correctness/noUnusedImports: Used in a tsdoc comment
|
||||||
|
import type { PreUseInterruptAttr } from "#types/move-types";
|
||||||
|
import { applyChallenges } from "#utils/challenge-utils";
|
||||||
|
import { BooleanHolder, NumberHolder } from "#utils/common";
|
||||||
import { enumValueToKey } from "#utils/enums";
|
import { enumValueToKey } from "#utils/enums";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
|
||||||
@ -41,6 +48,9 @@ export class MovePhase extends BattlePhase {
|
|||||||
/** Whether the current move should fail and retain PP. */
|
/** Whether the current move should fail and retain PP. */
|
||||||
protected cancelled = false;
|
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 {
|
public get pokemon(): Pokemon {
|
||||||
return this._pokemon;
|
return this._pokemon;
|
||||||
}
|
}
|
||||||
@ -84,19 +94,6 @@ export class MovePhase extends BattlePhase {
|
|||||||
this.forcedLast = forcedLast;
|
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 */
|
/** Signifies the current move should fail but still use PP */
|
||||||
public fail(): void {
|
public fail(): void {
|
||||||
this.failed = true;
|
this.failed = true;
|
||||||
@ -115,24 +112,78 @@ export class MovePhase extends BattlePhase {
|
|||||||
return this.forcedLast;
|
return this.forcedLast;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check the first round of failure checks.
|
||||||
|
*
|
||||||
|
* @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
|
||||||
|
if (
|
||||||
|
this.checkSleep() ||
|
||||||
|
this.checkFreeze() ||
|
||||||
|
this.checkPP() ||
|
||||||
|
this.checkValidity() ||
|
||||||
|
this.checkTagCancel(BattlerTagType.TRUANT, true) ||
|
||||||
|
this.checkPreUseInterrupt() ||
|
||||||
|
this.checkTagCancel(BattlerTagType.FLINCHED) ||
|
||||||
|
this.checkTagCancel(BattlerTagType.DISABLED, true) ||
|
||||||
|
this.checkGravity() ||
|
||||||
|
this.checkTagCancel(BattlerTagType.TAUNT, true) ||
|
||||||
|
this.checkTagCancel(BattlerTagType.IMPRISON) ||
|
||||||
|
this.checkTagCancel(BattlerTagType.CONFUSED) ||
|
||||||
|
this.checkPara() ||
|
||||||
|
this.checkTagCancel(BattlerTagType.INFATUATED)
|
||||||
|
) {
|
||||||
|
this.handlePreMoveFailures();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public start(): void {
|
public start(): void {
|
||||||
super.start();
|
super.start();
|
||||||
|
|
||||||
console.log(MoveId[this.move.moveId], enumValueToKey(MoveUseMode, this.useMode));
|
if (!this.pokemon.isActive(true)) {
|
||||||
|
|
||||||
// Check if move is unusable (e.g. running out of PP due to a mid-turn Spite
|
|
||||||
// or the user no longer being on field), ending the phase early if not.
|
|
||||||
if (!this.canMove(true)) {
|
|
||||||
if (this.pokemon.isActive(true)) {
|
|
||||||
this.fail();
|
|
||||||
this.showMoveText();
|
|
||||||
this.showFailedText();
|
|
||||||
}
|
|
||||||
this.end();
|
this.end();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Removing gigaton hammer always happens first
|
||||||
|
this.pokemon.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.
|
||||||
this.pokemon.turnData.acted = true;
|
this.pokemon.turnData.acted = true;
|
||||||
|
// TODO: skip this check for moves like metronome.
|
||||||
|
if (this.firstFailureCheck()) {
|
||||||
|
// Lapse all other pre-move tags
|
||||||
|
this.pokemon.lapseTags(BattlerTagLapseType.PRE_MOVE);
|
||||||
|
this.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Begin second failure checks..
|
||||||
|
|
||||||
// Reset hit-related turn data when starting follow-up moves (e.g. Metronomed moves, Dancer repeats)
|
// Reset hit-related turn data when starting follow-up moves (e.g. Metronomed moves, Dancer repeats)
|
||||||
if (isVirtual(this.useMode)) {
|
if (isVirtual(this.useMode)) {
|
||||||
@ -155,10 +206,6 @@ export class MovePhase extends BattlePhase {
|
|||||||
|
|
||||||
this.resolveCounterAttackTarget();
|
this.resolveCounterAttackTarget();
|
||||||
|
|
||||||
this.resolvePreMoveStatusEffects();
|
|
||||||
|
|
||||||
this.lapsePreMoveAndMoveTags();
|
|
||||||
|
|
||||||
if (!(this.failed || this.cancelled)) {
|
if (!(this.failed || this.cancelled)) {
|
||||||
this.resolveFinalPreMoveCancellationChecks();
|
this.resolveFinalPreMoveCancellationChecks();
|
||||||
}
|
}
|
||||||
@ -187,6 +234,8 @@ export class MovePhase extends BattlePhase {
|
|||||||
this.showMoveText();
|
this.showMoveText();
|
||||||
this.showFailedText();
|
this.showFailedText();
|
||||||
this.cancel();
|
this.cancel();
|
||||||
|
} else {
|
||||||
|
this.pokemon.lapseTags(BattlerTagLapseType.MOVE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -195,96 +244,220 @@ 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
|
||||||
*/
|
*/
|
||||||
protected resolvePreMoveStatusEffects(): void {
|
private cureStatus(effect: StatusEffect): void {
|
||||||
// Skip for follow ups/reflected moves, no status condition or post turn statuses (e.g. Poison/Toxic)
|
const pokemon = this.pokemon;
|
||||||
if (!this.pokemon.status?.effect || this.pokemon.status.isPostTurn() || isIgnoreStatus(this.useMode)) {
|
globalScene.phaseManager.queueMessage(getStatusEffectHealText(effect, getPokemonNameWithAffix(pokemon)));
|
||||||
return;
|
pokemon.resetStatus();
|
||||||
}
|
pokemon.updateInfo();
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lapse {@linkcode BattlerTagLapseType.PRE_MOVE | PRE_MOVE} tags that trigger before a move is used, regardless of whether or not it failed.
|
* Queue the status activation message, play its animation, and cancel the move
|
||||||
* Also lapse {@linkcode BattlerTagLapseType.MOVE | MOVE} tags if the move is successful and not called indirectly.
|
* @param effect - The effect being triggered
|
||||||
*/
|
*/
|
||||||
protected lapsePreMoveAndMoveTags(): void {
|
private triggerStatus(effect: StatusEffect): void {
|
||||||
this.pokemon.lapseTags(BattlerTagLapseType.PRE_MOVE);
|
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) {
|
* Handle the sleep check
|
||||||
this.pokemon.lapseTags(BattlerTagLapseType.MOVE);
|
* @returns Whether the move was cancelled due to sleep
|
||||||
|
*/
|
||||||
|
protected checkSleep(): boolean {
|
||||||
|
if (this.pokemon.status?.effect !== StatusEffect.SLEEP) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Heal the user if it thaws from the move or random chance
|
||||||
|
// Check if the user will thaw due to a move
|
||||||
|
|
||||||
|
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) {
|
||||||
|
// TODO: Create a locale key with some failure text
|
||||||
|
} 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, checkIgnoreStatus = false): boolean {
|
||||||
|
if (checkIgnoreStatus && isIgnoreStatus(this.useMode)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
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 useMove(): void {
|
protected useMove(): void {
|
||||||
@ -295,14 +468,6 @@ export class MovePhase extends BattlePhase {
|
|||||||
// form changes happen even before we know that the move wll execute.
|
// form changes happen even before we know that the move wll execute.
|
||||||
globalScene.triggerPokemonFormChange(this.pokemon, SpeciesFormChangePreMoveTrigger);
|
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear out any two turn moves once they've been used.
|
// Clear out any two turn moves once they've been used.
|
||||||
// TODO: Refactor move queues and remove this assignment;
|
// TODO: Refactor move queues and remove this assignment;
|
||||||
// Move queues should be handled by the calling `CommandPhase` or a manager for it
|
// Move queues should be handled by the calling `CommandPhase` or a manager for it
|
||||||
@ -332,10 +497,10 @@ export class MovePhase extends BattlePhase {
|
|||||||
* Move conditions assume the move has a single target
|
* Move conditions assume the move has a single target
|
||||||
* TODO: is this sustainable?
|
* TODO: is this sustainable?
|
||||||
*/
|
*/
|
||||||
const failsConditions = !move.applyConditions(this.pokemon, targets[0], move);
|
const failsConditions = !move.applyConditions(this.pokemon, targets[0]);
|
||||||
const failedDueToWeather = globalScene.arena.isMoveWeatherCancelled(this.pokemon, move);
|
const failedDueToWeather = globalScene.arena.isMoveWeatherCancelled(this.pokemon, move);
|
||||||
const failedDueToTerrain = globalScene.arena.isMoveTerrainCancelled(this.pokemon, this.targets, move);
|
const failedDueToTerrain = globalScene.arena.isMoveTerrainCancelled(this.pokemon, this.targets, move);
|
||||||
failed ||= failsConditions || failedDueToWeather || failedDueToTerrain;
|
const failed = failsConditions || failedDueToWeather || failedDueToTerrain;
|
||||||
|
|
||||||
if (failed) {
|
if (failed) {
|
||||||
this.failMove(true, failedDueToWeather, failedDueToTerrain);
|
this.failMove(true, failedDueToWeather, failedDueToTerrain);
|
||||||
@ -442,7 +607,7 @@ export class MovePhase extends BattlePhase {
|
|||||||
const move = this.move.getMove();
|
const move = this.move.getMove();
|
||||||
const targets = this.getActiveTargetPokemon();
|
const targets = this.getActiveTargetPokemon();
|
||||||
|
|
||||||
if (!move.applyConditions(this.pokemon, targets[0], move)) {
|
if (!move.applyConditions(this.pokemon, targets[0])) {
|
||||||
this.failMove(true);
|
this.failMove(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
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",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user