diff --git a/src/data/ability.ts b/src/data/ability.ts old mode 100644 new mode 100755 index 925a7efb79b..fde39ebb152 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -1085,7 +1085,7 @@ export class PostDefendMoveDisableAbAttr extends PostDefendAbAttr { } applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: 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()) { if (simulated) { return true; @@ -1093,21 +1093,12 @@ export class PostDefendMoveDisableAbAttr extends PostDefendAbAttr { 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 i18next.t("abilityTriggers:postDefendMoveDisable", { - pokemonNameWithAffix: getPokemonNameWithAffix(this.attacker), - moveName: this.move.name, - }); - } } export class PostStatStageChangeStatStageChangeAbAttr extends PostStatStageChangeAbAttr { diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 66bcc7b9c3c..ef91dda7b63 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -98,6 +98,127 @@ export interface TerrainBattlerTag { terrainTypes: TerrainType[]; } +/** + * Base class for tags that restrict the usage of moves. This effect is generally referred to as "disabling" a move + * in-game. This is not to be confused with {@linkcode Moves.DISABLE}. + * + * Descendants can override {@linkcode isMoveRestricted} to restrict moves that + * match a condition. A restricted move gets cancelled before it is used. Players and enemies should not be allowed + * to select restricted moves. + */ +export abstract class MoveRestrictionBattlerTag extends BattlerTag { + constructor(tagType: BattlerTagType, turnCount: integer, sourceMove?: Moves, sourceId?: integer) { + super(tagType, [ BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.TURN_END ], turnCount, sourceMove, sourceId); + } + + /** @override */ + override lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { + if (lapseType === BattlerTagLapseType.PRE_MOVE) { + // Cancel the affected pokemon's selected move + const phase = pokemon.scene.getCurrentPhase() as MovePhase; + const move = phase.move; + + if (this.isMoveRestricted(move.moveId)) { + pokemon.scene.queueMessage(this.interruptedText(pokemon, move.moveId)); + phase.cancel(); + } + + return true; + } + + return super.lapse(pokemon, lapseType); + } + + /** + * Gets whether this tag is restricting a move. + * + * @param {Moves} move {@linkcode Moves} ID to check restriction for. + * @returns {boolean} `true` if the move is restricted by this tag, otherwise `false`. + */ + abstract isMoveRestricted(move: Moves): boolean; + + /** + * Gets the text to display when the player attempts to select a move that is restricted by this tag. + * + * @param {Pokemon} pokemon {@linkcode Pokemon} for which the player is attempting to select the restricted move + * @param {Moves} move {@linkcode Moves} ID of the move that is having its selection denied + * @returns {string} text to display when the player attempts to select the restricted move + */ + abstract selectionDeniedText(pokemon: Pokemon, move: Moves): string; + + /** + * Gets the text to display when a move's execution is prevented as a result of the restriction. + * Because restriction effects also prevent selection of the move, this situation can only arise if a + * pokemon first selects a move, then gets outsped by a pokemon using a move that restricts the selected move. + * + * @param {Pokemon} pokemon {@linkcode Pokemon} attempting to use the restricted move + * @param {Moves} move {@linkcode Moves} ID of the move being interrupted + * @returns {string} text to display when the move is interrupted + */ + abstract interruptedText(pokemon: Pokemon, move: Moves): string; +} + +/** + * 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 MoveRestrictionBattlerTag { + /** The move being disabled. Gets set when {@linkcode onAdd} is called for this tag. */ + private moveId: Moves = Moves.NONE; + + constructor(sourceId: number) { + super(BattlerTagType.DISABLED, 4, Moves.DISABLE, sourceId); + } + + /** @override */ + override isMoveRestricted(move: Moves): boolean { + return move === this.moveId; + } + + /** + * @override + * + * Ensures that move history exists on `pokemon` and has a valid move. If so, sets the {@link moveId} and shows a message. + * Otherwise the move ID will not get assigned and this tag will get removed next turn. + */ + override onAdd(pokemon: Pokemon): void { + super.onAdd(pokemon); + + const move = pokemon.getLastXMoves() + .find(m => m.move !== Moves.NONE && m.move !== Moves.STRUGGLE && !m.virtual); + if (move === undefined) { + return; + } + + this.moveId = move.move; + + pokemon.scene.queueMessage(i18next.t("battlerTags:disabledOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), moveName: allMoves[this.moveId].name })); + } + + /** @override */ + override onRemove(pokemon: Pokemon): void { + super.onRemove(pokemon); + + pokemon.scene.queueMessage(i18next.t("battlerTags:disabledLapse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), moveName: allMoves[this.moveId].name })); + } + + /** @override */ + override selectionDeniedText(pokemon: Pokemon, move: Moves): string { + return i18next.t("battle:moveDisabled", { moveName: allMoves[move].name }); + } + + /** @override */ + override interruptedText(pokemon: Pokemon, move: Moves): string { + return i18next.t("battle:disableInterruptedMove", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), moveName: allMoves[move].name }); + } + + /** @override */ + override loadTag(source: BattlerTag | any): void { + super.loadTag(source); + this.moveId = source.moveId; + } +} + /** * BattlerTag that represents the "recharge" effects of moves like Hyper Beam. */ @@ -1995,6 +2116,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source return new StockpilingTag(sourceMove); case BattlerTagType.OCTOLOCK: return new OctolockTag(sourceId); + case BattlerTagType.DISABLED: + return new DisabledTag(sourceId); case BattlerTagType.IGNORE_GHOST: return new ExposedTag(tagType, sourceMove, Type.GHOST, [Type.NORMAL, Type.FIGHTING]); case BattlerTagType.IGNORE_DARK: diff --git a/src/data/move.ts b/src/data/move.ts index ddf043c554d..a591f12df90 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -4332,72 +4332,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 | undefined; - 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!; // TODO: is this bang correct? - target.summonData.disabledTurns = 4; - - user.scene.queueMessage(i18next.t("abilityTriggers:postDefendMoveDisable", { pokemonNameWithAffix: getPokemonNameWithAffix(target), moveName: disabledMove?.getName()})); - - return true; - } - - return false; - } - - getCondition(): MoveConditionFunc { - return (user, target, move): boolean => { // TODO: Not sure what to do here - if (target.summonData.disabledMove || target.isMax()) { - return false; - } - - const moveQueue = target.getLastXMoves(); - let turnMove: TurnMove | undefined; - 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; - } - - return false; - }; - } - - getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer { - return -5; - } -} - export class FrenzyAttr extends MoveEffectAttr { constructor() { super(true, MoveEffectTrigger.HIT, false, true); @@ -4488,6 +4422,7 @@ export class AddBattlerTagAttr extends MoveEffectAttr { case BattlerTagType.INFATUATED: case BattlerTagType.NIGHTMARE: case BattlerTagType.DROWSY: + case BattlerTagType.DISABLED: return -5; case BattlerTagType.SEEDED: case BattlerTagType.SALT_CURED: @@ -6673,7 +6608,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) + .condition((user, target, move) => target.getMoveHistory().reverse().find(m => m.move !== Moves.NONE && m.move !== Moves.STRUGGLE && !m.virtual) !== undefined) .condition(failOnMaxCondition), new AttackMove(Moves.ACID, Type.POISON, MoveCategory.SPECIAL, 40, 100, 30, 10, 0, 1) .attr(StatStageChangeAttr, [ Stat.SPDEF ], -1) diff --git a/src/enums/battler-tag-type.ts b/src/enums/battler-tag-type.ts index 20ceb1b331f..a2bcf9e4c0e 100644 --- a/src/enums/battler-tag-type.ts +++ b/src/enums/battler-tag-type.ts @@ -64,6 +64,7 @@ export enum BattlerTagType { STOCKPILING = "STOCKPILING", RECEIVE_DOUBLE_DAMAGE = "RECEIVE_DOUBLE_DAMAGE", ALWAYS_GET_HIT = "ALWAYS_GET_HIT", + DISABLED = "DISABLED", IGNORE_GHOST = "IGNORE_GHOST", IGNORE_DARK = "IGNORE_DARK", GULP_MISSILE_ARROKUDA = "GULP_MISSILE_ARROKUDA", diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index e0a9a4a86ce..269d0b1dba5 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -17,7 +17,7 @@ import { initMoveAnim, loadMoveAnimAssets } from "../data/battle-anims"; import { Status, StatusEffect, getRandomStatus } from "../data/status-effect"; import { pokemonEvolutions, pokemonPrevolutions, SpeciesFormEvolution, SpeciesEvolutionCondition, FusionSpeciesFormEvolution } from "../data/pokemon-evolutions"; import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "../data/tms"; -import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag } from "../data/battler-tags"; +import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, MoveRestrictionBattlerTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag } from "../data/battler-tags"; import { WeatherType } from "../data/weather"; import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "../data/arena-tag"; import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr } from "../data/ability"; @@ -2670,6 +2670,33 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.updateInfo(); } + /** + * Gets whether the given move is currently disabled for this Pokemon. + * + * @param {Moves} moveId {@linkcode Moves} ID of the move to check + * @returns {boolean} `true` if the move is disabled for this Pokemon, otherwise `false` + * + * @see {@linkcode MoveRestrictionBattlerTag} + */ + isMoveRestricted(moveId: Moves): boolean { + return this.getRestrictingTag(moveId) !== null; + } + + /** + * Gets the {@link MoveRestrictionBattlerTag} that is restricting a move, if it exists. + * + * @param {Moves} moveId {@linkcode Moves} ID of the move to check + * @returns {MoveRestrictionBattlerTag | null} the first tag on this Pokemon that restricts the move, or `null` if the move is not restricted. + */ + getRestrictingTag(moveId: Moves): MoveRestrictionBattlerTag | null { + for (const tag of this.findTags(t => t instanceof MoveRestrictionBattlerTag)) { + if ((tag as MoveRestrictionBattlerTag).isMoveRestricted(moveId)) { + return tag as MoveRestrictionBattlerTag; + } + } + return null; + } + getMoveHistory(): TurnMove[] { return this.battleSummonData.moveHistory; } @@ -4458,8 +4485,6 @@ export interface AttackMoveResult { export class PokemonSummonData { public statStages: number[] = [ 0, 0, 0, 0, 0, 0, 0 ]; public moveQueue: QueuedMove[] = []; - public disabledMove: Moves = Moves.NONE; - public disabledTurns: number = 0; public tags: BattlerTag[] = []; public abilitySuppressed: boolean = false; public abilitiesApplied: Abilities[] = []; @@ -4540,7 +4565,7 @@ export type DamageResult = HitResult.EFFECTIVE | HitResult.SUPER_EFFECTIVE | Hit * It links to {@linkcode Move} class via the move ID. * Compared to {@linkcode Move}, this class also tracks if a move has received. * PP Ups, amount of PP used, and things like that. - * @see {@linkcode isUsable} - checks if move is disabled, out of PP, or not implemented. + * @see {@linkcode isUsable} - checks if move is restricted, out of PP, or not implemented. * @see {@linkcode getMove} - returns {@linkcode Move} object by looking it up via ID. * @see {@linkcode usePp} - removes a point of PP from the move. * @see {@linkcode getMovePp} - returns amount of PP a move currently has. @@ -4560,11 +4585,25 @@ export class PokemonMove { this.virtual = !!virtual; } - isUsable(pokemon: Pokemon, ignorePp?: boolean): boolean { - if (this.moveId && pokemon.summonData?.disabledMove === this.moveId) { + /** + * Checks whether the move can be selected or 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. + * + * @param {Pokemon} pokemon {@linkcode Pokemon} that would be using this move + * @param {boolean} ignorePp If `true`, skips the PP check + * @param {boolean} ignoreRestrictionTags If `true`, skips the check for move restriction tags (see {@link MoveRestrictionBattlerTag}) + * @returns `true` if the move can be selected and used by the Pokemon, otherwise `false`. + */ + isUsable(pokemon: Pokemon, ignorePp?: boolean, ignoreRestrictionTags?: boolean): boolean { + if (this.moveId && !ignoreRestrictionTags && pokemon.isMoveRestricted(this.moveId)) { return false; } - return (ignorePp || this.ppUsed < this.getMovePp() || this.getMove().pp === -1) && !this.getMove().name.endsWith(" (N)"); + + if (this.getMove().name.endsWith(" (N)")) { + return false; + } + + return (ignorePp || this.ppUsed < this.getMovePp() || this.getMove().pp === -1); } getMove(): Move { diff --git a/src/locales/en/battle.json b/src/locales/en/battle.json index 918fb38b520..120ac749acb 100644 --- a/src/locales/en/battle.json +++ b/src/locales/en/battle.json @@ -44,6 +44,7 @@ "moveNotImplemented": "{{moveName}} is not yet implemented and cannot be selected.", "moveNoPP": "There's no PP left for\nthis move!", "moveDisabled": "{{moveName}} is disabled!", + "disableInterruptedMove": "{{pokemonNameWithAffix}}'s {{moveName}}\nis disabled!", "noPokeballForce": "An unseen force\nprevents using Poké Balls.", "noPokeballTrainer": "You can't catch\nanother trainer's Pokémon!", "noPokeballMulti": "You can only throw a Poké Ball\nwhen there is one Pokémon remaining!", diff --git a/src/locales/en/battler-tags.json b/src/locales/en/battler-tags.json index 94ea3b14958..222aee4087c 100644 --- a/src/locales/en/battler-tags.json +++ b/src/locales/en/battler-tags.json @@ -67,5 +67,7 @@ "saltCuredLapse": "{{pokemonNameWithAffix}} is hurt by {{moveName}}!", "cursedOnAdd": "{{pokemonNameWithAffix}} cut its own HP and put a curse on the {{pokemonName}}!", "cursedLapse": "{{pokemonNameWithAffix}} is afflicted by the Curse!", - "stockpilingOnAdd": "{{pokemonNameWithAffix}} stockpiled {{stockpiledCount}}!" -} \ No newline at end of file + "stockpilingOnAdd": "{{pokemonNameWithAffix}} stockpiled {{stockpiledCount}}!", + "disabledOnAdd": "{{pokemonNameWithAffix}}'s {{moveName}}\nwas disabled!", + "disabledLapse": "{{pokemonNameWithAffix}}'s {{moveName}}\nis no longer disabled." +} diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index 9681a6eeee8..47d212aa598 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -107,8 +107,9 @@ 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" : - move.getName().endsWith(" (N)") ? "battle:moveNotImplemented" : "battle:moveNoPP"; + playerPokemon.isMoveRestricted(move.moveId) + ? playerPokemon.getRestrictingTag(move.moveId)!.selectionDeniedText(playerPokemon, move.moveId) + : move.getName().endsWith(" (N)") ? "battle:moveNotImplemented" : "battle:moveNoPP"; const moveName = move.getName().replace(" (N)", ""); // Trims off the indicator this.scene.ui.showText(i18next.t(errorMessage, { moveName: moveName }), null, () => { diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index e2893d587a7..0ccf19a462f 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -44,8 +44,8 @@ export class MovePhase extends BattlePhase { this.cancelled = false; } - canMove(): boolean { - return this.pokemon.isActive(true) && this.move.isUsable(this.pokemon, this.ignorePp) && !!this.targets.length; + canMove(ignoreDisableTags?: boolean): boolean { + return this.pokemon.isActive(true) && this.move.isUsable(this.pokemon, this.ignorePp, ignoreDisableTags) && !!this.targets.length; } /**Signifies the current move should fail but still use PP */ @@ -63,10 +63,7 @@ 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(i18next.t("battle:moveDisabled", { moveName: this.move.getName() })); - } + if (!this.canMove(true)) { if (this.pokemon.isActive(true) && this.move.ppUsed >= this.move.getMovePp()) { // if the move PP was reduced from Spite or otherwise, the move fails this.fail(); this.showMoveText(); diff --git a/src/phases/turn-end-phase.ts b/src/phases/turn-end-phase.ts index 9f4de46b0fa..c8bd3398bb5 100644 --- a/src/phases/turn-end-phase.ts +++ b/src/phases/turn-end-phase.ts @@ -1,9 +1,7 @@ import BattleScene from "#app/battle-scene.js"; import { applyPostTurnAbAttrs, PostTurnAbAttr } from "#app/data/ability.js"; import { BattlerTagLapseType } from "#app/data/battler-tags.js"; -import { allMoves } from "#app/data/move.js"; import { TerrainType } from "#app/data/terrain.js"; -import { Moves } from "#app/enums/moves.js"; import { WeatherType } from "#app/enums/weather-type.js"; import { TurnEndEvent } from "#app/events/battle-scene.js"; import Pokemon from "#app/field/pokemon.js"; @@ -11,7 +9,6 @@ import { getPokemonNameWithAffix } from "#app/messages.js"; import { TurnHealModifier, EnemyTurnHealModifier, EnemyStatusEffectHealChanceModifier, TurnStatusEffectModifier, TurnHeldItemTransferModifier } from "#app/modifier/modifier.js"; import i18next from "i18next"; import { FieldPhase } from "./field-phase"; -import { MessagePhase } from "./message-phase"; import { PokemonHealPhase } from "./pokemon-heal-phase"; export class TurnEndPhase extends FieldPhase { @@ -28,11 +25,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()) { diff --git a/src/system/pokemon-data.ts b/src/system/pokemon-data.ts index 9a743ceb1d2..1fafcbf8acc 100644 --- a/src/system/pokemon-data.ts +++ b/src/system/pokemon-data.ts @@ -127,8 +127,6 @@ export default class PokemonData { this.summonData.stats = source.summonData.stats; this.summonData.statStages = source.summonData.statStages; 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.abilitiesApplied = source.summonData.abilitiesApplied; diff --git a/src/test/moves/disable.test.ts b/src/test/moves/disable.test.ts new file mode 100644 index 00000000000..3d207035ce3 --- /dev/null +++ b/src/test/moves/disable.test.ts @@ -0,0 +1,129 @@ +import { BattlerIndex } from "#app/battle"; +import { MoveResult } from "#app/field/pokemon"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import { SPLASH_ONLY } from "#test/utils/testUtils"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Moves - Disable", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + game.override + .battleType("single") + .ability(Abilities.BALL_FETCH) + .enemyAbility(Abilities.BALL_FETCH) + .moveset([Moves.DISABLE, Moves.SPLASH]) + .enemyMoveset(SPLASH_ONLY) + .starterSpecies(Species.PIKACHU) + .enemySpecies(Species.SHUCKLE); + }); + + it("restricts moves", async () => { + await game.classicMode.startBattle(); + + const enemyMon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.DISABLE); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + await game.toNextTurn(); + + expect(enemyMon.getMoveHistory()).toHaveLength(1); + expect(enemyMon.isMoveRestricted(Moves.SPLASH)).toBe(true); + }); + + it("fails if enemy has no move history", async() => { + await game.classicMode.startBattle(); + + const playerMon = game.scene.getPlayerPokemon()!; + const enemyMon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.DISABLE); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); + await game.toNextTurn(); + + expect(playerMon.getMoveHistory()[0]).toMatchObject({ move: Moves.DISABLE, result: MoveResult.FAIL }); + expect(enemyMon.isMoveRestricted(Moves.SPLASH)).toBe(false); + }, 20000); + + it("causes STRUGGLE if all usable moves are disabled", async() => { + await game.classicMode.startBattle(); + + const enemyMon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.DISABLE); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + await game.toNextTurn(); + + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + + const enemyHistory = enemyMon.getMoveHistory(); + expect(enemyHistory).toHaveLength(2); + expect(enemyHistory[0].move).toBe(Moves.SPLASH); + expect(enemyHistory[1].move).toBe(Moves.STRUGGLE); + }, 20000); + + it("cannot disable STRUGGLE", async() => { + game.override.enemyMoveset(Array(4).fill(Moves.STRUGGLE)); + await game.classicMode.startBattle(); + + const playerMon = game.scene.getPlayerPokemon()!; + const enemyMon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.DISABLE); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + await game.toNextTurn(); + + expect(playerMon.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + expect(enemyMon.getLastXMoves()[0].move).toBe(Moves.STRUGGLE); + expect(enemyMon.isMoveRestricted(Moves.STRUGGLE)).toBe(false); + }, 20000); + + it("interrupts target's move when target moves after", async() => { + await game.classicMode.startBattle(); + + const enemyMon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + + // Both mons just used Splash last turn; now have player use Disable. + game.move.select(Moves.DISABLE); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); + await game.toNextTurn(); + + const enemyHistory = enemyMon.getMoveHistory(); + expect(enemyHistory).toHaveLength(2); + expect(enemyHistory[0]).toMatchObject({ move: Moves.SPLASH, result: MoveResult.SUCCESS }); + expect(enemyHistory[1].result).toBe(MoveResult.FAIL); + }, 20000); + + it("disables NATURE POWER, not the move invoked by it", async() => { + game.override.enemyMoveset(Array(4).fill(Moves.NATURE_POWER)); + await game.classicMode.startBattle(); + + const enemyMon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.DISABLE); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + await game.toNextTurn(); + + expect(enemyMon.isMoveRestricted(Moves.NATURE_POWER)).toBe(true); + expect(enemyMon.isMoveRestricted(enemyMon.getLastXMoves(2)[1].move)).toBe(false); + }, 20000); +});