[Bug] Fix reloads erasing weather on first wave of biome (#4078)

* [Bug] Fix reloads erasing weather on first wave of biome

* Minor revisions

* Minor revisions 2

* Update test

---------

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
This commit is contained in:
PigeonBar 2024-09-16 15:30:42 -04:00 committed by GitHub
parent 128df1b6d2
commit 009fd3fc5c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 98 additions and 32 deletions

View File

@ -216,8 +216,8 @@ export class EncounterPhase extends BattlePhase {
this.scene.ui.setMode(Mode.MESSAGE).then(() => { this.scene.ui.setMode(Mode.MESSAGE).then(() => {
if (!this.loaded) { if (!this.loaded) {
//@ts-ignore this.trySetWeatherIfNewBiome(); // Set weather before session gets saved
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.gameData.saveAll(this.scene, true, battle.waveIndex % 10 === 1 || (this.scene.lastSavePlayTime ?? 0) >= 300).then(success => {
this.scene.disableMenu = false; this.scene.disableMenu = false;
if (!success) { if (!success) {
return this.scene.reset(true); return this.scene.reset(true);
@ -250,10 +250,6 @@ export class EncounterPhase extends BattlePhase {
} }
} }
if (!this.loaded) {
this.scene.arena.trySetWeather(getRandomWeatherType(this.scene.arena), false);
}
const enemyField = this.scene.getEnemyField(); const enemyField = this.scene.getEnemyField();
this.scene.tweens.add({ this.scene.tweens.add({
targets: [this.scene.arenaEnemy, this.scene.currentBattle.trainer, enemyField, this.scene.arenaPlayer, this.scene.trainer].flat(), targets: [this.scene.arenaEnemy, this.scene.currentBattle.trainer, enemyField, this.scene.arenaPlayer, this.scene.trainer].flat(),
@ -519,4 +515,18 @@ export class EncounterPhase extends BattlePhase {
} }
return false; 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);
}
}
} }

View File

@ -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())) { for (const pokemon of this.scene.getParty().filter(p => p.isOnField())) {
applyAbAttrs(PostBiomeChangeAbAttr, pokemon, null); applyAbAttrs(PostBiomeChangeAbAttr, pokemon, null);
} }
@ -41,4 +39,11 @@ export class NewBiomeEncounterPhase extends NextEncounterPhase {
} }
}); });
} }
/**
* Set biome weather.
*/
trySetWeatherIfNewBiome(): void {
this.scene.arena.trySetWeather(getRandomWeatherType(this.scene.arena), false);
}
} }

View File

@ -67,4 +67,10 @@ export class NextEncounterPhase extends EncounterPhase {
} }
}); });
} }
/**
* Do nothing (since this is simply the next wave in the same biome).
*/
trySetWeatherIfNewBiome(): void {
}
} }

View File

