diff --git a/src/data/ability.ts b/src/data/ability.ts index f36ce64adef..3e2f1d30f40 100755 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -971,22 +971,16 @@ export class PostDefendMoveDisableAbAttr extends PostDefendAbAttr { } applyPostDefend(pokemon: Pokemon, passive: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean { - if (!attacker.summonData.disabledMove) { + if (attacker.getTag(BattlerTagType.DISABLED) === null) { if (move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon) && (this.chance === -1 || pokemon.randSeedInt(100) < this.chance) && !attacker.isMax()) { this.attacker = attacker; this.move = move; - - attacker.summonData.disabledMove = move.id; - attacker.summonData.disabledTurns = 4; + this.attacker.addTag(BattlerTagType.DISABLED, 4, 0, pokemon.id); return true; } } return false; } - - getTriggerMessage(pokemon: Pokemon, abilityName: string, ...args: any[]): string { - return getPokemonMessage(this.attacker, `'s ${this.move.name}\nwas disabled!`); - } } export class PostStatChangeStatChangeAbAttr extends PostStatChangeAbAttr { diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index bfdeb2189dc..414c114ae9b 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -1,5 +1,5 @@ import { CommonAnim, CommonBattleAnim } from "./battle-anims"; -import { CommonAnimPhase, MoveEffectPhase, MovePhase, PokemonHealPhase, ShowAbilityPhase, StatChangePhase } from "../phases"; +import { CommonAnimPhase, MessagePhase, MoveEffectPhase, MovePhase, PokemonHealPhase, ShowAbilityPhase, StatChangePhase } from "../phases"; import { getPokemonMessage, getPokemonNameWithAffix } from "../messages"; import Pokemon, { MoveResult, HitResult } from "../field/pokemon"; import { Stat, getStatName } from "./pokemon-stat"; @@ -17,6 +17,7 @@ import { BattleStat } from "./battle-stat"; import { allAbilities } from "./ability"; import { SpeciesFormChangeManualTrigger } from "./pokemon-forms"; import { Species } from "./enums/species"; +import i18next from "i18next"; export enum BattlerTagLapseType { FAINT, @@ -91,6 +92,84 @@ export interface TerrainBattlerTag { terrainTypes: TerrainType[]; } +/** + * Base class for tags that disable moves. Descendants can override {@linkcode moveIsDisabled} to disable moves that match a condition. + */ +export abstract class DisablingBattlerTag extends BattlerTag { + public abstract moveIsDisabled(move: Moves): boolean; + + constructor(tagType: BattlerTagType, lapseType?: BattlerTagLapseType, turnCount?: integer, sourceMove?: Moves, sourceId?: integer) { + super(tagType, lapseType ?? BattlerTagLapseType.TURN_END, turnCount ?? 3, sourceMove, sourceId); + } + + lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { + if (!super.lapse(pokemon, lapseType)) { + return false; + } + + return true; + } +} + +/** + * Tag representing the "disabling" effect performed by {@linkcode Moves.DISABLE} and {@linkcode Abilities.CURSED_BODY}. + * When the tag is added, the last used move of the tag holder is set as the disabled move. + */ +export class DisabledTag extends DisablingBattlerTag { + /** The move being disabled. Gets set when {@linkcode onAdd} is called for this tag. */ + private moveId: integer = 0; + + public override moveIsDisabled(move: Moves): boolean { + return move === this.moveId; + } + + constructor(turnCount: integer, sourceId: integer) { + super(BattlerTagType.DISABLED, BattlerTagLapseType.TURN_END, turnCount, Moves.DISABLE, sourceId); + } + + lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { + if (!super.lapse(pokemon, lapseType)) { + return false; + } + + if (this.moveId === 0) { + console.warn(`attempt to disable move ID 0 on ${pokemon}`); + return false; + } + + return true; + } + + onAdd(pokemon: Pokemon): void { + const history = pokemon.getLastXMoves(); + + if (history.length === 0) { + return; + } + + const move = history.find(m => m.move !== Moves.NONE); + if (move === undefined) { + return; + } + + this.moveId = move.move; + + pokemon.scene.queueMessage(this.generateAddMessage(pokemon)); + } + + onRemove(pokemon: Pokemon): void { + if (this.moveId === 0) { + return; + } + + pokemon.scene.pushPhase(new MessagePhase(pokemon.scene, i18next.t("battle:notDisabled", { pokemonName: getPokemonNameWithAffix(pokemon), moveName: allMoves[this.moveId].name }))); + } + + private generateAddMessage(pokemon: Pokemon): string { + return getPokemonMessage(pokemon, `'s ${allMoves[this.moveId].name}\nwas disabled!`); + } +} + export class RechargingTag extends BattlerTag { constructor(sourceMove: Moves) { super(BattlerTagType.RECHARGING, BattlerTagLapseType.PRE_MOVE, 1, sourceMove); @@ -1522,6 +1601,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: integer, sourc return new DestinyBondTag(sourceMove, sourceId); case BattlerTagType.ICE_FACE: return new IceFaceTag(sourceMove); + case BattlerTagType.DISABLED: + return new DisabledTag(turnCount, sourceId); case BattlerTagType.NONE: default: return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId); diff --git a/src/data/enums/battler-tag-type.ts b/src/data/enums/battler-tag-type.ts index 98f01c9c375..9596e860a2c 100644 --- a/src/data/enums/battler-tag-type.ts +++ b/src/data/enums/battler-tag-type.ts @@ -58,5 +58,6 @@ export enum BattlerTagType { MAGNET_RISEN = "MAGNET_RISEN", MINIMIZED = "MINIMIZED", DESTINY_BOND = "DESTINY_BOND", - ICE_FACE = "ICE_FACE" + ICE_FACE = "ICE_FACE", + DISABLED = "DISABLED" } diff --git a/src/data/move.ts b/src/data/move.ts index 103f4840ef7..f99717c28bf 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -3690,70 +3690,6 @@ export class TypelessAttr extends MoveAttr { } */ export class BypassRedirectAttr extends MoveAttr { } -export class DisableMoveAttr extends MoveEffectAttr { - constructor() { - super(false); - } - - apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - if (!super.apply(user, target, move, args)) { - return false; - } - - const moveQueue = target.getLastXMoves(); - let turnMove: TurnMove; - while (moveQueue.length) { - turnMove = moveQueue.shift(); - if (turnMove.virtual) { - continue; - } - - const moveIndex = target.getMoveset().findIndex(m => m.moveId === turnMove.move); - if (moveIndex === -1) { - return false; - } - - const disabledMove = target.getMoveset()[moveIndex]; - target.summonData.disabledMove = disabledMove.moveId; - target.summonData.disabledTurns = 4; - - user.scene.queueMessage(getPokemonMessage(target, `'s ${disabledMove.getName()}\nwas disabled!`)); - - return true; - } - - return false; - } - - getCondition(): MoveConditionFunc { - return (user, target, move) => { - if (target.summonData.disabledMove || target.isMax()) { - return false; - } - - const moveQueue = target.getLastXMoves(); - let turnMove: TurnMove; - while (moveQueue.length) { - turnMove = moveQueue.shift(); - if (turnMove.virtual) { - continue; - } - - const move = target.getMoveset().find(m => m.moveId === turnMove.move); - if (!move) { - continue; - } - - return true; - } - }; - } - - getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer { - return -5; - } -} - export class FrenzyAttr extends MoveEffectAttr { constructor() { super(true, MoveEffectTrigger.HIT); @@ -3842,6 +3778,7 @@ export class AddBattlerTagAttr extends MoveEffectAttr { case BattlerTagType.NIGHTMARE: case BattlerTagType.DROWSY: case BattlerTagType.NO_CRIT: + case BattlerTagType.DISABLED: return -5; case BattlerTagType.SEEDED: case BattlerTagType.SALT_CURED: @@ -5306,6 +5243,9 @@ export class hitsSameTypeAttr extends VariableMoveTypeMultiplierAttr { const unknownTypeCondition: MoveConditionFunc = (user, target, move) => !user.getTypes().includes(Type.UNKNOWN); +/** Ensures that the target has at least one non-virtual, non-NONE move in its history. */ +const targetHasMoveHistoryCondition: MoveConditionFunc = (user, target, move) => target.getLastXMoves().filter(m => m.move !== Moves.NONE && !m.virtual).length >= 1; + export type MoveTargetSet = { targets: BattlerIndex[]; multiple: boolean; @@ -5506,7 +5446,8 @@ export function initMoves() { new AttackMove(Moves.SONIC_BOOM, Type.NORMAL, MoveCategory.SPECIAL, -1, 90, 20, -1, 0, 1) .attr(FixedDamageAttr, 20), new StatusMove(Moves.DISABLE, Type.NORMAL, 100, 20, -1, 0, 1) - .attr(DisableMoveAttr) + .attr(AddBattlerTagAttr, BattlerTagType.DISABLED, false, true, 4, 4) + .condition(targetHasMoveHistoryCondition) .condition(failOnMaxCondition), new AttackMove(Moves.ACID, Type.POISON, MoveCategory.SPECIAL, 40, 100, 30, 10, 0, 1) .attr(StatChangeAttr, BattleStat.SPDEF, -1) diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 9ac45c4c299..8bbf9c7a378 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -19,7 +19,7 @@ import { pokemonEvolutions, pokemonPrevolutions, SpeciesFormEvolution, SpeciesEv import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "../data/tms"; import { DamagePhase, FaintPhase, LearnMovePhase, ObtainStatusEffectPhase, StatChangePhase, SwitchSummonPhase, ToggleDoublePositionPhase } from "../phases"; import { BattleStat } from "../data/battle-stat"; -import { BattlerTag, BattlerTagLapseType, EncoreTag, HelpingHandTag, HighestStatBoostTag, TypeBoostTag, getBattlerTag } from "../data/battler-tags"; +import { BattlerTag, BattlerTagLapseType, DisablingBattlerTag, EncoreTag, HelpingHandTag, HighestStatBoostTag, TypeBoostTag, getBattlerTag } from "../data/battler-tags"; import { BattlerTagType } from "../data/enums/battler-tag-type"; import { Species } from "../data/enums/species"; import { WeatherType } from "../data/weather"; @@ -2139,6 +2139,18 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.updateInfo(); } + /** + * Gets whether a move is currently disabled for this Pokemon. + * @see {@linkcode DisablingBattlerTag} + */ + isMoveDisabled(moveId: Moves): boolean { + for (const tag of this.findTags(t => t instanceof DisablingBattlerTag)) { + if ((tag as DisablingBattlerTag).moveIsDisabled(moveId)) { + return true; + } + } + } + getMoveHistory(): TurnMove[] { return this.battleSummonData.moveHistory; } @@ -3749,8 +3761,6 @@ export interface AttackMoveResult { export class PokemonSummonData { public battleStats: integer[] = [ 0, 0, 0, 0, 0, 0, 0 ]; public moveQueue: QueuedMove[] = []; - public disabledMove: Moves = Moves.NONE; - public disabledTurns: integer = 0; public tags: BattlerTag[] = []; public abilitySuppressed: boolean = false; @@ -3844,9 +3854,16 @@ export class PokemonMove { } isUsable(pokemon: Pokemon, ignorePp?: boolean): boolean { - if (this.moveId && pokemon.summonData?.disabledMove === this.moveId) { + if (!this.moveId) { return false; } + + for (const tag of pokemon.findTags(t => t instanceof DisablingBattlerTag)) { + if ((tag as DisablingBattlerTag).moveIsDisabled(this.moveId)) { + return false; + } + } + return (ignorePp || this.ppUsed < this.getMovePp() || this.getMove().pp === -1) && !this.getMove().name.endsWith(" (N)"); } diff --git a/src/phases.ts b/src/phases.ts index d5e64ebae2d..a50cfac439e 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -1916,7 +1916,7 @@ export class CommandPhase extends FieldPhase { // Decides between a Disabled, Not Implemented, or No PP translation message const errorMessage = - playerPokemon.summonData.disabledMove === move.moveId ? "battle:moveDisabled" : + playerPokemon.isMoveDisabled(move.moveId) ? "battle:moveDisabled" : move.getName().endsWith(" (N)") ? "battle:moveNotImplemented" : "battle:moveNoPP"; const moveName = move.getName().replace(" (N)", ""); // Trims off the indicator @@ -2355,11 +2355,6 @@ export class TurnEndPhase extends FieldPhase { const handlePokemon = (pokemon: Pokemon) => { pokemon.lapseTags(BattlerTagLapseType.TURN_END); - if (pokemon.summonData.disabledMove && !--pokemon.summonData.disabledTurns) { - this.scene.pushPhase(new MessagePhase(this.scene, i18next.t("battle:notDisabled", { pokemonName: getPokemonNameWithAffix(pokemon), moveName: allMoves[pokemon.summonData.disabledMove].name }))); - pokemon.summonData.disabledMove = Moves.NONE; - } - this.scene.applyModifiers(TurnHealModifier, pokemon.isPlayer(), pokemon); if (this.scene.arena.terrain?.terrainType === TerrainType.GRASSY && pokemon.isGrounded()) { @@ -2521,10 +2516,9 @@ export class MovePhase extends BattlePhase { console.log(Moves[this.move.moveId]); if (!this.canMove()) { - if (this.move.moveId && this.pokemon.summonData?.disabledMove === this.move.moveId) { - this.scene.queueMessage(`${this.move.getName()} is disabled!`); - } - return this.end(); + this.fail(); + this.showMoveText(); + this.showFailedText(); } if (!this.followUp) { diff --git a/src/system/pokemon-data.ts b/src/system/pokemon-data.ts index 0aa72f97801..791170cdf76 100644 --- a/src/system/pokemon-data.ts +++ b/src/system/pokemon-data.ts @@ -118,8 +118,6 @@ export default class PokemonData { if (!forHistory && source.summonData) { this.summonData.battleStats = source.summonData.battleStats; this.summonData.moveQueue = source.summonData.moveQueue; - this.summonData.disabledMove = source.summonData.disabledMove; - this.summonData.disabledTurns = source.summonData.disabledTurns; this.summonData.abilitySuppressed = source.summonData.abilitySuppressed; this.summonData.ability = source.summonData.ability;