From 988ec664e94561adbba80c23e15d7b88d06b5918 Mon Sep 17 00:00:00 2001 From: ImperialSympathizer <110984302+ben-lear@users.noreply.github.com> Date: Sun, 11 Aug 2024 14:40:47 -0400 Subject: [PATCH] Disable endless boss passives (#3451) * fix strict null broken * disable endless boss passives * jsdocs on mock objects and move helper function to gameManager.ts * Apply suggestions from flx's review Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com> * fix broken test * fix lint --------- Co-authored-by: ImperialSympathizer Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com> --- src/field/pokemon.ts | 9 +- src/test/endless_boss.test.ts | 88 +++++++++++++++++++ src/test/final_boss.test.ts | 42 ++------- src/test/utils/gameManager.ts | 32 +++++++ src/test/utils/gameWrapper.ts | 4 + src/test/utils/mocks/mockTextureManager.ts | 13 ++- .../utils/mocks/mocksContainer/mockTexture.ts | 42 +++++++++ 7 files changed, 190 insertions(+), 40 deletions(-) create mode 100644 src/test/endless_boss.test.ts create mode 100644 src/test/utils/mocks/mocksContainer/mockTexture.ts diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index b4461c21354..618b4343950 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -1082,8 +1082,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return true; } - // Final boss does not have passive - if (this.scene.currentBattle?.battleSpec === BattleSpec.FINAL_BOSS && this instanceof EnemyPokemon) { + // Classic Final boss and Endless Minor/Major bosses do not have passive + const { currentBattle, gameMode } = this.scene; + const waveIndex = currentBattle?.waveIndex; + if (this instanceof EnemyPokemon && + (currentBattle?.battleSpec === BattleSpec.FINAL_BOSS || + gameMode.isEndlessMinorBoss(waveIndex) || + gameMode.isEndlessMajorBoss(waveIndex))) { return false; } diff --git a/src/test/endless_boss.test.ts b/src/test/endless_boss.test.ts new file mode 100644 index 00000000000..e983be245b6 --- /dev/null +++ b/src/test/endless_boss.test.ts @@ -0,0 +1,88 @@ +import { Biome } from "#app/enums/biome"; +import { Species } from "#app/enums/species"; +import { GameModes } from "#app/game-mode"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import GameManager from "./utils/gameManager"; + +const EndlessBossWave = { + Minor: 250, + Major: 1000 +}; + +describe("Endless Boss", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .startingBiome(Biome.END) + .disableCrits(); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + it(`should spawn a minor boss every ${EndlessBossWave.Minor} waves in END biome in Endless`, async () => { + game.override.startingWave(EndlessBossWave.Minor); + await game.runToFinalBossEncounter(game, [Species.BIDOOF], GameModes.ENDLESS); + + expect(game.scene.currentBattle.waveIndex).toBe(EndlessBossWave.Minor); + expect(game.scene.arena.biomeType).toBe(Biome.END); + const eternatus = game.scene.getEnemyPokemon(); + expect(eternatus?.species.speciesId).toBe(Species.ETERNATUS); + expect(eternatus?.hasPassive()).toBe(false); + expect(eternatus?.formIndex).toBe(0); + }); + + it(`should spawn a major boss every ${EndlessBossWave.Major} waves in END biome in Endless`, async () => { + game.override.startingWave(EndlessBossWave.Major); + await game.runToFinalBossEncounter(game, [Species.BIDOOF], GameModes.ENDLESS); + + expect(game.scene.currentBattle.waveIndex).toBe(EndlessBossWave.Major); + expect(game.scene.arena.biomeType).toBe(Biome.END); + const eternatus = game.scene.getEnemyPokemon(); + expect(eternatus?.species.speciesId).toBe(Species.ETERNATUS); + expect(eternatus?.hasPassive()).toBe(false); + expect(eternatus?.formIndex).toBe(1); + }); + + it(`should spawn a minor boss every ${EndlessBossWave.Minor} waves in END biome in Spliced Endless`, async () => { + game.override.startingWave(EndlessBossWave.Minor); + await game.runToFinalBossEncounter(game, [Species.BIDOOF], GameModes.SPLICED_ENDLESS); + + expect(game.scene.currentBattle.waveIndex).toBe(EndlessBossWave.Minor); + expect(game.scene.arena.biomeType).toBe(Biome.END); + const eternatus = game.scene.getEnemyPokemon(); + expect(eternatus?.species.speciesId).toBe(Species.ETERNATUS); + expect(eternatus?.hasPassive()).toBe(false); + expect(eternatus?.formIndex).toBe(0); + }); + + it(`should spawn a major boss every ${EndlessBossWave.Major} waves in END biome in Spliced Endless`, async () => { + game.override.startingWave(EndlessBossWave.Major); + await game.runToFinalBossEncounter(game, [Species.BIDOOF], GameModes.SPLICED_ENDLESS); + + expect(game.scene.currentBattle.waveIndex).toBe(EndlessBossWave.Major); + expect(game.scene.arena.biomeType).toBe(Biome.END); + const eternatus = game.scene.getEnemyPokemon(); + expect(eternatus?.species.speciesId).toBe(Species.ETERNATUS); + expect(eternatus?.hasPassive()).toBe(false); + expect(eternatus?.formIndex).toBe(1); + }); + + it(`should NOT spawn major or minor boss outside wave ${EndlessBossWave.Minor}s in END biome`, async () => { + game.override.startingWave(EndlessBossWave.Minor - 1); + await game.runToFinalBossEncounter(game, [Species.BIDOOF], GameModes.ENDLESS); + + expect(game.scene.currentBattle.waveIndex).not.toBe(EndlessBossWave.Minor); + expect(game.scene.getEnemyPokemon()!.species.speciesId).not.toBe(Species.ETERNATUS); + }); +}); diff --git a/src/test/final_boss.test.ts b/src/test/final_boss.test.ts index 3e1225295e0..a57d71534a3 100644 --- a/src/test/final_boss.test.ts +++ b/src/test/final_boss.test.ts @@ -1,11 +1,8 @@ import { Biome } from "#app/enums/biome.js"; import { Species } from "#app/enums/species.js"; -import { GameModes, getGameMode } from "#app/game-mode.js"; -import { EncounterPhase, SelectStarterPhase } from "#app/phases.js"; -import { Mode } from "#app/ui/ui.js"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import GameManager from "./utils/gameManager"; -import { generateStarter } from "./utils/gameManagerUtils"; +import { GameModes } from "#app/game-mode"; const FinalWave = { Classic: 200, @@ -31,7 +28,7 @@ describe("Final Boss", () => { }); it("should spawn Eternatus on wave 200 in END biome", async () => { - await runToFinalBossEncounter(game, [Species.BIDOOF]); + await game.runToFinalBossEncounter(game, [Species.BIDOOF], GameModes.CLASSIC); expect(game.scene.currentBattle.waveIndex).toBe(FinalWave.Classic); expect(game.scene.arena.biomeType).toBe(Biome.END); @@ -40,7 +37,7 @@ describe("Final Boss", () => { it("should NOT spawn Eternatus before wave 200 in END biome", async () => { game.override.startingWave(FinalWave.Classic - 1); - await runToFinalBossEncounter(game, [Species.BIDOOF]); + await game.runToFinalBossEncounter(game, [Species.BIDOOF], GameModes.CLASSIC); expect(game.scene.currentBattle.waveIndex).not.toBe(FinalWave.Classic); expect(game.scene.arena.biomeType).toBe(Biome.END); @@ -49,7 +46,7 @@ describe("Final Boss", () => { it("should NOT spawn Eternatus outside of END biome", async () => { game.override.startingBiome(Biome.FOREST); - await runToFinalBossEncounter(game, [Species.BIDOOF]); + await game.runToFinalBossEncounter(game, [Species.BIDOOF], GameModes.CLASSIC); expect(game.scene.currentBattle.waveIndex).toBe(FinalWave.Classic); expect(game.scene.arena.biomeType).not.toBe(Biome.END); @@ -57,7 +54,7 @@ describe("Final Boss", () => { }); it("should not have passive enabled on Eternatus", async () => { - await runToFinalBossEncounter(game, [Species.BIDOOF]); + await game.runToFinalBossEncounter(game, [Species.BIDOOF], GameModes.CLASSIC); const eternatus = game.scene.getEnemyPokemon(); expect(eternatus?.species.speciesId).toBe(Species.ETERNATUS); @@ -66,32 +63,3 @@ describe("Final Boss", () => { it.todo("should change form on direct hit down to last boss fragment", () => {}); }); - -/** - * Helper function to run to the final boss encounter as it's a bit tricky due to extra dialogue - * @param game - The game manager - */ -async function runToFinalBossEncounter(game: GameManager, species: Species[]) { - console.log("===to final boss encounter==="); - await game.runToTitle(); - - game.onNextPrompt("TitlePhase", Mode.TITLE, () => { - game.scene.gameMode = getGameMode(GameModes.CLASSIC); - const starters = generateStarter(game.scene, species); - const selectStarterPhase = new SelectStarterPhase(game.scene); - game.scene.pushPhase(new EncounterPhase(game.scene, false)); - selectStarterPhase.initBattle(starters); - }); - - game.onNextPrompt("EncounterPhase", Mode.MESSAGE, async () => { - // This will skip all entry dialogue (I can't figure out a way to sequentially handle the 8 chained messages via 1 prompt handler) - game.setMode(Mode.MESSAGE); - const encounterPhase = game.scene.getCurrentPhase() as EncounterPhase; - - // No need to end phase, this will do it for you - encounterPhase.doEncounterCommon(false); - }); - - await game.phaseInterceptor.to(EncounterPhase, true); - console.log("===finished run to final boss encounter==="); -} diff --git a/src/test/utils/gameManager.ts b/src/test/utils/gameManager.ts index c782c613bb0..8d9181352ac 100644 --- a/src/test/utils/gameManager.ts +++ b/src/test/utils/gameManager.ts @@ -141,6 +141,38 @@ export default class GameManager { } } + /** + * Helper function to run to the final boss encounter as it's a bit tricky due to extra dialogue + * Also handles Major/Minor bosses from endless modes + * @param game - The game manager + * @param species + * @param mode + */ + async runToFinalBossEncounter(game: GameManager, species: Species[], mode: GameModes) { + console.log("===to final boss encounter==="); + await game.runToTitle(); + + game.onNextPrompt("TitlePhase", Mode.TITLE, () => { + game.scene.gameMode = getGameMode(mode); + const starters = generateStarter(game.scene, species); + const selectStarterPhase = new SelectStarterPhase(game.scene); + game.scene.pushPhase(new EncounterPhase(game.scene, false)); + selectStarterPhase.initBattle(starters); + }); + + game.onNextPrompt("EncounterPhase", Mode.MESSAGE, async () => { + // This will skip all entry dialogue (I can't figure out a way to sequentially handle the 8 chained messages via 1 prompt handler) + game.setMode(Mode.MESSAGE); + const encounterPhase = game.scene.getCurrentPhase() as EncounterPhase; + + // No need to end phase, this will do it for you + encounterPhase.doEncounterCommon(false); + }); + + await game.phaseInterceptor.to(EncounterPhase, true); + console.log("===finished run to final boss encounter==="); + } + /** * Transitions to the start of a battle. * @param species - Optional array of species to start the battle with. diff --git a/src/test/utils/gameWrapper.ts b/src/test/utils/gameWrapper.ts index 80529e47194..49044c260fa 100644 --- a/src/test/utils/gameWrapper.ts +++ b/src/test/utils/gameWrapper.ts @@ -89,6 +89,7 @@ export default class GameWrapper { frames: {}, }); Pokemon.prototype.enableMask = () => null; + Pokemon.prototype.updateFusionPalette = () => null; } setScene(scene: BattleScene) { @@ -128,7 +129,9 @@ export default class GameWrapper { manager: { game: this.game, }, + destroy: () => null, setVolume: () => null, + stop: () => null, stopByKey: () => null, on: (evt, callback) => callback(), key: "", @@ -202,6 +205,7 @@ export default class GameWrapper { }; const mockTextureManager = new MockTextureManager(this.scene); this.scene.add = mockTextureManager.add; + this.scene.textures = mockTextureManager; this.scene.sys.displayList = this.scene.add.displayList; this.scene.sys.updateList = new UpdateList(this.scene); this.scene.systems = this.scene.sys; diff --git a/src/test/utils/mocks/mockTextureManager.ts b/src/test/utils/mocks/mockTextureManager.ts index 95e8996836f..330409e9776 100644 --- a/src/test/utils/mocks/mockTextureManager.ts +++ b/src/test/utils/mocks/mockTextureManager.ts @@ -6,8 +6,11 @@ import MockImage from "#test/utils/mocks/mocksContainer/mockImage"; import MockText from "#test/utils/mocks/mocksContainer/mockText"; import MockPolygon from "#test/utils/mocks/mocksContainer/mockPolygon"; import { MockGameObject } from "./mockGameObject"; +import MockTexture from "#test/utils/mocks/mocksContainer/mockTexture"; - +/** + * Stub class for Phaser.Textures.TextureManager + */ export default class MockTextureManager { private textures: Map; private scene; @@ -54,6 +57,14 @@ export default class MockTextureManager { // } } + /** + * Returns a mock texture + * @param key + */ + get(key) { + return new MockTexture(this, key, null); + } + rectangle(x, y, width, height, fillColor) { const rectangle = new MockRectangle(this, x, y, width, height, fillColor); this.list.push(rectangle); diff --git a/src/test/utils/mocks/mocksContainer/mockTexture.ts b/src/test/utils/mocks/mocksContainer/mockTexture.ts new file mode 100644 index 00000000000..03bedb4751b --- /dev/null +++ b/src/test/utils/mocks/mocksContainer/mockTexture.ts @@ -0,0 +1,42 @@ +import { MockGameObject } from "../mockGameObject"; +import MockTextureManager from "#test/utils/mocks/mockTextureManager"; + + +/** + * Stub for Phaser.Textures.Texture object + * Just mocks the function calls and data required for use in tests + */ +export default class MockTexture implements MockGameObject { + public manager: MockTextureManager; + public key: string; + public source; + public frames: object; + public firstFrame: string; + + constructor(manager, key: string, source) { + this.manager = manager; + this.key = key; + this.source = source; + + const mockFrame = { + width: 100, + height: 100, + cutX: 0, + cutY: 0 + }; + this.frames = { + firstFrame: mockFrame, + 0: mockFrame, + 1: mockFrame, + 2: mockFrame, + 3: mockFrame, + 4: mockFrame + }; + this.firstFrame = "firstFrame"; + } + + /** Mocks the function call that gets an HTMLImageElement, see {@link Pokemon.updateFusionPalette} */ + getSourceImage() { + return null; + } +}