@ -24,7 +24,8 @@ import { Moves } from "#enums/moves";
import { PlayerGender } from "#enums/player-gender"; import { PlayerGender } from "#enums/player-gender";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import Phaser from "phaser"; import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { Biome } from "#app/enums/biome";
describe("Test Battle Phase", () => { describe("Test Battle Phase", () => {
let phaserGame: Phaser.Game; let phaserGame: Phaser.Game;
@ -290,22 +291,27 @@ describe("Test Battle Phase", () => {
expect(game.scene.currentBattle.turn).toBeGreaterThan(turn); expect(game.scene.currentBattle.turn).toBeGreaterThan(turn);
}, 20000); }, 20000);
it("to next wave with pokemon killed, single", async () => { it("does not set new weather if staying in same biome", async () => {
const moveToUse = Moves.SPLASH; const moveToUse = Moves.SPLASH;
game.override.battleType("single"); game.override
game.override.starterSpecies(Species.MEWTWO); .battleType("single")
game.override.enemySpecies(Species.RATTATA); .starterSpecies(Species.MEWTWO)
game.override.enemyAbility(Abilities.HYDRATION); .enemySpecies(Species.RATTATA)
game.override.ability(Abilities.ZEN_MODE); .enemyAbility(Abilities.HYDRATION)
game.override.startingLevel(2000); .ability(Abilities.ZEN_MODE)
game.override.startingWave(3); .startingLevel(2000)
game.override.moveset([moveToUse]); .startingWave(3)
.startingBiome(Biome.LAKE)
.moveset([moveToUse]);
game.override.enemyMoveset([Moves.TACKLE, Moves.TACKLE, Moves.TACKLE, Moves.TACKLE]); game.override.enemyMoveset([Moves.TACKLE, Moves.TACKLE, Moves.TACKLE, Moves.TACKLE]);
await game.startBattle(); await game.classicMode.startBattle();
const waveIndex = game.scene.currentBattle.waveIndex; const waveIndex = game.scene.currentBattle.waveIndex;
game.move.select(moveToUse); game.move.select(moveToUse);
vi.spyOn(game.scene.arena, "trySetWeather");
await game.doKillOpponents(); await game.doKillOpponents();
await game.toNextWave(); await game.toNextWave();
expect(game.scene.arena.trySetWeather).not.toHaveBeenCalled();
expect(game.scene.currentBattle.waveIndex).toBeGreaterThan(waveIndex); expect(game.scene.currentBattle.waveIndex).toBeGreaterThan(waveIndex);
}, 20000); }, 20000);

View File

@ -38,16 +38,15 @@ describe("Reload", () => {
it("should not have RNG inconsistencies after a biome switch", async () => { it("should not have RNG inconsistencies after a biome switch", async () => {
game.override game.override
.startingWave(10) .startingWave(10)
.startingBiome(Biome.CAVE) // Will lead to biomes with randomly generated weather
.battleType("single") .battleType("single")
.startingLevel(100) .startingLevel(100) // Avoid levelling up
.enemyLevel(1000) .enemyLevel(1000) // Avoid opponent dying before game.doKillOpponents()
.disableTrainerWaves() .disableTrainerWaves()
.moveset([Moves.KOWTOW_CLEAVE]) .moveset([Moves.KOWTOW_CLEAVE])
.enemyMoveset(Moves.SPLASH); .enemyMoveset(Moves.SPLASH);
await game.dailyMode.startBattle(); await game.dailyMode.startBattle();
// 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); game.move.select(Moves.KOWTOW_CLEAVE);
await game.phaseInterceptor.to("DamagePhase"); await game.phaseInterceptor.to("DamagePhase");
await game.doKillOpponents(); await game.doKillOpponents();
@ -63,6 +62,34 @@ describe("Reload", () => {
expect(preReloadRngState).toBe(postReloadRngState); expect(preReloadRngState).toBe(postReloadRngState);
}, 20000); }, 20000);
it("should not have weather inconsistencies after a biome switch", async () => {
game.override
.startingWave(10)
.startingBiome(Biome.ICE_CAVE) // Will lead to Snowy Forest with randomly generated weather
.battleType("single")
.startingLevel(100) // Avoid levelling up
.enemyLevel(1000) // Avoid opponent dying before game.doKillOpponents()
.disableTrainerWaves()
.moveset([Moves.KOWTOW_CLEAVE])
.enemyMoveset(Moves.SPLASH);
await game.classicMode.startBattle(); // Apparently daily mode would override the biome
// 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();
await game.toNextWave();
expect(game.phaseInterceptor.log).toContain("NewBiomeEncounterPhase");
const preReloadWeather = game.scene.arena.weather;
await game.reload.reloadSession();
const postReloadWeather = game.scene.arena.weather;
expect(postReloadWeather).toStrictEqual(preReloadWeather);
}, 20000);
it("should not have RNG inconsistencies at a Daily run wild Pokemon fight", async () => { it("should not have RNG inconsistencies at a Daily run wild Pokemon fight", async () => {
await game.dailyMode.startBattle(); await game.dailyMode.startBattle();

View File

@ -31,7 +31,6 @@ import TargetSelectUiHandler from "#app/ui/target-select-ui-handler";
import { Mode } from "#app/ui/ui"; import { Mode } from "#app/ui/ui";
import { Button } from "#enums/buttons"; import { Button } from "#enums/buttons";
import { ExpNotification } from "#enums/exp-notification"; import { ExpNotification } from "#enums/exp-notification";
import { GameDataType } from "#enums/game-data-type";
import { PlayerGender } from "#enums/player-gender"; import { PlayerGender } from "#enums/player-gender";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import { generateStarter, waitUntil } from "#test/utils/gameManagerUtils"; import { generateStarter, waitUntil } from "#test/utils/gameManagerUtils";
@ -371,13 +370,11 @@ export default class GameManager {
* @returns A promise that resolves with the exported save data. * @returns A promise that resolves with the exported save data.
*/ */
exportSaveToTest(): Promise<string> { exportSaveToTest(): Promise<string> {
const saveKey = "x0i2O7WRiANTqPmZ";
return new Promise(async (resolve) => { return new Promise(async (resolve) => {
await this.scene.gameData.saveAll(this.scene, true, true, true, true); const sessionSaveData = this.scene.gameData.getSessionSaveData(this.scene);
this.scene.reset(true); const encryptedSaveData = AES.encrypt(JSON.stringify(sessionSaveData), saveKey).toString();
await waitUntil(() => this.scene.ui?.getMode() === Mode.TITLE); resolve(encryptedSaveData);
await this.scene.gameData.tryExportData(GameDataType.SESSION, 0);
await waitUntil(() => localStorage.hasOwnProperty("toExport"));
return resolve(localStorage.getItem("toExport")!); // TODO: is this bang correct?;
}); });
} }

View File

@ -5,11 +5,27 @@ import { vi } from "vitest";
import { BattleStyle } from "#app/enums/battle-style"; import { BattleStyle } from "#app/enums/battle-style";
import { CommandPhase } from "#app/phases/command-phase"; import { CommandPhase } from "#app/phases/command-phase";
import { TurnInitPhase } from "#app/phases/turn-init-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. * Helper to allow reloading sessions in unit tests.
*/ */
export class ReloadHelper extends GameManagerHelper { 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<boolean>((resolve, reject) => {
this.sessionData = scene.gameData.getSessionSaveData(scene);
resolve(true);
});
});
}
/** /**
* Simulate reloading the session from the title screen, until reaching the * Simulate reloading the session from the title screen, until reaching the
* beginning of the first turn (equivalent to running `startBattle()`) for * beginning of the first turn (equivalent to running `startBattle()`) for
@ -17,7 +33,6 @@ export class ReloadHelper extends GameManagerHelper {
*/ */
async reloadSession() : Promise<void> { async reloadSession() : Promise<void> {
const scene = this.game.scene; const scene = this.game.scene;
const sessionData = scene.gameData.getSessionSaveData(scene);
const titlePhase = new TitlePhase(scene); const titlePhase = new TitlePhase(scene);
scene.clearPhaseQueue(); scene.clearPhaseQueue();
@ -25,7 +40,7 @@ export class ReloadHelper extends GameManagerHelper {
// Set the last saved session to the desired session data // Set the last saved session to the desired session data
vi.spyOn(scene.gameData, "getSession").mockReturnValue( vi.spyOn(scene.gameData, "getSession").mockReturnValue(
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
resolve(sessionData); resolve(this.sessionData);
}) })
); );
scene.unshiftPhase(titlePhase); scene.unshiftPhase(titlePhase);