diff --git a/src/phases/encounter-phase.ts b/src/phases/encounter-phase.ts index 3f37095569a..d4fac5f0f86 100644 --- a/src/phases/encounter-phase.ts +++ b/src/phases/encounter-phase.ts @@ -154,6 +154,7 @@ export class EncounterPhase extends BattlePhase { this.scene.ui.setMode(Mode.MESSAGE).then(() => { if (!this.loaded) { + this.trySetWeatherIfNewBiome(); // Set weather before session gets saved //@ts-ignore this.scene.gameData.saveAll(this.scene, true, battle.waveIndex % 10 === 1 || this.scene.lastSavePlayTime >= 300).then(success => { // TODO: get rid of ts-ignore this.scene.disableMenu = false; @@ -188,10 +189,6 @@ export class EncounterPhase extends BattlePhase { } } - if (!this.loaded) { - this.scene.arena.trySetWeather(getRandomWeatherType(this.scene.arena), false); - } - const enemyField = this.scene.getEnemyField(); this.scene.tweens.add({ targets: [this.scene.arenaEnemy, this.scene.currentBattle.trainer, enemyField, this.scene.arenaPlayer, this.scene.trainer].flat(), @@ -387,4 +384,18 @@ export class EncounterPhase extends BattlePhase { } return false; } + + /** + * Set biome weather if and only if this encounter is the start of a new biome. + * + * By using function overrides, this should happen if and only if this phase + * is exactly a NewBiomeEncounterPhase or an EncounterPhase (to account for + * Wave 1 of a Daily Run), but NOT NextEncounterPhase (which starts the next + * wave in the same biome). + */ + trySetWeatherIfNewBiome(): void { + if (!this.loaded) { + this.scene.arena.trySetWeather(getRandomWeatherType(this.scene.arena), false); + } + } } diff --git a/src/phases/new-biome-encounter-phase.ts b/src/phases/new-biome-encounter-phase.ts index c447e78f7b1..87635c402fc 100644 --- a/src/phases/new-biome-encounter-phase.ts +++ b/src/phases/new-biome-encounter-phase.ts @@ -17,8 +17,6 @@ export class NewBiomeEncounterPhase extends NextEncounterPhase { } } - this.scene.arena.trySetWeather(getRandomWeatherType(this.scene.arena), false); - for (const pokemon of this.scene.getParty().filter(p => p.isOnField())) { applyAbAttrs(PostBiomeChangeAbAttr, pokemon, null); } @@ -35,4 +33,11 @@ export class NewBiomeEncounterPhase extends NextEncounterPhase { } }); } + + /** + * Set biome weather. + */ + trySetWeatherIfNewBiome(): void { + this.scene.arena.trySetWeather(getRandomWeatherType(this.scene.arena), false); + } } diff --git a/src/phases/next-encounter-phase.ts b/src/phases/next-encounter-phase.ts index 89987534fc0..496dd86fec4 100644 --- a/src/phases/next-encounter-phase.ts +++ b/src/phases/next-encounter-phase.ts @@ -43,4 +43,10 @@ export class NextEncounterPhase extends EncounterPhase { } }); } + + /** + * Do nothing (since this is simply the next wave in the same biome). + */ + trySetWeatherIfNewBiome(): void { + } } diff --git a/src/test/battle/battle.test.ts b/src/test/battle/battle.test.ts index 25dfbc765bd..028e7804da8 100644 --- a/src/test/battle/battle.test.ts +++ b/src/test/battle/battle.test.ts @@ -24,8 +24,9 @@ import { Moves } from "#enums/moves"; import { PlayerGender } from "#enums/player-gender"; import { Species } from "#enums/species"; import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { SPLASH_ONLY } from "../utils/testUtils"; +import { Biome } from "#app/enums/biome"; describe("Test Battle Phase", () => { let phaserGame: Phaser.Game; @@ -291,7 +292,8 @@ describe("Test Battle Phase", () => { expect(game.scene.currentBattle.turn).toBeGreaterThan(turn); }, 20000); - it("to next wave with pokemon killed, single", async () => { + // Made redundant by the "same biome" test, which is copied from this test, should this be removed? + it.skip("to next wave with pokemon killed, single", async () => { const moveToUse = Moves.SPLASH; game.override.battleType("single"); game.override.starterSpecies(Species.MEWTWO); @@ -310,6 +312,30 @@ describe("Test Battle Phase", () => { expect(game.scene.currentBattle.waveIndex).toBeGreaterThan(waveIndex); }, 20000); + it("does not set new weather if staying in same biome", async () => { + const moveToUse = Moves.SPLASH; + game.override + .battleType("single") + .starterSpecies(Species.MEWTWO) + .enemySpecies(Species.RATTATA) + .enemyAbility(Abilities.HYDRATION) + .ability(Abilities.ZEN_MODE) + .startingLevel(2000) + .startingWave(3) + .startingBiome(Biome.LAKE) + .moveset([moveToUse]); + game.override.enemyMoveset([Moves.TACKLE, Moves.TACKLE, Moves.TACKLE, Moves.TACKLE]); + await game.classicMode.startBattle(); + const waveIndex = game.scene.currentBattle.waveIndex; + game.move.select(moveToUse); + + vi.spyOn(game.scene.arena, "trySetWeather"); + await game.doKillOpponents(); + await game.toNextWave(); + expect(game.scene.arena.trySetWeather).not.toHaveBeenCalled(); + expect(game.scene.currentBattle.waveIndex).toBeGreaterThan(waveIndex); + }, 20000); + it("does not force switch if active pokemon faints at same time as enemy mon and is revived in post-battle", async () => { const moveToUse = Moves.TAKE_DOWN; game.override diff --git a/src/test/reload.test.ts b/src/test/reload.test.ts index 0a712fcc7df..777fa5192a5 100644 --- a/src/test/reload.test.ts +++ b/src/test/reload.test.ts @@ -36,19 +36,19 @@ describe("Reload", () => { expect(preReloadRngState).toBe(postReloadRngState); }, 20000); - it("should not have RNG inconsistencies after a biome switch", async () => { + it("should not have RNG or weather inconsistencies after a biome switch", async () => { game.override .startingWave(10) - .startingBiome(Biome.CAVE) // Will lead to biomes with randomly generated weather + .startingBiome(Biome.ICE_CAVE) // Will lead to Snowy Forest with randomly generated weather .battleType("single") - .startingLevel(100) - .enemyLevel(1000) + .startingLevel(100) // Avoid levelling up + .enemyLevel(1000) // Avoid opponent dying before game.doKillOpponents() .disableTrainerWaves() .moveset([Moves.KOWTOW_CLEAVE]) .enemyMoveset(SPLASH_ONLY); - await game.dailyMode.startBattle(); + await game.classicMode.startBattle(); // Apparently daily mode would override the biome - // Transition from Daily Run Wave 10 to Wave 11 in order to trigger biome switch + // Transition from Wave 10 to Wave 11 in order to trigger biome switch game.move.select(Moves.KOWTOW_CLEAVE); await game.phaseInterceptor.to("DamagePhase"); await game.doKillOpponents(); @@ -56,12 +56,15 @@ describe("Reload", () => { expect(game.phaseInterceptor.log).toContain("NewBiomeEncounterPhase"); const preReloadRngState = Phaser.Math.RND.state(); + const preReloadWeather = game.scene.arena.weather; await game.reload.reloadSession(); const postReloadRngState = Phaser.Math.RND.state(); + const postReloadWeather = game.scene.arena.weather; expect(preReloadRngState).toBe(postReloadRngState); + expect(preReloadWeather).toStrictEqual(postReloadWeather); }, 20000); it("should not have RNG inconsistencies at a Daily run wild Pokemon fight", async () => { diff --git a/src/test/utils/gameManager.ts b/src/test/utils/gameManager.ts index ade33aa1148..2ef17099921 100644 --- a/src/test/utils/gameManager.ts +++ b/src/test/utils/gameManager.ts @@ -32,7 +32,6 @@ import TargetSelectUiHandler from "#app/ui/target-select-ui-handler"; import { Mode } from "#app/ui/ui"; import { Button } from "#enums/buttons"; import { ExpNotification } from "#enums/exp-notification"; -import { GameDataType } from "#enums/game-data-type"; import { PlayerGender } from "#enums/player-gender"; import { Species } from "#enums/species"; import { generateStarter, waitUntil } from "#test/utils/gameManagerUtils"; @@ -330,13 +329,11 @@ export default class GameManager { * @returns A promise that resolves with the exported save data. */ exportSaveToTest(): Promise { + const saveKey = "x0i2O7WRiANTqPmZ"; return new Promise(async (resolve) => { - await this.scene.gameData.saveAll(this.scene, true, true, true, true); - this.scene.reset(true); - await waitUntil(() => this.scene.ui?.getMode() === Mode.TITLE); - await this.scene.gameData.tryExportData(GameDataType.SESSION, 0); - await waitUntil(() => localStorage.hasOwnProperty("toExport")); - return resolve(localStorage.getItem("toExport")!); // TODO: is this bang correct?; + const sessionSaveData = this.scene.gameData.getSessionSaveData(this.scene); + const encryptedSaveData = AES.encrypt(JSON.stringify(sessionSaveData), saveKey).toString(); + resolve(encryptedSaveData); }); } diff --git a/src/test/utils/helpers/reloadHelper.ts b/src/test/utils/helpers/reloadHelper.ts index c15347b08c9..e0e538120cc 100644 --- a/src/test/utils/helpers/reloadHelper.ts +++ b/src/test/utils/helpers/reloadHelper.ts @@ -5,11 +5,27 @@ import { vi } from "vitest"; import { BattleStyle } from "#app/enums/battle-style"; import { CommandPhase } from "#app/phases/command-phase"; import { TurnInitPhase } from "#app/phases/turn-init-phase"; +import { SessionSaveData } from "#app/system/game-data"; +import GameManager from "../gameManager"; /** * Helper to allow reloading sessions in unit tests. */ export class ReloadHelper extends GameManagerHelper { + sessionData: SessionSaveData; + + constructor(game: GameManager) { + super(game); + + // Whenever the game saves the session, save it to the reloadHelper instead + vi.spyOn(game.scene.gameData, "saveAll").mockImplementation((scene) => { + return new Promise((resolve, reject) => { + this.sessionData = scene.gameData.getSessionSaveData(scene); + resolve(true); + }); + }); + } + /** * Simulate reloading the session from the title screen, until reaching the * beginning of the first turn (equivalent to running `startBattle()`) for @@ -17,7 +33,6 @@ export class ReloadHelper extends GameManagerHelper { */ async reloadSession() : Promise { const scene = this.game.scene; - const sessionData = scene.gameData.getSessionSaveData(scene); const titlePhase = new TitlePhase(scene); scene.clearPhaseQueue(); @@ -25,7 +40,7 @@ export class ReloadHelper extends GameManagerHelper { // Set the last saved session to the desired session data vi.spyOn(scene.gameData, "getSession").mockReturnValue( new Promise((resolve, reject) => { - resolve(sessionData); + resolve(this.sessionData); }) ); scene.unshiftPhase(titlePhase);