From 6ba75e1415e8572b938d4e671c4ff2694864c76a Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Tue, 20 May 2025 13:04:43 -0400 Subject: [PATCH 01/16] Added additional tests for intimidate & ability-ignoring moves --- test/abilities/intimidate.test.ts | 87 ++++++++++------------- test/moves/ignore-abilities.test.ts | 105 ++++++++++++++++++++++++++++ test/moves/moongeist_beam.test.ts | 61 ---------------- 3 files changed, 141 insertions(+), 112 deletions(-) create mode 100644 test/moves/ignore-abilities.test.ts delete mode 100644 test/moves/moongeist_beam.test.ts diff --git a/test/abilities/intimidate.test.ts b/test/abilities/intimidate.test.ts index 8db39270dcf..2b35d81283e 100644 --- a/test/abilities/intimidate.test.ts +++ b/test/abilities/intimidate.test.ts @@ -1,12 +1,11 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import Phaser from "phaser"; import GameManager from "#test/testUtils/gameManager"; -import { UiMode } from "#enums/ui-mode"; import { Stat } from "#enums/stat"; -import { getMovePosition } from "#test/testUtils/gameManagerUtils"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { BattleType } from "#enums/battle-type"; describe("Abilities - Intimidate", () => { let phaserGame: Phaser.Game; @@ -28,24 +27,13 @@ describe("Abilities - Intimidate", () => { .battleStyle("single") .enemySpecies(Species.RATTATA) .enemyAbility(Abilities.INTIMIDATE) - .enemyPassiveAbility(Abilities.HYDRATION) .ability(Abilities.INTIMIDATE) - .startingWave(3) + .moveset(Moves.SPLASH) .enemyMoveset(Moves.SPLASH); }); it("should lower ATK stat stage by 1 of enemy Pokemon on entry and player switch", async () => { - await game.classicMode.runToSummon([Species.MIGHTYENA, Species.POOCHYENA]); - game.onNextPrompt( - "CheckSwitchPhase", - UiMode.CONFIRM, - () => { - game.setMode(UiMode.MESSAGE); - game.endPhase(); - }, - () => game.isCurrentPhase("CommandPhase") || game.isCurrentPhase("TurnInitPhase"), - ); - await game.phaseInterceptor.to("CommandPhase", false); + await game.classicMode.startBattle([Species.MIGHTYENA, Species.POOCHYENA]); let playerPokemon = game.scene.getPlayerPokemon()!; const enemyPokemon = game.scene.getEnemyPokemon()!; @@ -55,28 +43,17 @@ describe("Abilities - Intimidate", () => { expect(playerPokemon.getStatStage(Stat.ATK)).toBe(-1); game.doSwitchPokemon(1); - await game.phaseInterceptor.run("CommandPhase"); - await game.phaseInterceptor.to("CommandPhase"); + await game.toNextTurn(); playerPokemon = game.scene.getPlayerPokemon()!; expect(playerPokemon.species.speciesId).toBe(Species.POOCHYENA); expect(playerPokemon.getStatStage(Stat.ATK)).toBe(0); expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-2); - }, 20000); + }); it("should lower ATK stat stage by 1 for every enemy Pokemon in a double battle on entry", async () => { - game.override.battleStyle("double").startingWave(3); - await game.classicMode.runToSummon([Species.MIGHTYENA, Species.POOCHYENA]); - game.onNextPrompt( - "CheckSwitchPhase", - UiMode.CONFIRM, - () => { - game.setMode(UiMode.MESSAGE); - game.endPhase(); - }, - () => game.isCurrentPhase("CommandPhase") || game.isCurrentPhase("TurnInitPhase"), - ); - await game.phaseInterceptor.to("CommandPhase", false); + game.override.battleStyle("double"); + await game.classicMode.startBattle([Species.MIGHTYENA, Species.POOCHYENA]); const playerField = game.scene.getPlayerField()!; const enemyField = game.scene.getEnemyField()!; @@ -85,11 +62,9 @@ describe("Abilities - Intimidate", () => { expect(enemyField[1].getStatStage(Stat.ATK)).toBe(-2); expect(playerField[0].getStatStage(Stat.ATK)).toBe(-2); expect(playerField[1].getStatStage(Stat.ATK)).toBe(-2); - }, 20000); + }); it("should not activate again if there is no switch or new entry", async () => { - game.override.startingWave(2); - game.override.moveset([Moves.SPLASH]); await game.classicMode.startBattle([Species.MIGHTYENA, Species.POOCHYENA]); const playerPokemon = game.scene.getPlayerPokemon()!; @@ -103,32 +78,42 @@ describe("Abilities - Intimidate", () => { expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-1); expect(playerPokemon.getStatStage(Stat.ATK)).toBe(-1); - }, 20000); + }); - it("should lower ATK stat stage by 1 for every switch", async () => { - game.override.moveset([Moves.SPLASH]).enemyMoveset([Moves.VOLT_SWITCH]).startingWave(5); - await game.classicMode.startBattle([Species.MIGHTYENA, Species.POOCHYENA]); + it("should NOT trigger on switching moves used by wild Pokemon", async () => { + game.override.enemyMoveset(Moves.VOLT_SWITCH).battleType(BattleType.WILD); + await game.classicMode.startBattle([Species.MIGHTYENA]); const playerPokemon = game.scene.getPlayerPokemon()!; - let enemyPokemon = game.scene.getEnemyPokemon()!; - - expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-1); expect(playerPokemon.getStatStage(Stat.ATK)).toBe(-1); - game.move.select(getMovePosition(game.scene, 0, Moves.SPLASH)); - await game.toNextTurn(); - - enemyPokemon = game.scene.getEnemyPokemon()!; - - expect(playerPokemon.getStatStage(Stat.ATK)).toBe(-2); - expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0); - game.move.select(Moves.SPLASH); await game.toNextTurn(); + // doesn't lower attack due to not actually switching out + expect(playerPokemon.getStatStage(Stat.ATK)).toBe(-1); + }); - enemyPokemon = game.scene.getEnemyPokemon()!; + it("should trigger on moves that switch user/target out during trainer battles", async () => { + game.override + .moveset([Moves.SPLASH, Moves.DRAGON_TAIL]) + .enemyMoveset([Moves.SPLASH, Moves.TELEPORT]) + .battleType(BattleType.TRAINER) + .startingWave(8) + .passiveAbility(Abilities.NO_GUARD); + await game.classicMode.startBattle([Species.MIGHTYENA]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + expect(playerPokemon.getStatStage(Stat.ATK)).toBe(-1); + + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.TELEPORT); + await game.toNextTurn(); + expect(playerPokemon.getStatStage(Stat.ATK)).toBe(-2); + + game.move.select(Moves.DRAGON_TAIL); + await game.forceEnemyMove(Moves.SPLASH); + await game.toNextTurn(); expect(playerPokemon.getStatStage(Stat.ATK)).toBe(-3); - expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0); - }, 200000); + }); }); diff --git a/test/moves/ignore-abilities.test.ts b/test/moves/ignore-abilities.test.ts new file mode 100644 index 00000000000..7c4bcf365fe --- /dev/null +++ b/test/moves/ignore-abilities.test.ts @@ -0,0 +1,105 @@ +import { BattlerIndex } from "#app/battle"; +import { RandomMoveAttr } from "#app/data/moves/move"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/testUtils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +describe("Moves - Ability Ignores", () => { + 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.MOONGEIST_BEAM, Moves.SUNSTEEL_STRIKE, Moves.PHOTON_GEYSER, Moves.METRONOME]) + .ability(Abilities.STURDY) + .startingLevel(200) + .battleStyle("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.STURDY) + .enemyMoveset(Moves.SPLASH); + }); + + it.each<{ name: string; move: Moves }>([ + { name: "Sunsteel Strike", move: Moves.SUNSTEEL_STRIKE }, + { name: "Moongeist Beam", move: Moves.MOONGEIST_BEAM }, + { name: "Photon Geyser", move: Moves.PHOTON_GEYSER }, + ])("$name should ignore enemy abilities during move use", async () => { + await game.classicMode.startBattle([Species.NECROZMA]); + + const player = game.scene.getPlayerPokemon()!; + const enemy = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.MOONGEIST_BEAM); + await game.phaseInterceptor.to("MoveEffectPhase"); + + expect(game.scene.arena.ignoreAbilities).toBe(true); + expect(game.scene.arena.ignoringEffectSource).toBe(player.getBattlerIndex()); + + await game.phaseInterceptor.to("TurnEndPhase"); + expect(game.scene.arena.ignoreAbilities).toBe(false); + expect(enemy.isFainted()).toBe(true); + }); + + it("should not ignore enemy abilities when called by metronome", async () => { + await game.classicMode.startBattle([Species.MILOTIC]); + vi.spyOn(RandomMoveAttr.prototype, "getMoveOverride").mockReturnValue(Moves.PHOTON_GEYSER); + + const enemy = game.scene.getEnemyPokemon()!; + game.move.select(Moves.METRONOME); + await game.phaseInterceptor.to("BerryPhase"); + + expect(enemy.isFainted()).toBe(false); + expect(game.scene.getPlayerPokemon()?.getLastXMoves()[0].move).toBe(Moves.PHOTON_GEYSER); + }); + + it("should not ignore enemy abilities when called by Mirror Move", async () => { + game.override.moveset(Moves.MIRROR_MOVE).enemyMoveset(Moves.SUNSTEEL_STRIKE); + + await game.classicMode.startBattle([Species.MILOTIC]); + + const enemy = game.scene.getEnemyPokemon()!; + game.move.select(Moves.MIRROR_MOVE); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + await game.phaseInterceptor.to("BerryPhase"); + + expect(enemy.isFainted()).toBe(false); + expect(game.scene.getPlayerPokemon()?.getLastXMoves()[0].move).toBe(Moves.SUNSTEEL_STRIKE); + }); + + it("should ignore enemy abilities when called by Instruct", async () => { + game.override.moveset([Moves.SUNSTEEL_STRIKE, Moves.INSTRUCT]).battleStyle("double"); + await game.classicMode.startBattle([Species.SOLGALEO, Species.LUNALA]); + + const solgaleo = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.SUNSTEEL_STRIKE, BattlerIndex.PLAYER, BattlerIndex.ENEMY); + game.move.select(Moves.INSTRUCT, BattlerIndex.PLAYER_2, BattlerIndex.PLAYER); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2]); + + await game.phaseInterceptor.to("MoveEffectPhase"); // initial attack + await game.phaseInterceptor.to("MoveEffectPhase"); // instruct + + expect(game.scene.arena.ignoreAbilities).toBe(true); + expect(game.scene.arena.ignoringEffectSource).toBe(solgaleo.getBattlerIndex()); + + await game.phaseInterceptor.to("BerryPhase"); + const [enemy1, enemy2] = game.scene.getEnemyField(); + expect(enemy1.isFainted()).toBe(true); + expect(enemy2.isFainted()).toBe(true); + }); +}); diff --git a/test/moves/moongeist_beam.test.ts b/test/moves/moongeist_beam.test.ts deleted file mode 100644 index 82a2567377b..00000000000 --- a/test/moves/moongeist_beam.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { allMoves, RandomMoveAttr } from "#app/data/moves/move"; -import { Abilities } from "#enums/abilities"; -import { Moves } from "#enums/moves"; -import { Species } from "#enums/species"; -import GameManager from "#test/testUtils/gameManager"; -import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; - -describe("Moves - Moongeist Beam", () => { - 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.MOONGEIST_BEAM, Moves.METRONOME]) - .ability(Abilities.BALL_FETCH) - .startingLevel(200) - .battleStyle("single") - .disableCrits() - .enemySpecies(Species.MAGIKARP) - .enemyAbility(Abilities.STURDY) - .enemyMoveset(Moves.SPLASH); - }); - - // Also covers Photon Geyser and Sunsteel Strike - it("should ignore enemy abilities", async () => { - await game.classicMode.startBattle([Species.MILOTIC]); - - const enemy = game.scene.getEnemyPokemon()!; - - game.move.select(Moves.MOONGEIST_BEAM); - await game.phaseInterceptor.to("BerryPhase"); - - expect(enemy.isFainted()).toBe(true); - }); - - // Also covers Photon Geyser and Sunsteel Strike - it("should not ignore enemy abilities when called by another move, such as metronome", async () => { - await game.classicMode.startBattle([Species.MILOTIC]); - vi.spyOn(allMoves[Moves.METRONOME].getAttrs(RandomMoveAttr)[0], "getMoveOverride").mockReturnValue( - Moves.MOONGEIST_BEAM, - ); - - game.move.select(Moves.METRONOME); - await game.phaseInterceptor.to("BerryPhase"); - - expect(game.scene.getEnemyPokemon()!.isFainted()).toBe(false); - expect(game.scene.getPlayerPokemon()!.getLastXMoves()[0].move).toBe(Moves.MOONGEIST_BEAM); - }); -}); From 69fbe2026e1b05e0d61c32decef8fa9f6488a5ba Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Tue, 20 May 2025 23:15:03 -0400 Subject: [PATCH 02/16] Added a few basic tests for Fishious Rend and Bolt Beak --- test/moves/first-attack-double-power.test.ts | 96 ++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 test/moves/first-attack-double-power.test.ts diff --git a/test/moves/first-attack-double-power.test.ts b/test/moves/first-attack-double-power.test.ts new file mode 100644 index 00000000000..d297f60d052 --- /dev/null +++ b/test/moves/first-attack-double-power.test.ts @@ -0,0 +1,96 @@ +import { BattlerIndex } from "#app/battle"; +import { allMoves } from "#app/data/moves/move"; +import { Abilities } from "#enums/abilities"; +import { BattleType } from "#enums/battle-type"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/testUtils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi, type MockInstance } from "vitest"; + +describe.each<{ name: string; move: Moves }>([ + { name: "Fishious Rend", move: Moves.FISHIOUS_REND }, + { name: "Bolt Beak", move: Moves.BOLT_BEAK }, +])("Moves - $name", ({ move }) => { + let phaserGame: Phaser.Game; + let game: GameManager; + let powerSpy: MockInstance; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .moveset(move) + .ability(Abilities.BALL_FETCH) + .battleStyle("single") + .battleType(BattleType.TRAINER) + .disableCrits() + .enemyLevel(100) + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + + powerSpy = vi.spyOn(allMoves[move], "calculateBattlePower"); + }); + + it("should double power when the user moves after the target", async () => { + await game.classicMode.startBattle([Species.FEEBAS]); + + // turn 1: enemy, then player (no boost) + game.move.select(move); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + await game.toNextTurn(); + + expect(powerSpy).toHaveLastReturnedWith(allMoves[move].power); + + // turn 2: player, then enemy (boost) + game.move.select(move); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + await game.toNextTurn(); + + expect(powerSpy).toHaveLastReturnedWith(allMoves[move].power * 2); + }); + + it("should double power on the turn the target switches in", async () => { + await game.classicMode.startBattle([Species.FEEBAS]); + + game.move.select(move); + game.forceEnemyToSwitch(); + await game.toNextTurn(); + + expect(powerSpy).toHaveLastReturnedWith(allMoves[move].power * 2); + }); + + // TODO: Verify behavior with Instruct/Dancer + it.todo.each<{ type: string; allyMove: Moves }>([ + { type: "a Dancer-induced", allyMove: Moves.FIERY_DANCE }, + { type: "an Instructed", allyMove: Moves.INSTRUCT }, + ])("should double power if $type move is used as the target's first action", async ({ allyMove }) => { + game.override.battleStyle("double").moveset([move, allyMove]).ability(Abilities.DANCER); + await game.classicMode.startBattle([Species.DRACOVISH, Species.ARCTOZOLT]); + + // Simulate enemy having used splash last turn to allow Instruct to copy it + const enemy = game.scene.getEnemyPokemon()!; + enemy.pushMoveHistory({ + move: Moves.SPLASH, + targets: [BattlerIndex.ENEMY], + turn: game.scene.currentBattle.turn - 1, + }); + + game.move.select(move, BattlerIndex.PLAYER, BattlerIndex.ENEMY); + game.move.select(allyMove, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY); + await game.setTurnOrder([BattlerIndex.PLAYER_2, BattlerIndex.PLAYER, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2]); + await game.toNextTurn(); + + expect(powerSpy).toHaveLastReturnedWith(allMoves[move].power); + }); +}); From 0d4086d7c26b9ddc1e9c6d6f13e4d1e437a8d58a Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Tue, 20 May 2025 23:22:57 -0400 Subject: [PATCH 03/16] Fixed comment --- test/moves/ignore-abilities.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/moves/ignore-abilities.test.ts b/test/moves/ignore-abilities.test.ts index 7c4bcf365fe..7bb932b23b0 100644 --- a/test/moves/ignore-abilities.test.ts +++ b/test/moves/ignore-abilities.test.ts @@ -98,6 +98,8 @@ describe("Moves - Ability Ignores", () => { expect(game.scene.arena.ignoringEffectSource).toBe(solgaleo.getBattlerIndex()); await game.phaseInterceptor.to("BerryPhase"); + + // Both the initial and redirected instruct use ignored sturdy const [enemy1, enemy2] = game.scene.getEnemyField(); expect(enemy1.isFainted()).toBe(true); expect(enemy2.isFainted()).toBe(true); From 0fa44d13a5cc4330f07636f926a15dcf0d6d5d3c Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Tue, 20 May 2025 23:27:32 -0400 Subject: [PATCH 04/16] Fixe test and added commetn --- test/moves/ignore-abilities.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/moves/ignore-abilities.test.ts b/test/moves/ignore-abilities.test.ts index 7bb932b23b0..c4fca5f9e28 100644 --- a/test/moves/ignore-abilities.test.ts +++ b/test/moves/ignore-abilities.test.ts @@ -81,6 +81,7 @@ describe("Moves - Ability Ignores", () => { expect(game.scene.getPlayerPokemon()?.getLastXMoves()[0].move).toBe(Moves.SUNSTEEL_STRIKE); }); + // TODO: Verify this behavior on cart it("should ignore enemy abilities when called by Instruct", async () => { game.override.moveset([Moves.SUNSTEEL_STRIKE, Moves.INSTRUCT]).battleStyle("double"); await game.classicMode.startBattle([Species.SOLGALEO, Species.LUNALA]); @@ -93,6 +94,7 @@ describe("Moves - Ability Ignores", () => { await game.phaseInterceptor.to("MoveEffectPhase"); // initial attack await game.phaseInterceptor.to("MoveEffectPhase"); // instruct + await game.phaseInterceptor.to("MoveEffectPhase"); // instructed move use expect(game.scene.arena.ignoreAbilities).toBe(true); expect(game.scene.arena.ignoringEffectSource).toBe(solgaleo.getBattlerIndex()); From 3f5d78266a4324e4579f6afffa22d6924980a8bc Mon Sep 17 00:00:00 2001 From: Bertie690 <136088738+Bertie690@users.noreply.github.com> Date: Tue, 20 May 2025 23:38:15 -0400 Subject: [PATCH 05/16] Update first-attack-double-power.test.ts --- test/moves/first-attack-double-power.test.ts | 25 +++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/test/moves/first-attack-double-power.test.ts b/test/moves/first-attack-double-power.test.ts index d297f60d052..4f93058955b 100644 --- a/test/moves/first-attack-double-power.test.ts +++ b/test/moves/first-attack-double-power.test.ts @@ -9,8 +9,8 @@ import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi, type MockInstance } from "vitest"; describe.each<{ name: string; move: Moves }>([ - { name: "Fishious Rend", move: Moves.FISHIOUS_REND }, { name: "Bolt Beak", move: Moves.BOLT_BEAK }, + { name: "Fishious Rend", move: Moves.FISHIOUS_REND }, ])("Moves - $name", ({ move }) => { let phaserGame: Phaser.Game; let game: GameManager; @@ -35,14 +35,14 @@ describe.each<{ name: string; move: Moves }>([ .battleType(BattleType.TRAINER) .disableCrits() .enemyLevel(100) - .enemySpecies(Species.MAGIKARP) + .enemySpecies(Species.DRACOVISH) .enemyAbility(Abilities.BALL_FETCH) .enemyMoveset(Moves.SPLASH); powerSpy = vi.spyOn(allMoves[move], "calculateBattlePower"); }); - it("should double power when the user moves after the target", async () => { + it("should double power if the user moves before the target", async () => { await game.classicMode.startBattle([Species.FEEBAS]); // turn 1: enemy, then player (no boost) @@ -54,7 +54,20 @@ describe.each<{ name: string; move: Moves }>([ // turn 2: player, then enemy (boost) game.move.select(move); - await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); + await game.toNextTurn(); + + expect(powerSpy).toHaveLastReturnedWith(allMoves[move].power * 2); + }); + + it("should only consider the selected target in Double Battles", async () => { + game.override.battleStyle("double").moveset([move, Moves.SPLASH]); + await game.classicMode.startBattle([Species.FEEBAS, Species.MILOTIC]); + + // Use move after everyone but P1 and enemy 1 have already moved + game.move.select(move, BattlerIndex.PLAYER, BattlerIndex.ENEMY); + game.move.select(Moves.SPLASH, BattlerIndex.PLAYER_2); + await game.setTurnOrder([BattlerIndex.PLAYER_2, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.toNextTurn(); expect(powerSpy).toHaveLastReturnedWith(allMoves[move].power * 2); @@ -74,8 +87,8 @@ describe.each<{ name: string; move: Moves }>([ it.todo.each<{ type: string; allyMove: Moves }>([ { type: "a Dancer-induced", allyMove: Moves.FIERY_DANCE }, { type: "an Instructed", allyMove: Moves.INSTRUCT }, - ])("should double power if $type move is used as the target's first action", async ({ allyMove }) => { - game.override.battleStyle("double").moveset([move, allyMove]).ability(Abilities.DANCER); + ])("should double power if $type move is used as the target's first action that turn", async ({ allyMove }) => { + game.override.battleStyle("double").moveset([move, allyMove]).enemyAbility(Abilities.DANCER); await game.classicMode.startBattle([Species.DRACOVISH, Species.ARCTOZOLT]); // Simulate enemy having used splash last turn to allow Instruct to copy it From a1890d78e02ff7c560f884f92d05e84f41f8a6a9 Mon Sep 17 00:00:00 2001 From: Bertie690 <136088738+Bertie690@users.noreply.github.com> Date: Tue, 20 May 2025 23:39:23 -0400 Subject: [PATCH 06/16] Fixed incorrect ignore-abilities.test.ts test suite description --- test/moves/ignore-abilities.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/moves/ignore-abilities.test.ts b/test/moves/ignore-abilities.test.ts index c4fca5f9e28..6d73fe68a40 100644 --- a/test/moves/ignore-abilities.test.ts +++ b/test/moves/ignore-abilities.test.ts @@ -7,7 +7,7 @@ import GameManager from "#test/testUtils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -describe("Moves - Ability Ignores", () => { +describe("Moves - Ability Ignores -", () => { let phaserGame: Phaser.Game; let game: GameManager; From 36135ea298c681ec7efa44f2f99005e08ee42125 Mon Sep 17 00:00:00 2001 From: Bertie690 <136088738+Bertie690@users.noreply.github.com> Date: Wed, 21 May 2025 15:44:30 -0400 Subject: [PATCH 07/16] Update ignore-abilities.test.ts suite name --- test/moves/ignore-abilities.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/moves/ignore-abilities.test.ts b/test/moves/ignore-abilities.test.ts index 6d73fe68a40..76154313184 100644 --- a/test/moves/ignore-abilities.test.ts +++ b/test/moves/ignore-abilities.test.ts @@ -7,7 +7,7 @@ import GameManager from "#test/testUtils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -describe("Moves - Ability Ignores -", () => { +describe("Moves - Ability-Ignoring Moves", () => { let phaserGame: Phaser.Game; let game: GameManager; From 00c93ef8ab9b56ef1531802e62ab9386b324b5b3 Mon Sep 17 00:00:00 2001 From: Bertie690 <136088738+Bertie690@users.noreply.github.com> Date: Thu, 29 May 2025 15:28:35 -0400 Subject: [PATCH 08/16] Fix first-attack-double-power.test.ts --- test/moves/first-attack-double-power.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/moves/first-attack-double-power.test.ts b/test/moves/first-attack-double-power.test.ts index 4f93058955b..f3bcc1a3eed 100644 --- a/test/moves/first-attack-double-power.test.ts +++ b/test/moves/first-attack-double-power.test.ts @@ -1,5 +1,5 @@ import { BattlerIndex } from "#app/battle"; -import { allMoves } from "#app/data/moves/move"; +import { allMoves } from "#app/data/data-lists.ts"; import { Abilities } from "#enums/abilities"; import { BattleType } from "#enums/battle-type"; import { Moves } from "#enums/moves"; @@ -59,7 +59,7 @@ describe.each<{ name: string; move: Moves }>([ expect(powerSpy).toHaveLastReturnedWith(allMoves[move].power * 2); }); - + it("should only consider the selected target in Double Battles", async () => { game.override.battleStyle("double").moveset([move, Moves.SPLASH]); await game.classicMode.startBattle([Species.FEEBAS, Species.MILOTIC]); From 3de7b87fc4748b75cdf690ce18099b7240a2bfd1 Mon Sep 17 00:00:00 2001 From: Bertie690 <136088738+Bertie690@users.noreply.github.com> Date: Thu, 29 May 2025 15:31:12 -0400 Subject: [PATCH 09/16] Actually fixed import --- test/moves/first-attack-double-power.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/moves/first-attack-double-power.test.ts b/test/moves/first-attack-double-power.test.ts index f3bcc1a3eed..a1e9a7f26af 100644 --- a/test/moves/first-attack-double-power.test.ts +++ b/test/moves/first-attack-double-power.test.ts @@ -1,5 +1,5 @@ import { BattlerIndex } from "#app/battle"; -import { allMoves } from "#app/data/data-lists.ts"; +import { allMoves } from "#app/data/data-lists"; import { Abilities } from "#enums/abilities"; import { BattleType } from "#enums/battle-type"; import { Moves } from "#enums/moves"; From 44d0e1d15a27169586a43eafdf7ac149356de2fe Mon Sep 17 00:00:00 2001 From: Bertie690 <136088738+Bertie690@users.noreply.github.com> Date: Tue, 3 Jun 2025 07:54:24 -0400 Subject: [PATCH 10/16] Update intimidate.test.ts --- test/abilities/intimidate.test.ts | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/test/abilities/intimidate.test.ts b/test/abilities/intimidate.test.ts index 2b35d81283e..640b303f5d4 100644 --- a/test/abilities/intimidate.test.ts +++ b/test/abilities/intimidate.test.ts @@ -94,25 +94,20 @@ describe("Abilities - Intimidate", () => { }); it("should trigger on moves that switch user/target out during trainer battles", async () => { - game.override - .moveset([Moves.SPLASH, Moves.DRAGON_TAIL]) - .enemyMoveset([Moves.SPLASH, Moves.TELEPORT]) - .battleType(BattleType.TRAINER) - .startingWave(8) - .passiveAbility(Abilities.NO_GUARD); + game.override.battleType(BattleType.TRAINER).passiveAbility(Abilities.NO_GUARD); await game.classicMode.startBattle([Species.MIGHTYENA]); const playerPokemon = game.scene.getPlayerPokemon()!; expect(playerPokemon.getStatStage(Stat.ATK)).toBe(-1); - game.move.select(Moves.SPLASH); - await game.forceEnemyMove(Moves.TELEPORT); + game.move.use(Moves.SPLASH); + await game.move.forceEnemyMove(Moves.TELEPORT); await game.toNextTurn(); expect(playerPokemon.getStatStage(Stat.ATK)).toBe(-2); - game.move.select(Moves.DRAGON_TAIL); - await game.forceEnemyMove(Moves.SPLASH); + game.move.use(Moves.DRAGON_TAIL); + await game.move.forceEnemyMove(Moves.SPLASH); await game.toNextTurn(); expect(playerPokemon.getStatStage(Stat.ATK)).toBe(-3); }); From 879a99852f0812951c8d5230923d7be9fe527a99 Mon Sep 17 00:00:00 2001 From: Bertie690 <136088738+Bertie690@users.noreply.github.com> Date: Fri, 13 Jun 2025 18:02:03 -0400 Subject: [PATCH 11/16] Fix test imprts --- test/moves/first-attack-double-power.test.ts | 42 ++++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/test/moves/first-attack-double-power.test.ts b/test/moves/first-attack-double-power.test.ts index a1e9a7f26af..4478fcba6e9 100644 --- a/test/moves/first-attack-double-power.test.ts +++ b/test/moves/first-attack-double-power.test.ts @@ -1,16 +1,16 @@ import { BattlerIndex } from "#app/battle"; import { allMoves } from "#app/data/data-lists"; -import { Abilities } from "#enums/abilities"; +import { AbilityId } from "#enums/ability-id"; import { BattleType } from "#enums/battle-type"; -import { Moves } from "#enums/moves"; -import { Species } from "#enums/species"; +import { MoveId } from "#enums/move-id"; +import { SpeciesIs } from "#enums/species-id"; import GameManager from "#test/testUtils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi, type MockInstance } from "vitest"; -describe.each<{ name: string; move: Moves }>([ - { name: "Bolt Beak", move: Moves.BOLT_BEAK }, - { name: "Fishious Rend", move: Moves.FISHIOUS_REND }, +describe.each<{ name: string; move: MoveId }>([ + { name: "Bolt Beak", move: MoveId.BOLT_BEAK }, + { name: "Fishious Rend", move: MoveId.FISHIOUS_REND }, ])("Moves - $name", ({ move }) => { let phaserGame: Phaser.Game; let game: GameManager; @@ -35,15 +35,15 @@ describe.each<{ name: string; move: Moves }>([ .battleType(BattleType.TRAINER) .disableCrits() .enemyLevel(100) - .enemySpecies(Species.DRACOVISH) - .enemyAbility(Abilities.BALL_FETCH) - .enemyMoveset(Moves.SPLASH); + .enemySpecies(SpeciesId.DRACOVISH) + .enemyAbility(AbilityId.BALL_FETCH) + .enemyMoveset(MoveId.SPLASH); powerSpy = vi.spyOn(allMoves[move], "calculateBattlePower"); }); it("should double power if the user moves before the target", async () => { - await game.classicMode.startBattle([Species.FEEBAS]); + await game.classicMode.startBattle([SpeciesId.FEEBAS]); // turn 1: enemy, then player (no boost) game.move.select(move); @@ -61,12 +61,12 @@ describe.each<{ name: string; move: Moves }>([ }); it("should only consider the selected target in Double Battles", async () => { - game.override.battleStyle("double").moveset([move, Moves.SPLASH]); - await game.classicMode.startBattle([Species.FEEBAS, Species.MILOTIC]); + game.override.battleStyle("double"); + await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.MILOTIC]); // Use move after everyone but P1 and enemy 1 have already moved - game.move.select(move, BattlerIndex.PLAYER, BattlerIndex.ENEMY); - game.move.select(Moves.SPLASH, BattlerIndex.PLAYER_2); + game.move.use(move, BattlerIndex.PLAYER, BattlerIndex.ENEMY); + game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER_2); await game.setTurnOrder([BattlerIndex.PLAYER_2, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.toNextTurn(); @@ -74,7 +74,7 @@ describe.each<{ name: string; move: Moves }>([ }); it("should double power on the turn the target switches in", async () => { - await game.classicMode.startBattle([Species.FEEBAS]); + await game.classicMode.startBattle([SpeciesId.FEEBAS]); game.move.select(move); game.forceEnemyToSwitch(); @@ -84,17 +84,17 @@ describe.each<{ name: string; move: Moves }>([ }); // TODO: Verify behavior with Instruct/Dancer - it.todo.each<{ type: string; allyMove: Moves }>([ - { type: "a Dancer-induced", allyMove: Moves.FIERY_DANCE }, - { type: "an Instructed", allyMove: Moves.INSTRUCT }, + it.todo.each<{ type: string; allyMove: MoveId }>([ + { type: "a Dancer-induced", allyMove: MoveId.FIERY_DANCE }, + { type: "an Instructed", allyMove: MoveId.INSTRUCT }, ])("should double power if $type move is used as the target's first action that turn", async ({ allyMove }) => { - game.override.battleStyle("double").moveset([move, allyMove]).enemyAbility(Abilities.DANCER); - await game.classicMode.startBattle([Species.DRACOVISH, Species.ARCTOZOLT]); + game.override.battleStyle("double").moveset([move, allyMove]).enemyAbility(AbilityId.DANCER); + await game.classicMode.startBattle([SpeciesIs.DRACOVISH, SpeciesId.ARCTOZOLT]); // Simulate enemy having used splash last turn to allow Instruct to copy it const enemy = game.scene.getEnemyPokemon()!; enemy.pushMoveHistory({ - move: Moves.SPLASH, + move: MoveId.SPLASH, targets: [BattlerIndex.ENEMY], turn: game.scene.currentBattle.turn - 1, }); From e02d07484fbc3be340d3719f6acd7aab000664e8 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Mon, 16 Jun 2025 16:58:52 -0400 Subject: [PATCH 12/16] Added guard dog tests --- test/abilities/guard-dog.test.ts | 39 ++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 test/abilities/guard-dog.test.ts diff --git a/test/abilities/guard-dog.test.ts b/test/abilities/guard-dog.test.ts new file mode 100644 index 00000000000..ee559a79239 --- /dev/null +++ b/test/abilities/guard-dog.test.ts @@ -0,0 +1,39 @@ +import { AbilityId } from "#enums/ability-id"; +import { SpeciesId } from "#enums/species-id"; +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("Ability - Guard Dog", () => { + 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 + .ability(AbilityId.GUARD_DOG) + .battleStyle("single") + .enemySpecies(SpeciesId.MAGIKARP) + .enemyAbility(AbilityId.INTIMIDATE); + }); + + it("should raise attack by 1 stage when Intimidated instead of being lowered", async () => { + await game.classicMode.startBattle([SpeciesId.MABOSSTIFF]); + + const mabostiff = game.field.getPlayerPokemon(); + expect(mabostiff.getStatStage(Stat.ATK)).toBe(1); + expect(mabostiff.waveData.abilitiesApplied.has(AbilityId.GUARD_DOG)).toBe(true); + expect(game.phaseInterceptor.log.filter(l => l === "StatStageChangePhase")).toHaveLength(1); + }); +}); From bb136fea81bf642de08bf47485697580de8572a4 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Mon, 16 Jun 2025 17:06:12 -0400 Subject: [PATCH 13/16] Fixed tests --- test/moves/first-attack-double-power.test.ts | 16 +++++++++------- test/moves/ignore-abilities.test.ts | 2 +- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/test/moves/first-attack-double-power.test.ts b/test/moves/first-attack-double-power.test.ts index 4478fcba6e9..af8f257d7c1 100644 --- a/test/moves/first-attack-double-power.test.ts +++ b/test/moves/first-attack-double-power.test.ts @@ -1,12 +1,13 @@ -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex } from "#enums/battler-index"; import { allMoves } from "#app/data/data-lists"; import { AbilityId } from "#enums/ability-id"; import { BattleType } from "#enums/battle-type"; import { MoveId } from "#enums/move-id"; -import { SpeciesIs } from "#enums/species-id"; +import { SpeciesId } from "#enums/species-id"; import GameManager from "#test/testUtils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi, type MockInstance } from "vitest"; +import { MoveUseMode } from "#enums/move-use-mode"; describe.each<{ name: string; move: MoveId }>([ { name: "Bolt Beak", move: MoveId.BOLT_BEAK }, @@ -30,10 +31,10 @@ describe.each<{ name: string; move: MoveId }>([ game = new GameManager(phaserGame); game.override .moveset(move) - .ability(Abilities.BALL_FETCH) + .ability(AbilityId.BALL_FETCH) .battleStyle("single") .battleType(BattleType.TRAINER) - .disableCrits() + .criticalHits(false) .enemyLevel(100) .enemySpecies(SpeciesId.DRACOVISH) .enemyAbility(AbilityId.BALL_FETCH) @@ -59,7 +60,7 @@ describe.each<{ name: string; move: MoveId }>([ expect(powerSpy).toHaveLastReturnedWith(allMoves[move].power * 2); }); - + it("should only consider the selected target in Double Battles", async () => { game.override.battleStyle("double"); await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.MILOTIC]); @@ -89,14 +90,15 @@ describe.each<{ name: string; move: MoveId }>([ { type: "an Instructed", allyMove: MoveId.INSTRUCT }, ])("should double power if $type move is used as the target's first action that turn", async ({ allyMove }) => { game.override.battleStyle("double").moveset([move, allyMove]).enemyAbility(AbilityId.DANCER); - await game.classicMode.startBattle([SpeciesIs.DRACOVISH, SpeciesId.ARCTOZOLT]); + await game.classicMode.startBattle([SpeciesId.DRACOVISH, SpeciesId.ARCTOZOLT]); // Simulate enemy having used splash last turn to allow Instruct to copy it - const enemy = game.scene.getEnemyPokemon()!; + const enemy = game.field.getEnemyPokemon(); enemy.pushMoveHistory({ move: MoveId.SPLASH, targets: [BattlerIndex.ENEMY], turn: game.scene.currentBattle.turn - 1, + useMode: MoveUseMode.NORMAL, }); game.move.select(move, BattlerIndex.PLAYER, BattlerIndex.ENEMY); diff --git a/test/moves/ignore-abilities.test.ts b/test/moves/ignore-abilities.test.ts index 181453c8966..3fa09f75948 100644 --- a/test/moves/ignore-abilities.test.ts +++ b/test/moves/ignore-abilities.test.ts @@ -28,7 +28,7 @@ describe("Moves - Ability-Ignoring Moves", () => { .ability(AbilityId.STURDY) .startingLevel(200) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.STURDY) .enemyMoveset(MoveId.SPLASH); From 312884a131d3ed811b26ce9418993505d49cc452 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Tue, 17 Jun 2025 08:16:53 -0400 Subject: [PATCH 14/16] Renamed test file --- ...gnore-abilities.test.ts => ability-ignore-moves.test.ts} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename test/moves/{ignore-abilities.test.ts => ability-ignore-moves.test.ts} (95%) diff --git a/test/moves/ignore-abilities.test.ts b/test/moves/ability-ignore-moves.test.ts similarity index 95% rename from test/moves/ignore-abilities.test.ts rename to test/moves/ability-ignore-moves.test.ts index 3fa09f75948..f29d32dea8e 100644 --- a/test/moves/ignore-abilities.test.ts +++ b/test/moves/ability-ignore-moves.test.ts @@ -25,7 +25,7 @@ describe("Moves - Ability-Ignoring Moves", () => { game = new GameManager(phaserGame); game.override .moveset([MoveId.MOONGEIST_BEAM, MoveId.SUNSTEEL_STRIKE, MoveId.PHOTON_GEYSER, MoveId.METRONOME]) - .ability(AbilityId.STURDY) + .ability(AbilityId.BALL_FETCH) .startingLevel(200) .battleStyle("single") .criticalHits(false) @@ -50,7 +50,7 @@ describe("Moves - Ability-Ignoring Moves", () => { expect(game.scene.arena.ignoreAbilities).toBe(true); expect(game.scene.arena.ignoringEffectSource).toBe(player.getBattlerIndex()); - await game.phaseInterceptor.to("TurnEndPhase"); + await game.toEndOfTurn(); expect(game.scene.arena.ignoreAbilities).toBe(false); expect(enemy.isFainted()).toBe(true); }); @@ -64,7 +64,7 @@ describe("Moves - Ability-Ignoring Moves", () => { await game.toEndOfTurn(); expect(enemy.isFainted()).toBe(false); - expect(game.scene.getPlayerPokemon()?.getLastXMoves()[0].move).toBe(MoveId.PHOTON_GEYSER); + expect(game.field.getPlayerPokemon().getLastXMoves()[0].move).toBe(MoveId.PHOTON_GEYSER); }); it("should not ignore enemy abilities when called by Mirror Move", async () => { From 9bb67df9e7d5389e10e70d30166ccd418be0bc67 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Tue, 17 Jun 2025 09:07:53 -0400 Subject: [PATCH 15/16] Added Payback tests --- src/data/moves/move.ts | 31 ++------- test/abilities/intimidate.test.ts | 45 +++---------- test/moves/first-attack-double-power.test.ts | 57 +++++++++------- test/moves/payback.test.ts | 68 ++++++++++++++++++++ 4 files changed, 116 insertions(+), 85 deletions(-) create mode 100644 test/moves/payback.test.ts diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index f61e8debc9f..4f5ba6ec59d 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -4073,30 +4073,6 @@ export class OpponentHighHpPowerAttr extends VariablePowerAttr { } } -/** - * Attribute to double this move's power if the target hasn't acted yet in the current turn. - * Used by {@linkcode Moves.BOLT_BEAK} and {@linkcode Moves.FISHIOUS_REND} - */ -export class FirstAttackDoublePowerAttr extends VariablePowerAttr { - /** - * Double this move's power if the user is acting before the target. - * @param user - Unused - * @param target - The {@linkcode Pokemon} being targeted by this move - * @param move - Unused - * @param args `[0]` - A {@linkcode NumberHolder} containing move base power - * @returns Whether the attribute was successfully applied - */ - apply(_user: Pokemon, target: Pokemon, move: Move, args: [NumberHolder]): boolean { - if (target.turnData.acted) { - return false; - } - - args[0].value *= 2; - return true; - } -} - - export class TurnDamagedDoublePowerAttr extends VariablePowerAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { if (user.turnData.attacksReceived.find(r => r.damage && r.sourceId === target.id)) { @@ -9579,7 +9555,8 @@ export function initMoves() { new AttackMove(MoveId.CLOSE_COMBAT, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 120, 100, 5, -1, 0, 4) .attr(StatStageChangeAttr, [ Stat.DEF, Stat.SPDEF ], -1, true), new AttackMove(MoveId.PAYBACK, PokemonType.DARK, MoveCategory.PHYSICAL, 50, 100, 10, -1, 0, 4) - .attr(MovePowerMultiplierAttr, (user, target, move) => target.getLastXMoves(1).find(m => m.turn === globalScene.currentBattle.turn) || globalScene.currentBattle.turnCommands[target.getBattlerIndex()]?.command === Command.BALL ? 2 : 1), + // Payback boosts power on enemy item use + .attr(MovePowerMultiplierAttr, (_user, target) => target.turnData.acted || globalScene.currentBattle.turnCommands[target.getBattlerIndex()]?.command === Command.BALL ? 2 : 1), new AttackMove(MoveId.ASSURANCE, PokemonType.DARK, MoveCategory.PHYSICAL, 60, 100, 10, -1, 0, 4) .attr(MovePowerMultiplierAttr, (user, target, move) => target.turnData.damageTaken > 0 ? 2 : 1), new StatusMove(MoveId.EMBARGO, PokemonType.DARK, 100, 15, -1, 0, 4) @@ -10791,9 +10768,9 @@ export function initMoves() { .condition(failIfGhostTypeCondition) .attr(AddBattlerTagAttr, BattlerTagType.OCTOLOCK, false, true, 1), new AttackMove(MoveId.BOLT_BEAK, PokemonType.ELECTRIC, MoveCategory.PHYSICAL, 85, 100, 10, -1, 0, 8) - .attr(FirstAttackDoublePowerAttr), + .attr(MovePowerMultiplierAttr, (_user, target) => target.turnData.acted ? 1 : 2), new AttackMove(MoveId.FISHIOUS_REND, PokemonType.WATER, MoveCategory.PHYSICAL, 85, 100, 10, -1, 0, 8) - .attr(FirstAttackDoublePowerAttr) + .attr(MovePowerMultiplierAttr, (_user, target) => target.turnData.acted ? 1 : 2) .bitingMove(), new StatusMove(MoveId.COURT_CHANGE, PokemonType.NORMAL, 100, 10, -1, 0, 8) .attr(SwapArenaTagsAttr, [ ArenaTagType.AURORA_VEIL, ArenaTagType.LIGHT_SCREEN, ArenaTagType.MIST, ArenaTagType.REFLECT, ArenaTagType.SPIKES, ArenaTagType.STEALTH_ROCK, ArenaTagType.STICKY_WEB, ArenaTagType.TAILWIND, ArenaTagType.TOXIC_SPIKES ]), diff --git a/test/abilities/intimidate.test.ts b/test/abilities/intimidate.test.ts index 7d8c8cf2eb3..e0a5bf533eb 100644 --- a/test/abilities/intimidate.test.ts +++ b/test/abilities/intimidate.test.ts @@ -32,65 +32,40 @@ describe("Abilities - Intimidate", () => { .enemyMoveset(MoveId.SPLASH); }); - it("should lower all non-immune opponents ATK stat stage by 1 of enemy Pokemon on entry and player switch", async () => { + it("should lower all opponents' ATK by 1 stage on entry and switch", async () => { await game.classicMode.startBattle([SpeciesId.MIGHTYENA, SpeciesId.POOCHYENA]); - const player = game.field.getPlayerPokemon() - const enemy = game.field.getEnemyPokemon() - - expect(player.species.speciesId).toBe(SpeciesId.MIGHTYENA); + const enemy = game.field.getEnemyPokemon(); expect(enemy.getStatStage(Stat.ATK)).toBe(-1); - expect(player.getStatStage(Stat.ATK)).toBe(-1); game.doSwitchPokemon(1); await game.toNextTurn(); - expect(game.field.getPlayerPokemon()).toBe(player); - expect(player.getStatStage(Stat.ATK)).toBe(0); expect(enemy.getStatStage(Stat.ATK)).toBe(-2); }); - it("should lower ATK stat stage by 1 for every enemy Pokemon in a double battle on entry", async () => { + it("should lower ATK of all opponents in a double battle", async () => { game.override.battleStyle("double"); - await game.classicMode.startBattle([SpeciesId.MIGHTYENA, SpeciesId.POOCHYENA]); + await game.classicMode.startBattle([SpeciesId.MIGHTYENA]); - const playerField = game.scene.getPlayerField(); - const enemyField = game.scene.getEnemyField(); + const [enemy1, enemy2] = game.scene.getEnemyField(); - expect(enemyField[0].getStatStage(Stat.ATK)).toBe(-2); - expect(enemyField[1].getStatStage(Stat.ATK)).toBe(-2); - expect(playerField[0].getStatStage(Stat.ATK)).toBe(-2); - expect(playerField[1].getStatStage(Stat.ATK)).toBe(-2); - }); - - it("should not activate again if there is no switch or new entry", async () => { - await game.classicMode.startBattle([SpeciesId.MIGHTYENA, SpeciesId.POOCHYENA]); - - const player = game.field.getPlayerPokemon() - const enemy = game.field.getEnemyPokemon() - - expect(player.getStatStage(Stat.ATK)).toBe(-1); - expect(enemy.getStatStage(Stat.ATK)).toBe(-1); - - game.move.select(MoveId.SPLASH); - await game.toNextTurn(); - - expect(player.getStatStage(Stat.ATK)).toBe(-1); - expect(enemy.getStatStage(Stat.ATK)).toBe(-1); + expect(enemy1.getStatStage(Stat.ATK)).toBe(-1); + expect(enemy2.getStatStage(Stat.ATK)).toBe(-1); }); it("should not trigger on switching moves used by wild Pokemon", async () => { game.override.enemyMoveset(MoveId.VOLT_SWITCH).battleType(BattleType.WILD); await game.classicMode.startBattle([SpeciesId.MIGHTYENA]); - const playerPokemon = game.field.getPlayerPokemon() - expect(playerPokemon.getStatStage(Stat.ATK)).toBe(-1); + const player = game.field.getPlayerPokemon(); + expect(player.getStatStage(Stat.ATK)).toBe(-1); game.move.select(MoveId.SPLASH); await game.toNextTurn(); // doesn't lower attack due to not actually switching out - expect(playerPokemon.getStatStage(Stat.ATK)).toBe(-1); + expect(player.getStatStage(Stat.ATK)).toBe(-1); }); it("should trigger on moves that switch user/target out during trainer battles", async () => { diff --git a/test/moves/first-attack-double-power.test.ts b/test/moves/first-attack-double-power.test.ts index af8f257d7c1..97c2327e1df 100644 --- a/test/moves/first-attack-double-power.test.ts +++ b/test/moves/first-attack-double-power.test.ts @@ -9,10 +9,7 @@ import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi, type MockInstance } from "vitest"; import { MoveUseMode } from "#enums/move-use-mode"; -describe.each<{ name: string; move: MoveId }>([ - { name: "Bolt Beak", move: MoveId.BOLT_BEAK }, - { name: "Fishious Rend", move: MoveId.FISHIOUS_REND }, -])("Moves - $name", ({ move }) => { +describe("Moves - Fishious Rend & Bolt Beak", () => { let phaserGame: Phaser.Game; let game: GameManager; let powerSpy: MockInstance; @@ -30,7 +27,6 @@ describe.each<{ name: string; move: MoveId }>([ beforeEach(() => { game = new GameManager(phaserGame); game.override - .moveset(move) .ability(AbilityId.BALL_FETCH) .battleStyle("single") .battleType(BattleType.TRAINER) @@ -40,23 +36,27 @@ describe.each<{ name: string; move: MoveId }>([ .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH); - powerSpy = vi.spyOn(allMoves[move], "calculateBattlePower"); + powerSpy = vi.spyOn(allMoves[MoveId.BOLT_BEAK], "calculateBattlePower"); }); - it("should double power if the user moves before the target", async () => { + it.each<{ name: string; move: MoveId }>([ + { name: "Bolt Beak", move: MoveId.BOLT_BEAK }, + { name: "Fishious Rend", move: MoveId.FISHIOUS_REND }, + ])("$name should double power if the user moves before the target", async ({ move }) => { + powerSpy = vi.spyOn(allMoves[move], "calculateBattlePower"); await game.classicMode.startBattle([SpeciesId.FEEBAS]); // turn 1: enemy, then player (no boost) - game.move.select(move); + game.move.use(move); await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.toNextTurn(); expect(powerSpy).toHaveLastReturnedWith(allMoves[move].power); // turn 2: player, then enemy (boost) - game.move.select(move); + game.move.use(move); await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); - await game.toNextTurn(); + await game.toEndOfTurn(); expect(powerSpy).toHaveLastReturnedWith(allMoves[move].power * 2); }); @@ -66,30 +66,41 @@ describe.each<{ name: string; move: MoveId }>([ await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.MILOTIC]); // Use move after everyone but P1 and enemy 1 have already moved - game.move.use(move, BattlerIndex.PLAYER, BattlerIndex.ENEMY); + game.move.use(MoveId.BOLT_BEAK, BattlerIndex.PLAYER, BattlerIndex.ENEMY); game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER_2); await game.setTurnOrder([BattlerIndex.PLAYER_2, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER, BattlerIndex.ENEMY]); - await game.toNextTurn(); + await game.toEndOfTurn(); - expect(powerSpy).toHaveLastReturnedWith(allMoves[move].power * 2); + expect(powerSpy).toHaveLastReturnedWith(allMoves[MoveId.BOLT_BEAK].power * 2); }); it("should double power on the turn the target switches in", async () => { await game.classicMode.startBattle([SpeciesId.FEEBAS]); - game.move.select(move); + game.move.select(MoveId.BOLT_BEAK); game.forceEnemyToSwitch(); - await game.toNextTurn(); + await game.toEndOfTurn(); - expect(powerSpy).toHaveLastReturnedWith(allMoves[move].power * 2); + expect(powerSpy).toHaveLastReturnedWith(allMoves[MoveId.BOLT_BEAK].power * 2); }); - // TODO: Verify behavior with Instruct/Dancer - it.todo.each<{ type: string; allyMove: MoveId }>([ + it("should double power on forced switch-induced sendouts", async () => { + await game.classicMode.startBattle([SpeciesId.FEEBAS]); + + game.move.select(MoveId.BOLT_BEAK); + await game.move.forceEnemyMove(MoveId.U_TURN); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + await game.toEndOfTurn(); + + expect(powerSpy).toHaveLastReturnedWith(allMoves[MoveId.BOLT_BEAK].power * 2); + }); + + it.each<{ type: string; allyMove: MoveId }>([ { type: "a Dancer-induced", allyMove: MoveId.FIERY_DANCE }, { type: "an Instructed", allyMove: MoveId.INSTRUCT }, ])("should double power if $type move is used as the target's first action that turn", async ({ allyMove }) => { - game.override.battleStyle("double").moveset([move, allyMove]).enemyAbility(AbilityId.DANCER); + game.override.battleStyle("double").enemyAbility(AbilityId.DANCER); + powerSpy = vi.spyOn(allMoves[MoveId.FISHIOUS_REND], "calculateBattlePower"); await game.classicMode.startBattle([SpeciesId.DRACOVISH, SpeciesId.ARCTOZOLT]); // Simulate enemy having used splash last turn to allow Instruct to copy it @@ -101,11 +112,11 @@ describe.each<{ name: string; move: MoveId }>([ useMode: MoveUseMode.NORMAL, }); - game.move.select(move, BattlerIndex.PLAYER, BattlerIndex.ENEMY); - game.move.select(allyMove, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY); + game.move.use(MoveId.FISHIOUS_REND, BattlerIndex.PLAYER, BattlerIndex.ENEMY); + game.move.use(allyMove, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY); await game.setTurnOrder([BattlerIndex.PLAYER_2, BattlerIndex.PLAYER, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2]); - await game.toNextTurn(); + await game.toEndOfTurn(); - expect(powerSpy).toHaveLastReturnedWith(allMoves[move].power); + expect(powerSpy).toHaveLastReturnedWith(allMoves[MoveId.FISHIOUS_REND].power); }); }); diff --git a/test/moves/payback.test.ts b/test/moves/payback.test.ts new file mode 100644 index 00000000000..95ccb8c5496 --- /dev/null +++ b/test/moves/payback.test.ts @@ -0,0 +1,68 @@ +import { allMoves, allSpecies } from "#app/data/data-lists"; +import { AbilityId } from "#enums/ability-id"; +import { BattlerIndex } from "#enums/battler-index"; +import { MoveId } from "#enums/move-id"; +import { SpeciesId } from "#enums/species-id"; +import GameManager from "#test/testUtils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi, type MockInstance } from "vitest"; + +describe("Move - Payback", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let powerSpy: MockInstance; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .ability(AbilityId.BALL_FETCH) + .battleStyle("single") + .criticalHits(false) + .enemyLevel(100) + .enemySpecies(SpeciesId.DRACOVISH) + .enemyAbility(AbilityId.BALL_FETCH) + .enemyMoveset(MoveId.SPLASH); + + powerSpy = vi.spyOn(allMoves[MoveId.PAYBACK], "calculateBattlePower"); + }); + + it("should double power if the user moves after the target", async () => { + await game.classicMode.startBattle([SpeciesId.FEEBAS]); + + // turn 1: enemy, then player (boost) + game.move.use(MoveId.PAYBACK); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + await game.toNextTurn(); + + expect(powerSpy).toHaveLastReturnedWith(allMoves[MoveId.PAYBACK].power * 2); + + // turn 2: player, then enemy (no boost) + game.move.use(MoveId.PAYBACK); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); + await game.toEndOfTurn(); + + expect(powerSpy).toHaveLastReturnedWith(allMoves[MoveId.PAYBACK].power); + }); + + it("should trigger for enemies on player failed ball catch", async () => { + // Make dracovish uncatchable so we don't flake out + vi.spyOn(allSpecies[SpeciesId.DRACOVISH], "catchRate", "get").mockReturnValue(0); + await game.classicMode.startBattle([SpeciesId.FEEBAS]); + + game.doThrowPokeball(0); + await game.move.forceEnemyMove(MoveId.PAYBACK); + await game.toEndOfTurn(); + + expect(powerSpy).toHaveLastReturnedWith(allMoves[MoveId.PAYBACK].power * 2); + }); +}); From d6c9209c4299905ceabe81dba34e23689e000fa8 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Tue, 17 Jun 2025 16:30:07 -0400 Subject: [PATCH 16/16] Fixed accidental unusde class --- src/data/moves/move.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 4f5ba6ec59d..cd0a1862232 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -8252,7 +8252,6 @@ const MoveAttrs = Object.freeze({ CompareWeightPowerAttr, HpPowerAttr, OpponentHighHpPowerAttr, - FirstAttackDoublePowerAttr, TurnDamagedDoublePowerAttr, MagnitudePowerAttr, AntiSunlightPowerDecreaseAttr,