From e29c08ba1f1017312536742da512f55387eb0f2f Mon Sep 17 00:00:00 2001 From: Greenlamp2 <44787002+Greenlamp2@users.noreply.github.com> Date: Mon, 10 Jun 2024 16:10:23 +0200 Subject: [PATCH] [Test] Add Tests for Zen Mode Ability (#1978) * added tests for zen mode - change form in battle * added tests to run some battle against trainer/rival & boss * added a test with a method to kill a pokemon * added an override in the clock mocked to reduce the time of fainting pokemon and thus reducing test time from 5s to less than 1s * added some more tests + doAttack, doKillOpponents, toNextWave, toNextTurn helper * added some more tests + doAttack, doKillOpponents, toNextWave, toNextTurn helper + fix some tests --- src/phases.ts | 8 + src/test/abilities/zen_mode.test.ts | 142 ++++++++++++++++++ src/test/battle/battle.test.ts | 76 +++++++++- src/test/battle/error-handling.test.ts | 24 +-- src/test/battle/special_battle.test.ts | 137 +++++++++++++++++ src/test/utils/TextInterceptor.ts | 2 +- src/test/utils/gameManager.ts | 86 +++++++++-- src/test/utils/gameWrapper.ts | 9 +- src/test/utils/mocks/mockClock.ts | 7 + .../utils/mocks/mocksContainer/mockSprite.ts | 1 + src/test/utils/phaseInterceptor.ts | 65 ++++++-- 11 files changed, 516 insertions(+), 41 deletions(-) create mode 100644 src/test/abilities/zen_mode.test.ts create mode 100644 src/test/battle/special_battle.test.ts diff --git a/src/phases.ts b/src/phases.ts index d5e64ebae2d..e039bf43cf7 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -1079,6 +1079,10 @@ export class NextEncounterPhase extends EncounterPhase { super(scene); } + start() { + super.start(); + } + doEncounter(): void { this.scene.playBgm(undefined, true); @@ -1497,6 +1501,10 @@ export class SwitchSummonPhase extends SummonPhase { this.batonPass = batonPass; } + start(): void { + super.start(); + } + preSummon(): void { if (!this.player) { if (this.slotIndex === -1) { diff --git a/src/test/abilities/zen_mode.test.ts b/src/test/abilities/zen_mode.test.ts new file mode 100644 index 00000000000..76bc3231f8a --- /dev/null +++ b/src/test/abilities/zen_mode.test.ts @@ -0,0 +1,142 @@ +import {afterEach, beforeAll, beforeEach, describe, expect, it, vi} from "vitest"; +import Phaser from "phaser"; +import GameManager from "#app/test/utils/gameManager"; +import * as overrides from "#app/overrides"; +import {Abilities} from "#app/data/enums/abilities"; +import {Species} from "#app/data/enums/species"; +import { + CommandPhase, + DamagePhase, + EnemyCommandPhase, + MessagePhase, + PostSummonPhase, + SwitchPhase, + SwitchSummonPhase, + TurnEndPhase, TurnInitPhase, + TurnStartPhase, +} from "#app/phases"; +import {Mode} from "#app/ui/ui"; +import {Stat} from "#app/data/pokemon-stat"; +import {Moves} from "#app/data/enums/moves"; +import {getMovePosition} from "#app/test/utils/gameManagerUtils"; +import {Command} from "#app/ui/command-ui-handler"; +import {QuietFormChangePhase} from "#app/form-change-phase"; + + +describe("Abilities - Zen mode", () => { + 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.SPLASH; + vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.RATTATA); + vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.HYDRATION); + vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.ZEN_MODE); + vi.spyOn(overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(100); + vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([moveToUse]); + vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE,Moves.TACKLE,Moves.TACKLE,Moves.TACKLE]); + }); + + it("ZEN MODE - not enough damage to change form", async() => { + const moveToUse = Moves.SPLASH; + await game.startBattle([ + Species.DARMANITAN, + ]); + game.scene.getParty()[0].stats[Stat.SPD] = 1; + game.scene.getParty()[0].stats[Stat.HP] = 100; + game.scene.getParty()[0].hp = 100; + expect(game.scene.getParty()[0].formIndex).toBe(0); + + game.onNextPrompt("CommandPhase", Mode.COMMAND, () => { + game.scene.ui.setMode(Mode.FIGHT, (game.scene.getCurrentPhase() as CommandPhase).getFieldIndex()); + }); + game.onNextPrompt("CommandPhase", Mode.FIGHT, () => { + const movePosition = getMovePosition(game.scene, 0, moveToUse); + (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, movePosition, false); + }); + await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(DamagePhase, false); + // await game.phaseInterceptor.runFrom(DamagePhase).to(DamagePhase, false); + const damagePhase = game.scene.getCurrentPhase() as DamagePhase; + damagePhase.updateAmount(40); + await game.phaseInterceptor.runFrom(DamagePhase).to(TurnEndPhase, false); + expect(game.scene.getParty()[0].hp).toBeLessThan(100); + expect(game.scene.getParty()[0].formIndex).toBe(0); + }, 20000); + + it("ZEN MODE - enough damage to change form", async() => { + const moveToUse = Moves.SPLASH; + await game.startBattle([ + Species.DARMANITAN, + ]); + game.scene.getParty()[0].stats[Stat.SPD] = 1; + game.scene.getParty()[0].stats[Stat.HP] = 1000; + game.scene.getParty()[0].hp = 100; + expect(game.scene.getParty()[0].formIndex).toBe(0); + + game.onNextPrompt("CommandPhase", Mode.COMMAND, () => { + game.scene.ui.setMode(Mode.FIGHT, (game.scene.getCurrentPhase() as CommandPhase).getFieldIndex()); + }); + game.onNextPrompt("CommandPhase", Mode.FIGHT, () => { + const movePosition = getMovePosition(game.scene, 0, moveToUse); + (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, movePosition, false); + }); + await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(QuietFormChangePhase); + await game.phaseInterceptor.to(TurnInitPhase, false); + expect(game.scene.getParty()[0].hp).not.toBe(100); + expect(game.scene.getParty()[0].formIndex).not.toBe(0); + }, 20000); + + it("ZEN MODE - kill pokemon while on zen mode", async() => { + const moveToUse = Moves.SPLASH; + await game.startBattle([ + Species.DARMANITAN, + Species.CHARIZARD, + ]); + game.scene.getParty()[0].stats[Stat.SPD] = 1; + game.scene.getParty()[0].stats[Stat.HP] = 1000; + game.scene.getParty()[0].hp = 100; + expect(game.scene.getParty()[0].formIndex).toBe(0); + + game.onNextPrompt("CommandPhase", Mode.COMMAND, () => { + game.scene.ui.setMode(Mode.FIGHT, (game.scene.getCurrentPhase() as CommandPhase).getFieldIndex()); + }); + game.onNextPrompt("CommandPhase", Mode.FIGHT, () => { + const movePosition = getMovePosition(game.scene, 0, moveToUse); + (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, movePosition, false); + }); + await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(DamagePhase, false); + // await game.phaseInterceptor.runFrom(DamagePhase).to(DamagePhase, false); + const damagePhase = game.scene.getCurrentPhase() as DamagePhase; + damagePhase.updateAmount(80); + await game.phaseInterceptor.runFrom(DamagePhase).to(QuietFormChangePhase); + expect(game.scene.getParty()[0].hp).not.toBe(100); + expect(game.scene.getParty()[0].formIndex).not.toBe(0); + await game.killPokemon(game.scene.getParty()[0]); + expect(game.scene.getParty()[0].isFainted()).toBe(true); + await game.phaseInterceptor.run(MessagePhase); + await game.phaseInterceptor.run(EnemyCommandPhase); + await game.phaseInterceptor.run(TurnStartPhase); + game.onNextPrompt("SwitchPhase", Mode.PARTY, () => { + game.scene.unshiftPhase(new SwitchSummonPhase(game.scene, 0, 1, false, false)); + game.scene.ui.setMode(Mode.MESSAGE); + }); + game.onNextPrompt("SwitchPhase", Mode.MESSAGE, () => { + game.endPhase(); + }); + await game.phaseInterceptor.run(SwitchPhase); + await game.phaseInterceptor.to(PostSummonPhase); + expect(game.scene.getParty()[1].formIndex).toBe(1); + }, 20000); +}); diff --git a/src/test/battle/battle.test.ts b/src/test/battle/battle.test.ts index 28f98d936ce..52d014a74a5 100644 --- a/src/test/battle/battle.test.ts +++ b/src/test/battle/battle.test.ts @@ -6,7 +6,7 @@ import {Species} from "#app/data/enums/species"; import * as overrides from "../../overrides"; import {Command} from "#app/ui/command-ui-handler"; import { - CommandPhase, + CommandPhase, DamagePhase, EncounterPhase, EnemyCommandPhase, LoginPhase, @@ -15,7 +15,7 @@ import { SelectStarterPhase, SummonPhase, TitlePhase, - TurnInitPhase, + TurnInitPhase, VictoryPhase, } from "#app/phases"; import {Moves} from "#app/data/enums/moves"; import GameManager from "#app/test/utils/gameManager"; @@ -106,9 +106,7 @@ describe("Test Battle Phase", () => { const movePosition = getMovePosition(game.scene, 0, Moves.TACKLE); (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, movePosition, false); }); - await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(SelectModifierPhase); - expect(game.scene.ui?.getMode()).toBe(Mode.MODIFIER_SELECT); - expect(game.scene.getCurrentPhase().constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(SelectModifierPhase, false); }, 20000); it("do attack wave 3 - single battle - regular - NO OHKO with opponent using non damage attack", async() => { @@ -128,7 +126,7 @@ describe("Test Battle Phase", () => { const movePosition = getMovePosition(game.scene, 0, Moves.TACKLE); (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, movePosition, false); }); - await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(TurnInitPhase); + await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(TurnInitPhase, false); }, 20000); it("load 100% data file", async() => { @@ -259,5 +257,71 @@ describe("Test Battle Phase", () => { expect(game.scene.ui?.getMode()).toBe(Mode.COMMAND); expect(game.scene.getCurrentPhase().constructor.name).toBe(CommandPhase.name); }, 20000); + + it("kill opponent pokemon", async() => { + const moveToUse = Moves.SPLASH; + vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + vi.spyOn(overrides, "STARTER_SPECIES_OVERRIDE", "get").mockReturnValue(Species.MEWTWO); + vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.RATTATA); + vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.HYDRATION); + vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.ZEN_MODE); + vi.spyOn(overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(2000); + vi.spyOn(overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(3); + vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([moveToUse]); + vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE,Moves.TACKLE,Moves.TACKLE,Moves.TACKLE]); + await game.startBattle([ + Species.DARMANITAN, + Species.CHARIZARD, + ]); + + game.onNextPrompt("CommandPhase", Mode.COMMAND, () => { + game.scene.ui.setMode(Mode.FIGHT, (game.scene.getCurrentPhase() as CommandPhase).getFieldIndex()); + }); + game.onNextPrompt("CommandPhase", Mode.FIGHT, () => { + const movePosition = getMovePosition(game.scene, 0, moveToUse); + (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, movePosition, false); + }); + await game.phaseInterceptor.to(DamagePhase, false); + await game.killPokemon(game.scene.currentBattle.enemyParty[0]); + expect(game.scene.currentBattle.enemyParty[0].isFainted()).toBe(true); + await game.phaseInterceptor.to(VictoryPhase, false); + }, 200000); + + it("to next turn", async() => { + const moveToUse = Moves.SPLASH; + vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + vi.spyOn(overrides, "STARTER_SPECIES_OVERRIDE", "get").mockReturnValue(Species.MEWTWO); + vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.RATTATA); + vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.HYDRATION); + vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.ZEN_MODE); + vi.spyOn(overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(2000); + vi.spyOn(overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(3); + vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([moveToUse]); + vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE,Moves.TACKLE,Moves.TACKLE,Moves.TACKLE]); + await game.startBattle(); + const turn = game.scene.currentBattle.turn; + await game.doAttack(0); + await game.toNextTurn(); + expect(game.scene.currentBattle.turn).toBeGreaterThan(turn); + }, 20000); + + it("to next wave with pokemon killed, single", async() => { + const moveToUse = Moves.SPLASH; + vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + vi.spyOn(overrides, "STARTER_SPECIES_OVERRIDE", "get").mockReturnValue(Species.MEWTWO); + vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.RATTATA); + vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.HYDRATION); + vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.ZEN_MODE); + vi.spyOn(overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(2000); + vi.spyOn(overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(3); + vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([moveToUse]); + vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE,Moves.TACKLE,Moves.TACKLE,Moves.TACKLE]); + await game.startBattle(); + const waveIndex = game.scene.currentBattle.waveIndex; + await game.doAttack(0); + await game.doKillOpponents(); + await game.toNextWave(); + expect(game.scene.currentBattle.waveIndex).toBeGreaterThan(waveIndex); + }, 20000); }); diff --git a/src/test/battle/error-handling.test.ts b/src/test/battle/error-handling.test.ts index 2cb29aab1e7..0f661149dd3 100644 --- a/src/test/battle/error-handling.test.ts +++ b/src/test/battle/error-handling.test.ts @@ -1,4 +1,4 @@ -import {afterEach, beforeAll, beforeEach, describe, it, vi} from "vitest"; +import {afterEach, beforeAll, beforeEach, describe, expect, it, vi} from "vitest"; import GameManager from "#app/test/utils/gameManager"; import Phaser from "phaser"; import * as overrides from "#app/overrides"; @@ -22,18 +22,24 @@ describe("Test Battle Phase", () => { beforeEach(() => { game = new GameManager(phaserGame); - }); - - it("should start phase", async() => { + const moveToUse = Moves.SPLASH; + vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); vi.spyOn(overrides, "STARTER_SPECIES_OVERRIDE", "get").mockReturnValue(Species.MEWTWO); vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.RATTATA); + vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.HYDRATION); + vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.ZEN_MODE); vi.spyOn(overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(2000); vi.spyOn(overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(3); - vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE]); - vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.HYDRATION); - vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE, Moves.TACKLE, Moves.TACKLE, Moves.TACKLE]); - vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([moveToUse]); + vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE,Moves.TACKLE,Moves.TACKLE,Moves.TACKLE]); + }); + + it.skip("to next turn", async() => { await game.startBattle(); - }, 100000); + const turn = game.scene.currentBattle.turn; + await game.doAttack(0); + await game.toNextTurn(); + expect(game.scene.currentBattle.turn).toBeGreaterThan(turn); + }, 20000); }); diff --git a/src/test/battle/special_battle.test.ts b/src/test/battle/special_battle.test.ts new file mode 100644 index 00000000000..5a95d57afea --- /dev/null +++ b/src/test/battle/special_battle.test.ts @@ -0,0 +1,137 @@ +import {afterEach, beforeAll, beforeEach, describe, expect, it, vi} from "vitest"; +import {Mode} from "#app/ui/ui"; +import {Species} from "#app/data/enums/species"; +import * as overrides from "../../overrides"; +import { + CommandPhase, +} from "#app/phases"; +import {Moves} from "#app/data/enums/moves"; +import GameManager from "#app/test/utils/gameManager"; +import Phaser from "phaser"; +import {Abilities} from "#app/data/enums/abilities"; + +describe("Test Battle Phase", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.RATTATA); + vi.spyOn(overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(2000); + vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE]); + vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.HYDRATION); + vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.HYDRATION); + vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE, Moves.TACKLE, Moves.TACKLE, Moves.TACKLE]); + }); + + it("startBattle 2vs1 boss", async() => { + vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + vi.spyOn(overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(10); + await game.startBattle([ + Species.BLASTOISE, + Species.CHARIZARD, + ]); + expect(game.scene.ui?.getMode()).toBe(Mode.COMMAND); + expect(game.scene.getCurrentPhase().constructor.name).toBe(CommandPhase.name); + }, 20000); + + it("startBattle 2vs2 boss", async() => { + vi.spyOn(overrides, "DOUBLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + vi.spyOn(overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(10); + await game.startBattle([ + Species.BLASTOISE, + Species.CHARIZARD, + ]); + expect(game.scene.ui?.getMode()).toBe(Mode.COMMAND); + expect(game.scene.getCurrentPhase().constructor.name).toBe(CommandPhase.name); + }, 20000); + + it("startBattle 2vs2 trainer", async() => { + vi.spyOn(overrides, "DOUBLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + vi.spyOn(overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(5); + await game.startBattle([ + Species.BLASTOISE, + Species.CHARIZARD, + ]); + expect(game.scene.ui?.getMode()).toBe(Mode.COMMAND); + expect(game.scene.getCurrentPhase().constructor.name).toBe(CommandPhase.name); + }, 20000); + + it("startBattle 2vs1 trainer", async() => { + vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + vi.spyOn(overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(5); + await game.startBattle([ + Species.BLASTOISE, + Species.CHARIZARD, + ]); + expect(game.scene.ui?.getMode()).toBe(Mode.COMMAND); + expect(game.scene.getCurrentPhase().constructor.name).toBe(CommandPhase.name); + }, 20000); + + it("startBattle 2vs1 rival", async() => { + vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + vi.spyOn(overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(8); + await game.startBattle([ + Species.BLASTOISE, + Species.CHARIZARD, + ]); + expect(game.scene.ui?.getMode()).toBe(Mode.COMMAND); + expect(game.scene.getCurrentPhase().constructor.name).toBe(CommandPhase.name); + }, 20000); + + it("startBattle 2vs2 rival", async() => { + vi.spyOn(overrides, "DOUBLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + vi.spyOn(overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(8); + await game.startBattle([ + Species.BLASTOISE, + Species.CHARIZARD, + ]); + expect(game.scene.ui?.getMode()).toBe(Mode.COMMAND); + expect(game.scene.getCurrentPhase().constructor.name).toBe(CommandPhase.name); + }, 20000); + + it("startBattle 1vs1 trainer", async() => { + vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + vi.spyOn(overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(5); + await game.startBattle([ + Species.BLASTOISE, + ]); + expect(game.scene.ui?.getMode()).toBe(Mode.COMMAND); + expect(game.scene.getCurrentPhase().constructor.name).toBe(CommandPhase.name); + }, 20000); + + it("startBattle 2vs2 trainer", async() => { + vi.spyOn(overrides, "DOUBLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + vi.spyOn(overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(5); + await game.startBattle([ + Species.BLASTOISE, + Species.CHARIZARD, + ]); + expect(game.scene.ui?.getMode()).toBe(Mode.COMMAND); + expect(game.scene.getCurrentPhase().constructor.name).toBe(CommandPhase.name); + }, 20000); + + it("startBattle 4vs2 trainer", async() => { + vi.spyOn(overrides, "DOUBLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + vi.spyOn(overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(5); + await game.startBattle([ + Species.BLASTOISE, + Species.CHARIZARD, + Species.DARKRAI, + Species.GABITE, + ]); + expect(game.scene.ui?.getMode()).toBe(Mode.COMMAND); + expect(game.scene.getCurrentPhase().constructor.name).toBe(CommandPhase.name); + }, 20000); +}); + diff --git a/src/test/utils/TextInterceptor.ts b/src/test/utils/TextInterceptor.ts index d3048f23f74..e767953f4af 100644 --- a/src/test/utils/TextInterceptor.ts +++ b/src/test/utils/TextInterceptor.ts @@ -11,6 +11,6 @@ export default class TextInterceptor { } getLatestMessage(): string { - return this.logs[this.logs.length - 1]; + return this.logs.pop(); } } diff --git a/src/test/utils/gameManager.ts b/src/test/utils/gameManager.ts index dc1991d5659..c00b766bc85 100644 --- a/src/test/utils/gameManager.ts +++ b/src/test/utils/gameManager.ts @@ -3,19 +3,20 @@ import {Mode} from "#app/ui/ui"; import {generateStarter, waitUntil} from "#app/test/utils/gameManagerUtils"; import { CommandPhase, + DamagePhase, EncounterPhase, - LoginPhase, - PostSummonPhase, + FaintPhase, + LoginPhase, NewBattlePhase, SelectGenderPhase, SelectStarterPhase, - TitlePhase, + TitlePhase, TurnInitPhase, } from "#app/phases"; import BattleScene from "#app/battle-scene.js"; import PhaseInterceptor from "#app/test/utils/phaseInterceptor"; import TextInterceptor from "#app/test/utils/TextInterceptor"; import {GameModes, getGameMode} from "#app/game-mode"; import fs from "fs"; -import { AES, enc } from "crypto-js"; +import {AES, enc} from "crypto-js"; import {updateUserInfo} from "#app/account"; import {Species} from "#app/data/enums/species"; import {PlayerGender} from "#app/data/enums/player-gender"; @@ -23,6 +24,11 @@ import {GameDataType} from "#app/data/enums/game-data-type"; import InputsHandler from "#app/test/utils/inputsHandler"; import {ExpNotification} from "#app/enums/exp-notification"; import ErrorInterceptor from "#app/test/utils/errorInterceptor"; +import {EnemyPokemon, PlayerPokemon} from "#app/field/pokemon"; +import {MockClock} from "#app/test/utils/mocks/mockClock"; +import {Command} from "#app/ui/command-ui-handler"; +import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; +import {Button} from "#app/enums/buttons"; /** * Class to manage the game state and transitions between phases. @@ -84,8 +90,8 @@ export default class GameManager { * @param callback - The callback to execute. * @param expireFn - Optional function to determine if the prompt has expired. */ - onNextPrompt(phaseTarget: string, mode: Mode, callback: () => void, expireFn?: () => void) { - this.phaseInterceptor.addToNextPrompt(phaseTarget, mode, callback, expireFn); + onNextPrompt(phaseTarget: string, mode: Mode, callback: () => void, expireFn?: () => void, awaitingActionInput: boolean = false) { + this.phaseInterceptor.addToNextPrompt(phaseTarget, mode, callback, expireFn, awaitingActionInput); } /** @@ -142,17 +148,68 @@ export default class GameManager { this.onNextPrompt("CheckSwitchPhase", Mode.CONFIRM, () => { this.setMode(Mode.MESSAGE); this.endPhase(); - }, () => this.isCurrentPhase(CommandPhase)); + }, () => this.isCurrentPhase(CommandPhase) || this.isCurrentPhase(TurnInitPhase)); this.onNextPrompt("CheckSwitchPhase", Mode.CONFIRM, () => { this.setMode(Mode.MESSAGE); this.endPhase(); - }, () => this.isCurrentPhase(CommandPhase)); - await this.phaseInterceptor.runFrom(PostSummonPhase).to(CommandPhase).catch((e) => reject(e)); + }, () => this.isCurrentPhase(CommandPhase) || this.isCurrentPhase(TurnInitPhase)); + await this.phaseInterceptor.to(CommandPhase).catch((e) => reject(e)); console.log("==================[New Turn]=================="); return resolve(); }); } + doAttack(moveIndex: integer): Promise { + this.onNextPrompt("CommandPhase", Mode.COMMAND, () => { + this.scene.ui.setMode(Mode.FIGHT, (this.scene.getCurrentPhase() as CommandPhase).getFieldIndex()); + }); + this.onNextPrompt("CommandPhase", Mode.FIGHT, () => { + (this.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, moveIndex, false); + }); + return this.phaseInterceptor.to(DamagePhase); + } + + doKillOpponents() { + return new Promise(async(resolve, reject) => { + await this.killPokemon(this.scene.currentBattle.enemyParty[0]).catch((e) => reject(e)); + if (this.scene.currentBattle.double) { + await this.killPokemon(this.scene.currentBattle.enemyParty[1]).catch((e) => reject(e)); + } + return resolve(); + }); + } + + toNextTurn(): Promise { + return new Promise(async(resolve, reject) => { + await this.phaseInterceptor.to(CommandPhase).catch((e) => reject(e)); + return resolve(); + }); + } + + toNextWave(): Promise { + return new Promise(async(resolve, reject) => { + this.onNextPrompt("SelectModifierPhase", Mode.MODIFIER_SELECT, () => { + const handler = this.scene.ui.getHandler() as ModifierSelectUiHandler; + handler.processInput(Button.CANCEL); + }, () => this.isCurrentPhase(CommandPhase) || this.isCurrentPhase(NewBattlePhase), true); + this.onNextPrompt("SelectModifierPhase", Mode.CONFIRM, () => { + const handler = this.scene.ui.getHandler() as ModifierSelectUiHandler; + handler.processInput(Button.ACTION); + }, () => this.isCurrentPhase(CommandPhase) || this.isCurrentPhase(NewBattlePhase)); + this.onNextPrompt("CheckSwitchPhase", Mode.CONFIRM, () => { + this.setMode(Mode.MESSAGE); + this.endPhase(); + }, () => this.isCurrentPhase(TurnInitPhase)); + this.onNextPrompt("CheckSwitchPhase", Mode.CONFIRM, () => { + this.setMode(Mode.MESSAGE); + this.endPhase(); + }, () => this.isCurrentPhase(TurnInitPhase)); + await this.phaseInterceptor.to(CommandPhase).catch((e) => reject(e)); + + return resolve(); + }); + } + /** * Checks if the player has won the battle. * @returns True if the player has won, otherwise false. @@ -213,4 +270,15 @@ export default class GameManager { } return updateUserInfo(); } + + async killPokemon(pokemon: PlayerPokemon | EnemyPokemon) { + (this.scene.time as MockClock).overrideDelay = 0.01; + return new Promise(async(resolve, reject) => { + pokemon.hp = 0; + this.scene.pushPhase(new FaintPhase(this.scene, pokemon.getBattlerIndex(), true)); + await this.phaseInterceptor.to(FaintPhase).catch((e) => reject(e)); + (this.scene.time as MockClock).overrideDelay = undefined; + resolve(); + }); + } } diff --git a/src/test/utils/gameWrapper.ts b/src/test/utils/gameWrapper.ts index 7d0e4110351..da54471a7e3 100644 --- a/src/test/utils/gameWrapper.ts +++ b/src/test/utils/gameWrapper.ts @@ -120,7 +120,7 @@ export default class GameWrapper { pause: () => null, setRate: () => null, add: () => this.scene.sound, - get: () => this.scene.sound, + get: () => ({...this.scene.sound, totalDuration: 0}), getAllPlaying: () => [], manager: { game: this.game, @@ -131,6 +131,13 @@ export default class GameWrapper { key: "", }; + this.scene.cameras = { + main: { + setPostPipeline: () => null, + removePostPipeline: () => null, + }, + } + this.scene.tweens = { add: (data) => { if (data.onComplete) { diff --git a/src/test/utils/mocks/mockClock.ts b/src/test/utils/mocks/mockClock.ts index 0d5ea68ed59..ba12dc002cc 100644 --- a/src/test/utils/mocks/mockClock.ts +++ b/src/test/utils/mocks/mockClock.ts @@ -2,8 +2,10 @@ import Clock = Phaser.Time.Clock; export class MockClock extends Clock { + public overrideDelay: number; constructor(scene) { super(scene); + this.overrideDelay = undefined; setInterval(() => { /* To simulate frame update @@ -14,4 +16,9 @@ export class MockClock extends Clock { this.update(this.systems.game.loop.time, 100); }, 100); } + + addEvent(config: Phaser.Time.TimerEvent | Phaser.Types.Time.TimerEventConfig): Phaser.Time.TimerEvent { + const cfg = { ...config, delay: this.overrideDelay || config.delay}; + return super.addEvent(cfg); + } } diff --git a/src/test/utils/mocks/mocksContainer/mockSprite.ts b/src/test/utils/mocks/mocksContainer/mockSprite.ts index 30effe185ad..699dea31ad5 100644 --- a/src/test/utils/mocks/mocksContainer/mockSprite.ts +++ b/src/test/utils/mocks/mocksContainer/mockSprite.ts @@ -143,6 +143,7 @@ export default class MockSprite { play() { // return this.phaserSprite.play(); + return this; } setPipelineData(key, value) { diff --git a/src/test/utils/phaseInterceptor.ts b/src/test/utils/phaseInterceptor.ts index 49e67e8448c..9e7745c8a37 100644 --- a/src/test/utils/phaseInterceptor.ts +++ b/src/test/utils/phaseInterceptor.ts @@ -1,17 +1,43 @@ import { BattleEndPhase, BerryPhase, - CheckSwitchPhase, CommandPhase, DamagePhase, EggLapsePhase, - EncounterPhase, EnemyCommandPhase, FaintPhase, - LoginPhase, MessagePhase, MoveEffectPhase, MoveEndPhase, MovePhase, NewBattlePhase, NextEncounterPhase, + CheckSwitchPhase, + CommandPhase, + DamagePhase, + EggLapsePhase, + EncounterPhase, + EnemyCommandPhase, + FaintPhase, + LoginPhase, + MessagePhase, + MoveEffectPhase, + MoveEndPhase, + MovePhase, + NewBattlePhase, + NextEncounterPhase, PostSummonPhase, - SelectGenderPhase, SelectModifierPhase, - SelectStarterPhase, SelectTargetPhase, ShinySparklePhase, ShowAbilityPhase, StatChangePhase, SummonPhase, - TitlePhase, ToggleDoublePositionPhase, TurnEndPhase, TurnInitPhase, TurnStartPhase, UnavailablePhase, VictoryPhase + SelectGenderPhase, + SelectModifierPhase, + SelectStarterPhase, + SelectTargetPhase, + ShinySparklePhase, + ShowAbilityPhase, + StatChangePhase, + SummonPhase, + SwitchPhase, + SwitchSummonPhase, + TitlePhase, + ToggleDoublePositionPhase, + TurnEndPhase, + TurnInitPhase, + TurnStartPhase, + UnavailablePhase, + VictoryPhase } from "#app/phases"; import UI, {Mode} from "#app/ui/ui"; import {Phase} from "#app/phase"; import ErrorInterceptor from "#app/test/utils/errorInterceptor"; +import {QuietFormChangePhase} from "#app/form-change-phase"; export default class PhaseInterceptor { public scene; @@ -63,10 +89,13 @@ export default class PhaseInterceptor { [ShinySparklePhase, this.startPhase], [SelectTargetPhase, this.startPhase], [UnavailablePhase, this.startPhase], + [QuietFormChangePhase, this.startPhase], + [SwitchPhase, this.startPhase], + [SwitchSummonPhase, this.startPhase], ]; private endBySetMode = [ - TitlePhase, SelectGenderPhase, CommandPhase, SelectModifierPhase + TitlePhase, SelectGenderPhase, CommandPhase ]; /** @@ -78,8 +107,8 @@ export default class PhaseInterceptor { this.log = []; this.onHold = []; this.prompts = []; + this.startPromptHandler(); this.initPhases(); - this.startPromptHander(); } rejectAll(error) { @@ -109,8 +138,10 @@ export default class PhaseInterceptor { async to(phaseTo, runTarget: boolean = true): Promise { return new Promise(async (resolve, reject) => { ErrorInterceptor.getInstance().add(this); - await this.run(this.phaseFrom).catch((e) => reject(e)); - this.phaseFrom = null; + if (this.phaseFrom) { + await this.run(this.phaseFrom).catch((e) => reject(e)); + this.phaseFrom = null; + } const targetName = typeof phaseTo === "string" ? phaseTo : phaseTo.name; this.intervalRun = setInterval(async() => { const currentPhase = this.onHold?.length && this.onHold[0]; @@ -238,7 +269,6 @@ export default class PhaseInterceptor { */ superEndPhase() { const instance = this.scene.getCurrentPhase(); - console.log(`%c INTERCEPTED Super End Phase ${instance.constructor.name}`, "color:red;"); this.originalSuperEnd.apply(instance); this.inProgress?.callback(); this.inProgress = undefined; @@ -253,6 +283,9 @@ export default class PhaseInterceptor { const instance = this.scene.ui; console.log("setMode", mode, args); const ret = this.originalSetMode.apply(instance, [mode, ...args]); + if (!this.phases[currentPhase.constructor.name]) { + throw new Error(`missing ${currentPhase.constructor.name} in phaseInterceptior PHASES list`); + } if (this.phases[currentPhase.constructor.name].endBySetMode) { this.inProgress?.callback(); this.inProgress = undefined; @@ -263,16 +296,17 @@ export default class PhaseInterceptor { /** * Method to start the prompt handler. */ - startPromptHander() { + startPromptHandler() { this.promptInterval = setInterval(() => { if (this.prompts.length) { const actionForNextPrompt = this.prompts[0]; const expireFn = actionForNextPrompt.expireFn && actionForNextPrompt.expireFn(); const currentMode = this.scene.ui.getMode(); const currentPhase = this.scene.getCurrentPhase().constructor.name; + const currentHandler = this.scene.ui.getHandler(); if (expireFn) { this.prompts.shift(); - } else if (currentMode === actionForNextPrompt.mode && currentPhase === actionForNextPrompt.phaseTarget) { + } else if (currentMode === actionForNextPrompt.mode && currentPhase === actionForNextPrompt.phaseTarget && currentHandler.active && (!actionForNextPrompt.awaitingActionInput || (actionForNextPrompt.awaitingActionInput && currentHandler.awaitingActionInput))) { this.prompts.shift().callback(); } } @@ -286,12 +320,13 @@ export default class PhaseInterceptor { * @param callback - The callback function to execute. * @param expireFn - The function to determine if the prompt has expired. */ - addToNextPrompt(phaseTarget: string, mode: Mode, callback: () => void, expireFn: () => void) { + addToNextPrompt(phaseTarget: string, mode: Mode, callback: () => void, expireFn: () => void, awaitingActionInput: boolean = false) { this.prompts.push({ phaseTarget, mode, callback, - expireFn + expireFn, + awaitingActionInput }); }