From 69ab1687bcd84c0a8d4be5a0fa831a2ef7e4c108 Mon Sep 17 00:00:00 2001 From: MokaStitcher Date: Tue, 27 Aug 2024 11:28:42 +0200 Subject: [PATCH] implement test for final boss encounter phase switch --- src/test/final_boss.test.ts | 94 ++++++++++++++++++++++++++++++++++- src/test/utils/gameManager.ts | 10 +--- src/ui/ui.ts | 41 ++++++++------- 3 files changed, 119 insertions(+), 26 deletions(-) diff --git a/src/test/final_boss.test.ts b/src/test/final_boss.test.ts index 0f59572619b..a76601b0201 100644 --- a/src/test/final_boss.test.ts +++ b/src/test/final_boss.test.ts @@ -1,8 +1,14 @@ import { Biome } from "#app/enums/biome"; +import { Moves } from "#app/enums/moves"; 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"; +import { SPLASH_ONLY } from "./utils/testUtils"; +import { TurnHeldItemTransferModifier } from "#app/modifier/modifier"; +import { Abilities } from "#app/enums/abilities"; +import { CommandPhase } from "#app/phases/command-phase"; +import { StatusEffect } from "#app/data/status-effect"; const FinalWave = { Classic: 200, @@ -61,5 +67,91 @@ describe("Final Boss", () => { expect(eternatus?.hasPassive()).toBe(false); }); - it.todo("should change form on direct hit down to last boss fragment", () => {}); + it("should change form on direct hit down to last boss fragment", async () => { + game.override.startingLevel(10000); + game.override.starterSpecies(Species.KYUREM); + game.override.moveset([Moves.DRAGON_PULSE]); + game.override.enemyMoveset(SPLASH_ONLY); + game.override.enemyHeldItems([]); + + // This handles skipping all dialog at the start of the battle and when switching phase + await game.runToFinalBossEncounter(game, [Species.BIDOOF], GameModes.CLASSIC); + await game.phaseInterceptor.to(CommandPhase); + + // Eternatus phase 1 + let eternatus = game.scene.getEnemyPokemon()!; + const phase1Hp = eternatus.getMaxHp(); + expect(eternatus.species.speciesId).toBe(Species.ETERNATUS); + expect(eternatus.formIndex).toBe(0); + expect(eternatus.bossSegments).toBe(4); + expect(eternatus.bossSegmentIndex).toBe(3); + + game.move.select(Moves.DRAGON_PULSE); + await game.toNextTurn(); + + // Eternatus phase 2: changed form, healed and restored its shields + eternatus = game.scene.getEnemyPokemon()!; + expect(eternatus.species.speciesId).toBe(Species.ETERNATUS); + expect(eternatus.hp).toBeGreaterThan(phase1Hp); + expect(eternatus.hp).toBe(eternatus.getMaxHp()); + expect(eternatus.formIndex).toBe(1); + expect(eternatus.bossSegments).toBe(5); + expect(eternatus.bossSegmentIndex).toBe(4); + // should carry a mini black hole + const miniBlackHole = eternatus.getHeldItems().find(m => m instanceof TurnHeldItemTransferModifier); + expect(miniBlackHole).toBeDefined(); + expect(miniBlackHole?.stackCount).toBe(1); + }); + + it("should change form on status damage down to last boss fragment", async () => { + game.override.ability(Abilities.NO_GUARD); + game.override.moveset([ Moves.SPLASH, Moves.WILL_O_WISP ]); + game.override.enemyMoveset(SPLASH_ONLY); + game.override.enemyHeldItems([]); + + // This handles skipping all dialog at the start of the battle and when switching phase + await game.runToFinalBossEncounter(game, [Species.BIDOOF], GameModes.CLASSIC); + await game.phaseInterceptor.to(CommandPhase); + + // Eternatus phase 1 + let eternatus = game.scene.getEnemyPokemon()!; + const phase1Hp = eternatus.getMaxHp(); + expect(eternatus.species.speciesId).toBe(Species.ETERNATUS); + expect(eternatus.formIndex).toBe(0); + expect(eternatus.bossSegments).toBe(4); + expect(eternatus.bossSegmentIndex).toBe(3); + + // Burn the boss + game.move.select(Moves.WILL_O_WISP); + await game.toNextTurn(); + expect(eternatus.status?.effect).toBe(StatusEffect.BURN); + + const tickDamage = phase1Hp - eternatus.hp; + const lastShieldHp = Math.ceil(phase1Hp / eternatus.bossSegments); + // Stall until the burn is one hit away from breaking the last shield + while (eternatus.hp - tickDamage > lastShieldHp) { + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + } + + expect(eternatus.bossSegmentIndex).toBe(1); + + // Last burn should break the shield + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + + // Eternatus phase 2: changed form, healed and restored its shields + eternatus = game.scene.getEnemyPokemon()!; + expect(eternatus.hp).toBeGreaterThan(phase1Hp); + expect(eternatus.hp).toBe(eternatus.getMaxHp()); + expect(eternatus.status).toBeFalsy(); + expect(eternatus.formIndex).toBe(1); + expect(eternatus.bossSegments).toBe(5); + expect(eternatus.bossSegmentIndex).toBe(4); + // should carry a mini black hole + const miniBlackHole = eternatus.getHeldItems().find(m => m instanceof TurnHeldItemTransferModifier); + expect(miniBlackHole).toBeDefined(); + expect(miniBlackHole?.stackCount).toBe(1); + }); + }); diff --git a/src/test/utils/gameManager.ts b/src/test/utils/gameManager.ts index cb3c547744b..0defe6f2ac5 100644 --- a/src/test/utils/gameManager.ts +++ b/src/test/utils/gameManager.ts @@ -156,14 +156,8 @@ export default class GameManager { 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); - }); + // This will consider all battle entry dialog as seens and skip them + vi.spyOn(game.scene.ui, "shouldSkipDialogue").mockReturnValue(true); await game.phaseInterceptor.to(EncounterPhase, true); console.log("===finished run to final boss encounter==="); diff --git a/src/ui/ui.ts b/src/ui/ui.ts index 250a21544dc..c4a41320b85 100644 --- a/src/ui/ui.ts +++ b/src/ui/ui.ts @@ -295,26 +295,20 @@ export default class UI extends Phaser.GameObjects.Container { } showDialogue(text: string, name: string | undefined, delay: integer | null = 0, callback: Function, callbackDelay?: integer, promptDelay?: integer): void { - // First get the gender of the player (default male) (also used if UNSET) - let playerGenderPrefix = "PGM"; - if ((this.scene as BattleScene).gameData.gender === PlayerGender.FEMALE) { - playerGenderPrefix = "PGF"; + // Skip dialogue if the player has enabled the option and the dialogue has been already seen + if (this.shouldSkipDialogue(text)) { + console.log(`Dialogue ${text} skipped`); + callback(); + return; } - // Add the prefix to the text - const localizationKey: string = playerGenderPrefix + text; - // Get localized dialogue (if available) + // Get the localization key corresponding to the player's gender + const localizationKey: string = this.getGenderedLocalizationKey(text); + let hasi18n = false; if (i18next.exists(localizationKey) ) { text = i18next.t(localizationKey as ParseKeys); hasi18n = true; - - // Skip dialogue if the player has enabled the option and the dialogue has been already seen - if ((this.scene as BattleScene).skipSeenDialogues && (this.scene as BattleScene).gameData.getSeenDialogues()[localizationKey] === true) { - console.log(`Dialogue ${localizationKey} skipped`); - callback(); - return; - } } let showMessageAndCallback = () => { hasi18n && (this.scene as BattleScene).gameData.saveSeenDialogue(localizationKey); @@ -337,14 +331,27 @@ export default class UI extends Phaser.GameObjects.Container { } } - shouldSkipDialogue(text): boolean { + /** + * Adds the appropriate gender marker to a localization key based on the player's selected gender + * @param baseKey the localization key to change + * @returns the gendered version of the key + */ + getGenderedLocalizationKey(baseKey: string): string { let playerGenderPrefix = "PGM"; if ((this.scene as BattleScene).gameData.gender === PlayerGender.FEMALE) { playerGenderPrefix = "PGF"; } + return playerGenderPrefix + baseKey; + } - const key = playerGenderPrefix + text; - + /** + * Checks if a dialogue should be skipped based on whether the "skipping seen dialog" + * option is enabled and if the given dialog has been seen already + * @param text the localization key to use, without their gendered marker + * @returns true if the dialog should be skipped + */ + shouldSkipDialogue(text: string): boolean { + const key = this.getGenderedLocalizationKey(text); if (i18next.exists(key) ) { if ((this.scene as BattleScene).skipSeenDialogues && (this.scene as BattleScene).gameData.getSeenDialogues()[key] === true) { return true;