From 9b4495c16296392cbb303d413e0ab580d7ec4a41 Mon Sep 17 00:00:00 2001 From: Roman Merlick Date: Fri, 25 Apr 2025 15:49:25 -0500 Subject: [PATCH 1/5] Created Double Edge testing for recoil damage on substitutes --- test/moves/double_edge.test.ts | 77 ++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 test/moves/double_edge.test.ts diff --git a/test/moves/double_edge.test.ts b/test/moves/double_edge.test.ts new file mode 100644 index 00000000000..87443ba50f3 --- /dev/null +++ b/test/moves/double_edge.test.ts @@ -0,0 +1,77 @@ +import { Stat } from "#enums/stat"; +import { EnemyCommandPhase } from "#app/phases/enemy-command-phase"; +import { TurnEndPhase } from "#app/phases/turn-end-phase"; +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 } from "vitest"; + +describe("Moves - DOUBLE_EDGE", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + const moveToUse = Moves.DOUBLE_EDGE; + game.override.battleStyle("single"); + game.override.enemySpecies(Species.MAGIKARP); + game.override.startingLevel(1); + game.override.startingWave(97); + game.override.moveset([moveToUse]); + game.override.enemyMoveset([Moves.GROWTH, Moves.GROWTH, Moves.GROWTH, Moves.GROWTH]); + game.override.disableCrits(); + }); + + it("DOUBLE_EDGE against ghost", async () => { + const moveToUse = Moves.DOUBLE_EDGE; + game.override.enemySpecies(Species.GENGAR); + await game.classicMode.startBattle([Species.MIGHTYENA]); + const hpOpponent = game.scene.currentBattle.enemyParty[0].hp; + game.move.select(moveToUse); + await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(TurnEndPhase); + const hpLost = hpOpponent - game.scene.currentBattle.enemyParty[0].hp; + expect(hpLost).toBe(0); + }, 20000); + + it("DOUBLE_EDGE against not resistant", async () => { + const moveToUse = Moves.DOUBLE_EDGE; + await game.classicMode.startBattle([Species.MIGHTYENA]); + game.scene.currentBattle.enemyParty[0].stats[Stat.DEF] = 50; + game.scene.getPlayerParty()[0].stats[Stat.ATK] = 50; + + const hpOpponent = game.scene.currentBattle.enemyParty[0].hp; + + game.move.select(moveToUse); + await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(TurnEndPhase); + const hpLost = hpOpponent - game.scene.currentBattle.enemyParty[0].hp; + expect(hpLost).toBeGreaterThan(0); + expect(hpLost).toBeLessThan(8); + }, 20000); + + it("DOUBLE_EDGE against SUBSTITUTE does recoil", async () => { + const moveToUse = Moves.DOUBLE_EDGE; + game.override.enemySpecies(Species.PIDOVE); + game.override.enemyMoveset([Moves.SUBSTITUTE, Moves.SUBSTITUTE, Moves.SUBSTITUTE, Moves.SUBSTITUTE]); + await game.classicMode.startBattle([Species.TOGEPI]); + game.scene.currentBattle.enemyParty[0].stats[Stat.DEF] = 50; + game.scene.getPlayerParty()[0].stats[Stat.ATK] = 50; + + const hpTotal = game.scene.getPlayerParty()[0].stats[Stat.HP]; + + game.move.select(moveToUse); + await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(TurnEndPhase); + const hpLost = hpTotal - game.scene.getPlayerParty()[0].stats[Stat.HP]; + expect(hpLost).toBeGreaterThan(0); + }, 20000); +}); From ddde9356708cdd35c055363baa87ef0bdc85213b Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 25 Apr 2025 22:54:13 -0500 Subject: [PATCH 2/5] Changed the logic for substitute damage --- src/phases/move-effect-phase.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index 01085834ba5..4d0b8f050ed 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -844,6 +844,11 @@ export class MoveEffectPhase extends PokemonPhase { const isBlockedBySubstitute = substitute && this.move.hitsSubstitute(user, target); if (isBlockedBySubstitute) { substitute.hp -= dmg; + if (substitute.hp >= dmg) { + user.turnData.totalDamageDealt += dmg; + } else { + user.turnData.totalDamageDealt += substitute.hp; + } } else if (!target.isPlayer() && dmg >= target.hp) { globalScene.applyModifiers(EnemyEndureChanceModifier, false, target); } From a03257d557b089bedbb800305457b648b6c176fc Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 25 Apr 2025 22:56:19 -0500 Subject: [PATCH 3/5] Made testing for the updated substitute logic --- test/moves/double_edge.test.ts | 77 ------------------------------ test/moves/recoil-moves.test.ts | 83 +++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 77 deletions(-) delete mode 100644 test/moves/double_edge.test.ts create mode 100644 test/moves/recoil-moves.test.ts diff --git a/test/moves/double_edge.test.ts b/test/moves/double_edge.test.ts deleted file mode 100644 index 87443ba50f3..00000000000 --- a/test/moves/double_edge.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { Stat } from "#enums/stat"; -import { EnemyCommandPhase } from "#app/phases/enemy-command-phase"; -import { TurnEndPhase } from "#app/phases/turn-end-phase"; -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 } from "vitest"; - -describe("Moves - DOUBLE_EDGE", () => { - let phaserGame: Phaser.Game; - let game: GameManager; - - beforeAll(() => { - phaserGame = new Phaser.Game({ - type: Phaser.HEADLESS, - }); - }); - - afterEach(() => { - game.phaseInterceptor.restoreOg(); - }); - - beforeEach(() => { - game = new GameManager(phaserGame); - const moveToUse = Moves.DOUBLE_EDGE; - game.override.battleStyle("single"); - game.override.enemySpecies(Species.MAGIKARP); - game.override.startingLevel(1); - game.override.startingWave(97); - game.override.moveset([moveToUse]); - game.override.enemyMoveset([Moves.GROWTH, Moves.GROWTH, Moves.GROWTH, Moves.GROWTH]); - game.override.disableCrits(); - }); - - it("DOUBLE_EDGE against ghost", async () => { - const moveToUse = Moves.DOUBLE_EDGE; - game.override.enemySpecies(Species.GENGAR); - await game.classicMode.startBattle([Species.MIGHTYENA]); - const hpOpponent = game.scene.currentBattle.enemyParty[0].hp; - game.move.select(moveToUse); - await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(TurnEndPhase); - const hpLost = hpOpponent - game.scene.currentBattle.enemyParty[0].hp; - expect(hpLost).toBe(0); - }, 20000); - - it("DOUBLE_EDGE against not resistant", async () => { - const moveToUse = Moves.DOUBLE_EDGE; - await game.classicMode.startBattle([Species.MIGHTYENA]); - game.scene.currentBattle.enemyParty[0].stats[Stat.DEF] = 50; - game.scene.getPlayerParty()[0].stats[Stat.ATK] = 50; - - const hpOpponent = game.scene.currentBattle.enemyParty[0].hp; - - game.move.select(moveToUse); - await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(TurnEndPhase); - const hpLost = hpOpponent - game.scene.currentBattle.enemyParty[0].hp; - expect(hpLost).toBeGreaterThan(0); - expect(hpLost).toBeLessThan(8); - }, 20000); - - it("DOUBLE_EDGE against SUBSTITUTE does recoil", async () => { - const moveToUse = Moves.DOUBLE_EDGE; - game.override.enemySpecies(Species.PIDOVE); - game.override.enemyMoveset([Moves.SUBSTITUTE, Moves.SUBSTITUTE, Moves.SUBSTITUTE, Moves.SUBSTITUTE]); - await game.classicMode.startBattle([Species.TOGEPI]); - game.scene.currentBattle.enemyParty[0].stats[Stat.DEF] = 50; - game.scene.getPlayerParty()[0].stats[Stat.ATK] = 50; - - const hpTotal = game.scene.getPlayerParty()[0].stats[Stat.HP]; - - game.move.select(moveToUse); - await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(TurnEndPhase); - const hpLost = hpTotal - game.scene.getPlayerParty()[0].stats[Stat.HP]; - expect(hpLost).toBeGreaterThan(0); - }, 20000); -}); diff --git a/test/moves/recoil-moves.test.ts b/test/moves/recoil-moves.test.ts new file mode 100644 index 00000000000..fae6bd4d40c --- /dev/null +++ b/test/moves/recoil-moves.test.ts @@ -0,0 +1,83 @@ +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 } from "vitest"; +import { Abilities } from "#enums/abilities"; + +describe("Moves - Recoil Moves", () => { + 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.battleStyle("single"); + game.override.enemySpecies(Species.PIDOVE); + game.override.startingLevel(1); + game.override.enemyLevel(100); + game.override.enemyMoveset([Moves.SUBSTITUTE, Moves.SUBSTITUTE, Moves.SUBSTITUTE, Moves.SUBSTITUTE]); + game.override.disableCrits(); + }); + + describe.each([ + { moveName: "Double Edge", moveId: Moves.DOUBLE_EDGE }, + { moveName: "Brave Bird", moveId: Moves.BRAVE_BIRD }, + { moveName: "Flare Blitz", moveId: Moves.FLARE_BLITZ }, + { moveName: "Head Charge", moveId: Moves.HEAD_CHARGE }, + { moveName: "Head Smash", moveId: Moves.HEAD_SMASH }, + { moveName: "Light of Ruin", moveId: Moves.LIGHT_OF_RUIN }, + { moveName: "Struggle", moveId: Moves.STRUGGLE }, + { moveName: "Submission", moveId: Moves.SUBMISSION }, + { moveName: "Take Down", moveId: Moves.TAKE_DOWN }, + { moveName: "Volt Tackle", moveId: Moves.VOLT_TACKLE }, + { moveName: "Wave Crash", moveId: Moves.WAVE_CRASH }, + { moveName: "Wild Charge", moveId: Moves.WILD_CHARGE }, + { moveName: "Wood Hammer", moveId: Moves.WOOD_HAMMER }, + ])("Moves - $moveName", ({ moveId }) => { + it("against SUBSTITUTE does recoil", async () => { + game.override.ability(Abilities.NO_GUARD); + await game.classicMode.startBattle([Species.TOGEPI]); + + game.override.moveset([moveId]); + game.move.select(moveId); + await game.toNextTurn(); + + const playerPokemon = game.scene.getPlayerPokemon()!; + expect(playerPokemon.hp).toBeLessThan(playerPokemon.getMaxHp()); + }); + }); + + it("against SUBSTITUTE recoils properly in double battles", async () => { + game.override.battleStyle("double"); + game.override.enemySpecies(Species.PIDOVE); + game.override.startingLevel(1); + game.override.enemyLevel(100); + game.override.enemyMoveset([Moves.SUBSTITUTE]); + game.override.disableCrits(); + game.override.ability(Abilities.NO_GUARD); + await game.classicMode.startBattle([Species.TOGEPI, Species.TOGEPI]); + game.override.moveset([Moves.DOUBLE_EDGE]); + game.move.select(Moves.DOUBLE_EDGE, 0); + game.move.select(Moves.DOUBLE_EDGE, 1); + await game.forceEnemyMove(Moves.SUBSTITUTE, 0); + await game.forceEnemyMove(Moves.SUBSTITUTE, 1); + await game.phaseInterceptor.to("TurnEndPhase", false); + await await game.toNextTurn(); + + console.log(game.scene.getPlayerParty()[0].hp); + console.log(game.scene.getPlayerParty()[1].hp); + + const playerPokemon = game.scene.getPlayerPokemon()!; + expect(playerPokemon.hp).toBeLessThan(playerPokemon.getMaxHp()); + }); +}); From 2e2719fe4bc19f181be7e47add8d872ef4db7942 Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Fri, 25 Apr 2025 22:01:47 -0700 Subject: [PATCH 4/5] Update test formatting --- test/moves/recoil-moves.test.ts | 74 ++++++++++++++++----------------- 1 file changed, 35 insertions(+), 39 deletions(-) diff --git a/test/moves/recoil-moves.test.ts b/test/moves/recoil-moves.test.ts index fae6bd4d40c..1ceba4b39f7 100644 --- a/test/moves/recoil-moves.test.ts +++ b/test/moves/recoil-moves.test.ts @@ -21,15 +21,18 @@ describe("Moves - Recoil Moves", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleStyle("single"); - game.override.enemySpecies(Species.PIDOVE); - game.override.startingLevel(1); - game.override.enemyLevel(100); - game.override.enemyMoveset([Moves.SUBSTITUTE, Moves.SUBSTITUTE, Moves.SUBSTITUTE, Moves.SUBSTITUTE]); - game.override.disableCrits(); + game.override + .battleStyle("single") + .enemySpecies(Species.PIDOVE) + .startingLevel(1) + .enemyLevel(100) + .enemyMoveset(Moves.SUBSTITUTE) + .disableCrits() + .ability(Abilities.NO_GUARD) + .enemyAbility(Abilities.BALL_FETCH); }); - describe.each([ + it.each([ { moveName: "Double Edge", moveId: Moves.DOUBLE_EDGE }, { moveName: "Brave Bird", moveId: Moves.BRAVE_BIRD }, { moveName: "Flare Blitz", moveId: Moves.FLARE_BLITZ }, @@ -43,41 +46,34 @@ describe("Moves - Recoil Moves", () => { { moveName: "Wave Crash", moveId: Moves.WAVE_CRASH }, { moveName: "Wild Charge", moveId: Moves.WILD_CHARGE }, { moveName: "Wood Hammer", moveId: Moves.WOOD_HAMMER }, - ])("Moves - $moveName", ({ moveId }) => { - it("against SUBSTITUTE does recoil", async () => { - game.override.ability(Abilities.NO_GUARD); - await game.classicMode.startBattle([Species.TOGEPI]); + ])("$moveName causes recoil damage when hitting a substitute", async ({ moveId }) => { + game.override.moveset([moveId]); + await game.classicMode.startBattle([Species.TOGEPI]); - game.override.moveset([moveId]); - game.move.select(moveId); - await game.toNextTurn(); - - const playerPokemon = game.scene.getPlayerPokemon()!; - expect(playerPokemon.hp).toBeLessThan(playerPokemon.getMaxHp()); - }); - }); - - it("against SUBSTITUTE recoils properly in double battles", async () => { - game.override.battleStyle("double"); - game.override.enemySpecies(Species.PIDOVE); - game.override.startingLevel(1); - game.override.enemyLevel(100); - game.override.enemyMoveset([Moves.SUBSTITUTE]); - game.override.disableCrits(); - game.override.ability(Abilities.NO_GUARD); - await game.classicMode.startBattle([Species.TOGEPI, Species.TOGEPI]); - game.override.moveset([Moves.DOUBLE_EDGE]); - game.move.select(Moves.DOUBLE_EDGE, 0); - game.move.select(Moves.DOUBLE_EDGE, 1); - await game.forceEnemyMove(Moves.SUBSTITUTE, 0); - await game.forceEnemyMove(Moves.SUBSTITUTE, 1); - await game.phaseInterceptor.to("TurnEndPhase", false); - await await game.toNextTurn(); - - console.log(game.scene.getPlayerParty()[0].hp); - console.log(game.scene.getPlayerParty()[1].hp); + game.move.select(moveId); + await game.toNextTurn(); const playerPokemon = game.scene.getPlayerPokemon()!; expect(playerPokemon.hp).toBeLessThan(playerPokemon.getMaxHp()); }); + + it("causes recoil damage when hitting a substitute in a double battle", async () => { + game.override.battleStyle("double").moveset([Moves.DOUBLE_EDGE]); + + await game.classicMode.startBattle([Species.TOGEPI, Species.TOGEPI]); + + const [playerPokemon1, playerPokemon2] = game.scene.getPlayerField(); + + game.move.select(Moves.DOUBLE_EDGE, 0); + game.move.select(Moves.DOUBLE_EDGE, 1); + + await game.phaseInterceptor.to("TurnEndPhase", false); + await game.toNextTurn(); + + console.log(playerPokemon1.hp); + console.log(playerPokemon2.hp); + + expect(playerPokemon1.hp).toBeLessThan(playerPokemon1.getMaxHp()); + expect(playerPokemon2.hp).toBeLessThan(playerPokemon2.getMaxHp()); + }); }); From b607994f0922be24472a082d5ce17a64ea465c6e Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 8 May 2025 15:23:14 -0500 Subject: [PATCH 5/5] Fixed error in damage logic --- src/phases/move-effect-phase.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index 53728c2c80c..d0c44bd0643 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -840,12 +840,12 @@ export class MoveEffectPhase extends PokemonPhase { const substitute = target.getTag(SubstituteTag); const isBlockedBySubstitute = substitute && this.move.hitsSubstitute(user, target); if (isBlockedBySubstitute) { - substitute.hp -= dmg; if (substitute.hp >= dmg) { user.turnData.totalDamageDealt += dmg; } else { user.turnData.totalDamageDealt += substitute.hp; } + substitute.hp -= dmg; } else if (!target.isPlayer() && dmg >= target.hp) { globalScene.applyModifiers(EnemyEndureChanceModifier, false, target); }