diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index 35ed37379d6..82ba9495b3b 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -2119,28 +2119,16 @@ export class IntimidateImmunityAbAttr extends AbAttr { export class PostIntimidateStatStageChangeAbAttr extends AbAttr { private stats: BattleStat[]; private stages: number; - private overwrites: boolean; - constructor(stats: BattleStat[], stages: number, overwrites?: boolean) { + constructor(stats: BattleStat[], stages: number) { super(true); this.stats = stats; this.stages = stages; - this.overwrites = !!overwrites; } - override apply(pokemon: Pokemon, passive: boolean, simulated:boolean, cancelled: BooleanHolder, args: any[]): void { - if (simulated) { - cancelled.value = this.overwrites; - return; - } - - const newStatStageChangePhase = new StatStageChangePhase(pokemon.getBattlerIndex(), false, this.stats, this.stages); - if (globalScene.findPhase(m => m instanceof MovePhase)) { - globalScene.prependToPhase(newStatStageChangePhase, MovePhase); - } else if (globalScene.findPhase(m => m instanceof SwitchSummonPhase)) { - globalScene.prependToPhase(newStatStageChangePhase, SwitchSummonPhase); - } else { - globalScene.pushPhase(newStatStageChangePhase); + override apply(pokemon: Pokemon, _passive: boolean, simulated: boolean, _cancelled: BooleanHolder, _args: any[]): void { + if (!simulated) { + globalScene.unshiftPhase(new StatStageChangePhase(pokemon.getBattlerIndex(), false, this.stats, this.stages)); } cancelled.value = this.overwrites; } @@ -2348,8 +2336,6 @@ export class PostSummonStatStageChangeAbAttr extends PostSummonAbAttr { const cancelled = new BooleanHolder(false); if (this.intimidate) { applyAbAttrs(IntimidateImmunityAbAttr, opponent, cancelled, simulated); - applyAbAttrs(PostIntimidateStatStageChangeAbAttr, opponent, cancelled, simulated); - if (opponent.getTag(BattlerTagType.SUBSTITUTE)) { cancelled.value = true; } @@ -2357,6 +2343,7 @@ export class PostSummonStatStageChangeAbAttr extends PostSummonAbAttr { if (!cancelled.value) { globalScene.unshiftPhase(new StatStageChangePhase(opponent.getBattlerIndex(), false, this.stats, this.stages)); } + applyAbAttrs(PostIntimidateStatStageChangeAbAttr, opponent, cancelled, simulated); } } } @@ -7372,7 +7359,8 @@ export function initAbilities() { .attr(PostSummonStatStageChangeOnArenaAbAttr, ArenaTagType.TAILWIND) .ignorable(), new Ability(AbilityId.GUARD_DOG, 9) - .attr(PostIntimidateStatStageChangeAbAttr, [ Stat.ATK ], 1, true) + .attr(PostIntimidateStatStageChangeAbAttr, [ Stat.ATK ], 1) + .attr(IntimidateImmunityAbAttr) .attr(ForceSwitchOutImmunityAbAttr) .ignorable(), new Ability(AbilityId.ROCKY_PAYLOAD, 9) diff --git a/test/abilities/rattled.test.ts b/test/abilities/rattled.test.ts index c27dca87dbc..f3732688100 100644 --- a/test/abilities/rattled.test.ts +++ b/test/abilities/rattled.test.ts @@ -5,7 +5,10 @@ import GameManager from "#test/testUtils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { BattleType } from "#enums/battle-type"; -import { Stat } from "#enums/stat"; +import { getStatKey, getStatStageChangeDescriptionKey, Stat } from "#enums/stat"; +import { BattlerIndex } from "#app/battle"; +import i18next from "i18next"; +import { getPokemonNameWithAffix } from "#app/messages"; describe("Abilities - Rattled", () => { let phaserGame: Phaser.Game; @@ -24,43 +27,98 @@ describe("Abilities - Rattled", () => { beforeEach(() => { game = new GameManager(phaserGame); game.override - .moveset([MoveId.FALSE_SWIPE, MoveId.TRICK_ROOM]) .ability(AbilityId.RATTLED) .battleType(BattleType.TRAINER) .disableCrits() .battleStyle("single") - .enemySpecies(SpeciesId.MAGIKARP) + .enemySpecies(SpeciesId.DUSKULL) .enemyAbility(AbilityId.INTIMIDATE) - .enemyMoveset(MoveId.PIN_MISSILE); + .enemyPassiveAbility(AbilityId.NO_GUARD); }); - it("should trigger and boost speed immediately after Intimidate attack drop on initial send out", async () => { + it.each<{ type: string; move: MoveId }>([ + { type: "Bug", move: MoveId.TWINEEDLE }, + { type: "Ghost", move: MoveId.ASTONISH }, + { type: "Dark", move: MoveId.BEAT_UP }, + ])("should raise the user's Speed by 1 stage for each hit of a $type-type move", async ({ move }) => { + game.override.enemyAbility(AbilityId.BALL_FETCH); + await game.classicMode.startBattle([SpeciesId.GIMMIGHOUL]); + + game.move.use(MoveId.SPLASH); + await game.move.forceEnemyMove(move); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + game.phaseInterceptor.clearLogs(); + + await game.phaseInterceptor.to("MoveEffectPhase"); + const enemyHits = game.field.getEnemyPokemon().turnData.hitCount; + await game.phaseInterceptor.to("MoveEndPhase"); + + // Rattled should've raised speed once per hit, displaying a separate message each time + const gimmighoul = game.field.getPlayerPokemon(); + expect(gimmighoul.getStatStage(Stat.SPD)).toBe(enemyHits); + expect(game.phaseInterceptor.log.filter(p => p === "ShowAbilityPhase")).toHaveLength(enemyHits); + expect(game.phaseInterceptor.log.filter(p => p === "StatStageChangePhase")).toHaveLength(enemyHits); + const statChangeText = i18next.t(getStatStageChangeDescriptionKey(1, true), { + pokemonNameWithAffix: getPokemonNameWithAffix(gimmighoul), + stats: i18next.t(getStatKey(Stat.SPD)), + count: 1, + }); + expect(game.textInterceptor.logs.filter(t => t === statChangeText)).toHaveLength(enemyHits); + }); + + it.each<{ type: string; move: MoveId }>([ + { type: "Bug", move: MoveId.POWDER }, + { type: "Ghost", move: MoveId.CONFUSE_RAY }, + { type: "Dark", move: MoveId.TAUNT }, + ])("should not trigger from $type-type status moves", async ({ move }) => { + game.override.enemyAbility(AbilityId.BALL_FETCH); + await game.classicMode.startBattle([SpeciesId.GIMMIGHOUL]); + + game.move.use(MoveId.SPLASH); + await game.move.forceEnemyMove(move); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + await game.phaseInterceptor.to("MoveEndPhase"); + + const gimmighoul = game.field.getPlayerPokemon(); + expect(gimmighoul.getStatStage(Stat.SPD)).toBe(0); + expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase"); + expect(game.phaseInterceptor.log).not.toContain("StatStageChangePhase"); + }); + + it("should activate after Intimidate attack drop on initial send out", async () => { // `runToSummon` used instead of `startBattle` to avoid skipping past initial "post send out" effects await game.classicMode.runToSummon([SpeciesId.GIMMIGHOUL]); - const playerPokemon = game.field.getPlayerPokemon(); + // Intimidate await game.phaseInterceptor.to("StatStageChangePhase"); + const playerPokemon = game.field.getPlayerPokemon(); expect(playerPokemon.getStatStage(Stat.ATK)).toBe(-1); expect(playerPokemon.getStatStage(Stat.SPD)).toBe(0); + game.phaseInterceptor.clearLogs(); + // Rattled await game.phaseInterceptor.to("StatStageChangePhase"); expect(playerPokemon.getStatStage(Stat.ATK)).toBe(-1); expect(playerPokemon.getStatStage(Stat.SPD)).toBe(1); + // Nothing but show/hide ability phases should be visible + for (const log of game.phaseInterceptor.log) { + expect(log).toBeOneOf(["ShowAbilityPhase", "HideAbilityPhase", "StatStageChangePhase", "MessagePhase"]); + } }); - it("should activate Rattled from Intimidate before the Pokémon is switched out.", async () => { - game.override.enemyLevel(100); // Ensures the opponent switches first by overriding their Pokémon's level to 100. + it("should activate after Intimidate from enemy switch", async () => { await game.classicMode.startBattle([SpeciesId.GIMMIGHOUL, SpeciesId.BULBASAUR]); - const playerPokemon = game.field.getPlayerPokemon(); - + game.move.use(MoveId.SPLASH); game.forceEnemyToSwitch(); - game.doSwitchPokemon(1); - await game.phaseInterceptor.to("StatStageChangePhase"); + + const playerPokemon = game.field.getPlayerPokemon(); expect(playerPokemon.getStatStage(Stat.ATK)).toBe(-2); + expect(playerPokemon.getStatStage(Stat.SPD)).toBe(1); + await game.phaseInterceptor.to("StatStageChangePhase"); expect(playerPokemon.getStatStage(Stat.SPD)).toBe(2); });