diff --git a/src/@types/user-info.ts b/src/@types/user-info.ts index c8a0c6ecb26..31a329a474d 100644 --- a/src/@types/user-info.ts +++ b/src/@types/user-info.ts @@ -1,3 +1,4 @@ +// TODO: Document this with default values export interface UserInfo { username: string; lastSessionSlot: number; diff --git a/src/phases/title-phase.ts b/src/phases/title-phase.ts index 9535ea1c8e9..f0cf2fd25ce 100644 --- a/src/phases/title-phase.ts +++ b/src/phases/title-phase.ts @@ -15,20 +15,21 @@ import { getBiomeKey } from "#field/arena"; import type { Modifier } from "#modifiers/modifier"; import { getDailyRunStarterModifiers, regenerateModifierPoolThresholds } from "#modifiers/modifier-type"; import { vouchers } from "#system/voucher"; -import type { SessionSaveData } from "#types/save-data"; import type { OptionSelectConfig, OptionSelectItem } from "#ui/abstract-option-select-ui-handler"; import { SaveSlotUiMode } from "#ui/save-slot-select-ui-handler"; import { isLocal, isLocalServerConnected } from "#utils/common"; import { getPokemonSpecies } from "#utils/pokemon-utils"; import i18next from "i18next"; +const NO_SAVE_SLOT = -1; + export class TitlePhase extends Phase { public readonly phaseName = "TitlePhase"; private loaded = false; - private lastSessionData: SessionSaveData; + // TODO: Make `end` take a `GameModes` as a parameter rather than storing it on the class itself public gameMode: GameModes; - start(): void { + async start(): Promise { super.start(); globalScene.ui.clearText(); @@ -36,30 +37,46 @@ export class TitlePhase extends Phase { globalScene.playBgm("title", true); - globalScene.gameData - .getSession(loggedInUser?.lastSessionSlot ?? -1) - .then(sessionData => { - if (sessionData) { - this.lastSessionData = sessionData; - const biomeKey = getBiomeKey(sessionData.arena.biome); - const bgTexture = `${biomeKey}_bg`; - globalScene.arenaBg.setTexture(bgTexture); - } - this.showOptions(); - }) - .catch(err => { - console.error(err); - this.showOptions(); - }); + const lastSlot = await this.checkLastSaveSlot(); + await this.showOptions(lastSlot); } - showOptions(): void { + /** + * If a user is logged in, check the last save slot they loaded and adjust various variables + * to account for it. + * @returns A Promise that resolves with the last loaded session's slot ID. + * Returns `NO_SAVE_SLOT` if not logged in or no session was found. + */ + private async checkLastSaveSlot(): Promise { + if (loggedInUser == null) { + return NO_SAVE_SLOT; + } + try { + const sessionData = await globalScene.gameData.getSession(loggedInUser.lastSessionSlot); + if (!sessionData) { + return NO_SAVE_SLOT; + } + + globalScene.sessionSlotId = loggedInUser.lastSessionSlot; + // Set the BG texture to the last save's current biome + const biomeKey = getBiomeKey(sessionData.arena.biome); + const bgTexture = `${biomeKey}_bg`; + globalScene.arenaBg.setTexture(bgTexture); + return loggedInUser.lastSessionSlot; + } catch (err) { + console.error(err); + return NO_SAVE_SLOT; + } + } + + private async showOptions(lastSessionSlot: number): Promise { const options: OptionSelectItem[] = []; - if (loggedInUser && loggedInUser.lastSessionSlot > -1) { + // Add a "continue" menu if the session slot ID is >-1 + if (lastSessionSlot > NO_SAVE_SLOT) { options.push({ label: i18next.t("continue", { ns: "menu" }), handler: () => { - this.loadSaveSlot(this.lastSessionData || !loggedInUser ? -1 : loggedInUser.lastSessionSlot); + this.loadSaveSlot(lastSessionSlot); return true; }, }); @@ -136,8 +153,9 @@ export class TitlePhase extends Phase { label: i18next.t("menu:loadGame"), handler: () => { globalScene.ui.setOverlayMode(UiMode.SAVE_SLOT, SaveSlotUiMode.LOAD, (slotId: number) => { - if (slotId === -1) { - return this.showOptions(); + if (slotId === NO_SAVE_SLOT) { + console.warn("Attempted to load save slot of -1 through load game menu!"); + return this.showOptions(slotId); } this.loadSaveSlot(slotId); }); @@ -166,30 +184,26 @@ export class TitlePhase extends Phase { noCancel: true, yOffset: 47, }; - globalScene.ui.setMode(UiMode.TITLE, config); + await globalScene.ui.setMode(UiMode.TITLE, config); } - loadSaveSlot(slotId: number): void { - globalScene.sessionSlotId = slotId > -1 || !loggedInUser ? slotId : loggedInUser.lastSessionSlot; + // TODO: Make callers actually wait for the save slot to load + private async loadSaveSlot(slotId: number): Promise { + // TODO: Do we need to `await` this? globalScene.ui.setMode(UiMode.MESSAGE); globalScene.ui.resetModeChain(); - globalScene.gameData - .loadSession(slotId, slotId === -1 ? this.lastSessionData : undefined) - .then((success: boolean) => { - if (success) { - this.loaded = true; - if (loggedInUser) { - loggedInUser.lastSessionSlot = slotId; - } - globalScene.ui.showText(i18next.t("menu:sessionSuccess"), null, () => this.end()); - } else { - this.end(); - } - }) - .catch(err => { - console.error(err); - globalScene.ui.showText(i18next.t("menu:failedToLoadSession"), null); - }); + try { + const success = await globalScene.gameData.loadSession(slotId); + if (success) { + this.loaded = true; + globalScene.ui.showText(i18next.t("menu:sessionSuccess"), null, () => this.end()); + } else { + this.end(); + } + } catch (err) { + console.error(err); + globalScene.ui.showText(i18next.t("menu:failedToLoadSession"), null); + } } initDailyRun(): void { @@ -294,6 +308,7 @@ export class TitlePhase extends Phase { }); } + // TODO: Refactor this end(): void { if (!this.loaded && !globalScene.gameMode.isDaily) { globalScene.arena.preloadBgm(); @@ -324,6 +339,7 @@ export class TitlePhase extends Phase { } } + // TODO: Move this to a migrate script instead of running it on save slot load for (const achv of Object.keys(globalScene.gameData.achvUnlocks)) { if (vouchers.hasOwnProperty(achv) && achv !== "CLASSIC_VICTORY") { globalScene.validateVoucher(vouchers[achv]); diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 3ffa7482706..6b83647f5fc 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -77,6 +77,7 @@ import { applyChallenges } from "#utils/challenge-utils"; import { executeIf, fixedInt, isLocal, NumberHolder, randInt, randSeedItem } from "#utils/common"; import { decrypt, encrypt } from "#utils/data"; import { getEnumKeys } from "#utils/enums"; +import { getSaveDataLocalStorageKey } from "#utils/game-data-utils"; import { getPokemonSpecies } from "#utils/pokemon-utils"; import { isBeta } from "#utils/utility-vars"; import { AES, enc } from "crypto-js"; @@ -838,57 +839,45 @@ export class GameData { } as SessionSaveData; } - async getSession(slotId: number): Promise { - const { promise, resolve, reject } = Promise.withResolvers(); + async getSession(slotId: number): Promise { + // TODO: Do we need this fallback anymore? if (slotId < 0) { - resolve(null); - return promise; + return; } - const handleSessionData = async (sessionDataStr: string) => { - try { - const sessionData = this.parseSessionData(sessionDataStr); - resolve(sessionData); - } catch (err) { - reject(err); + + // Check local storage for the cached session data + if (bypassLogin || localStorage.getItem(getSaveDataLocalStorageKey(slotId))) { + const sessionData = localStorage.getItem(getSaveDataLocalStorageKey(slotId)); + if (!sessionData) { + console.error("No session data found!"); return; } - }; - - if (!bypassLogin && !localStorage.getItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`)) { - const response = await pokerogueApi.savedata.session.get({ slot: slotId, clientSessionId }); - - if (!response || response?.length === 0 || response?.[0] !== "{") { - console.error(response); - resolve(null); - return promise; - } - - localStorage.setItem( - `sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`, - encrypt(response, bypassLogin), - ); - - await handleSessionData(response); - return promise; + return this.parseSessionData(decrypt(sessionData, bypassLogin)); } - const sessionData = localStorage.getItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`); - if (sessionData) { - await handleSessionData(decrypt(sessionData, bypassLogin)); - return promise; + + // Ask the server API for the save data and store it in localstorage + const response = await pokerogueApi.savedata.session.get({ slot: slotId, clientSessionId }); + + // TODO: This is a far cry from proper JSON validation + if (response == null || response.length === 0 || response.charAt(0) !== "{") { + console.error("Invalid save data JSON detected!", response); + return; } - resolve(null); - return promise; + + localStorage.setItem(getSaveDataLocalStorageKey(slotId), encrypt(response, bypassLogin)); + + return this.parseSessionData(response); } async renameSession(slotId: number, newName: string): Promise { if (slotId < 0) { return false; } + // TODO: Why do we consider renaming to an empty string successful if it does nothing? if (newName === "") { return true; } - const sessionData: SessionSaveData | null = await this.getSession(slotId); - + const sessionData = await this.getSession(slotId); if (!sessionData) { return false; } @@ -902,10 +891,7 @@ export class GameData { const trainerId = this.trainerId; if (bypassLogin) { - localStorage.setItem( - `sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`, - encrypt(updatedDataStr, bypassLogin), - ); + localStorage.setItem(getSaveDataLocalStorageKey(slotId), encrypt(updatedDataStr, bypassLogin)); return true; } @@ -917,186 +903,177 @@ export class GameData { if (response) { return false; } - localStorage.setItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`, encrypted); + localStorage.setItem(getSaveDataLocalStorageKey(slotId), encrypted); const success = await updateUserInfo(); return !(success !== null && !success); } - async loadSession(slotId: number, sessionData?: SessionSaveData): Promise { - const { promise, resolve, reject } = Promise.withResolvers(); - try { - const initSessionFromData = (fromSession: SessionSaveData) => { - if (isLocal || isBeta) { - try { - console.debug( - this.parseSessionData( - JSON.stringify(fromSession, (_, v: any) => (typeof v === "bigint" ? v.toString() : v)), - ), - ); - } catch (err) { - console.debug("Attempt to log session data failed:", err); - } - } + /** + * Load stored session data and re-initialize the game with its contents. + * @param slotIndex - The 0-indexed position of the save slot to load. + * Values `<=0` will be considered invalid. + * @returns A Promise that resolves with whether the session load succeeded + * (i.e. whether a save in the given slot exists) + */ + public async loadSession(slotIndex: number): Promise { + const sessionData = await this.getSession(slotIndex); + if (!sessionData) { + return false; + } + this.initSessionFromData(sessionData); + return true; + } - globalScene.gameMode = getGameMode(fromSession.gameMode || GameModes.CLASSIC); - if (fromSession.challenges) { - globalScene.gameMode.challenges = fromSession.challenges.map(c => c.toChallenge()); - } - - globalScene.setSeed(fromSession.seed || globalScene.game.config.seed[0]); - globalScene.resetSeed(); - - console.log("Seed:", globalScene.seed); - - globalScene.sessionPlayTime = fromSession.playTime || 0; - globalScene.lastSavePlayTime = 0; - - const loadPokemonAssets: Promise[] = []; - - const party = globalScene.getPlayerParty(); - party.splice(0, party.length); - - for (const p of fromSession.party) { - const pokemon = p.toPokemon() as PlayerPokemon; - pokemon.setVisible(false); - loadPokemonAssets.push(pokemon.loadAssets(false)); - party.push(pokemon); - } - - Object.keys(globalScene.pokeballCounts).forEach((key: string) => { - globalScene.pokeballCounts[key] = fromSession.pokeballCounts[key] || 0; - }); - if (Overrides.POKEBALL_OVERRIDE.active) { - globalScene.pokeballCounts = Overrides.POKEBALL_OVERRIDE.pokeballs; - } - - globalScene.money = Math.floor(fromSession.money || 0); - globalScene.updateMoneyText(); - - if (globalScene.money > this.gameStats.highestMoney) { - this.gameStats.highestMoney = globalScene.money; - } - - globalScene.score = fromSession.score; - globalScene.updateScoreText(); - - globalScene.mysteryEncounterSaveData = new MysteryEncounterSaveData(fromSession.mysteryEncounterSaveData); - - globalScene.newArena(fromSession.arena.biome, fromSession.playerFaints); - - const battleType = fromSession.battleType || 0; - const trainerConfig = fromSession.trainer ? trainerConfigs[fromSession.trainer.trainerType] : null; - const mysteryEncounterType = - fromSession.mysteryEncounterType !== -1 ? fromSession.mysteryEncounterType : undefined; - const battle = globalScene.newBattle( - fromSession.waveIndex, - battleType, - fromSession.trainer, - battleType === BattleType.TRAINER - ? trainerConfig?.doubleOnly || fromSession.trainer?.variant === TrainerVariant.DOUBLE - : fromSession.enemyParty.length > 1, - mysteryEncounterType, + // TODO: This needs a giant refactor and overhaul + private async initSessionFromData(fromSession: SessionSaveData): Promise { + if (isLocal || isBeta) { + try { + console.debug( + this.parseSessionData(JSON.stringify(fromSession, (_, v: any) => (typeof v === "bigint" ? v.toString() : v))), ); - battle.enemyLevels = fromSession.enemyParty.map(p => p.level); - - globalScene.arena.init(); - - fromSession.enemyParty.forEach((enemyData, e) => { - const enemyPokemon = enemyData.toPokemon( - battleType, - e, - fromSession.trainer?.variant === TrainerVariant.DOUBLE, - ) as EnemyPokemon; - battle.enemyParty[e] = enemyPokemon; - if (battleType === BattleType.WILD) { - battle.seenEnemyPartyMemberIds.add(enemyPokemon.id); - } - - loadPokemonAssets.push(enemyPokemon.loadAssets()); - }); - - globalScene.arena.weather = fromSession.arena.weather; - globalScene.arena.eventTarget.dispatchEvent( - new WeatherChangedEvent( - WeatherType.NONE, - globalScene.arena.weather?.weatherType!, - globalScene.arena.weather?.turnsLeft!, - globalScene.arena.weather?.maxDuration!, - ), - ); // TODO: is this bang correct? - - globalScene.arena.terrain = fromSession.arena.terrain; - globalScene.arena.eventTarget.dispatchEvent( - new TerrainChangedEvent( - TerrainType.NONE, - globalScene.arena.terrain?.terrainType!, - globalScene.arena.terrain?.turnsLeft!, - globalScene.arena.terrain?.maxDuration!, - ), - ); // TODO: is this bang correct? - - globalScene.arena.playerTerasUsed = fromSession.arena.playerTerasUsed; - - globalScene.arena.tags = fromSession.arena.tags; - if (globalScene.arena.tags) { - for (const tag of globalScene.arena.tags) { - if (tag instanceof EntryHazardTag) { - const { tagType, side, turnCount, maxDuration, layers, maxLayers } = tag as EntryHazardTag; - globalScene.arena.eventTarget.dispatchEvent( - new TagAddedEvent(tagType, side, turnCount, maxDuration, layers, maxLayers), - ); - } else { - globalScene.arena.eventTarget.dispatchEvent( - new TagAddedEvent(tag.tagType, tag.side, tag.turnCount, tag.maxDuration), - ); - } - } - } - - globalScene.arena.positionalTagManager.tags = fromSession.arena.positionalTags.map(tag => - loadPositionalTag(tag), - ); - - if (globalScene.modifiers.length > 0) { - console.warn("Existing modifiers not cleared on session load, deleting..."); - globalScene.modifiers = []; - } - for (const modifierData of fromSession.modifiers) { - const modifier = modifierData.toModifier(Modifier[modifierData.className]); - if (modifier) { - globalScene.addModifier(modifier, true); - } - } - globalScene.updateModifiers(true); - - for (const enemyModifierData of fromSession.enemyModifiers) { - const modifier = enemyModifierData.toModifier(Modifier[enemyModifierData.className]); - if (modifier) { - globalScene.addEnemyModifier(modifier, true); - } - } - - globalScene.updateModifiers(false); - - Promise.all(loadPokemonAssets).then(() => resolve(true)); - }; - if (sessionData) { - initSessionFromData(sessionData); - } else { - this.getSession(slotId) - .then(data => { - return data && initSessionFromData(data); - }) - .catch(err => { - reject(err); - return; - }); + } catch (err) { + console.debug("Attempt to log session data failed: ", err); } - } catch (err) { - reject(err); } - return promise; + globalScene.gameMode = getGameMode(fromSession.gameMode || GameModes.CLASSIC); + if (fromSession.challenges) { + globalScene.gameMode.challenges = fromSession.challenges.map(c => c.toChallenge()); + } + + globalScene.setSeed(fromSession.seed || globalScene.game.config.seed[0]); + globalScene.resetSeed(); + + console.log("Seed:", globalScene.seed); + + globalScene.sessionPlayTime = fromSession.playTime || 0; + globalScene.lastSavePlayTime = 0; + + const loadPokemonAssets: Promise[] = []; + + const party = globalScene.getPlayerParty(); + party.splice(0, party.length); + + for (const p of fromSession.party) { + const pokemon = p.toPokemon() as PlayerPokemon; + pokemon.setVisible(false); + loadPokemonAssets.push(pokemon.loadAssets(false)); + party.push(pokemon); + } + + Object.keys(globalScene.pokeballCounts).forEach((key: string) => { + globalScene.pokeballCounts[key] = fromSession.pokeballCounts[key] || 0; + }); + if (Overrides.POKEBALL_OVERRIDE.active) { + globalScene.pokeballCounts = Overrides.POKEBALL_OVERRIDE.pokeballs; + } + + globalScene.money = Math.floor(fromSession.money || 0); + globalScene.updateMoneyText(); + + if (globalScene.money > this.gameStats.highestMoney) { + this.gameStats.highestMoney = globalScene.money; + } + + globalScene.score = fromSession.score; + globalScene.updateScoreText(); + + globalScene.mysteryEncounterSaveData = new MysteryEncounterSaveData(fromSession.mysteryEncounterSaveData); + + globalScene.newArena(fromSession.arena.biome, fromSession.playerFaints); + + const battleType = fromSession.battleType || 0; + const trainerConfig = fromSession.trainer ? trainerConfigs[fromSession.trainer.trainerType] : null; + const mysteryEncounterType = fromSession.mysteryEncounterType !== -1 ? fromSession.mysteryEncounterType : undefined; + const battle = globalScene.newBattle( + fromSession.waveIndex, + battleType, + fromSession.trainer, + battleType === BattleType.TRAINER + ? trainerConfig?.doubleOnly || fromSession.trainer?.variant === TrainerVariant.DOUBLE + : fromSession.enemyParty.length > 1, + mysteryEncounterType, + ); + battle.enemyLevels = fromSession.enemyParty.map(p => p.level); + + globalScene.arena.init(); + + fromSession.enemyParty.forEach((enemyData, e) => { + const enemyPokemon = enemyData.toPokemon( + battleType, + e, + fromSession.trainer?.variant === TrainerVariant.DOUBLE, + ) as EnemyPokemon; + battle.enemyParty[e] = enemyPokemon; + if (battleType === BattleType.WILD) { + battle.seenEnemyPartyMemberIds.add(enemyPokemon.id); + } + + loadPokemonAssets.push(enemyPokemon.loadAssets()); + }); + + globalScene.arena.weather = fromSession.arena.weather; + globalScene.arena.eventTarget.dispatchEvent( + new WeatherChangedEvent( + WeatherType.NONE, + globalScene.arena.weather?.weatherType!, + globalScene.arena.weather?.turnsLeft!, + globalScene.arena.weather?.maxDuration!, + ), + ); // TODO: is this bang correct? + + globalScene.arena.terrain = fromSession.arena.terrain; + globalScene.arena.eventTarget.dispatchEvent( + new TerrainChangedEvent( + TerrainType.NONE, + globalScene.arena.terrain?.terrainType!, + globalScene.arena.terrain?.turnsLeft!, + globalScene.arena.terrain?.maxDuration!, + ), + ); // TODO: is this bang correct? + + globalScene.arena.playerTerasUsed = fromSession.arena.playerTerasUsed; + + globalScene.arena.tags = fromSession.arena.tags; + if (globalScene.arena.tags) { + for (const tag of globalScene.arena.tags) { + if (tag instanceof EntryHazardTag) { + const { tagType, side, turnCount, maxDuration, layers, maxLayers } = tag as EntryHazardTag; + globalScene.arena.eventTarget.dispatchEvent( + new TagAddedEvent(tagType, side, turnCount, maxDuration, layers, maxLayers), + ); + } else { + globalScene.arena.eventTarget.dispatchEvent( + new TagAddedEvent(tag.tagType, tag.side, tag.turnCount, tag.maxDuration), + ); + } + } + } + + globalScene.arena.positionalTagManager.tags = fromSession.arena.positionalTags.map(tag => loadPositionalTag(tag)); + + if (globalScene.modifiers.length > 0) { + console.warn("Existing modifiers not cleared on session load, deleting..."); + globalScene.modifiers = []; + } + for (const modifierData of fromSession.modifiers) { + const modifier = modifierData.toModifier(Modifier[modifierData.className]); + if (modifier) { + globalScene.addModifier(modifier, true); + } + } + globalScene.updateModifiers(true); + + for (const enemyModifierData of fromSession.enemyModifiers) { + const modifier = enemyModifierData.toModifier(Modifier[enemyModifierData.className]); + if (modifier) { + globalScene.addEnemyModifier(modifier, true); + } + } + + globalScene.updateModifiers(false); + + await Promise.all(loadPokemonAssets); } /** @@ -1108,7 +1085,7 @@ export class GameData { deleteSession(slotId: number): Promise { return new Promise(resolve => { if (bypassLogin) { - localStorage.removeItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`); + localStorage.removeItem(getSaveDataLocalStorageKey(slotId)); return resolve(true); } @@ -1129,7 +1106,7 @@ export class GameData { loggedInUser.lastSessionSlot = -1; } - localStorage.removeItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`); + localStorage.removeItem(getSaveDataLocalStorageKey(slotId)); resolve(true); } }); @@ -1173,7 +1150,7 @@ export class GameData { let result: [boolean, boolean] = [false, false]; if (bypassLogin) { - localStorage.removeItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`); + localStorage.removeItem(getSaveDataLocalStorageKey(slotId)); result = [true, true]; } else { const sessionData = this.getSessionSaveData(); @@ -1188,7 +1165,7 @@ export class GameData { if (loggedInUser) { loggedInUser!.lastSessionSlot = -1; } - localStorage.removeItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`); + localStorage.removeItem(getSaveDataLocalStorageKey(slotId)); } else { if (jsonResponse?.error?.startsWith("session out of date")) { globalScene.phaseManager.clearPhaseQueue(); diff --git a/src/utils/game-data-utils.ts b/src/utils/game-data-utils.ts new file mode 100644 index 00000000000..8027f8f166d --- /dev/null +++ b/src/utils/game-data-utils.ts @@ -0,0 +1,15 @@ +import { loggedInUser } from "#app/account"; + +/** + * Obtain the local storage key corresponding to a given save slot. + * @param slotId - The numerical save slot ID + * Will throw an error if `<0` (in line with standard util functions) + * @returns The local storage key used to access the save data for the given slot. + */ +export function getSaveDataLocalStorageKey(slotId: number): string { + if (slotId < 0) { + throw new Error("Cannot access a negative save slot ID from localstorage!"); + } + + return `sessionData${slotId || ""}_${loggedInUser?.username}`; +} diff --git a/test/test-utils/helpers/reload-helper.ts b/test/test-utils/helpers/reload-helper.ts index e46096f3fab..7f275e97333 100644 --- a/test/test-utils/helpers/reload-helper.ts +++ b/test/test-utils/helpers/reload-helper.ts @@ -56,7 +56,7 @@ export class ReloadHelper extends GameManagerHelper { ); this.game.scene.modifiers = []; } - titlePhase.loadSaveSlot(-1); // Load the desired session data + titlePhase["loadSaveSlot"](0); // Load the desired session data this.game.phaseInterceptor.shiftPhase(); // Loading the save slot also ended TitlePhase, clean it up // Run through prompts for switching Pokemon, copied from classicModeHelper.ts