diff --git a/src/data/move.ts b/src/data/move.ts index dec52b04c57..b1479807d64 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -5290,7 +5290,16 @@ export class SwitchAbilitiesAttr extends MoveEffectAttr { } } +/** + * Attribute used for moves that suppress abilities like {@linkcode Moves.GASTRO_ACID}. + * A suppressed ability cannot be activated. + * + * @extends MoveEffectAttr + * @see {@linkcode apply} + * @see {@linkcode getCondition} + */ export class SuppressAbilitiesAttr extends MoveEffectAttr { + /** Sets ability suppression for the target pokemon and displays a message. */ apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { if (!super.apply(user, target, move, args)) { return false; @@ -5303,8 +5312,9 @@ export class SuppressAbilitiesAttr extends MoveEffectAttr { return true; } + /** Causes the effect to fail when the target's ability is unsupressable or already suppressed. */ getCondition(): MoveConditionFunc { - return (user, target, move) => !target.getAbility().hasAttr(UnsuppressableAbilityAbAttr); + return (user, target, move) => !target.getAbility().hasAttr(UnsuppressableAbilityAbAttr) && !target.summonData.abilitySuppressed; } } @@ -5319,7 +5329,7 @@ export class SuppressAbilitiesIfActedAttr extends MoveEffectAttr { * abillity cannot be suppressed. This is a secondary effect and has no bearing on the success or failure of the move. * * @returns True if the move occurred, otherwise false. Note that true will be returned even if the target has not - * yet moved or if the target's abiilty is un-suppressable. + * yet moved or if the suppression failed to apply. */ apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { if (!super.apply(user, target, move, args)) { diff --git a/src/test/moves/gastro_acid.test.ts b/src/test/moves/gastro_acid.test.ts new file mode 100644 index 00000000000..e38dd5f44f4 --- /dev/null +++ b/src/test/moves/gastro_acid.test.ts @@ -0,0 +1,91 @@ +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import GameManager from "../utils/gameManager"; +import { + Moves +} from "#app/enums/moves.js"; +import * as overrides from "#app/overrides"; +import { Abilities } from "#app/enums/abilities.js"; +import { BattlerIndex } from "#app/battle.js"; +import { getMovePosition } from "../utils/gameManagerUtils"; +import { MoveResult } from "#app/field/pokemon.js"; +import { Stat } from "#app/data/pokemon-stat.js"; + +const TIMEOUT = 20 * 1000; + +describe("Moves - Gastro Acid", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + vi.spyOn(overrides, "DOUBLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + vi.spyOn(overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(1); + vi.spyOn(overrides, "OPP_LEVEL_OVERRIDE", "get").mockReturnValue(100); + vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.NONE); + vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.GASTRO_ACID, Moves.WATER_GUN, Moves.SPLASH, Moves.CORE_ENFORCER]); + vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH]); + vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.WATER_ABSORB); + }); + + it("suppresses effect of ability", async () => { + /* + * Expected flow (enemies have WATER ABSORD, can only use SPLASH) + * - player mon 1 uses GASTRO ACID, player mon 2 uses SPLASH + * - both player mons use WATER GUN on their respective enemy mon + * - player mon 1 should have dealt damage, player mon 2 should have not + */ + + await game.startBattle(); + + game.doAttack(getMovePosition(game.scene, 0, Moves.GASTRO_ACID)); + game.doSelectTarget(BattlerIndex.ENEMY); + game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH)); + game.doSelectTarget(BattlerIndex.PLAYER_2); + + await game.phaseInterceptor.to("TurnInitPhase"); + + const enemyField = game.scene.getEnemyField(); + expect(enemyField[0].summonData.abilitySuppressed).toBe(true); + expect(enemyField[1].summonData.abilitySuppressed).toBe(false); + + game.doAttack(getMovePosition(game.scene, 0, Moves.WATER_GUN)); + game.doSelectTarget(BattlerIndex.ENEMY); + game.doAttack(getMovePosition(game.scene, 0, Moves.WATER_GUN)); + game.doSelectTarget(BattlerIndex.ENEMY_2); + + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(enemyField[0].hp).toBeLessThan(enemyField[0].getMaxHp()); + expect(enemyField[1].hp).toBe(enemyField[1].getMaxHp()); + }, TIMEOUT); + + it("fails if used on an enemy with an already-suppressed ability", async () => { + vi.spyOn(overrides, "DOUBLE_BATTLE_OVERRIDE", "get").mockReturnValue(false); + + await game.startBattle(); + + // Force player to be slower to enable Core Enforcer to proc its suppression effect + game.scene.getPlayerPokemon().stats[Stat.SPD] = 1; + game.scene.getEnemyPokemon().stats[Stat.SPD] = 2; + + game.doAttack(getMovePosition(game.scene, 0, Moves.CORE_ENFORCER)); + + await game.phaseInterceptor.to("TurnInitPhase"); + + game.doAttack(getMovePosition(game.scene, 0, Moves.GASTRO_ACID)); + + await game.phaseInterceptor.to("TurnInitPhase"); + + expect(game.scene.getPlayerPokemon().getLastXMoves()[0].result).toBe(MoveResult.FAIL); + }, TIMEOUT); +});