diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index 4dc817e1f9a..e8662d0e822 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -1246,7 +1246,7 @@ export class MoveTypeChangeAbAttr extends PreAttackAbAttr { /** * Determine if the move type change attribute can be applied - * + * * Can be applied if: * - The ability's condition is met, e.g. pixilate only boosts normal moves, * - The move is not forbidden from having its type changed by an ability, e.g. {@linkcode MoveId.MULTI_ATTACK} @@ -1262,7 +1262,7 @@ export class MoveTypeChangeAbAttr extends PreAttackAbAttr { */ override canApplyPreAttack(pokemon: Pokemon, _passive: boolean, _simulated: boolean, _defender: Pokemon | null, move: Move, _args: [NumberHolder?, NumberHolder?, ...any]): boolean { return (!this.condition || this.condition(pokemon, _defender, move)) && - !noAbilityTypeOverrideMoves.has(move.id) && + !noAbilityTypeOverrideMoves.has(move.id) && (!pokemon.isTerastallized || (move.id !== MoveId.TERA_BLAST && (move.id !== MoveId.TERA_STARSTORM || pokemon.getTeraType() !== PokemonType.STELLAR || !pokemon.hasSpecies(SpeciesId.TERAPAGOS)))); @@ -2119,31 +2119,17 @@ 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 + 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)); } - - 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); - } - - cancelled.value = this.overwrites; } } @@ -2349,8 +2335,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; } @@ -2358,6 +2342,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); } } } @@ -7401,7 +7386,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..ff183131bb0 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,79 @@ 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) .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("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); });