From 929392fe8bf80683d29b4c6b10411059eb2b9a37 Mon Sep 17 00:00:00 2001 From: Dean <69436131+emdeann@users.noreply.github.com> Date: Mon, 10 Mar 2025 20:02:51 -0700 Subject: [PATCH] [Bug] Fix #5358 Abilities that Redirect Moves Consider Move-Typings before Ability Modifiers (#5464) --- src/data/ability.ts | 18 ++++- src/phases/move-phase.ts | 2 +- test/abilities/lightningrod.test.ts | 115 ++++++++++++++++++++++++++++ test/abilities/storm_drain.test.ts | 115 ++++++++++++++++++++++++++++ 4 files changed, 245 insertions(+), 5 deletions(-) create mode 100644 test/abilities/lightningrod.test.ts create mode 100644 test/abilities/storm_drain.test.ts diff --git a/src/data/ability.ts b/src/data/ability.ts index 0b4e5ddb2c4..25ffa797140 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -4571,9 +4571,19 @@ export class PostFaintHPDamageAbAttr extends PostFaintAbAttr { } } +/** + * Redirects a move to the pokemon with this ability if it meets the conditions + */ export class RedirectMoveAbAttr extends AbAttr { + /** + * @param pokemon - The Pokemon with the redirection ability + * @param args - The args passed to the `AbAttr`: + * - `[0]` - The id of the {@linkcode Move} used + * - `[1]` - The target's battler index (before redirection) + * - `[2]` - The Pokemon that used the move being redirected + */ apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean { - if (this.canRedirect(args[0] as Moves)) { + if (this.canRedirect(args[0] as Moves, args[2] as Pokemon)) { const target = args[1] as Utils.NumberHolder; const newTarget = pokemon.getBattlerIndex(); if (target.value !== newTarget) { @@ -4585,7 +4595,7 @@ export class RedirectMoveAbAttr extends AbAttr { return false; } - canRedirect(moveId: Moves): boolean { + canRedirect(moveId: Moves, user: Pokemon): boolean { const move = allMoves[moveId]; return !![ MoveTarget.NEAR_OTHER, MoveTarget.OTHER ].find(t => move.moveTarget === t); } @@ -4599,8 +4609,8 @@ export class RedirectTypeMoveAbAttr extends RedirectMoveAbAttr { this.type = type; } - canRedirect(moveId: Moves): boolean { - return super.canRedirect(moveId) && allMoves[moveId].type === this.type; + canRedirect(moveId: Moves, user: Pokemon): boolean { + return super.canRedirect(moveId, user) && user.getMoveType(allMoves[moveId]) === this.type; } } diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index 16802f8e0ff..f8edaa56981 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -504,7 +504,7 @@ export class MovePhase extends BattlePhase { globalScene .getField(true) .filter(p => p !== this.pokemon) - .forEach(p => applyAbAttrs(RedirectMoveAbAttr, p, null, false, this.move.moveId, redirectTarget)); + .forEach(p => applyAbAttrs(RedirectMoveAbAttr, p, null, false, this.move.moveId, redirectTarget, this.pokemon)); /** `true` if an Ability is responsible for redirecting the move to another target; `false` otherwise */ let redirectedByAbility = currentTarget !== redirectTarget.value; diff --git a/test/abilities/lightningrod.test.ts b/test/abilities/lightningrod.test.ts new file mode 100644 index 00000000000..1ca6c6b1e89 --- /dev/null +++ b/test/abilities/lightningrod.test.ts @@ -0,0 +1,115 @@ +import { BattlerIndex } from "#app/battle"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { Stat } from "#enums/stat"; +import GameManager from "#test/testUtils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Abilities - Lightningrod", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .moveset([ Moves.SPLASH, Moves.SHOCK_WAVE ]) + .ability(Abilities.BALL_FETCH) + .battleType("double") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("should redirect electric type moves", async () => { + await game.classicMode.startBattle([ Species.FEEBAS, Species.MAGIKARP ]); + + const enemy1 = game.scene.getEnemyField()[0]; + const enemy2 = game.scene.getEnemyField()[1]; + + enemy2.summonData.ability = Abilities.LIGHTNING_ROD; + + game.move.select(Moves.SHOCK_WAVE, BattlerIndex.PLAYER, BattlerIndex.ENEMY); + game.move.select(Moves.SPLASH, BattlerIndex.PLAYER_2); + await game.phaseInterceptor.to("BerryPhase"); + + expect(enemy1.isFullHp()).toBe(true); + }); + + it("should not redirect non-electric type moves", async () => { + game.override.moveset([ Moves.SPLASH, Moves.AERIAL_ACE ]); + await game.classicMode.startBattle([ Species.FEEBAS, Species.MAGIKARP ]); + + const enemy1 = game.scene.getEnemyField()[0]; + const enemy2 = game.scene.getEnemyField()[1]; + + enemy2.summonData.ability = Abilities.LIGHTNING_ROD; + + game.move.select(Moves.AERIAL_ACE, BattlerIndex.PLAYER, BattlerIndex.ENEMY); + game.move.select(Moves.SPLASH, BattlerIndex.PLAYER_2); + await game.phaseInterceptor.to("BerryPhase"); + + expect(enemy1.isFullHp()).toBe(false); + }); + + it("should boost the user's spatk without damaging", async () => { + await game.classicMode.startBattle([ Species.FEEBAS, Species.MAGIKARP ]); + + const enemy2 = game.scene.getEnemyField()[1]; + + enemy2.summonData.ability = Abilities.LIGHTNING_ROD; + + game.move.select(Moves.SHOCK_WAVE, BattlerIndex.PLAYER, BattlerIndex.ENEMY); + game.move.select(Moves.SPLASH, BattlerIndex.PLAYER_2); + await game.phaseInterceptor.to("BerryPhase"); + + expect(enemy2.isFullHp()).toBe(true); + expect(enemy2.getStatStage(Stat.SPATK)).toBe(1); + }); + + it("should not redirect moves changed from electric type via ability", async () => { + game.override.ability(Abilities.NORMALIZE); + await game.classicMode.startBattle([ Species.FEEBAS, Species.MAGIKARP ]); + + const enemy1 = game.scene.getEnemyField()[0]; + const enemy2 = game.scene.getEnemyField()[1]; + + enemy2.summonData.ability = Abilities.LIGHTNING_ROD; + + game.move.select(Moves.SHOCK_WAVE, BattlerIndex.PLAYER, BattlerIndex.ENEMY); + game.move.select(Moves.SPLASH, BattlerIndex.PLAYER_2); + await game.phaseInterceptor.to("BerryPhase"); + + expect(enemy1.isFullHp()).toBe(false); + }); + + it("should redirect moves changed to electric type via ability", async () => { + game.override.ability(Abilities.GALVANIZE) + .moveset(Moves.TACKLE); + await game.classicMode.startBattle([ Species.FEEBAS, Species.MAGIKARP ]); + + const enemy1 = game.scene.getEnemyField()[0]; + const enemy2 = game.scene.getEnemyField()[1]; + + enemy2.summonData.ability = Abilities.LIGHTNING_ROD; + + game.move.select(Moves.TACKLE, BattlerIndex.PLAYER, BattlerIndex.ENEMY); + game.move.select(Moves.SPLASH, BattlerIndex.PLAYER_2); + await game.phaseInterceptor.to("BerryPhase"); + + expect(enemy1.isFullHp()).toBe(true); + expect(enemy2.getStatStage(Stat.SPATK)).toBe(1); + }); +}); diff --git a/test/abilities/storm_drain.test.ts b/test/abilities/storm_drain.test.ts new file mode 100644 index 00000000000..e2a7b3e212e --- /dev/null +++ b/test/abilities/storm_drain.test.ts @@ -0,0 +1,115 @@ +import { BattlerIndex } from "#app/battle"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { Stat } from "#enums/stat"; +import GameManager from "#test/testUtils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Abilities - Storm Drain", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .moveset([ Moves.SPLASH, Moves.WATER_GUN ]) + .ability(Abilities.BALL_FETCH) + .battleType("double") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("should redirect water type moves", async () => { + await game.classicMode.startBattle([ Species.FEEBAS, Species.MAGIKARP ]); + + const enemy1 = game.scene.getEnemyField()[0]; + const enemy2 = game.scene.getEnemyField()[1]; + + enemy2.summonData.ability = Abilities.STORM_DRAIN; + + game.move.select(Moves.WATER_GUN, BattlerIndex.PLAYER, BattlerIndex.ENEMY); + game.move.select(Moves.SPLASH, BattlerIndex.PLAYER_2); + await game.phaseInterceptor.to("BerryPhase"); + + expect(enemy1.isFullHp()).toBe(true); + }); + + it("should not redirect non-water type moves", async () => { + game.override.moveset([ Moves.SPLASH, Moves.AERIAL_ACE ]); + await game.classicMode.startBattle([ Species.FEEBAS, Species.MAGIKARP ]); + + const enemy1 = game.scene.getEnemyField()[0]; + const enemy2 = game.scene.getEnemyField()[1]; + + enemy2.summonData.ability = Abilities.STORM_DRAIN; + + game.move.select(Moves.AERIAL_ACE, BattlerIndex.PLAYER, BattlerIndex.ENEMY); + game.move.select(Moves.SPLASH, BattlerIndex.PLAYER_2); + await game.phaseInterceptor.to("BerryPhase"); + + expect(enemy1.isFullHp()).toBe(false); + }); + + it("should boost the user's spatk without damaging", async () => { + await game.classicMode.startBattle([ Species.FEEBAS, Species.MAGIKARP ]); + + const enemy2 = game.scene.getEnemyField()[1]; + + enemy2.summonData.ability = Abilities.STORM_DRAIN; + + game.move.select(Moves.WATER_GUN, BattlerIndex.PLAYER, BattlerIndex.ENEMY); + game.move.select(Moves.SPLASH, BattlerIndex.PLAYER_2); + await game.phaseInterceptor.to("BerryPhase"); + + expect(enemy2.isFullHp()).toBe(true); + expect(enemy2.getStatStage(Stat.SPATK)).toBe(1); + }); + + it("should not redirect moves changed from water type via ability", async () => { + game.override.ability(Abilities.NORMALIZE); + await game.classicMode.startBattle([ Species.FEEBAS, Species.MAGIKARP ]); + + const enemy1 = game.scene.getEnemyField()[0]; + const enemy2 = game.scene.getEnemyField()[1]; + + enemy2.summonData.ability = Abilities.STORM_DRAIN; + + game.move.select(Moves.WATER_GUN, BattlerIndex.PLAYER, BattlerIndex.ENEMY); + game.move.select(Moves.SPLASH, BattlerIndex.PLAYER_2); + await game.phaseInterceptor.to("BerryPhase"); + + expect(enemy1.isFullHp()).toBe(false); + }); + + it("should redirect moves changed to water type via ability", async () => { + game.override.ability(Abilities.LIQUID_VOICE) + .moveset(Moves.PSYCHIC_NOISE); + await game.classicMode.startBattle([ Species.FEEBAS, Species.MAGIKARP ]); + + const enemy1 = game.scene.getEnemyField()[0]; + const enemy2 = game.scene.getEnemyField()[1]; + + enemy2.summonData.ability = Abilities.STORM_DRAIN; + + game.move.select(Moves.PSYCHIC_NOISE, BattlerIndex.PLAYER, BattlerIndex.ENEMY); + game.move.select(Moves.SPLASH, BattlerIndex.PLAYER_2); + await game.phaseInterceptor.to("BerryPhase"); + + expect(enemy1.isFullHp()).toBe(true); + expect(enemy2.getStatStage(Stat.SPATK)).toBe(1); + }); +});