From 5217acaf8bfc96f4db0931b6b1e781e93d3e8bb0 Mon Sep 17 00:00:00 2001 From: Matheus Alves Date: Mon, 2 Jun 2025 16:31:53 +0100 Subject: [PATCH] Implement Name Run Feat Modified load session ui component, adding a submenu when selecting a 3 slot. This menu has 4 options: Load Game -> Behaves as before, allowing the player to continue progress from the last saved state in the slot. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename Run -> Overlays a rename form, allowing the player to type a name for the run, checking for string validity, with the option to cancel or confirm (Rename). Delete Run -> Prompts user confirmation to delete save data, removing the current save slot from the users save data. Cancel -> Hides menu overlay. Modified game data to implement a function to accept and store runNameText to the users data. Modified run info ui component, to display the chosen name when viewing run information. Example: When loading the game, the user can choose the Load Game menu option, then select a save slot, prompting the menu, then choose "Rename Run" and type the name "Monotype Water Run" then confirm, thus being able to better organize their save files. Signed-off-by: Matheus Alves Co-authored-by: Inês Simões --- src/system/game-data.ts | 42 +++++++ src/ui/run-info-ui-handler.ts | 4 + src/ui/save-slot-select-ui-handler.ts | 158 ++++++++++++++++++++++---- 3 files changed, 182 insertions(+), 22 deletions(-) diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 00b46b9e5f4..84b6367c38f 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -126,6 +126,7 @@ export interface SessionSaveData { battleType: BattleType; trainer: TrainerData; gameVersion: string; + runNameText: string; timestamp: number; challenges: ChallengeData[]; mysteryEncounterType: MysteryEncounterType | -1; // Only defined when current wave is ME, @@ -978,6 +979,47 @@ export class GameData { }); } + renameSession(slotId: number, newName: string): Promise { + return new Promise(async resolve => { + if (slotId < 0) { + return resolve(false); + } + + const sessionData: SessionSaveData | null = await this.getSession(slotId); + + if (!sessionData) return resolve(false); + + sessionData.runNameText = newName; + + const updatedDataStr = JSON.stringify(sessionData); + + if (bypassLogin) { + localStorage.setItem( + `sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`, + encrypt(updatedDataStr, bypassLogin), + ); + resolve(true); + } else { + const encrypted = encrypt(updatedDataStr, bypassLogin); + const secretId = this.secretId; + const trainerId = this.trainerId; + + const error = await pokerogueApi.savedata.session.update( + { slot: slotId, trainerId, secretId, clientSessionId }, + encrypted, + ); + + if (!error) { + localStorage.setItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`, encrypted); // Keep local in sync + resolve(true); + } else { + console.error("Failed to update session name:", error); + resolve(false); + } + } + }); + } + loadSession(slotId: number, sessionData?: SessionSaveData): Promise { // biome-ignore lint/suspicious/noAsyncPromiseExecutor: return new Promise(async (resolve, reject) => { diff --git a/src/ui/run-info-ui-handler.ts b/src/ui/run-info-ui-handler.ts index a4de2215fa4..a65802572da 100644 --- a/src/ui/run-info-ui-handler.ts +++ b/src/ui/run-info-ui-handler.ts @@ -206,6 +206,10 @@ export default class RunInfoUiHandler extends UiHandler { headerText.setOrigin(0, 0); headerText.setPositionRelative(headerBg, 8, 4); this.runContainer.add(headerText); + const runName = addTextObject(0, 0, this.runInfo.runNameText, TextStyle.WINDOW); + runName.setOrigin(0, 0); + runName.setPositionRelative(headerBg, 60, 4); + this.runContainer.add(runName); } /** diff --git a/src/ui/save-slot-select-ui-handler.ts b/src/ui/save-slot-select-ui-handler.ts index 7b4d46203c9..07f1ff14474 100644 --- a/src/ui/save-slot-select-ui-handler.ts +++ b/src/ui/save-slot-select-ui-handler.ts @@ -6,6 +6,7 @@ import { GameMode } from "../game-mode"; import * as Modifier from "#app/modifier/modifier"; import type { SessionSaveData } from "../system/game-data"; import type PokemonData from "../system/pokemon-data"; +import type { OptionSelectConfig } from "#app/ui/abstact-option-select-ui-handler"; import { isNullOrUndefined, fixedInt, getPlayTimeString, formatLargeNumber } from "#app/utils/common"; import MessageUiHandler from "./message-ui-handler"; import { TextStyle, addTextObject } from "./text"; @@ -14,7 +15,7 @@ import { addWindow } from "./ui-theme"; import { RunDisplayMode } from "#app/ui/run-info-ui-handler"; const SESSION_SLOTS_COUNT = 5; -const SLOTS_ON_SCREEN = 3; +const SLOTS_ON_SCREEN = 2; export enum SaveSlotUiMode { LOAD, @@ -32,6 +33,7 @@ export default class SaveSlotSelectUiHandler extends MessageUiHandler { private uiMode: SaveSlotUiMode; private saveSlotSelectCallback: SaveSlotSelectCallback | null; + protected manageDataConfig: OptionSelectConfig; private scrollCursor = 0; @@ -100,6 +102,7 @@ export default class SaveSlotSelectUiHandler extends MessageUiHandler { processInput(button: Button): boolean { const ui = this.getUi(); + const manageDataOptions: any[] = []; let success = false; let error = false; @@ -113,9 +116,107 @@ export default class SaveSlotSelectUiHandler extends MessageUiHandler { } else { switch (this.uiMode) { case SaveSlotUiMode.LOAD: - this.saveSlotSelectCallback = null; - originalCallback?.(cursor); + manageDataOptions.push({ + label: i18next.t("menu:loadGame"), + handler: () => { + globalScene.ui.revertMode(); + originalCallback?.(cursor); + return true; + }, + keepOpen: false, + }); + + manageDataOptions.push({ + label: i18next.t("saveSlotSelectUiHandler:renameRun"), + handler: () => { + globalScene.ui.revertMode(); + ui.setOverlayMode( + UiMode.RENAME_POKEMON, + { + buttonActions: [ + (sanitizedName: string) => { + const name = decodeURIComponent((atob(sanitizedName))); + globalScene.gameData.renameSession(cursor, name).then(response => { + if (response[0] === false) { + globalScene.reset(true); + } else { + this.clearSessionSlots(); + this.cursorObj = null; + this.populateSessionSlots(); + this.setScrollCursor(0); + this.setCursor(0); + ui.revertMode(); + ui.showText("", 0); + } + }); + }, + () => { + ui.revertMode(); + }, + ], + }, + "", + ); + return true; + }, + }); + + this.manageDataConfig = { + xOffset: 0, + yOffset: 48, + options: manageDataOptions, + maxOptions: 4, + }; + + manageDataOptions.push({ + label: i18next.t("saveSlotSelectUiHandler:deleteRun"), + handler: () => { + globalScene.ui.revertMode(); + ui.showText(i18next.t("saveSlotSelectUiHandler:deleteData"), null, () => { + ui.setOverlayMode( + UiMode.CONFIRM, + () => { + globalScene.gameData.tryClearSession(cursor).then(response => { + if (response[0] === false) { + globalScene.reset(true); + } else { + this.clearSessionSlots(); + this.cursorObj = null; + this.populateSessionSlots(); + this.setScrollCursor(0); + this.setCursor(0); + ui.revertMode(); + ui.showText("", 0); + } + }); + }, + () => { + ui.revertMode(); + ui.showText("", 0); + }, + false, + 0, + 19, + import.meta.env.DEV ? 300 : 2000, + ); + }); + return true; + }, + keepOpen: false, + }); + + manageDataOptions.push({ + label: i18next.t("menuUiHandler:cancel"), + handler: () => { + globalScene.ui.revertMode(); + return true; + }, + keepOpen: true, + }); + + ui.setOverlayMode(UiMode.MENU_OPTION_SELECT, this.manageDataConfig); break; + case SaveSlotUiMode.SAVE: { const saveAndCallback = () => { const originalCallback = this.saveSlotSelectCallback; @@ -160,6 +261,7 @@ export default class SaveSlotSelectUiHandler extends MessageUiHandler { } } else { this.saveSlotSelectCallback = null; + ui.showText("", 0); // Clear any UI text if needed originalCallback?.(-1); success = true; } @@ -266,33 +368,34 @@ export default class SaveSlotSelectUiHandler extends MessageUiHandler { this.cursorObj = globalScene.add.container(0, 0); const cursorBox = globalScene.add.nineslice( 0, - 0, + 15, "select_cursor_highlight_thick", undefined, - 296, - 44, + 294, + this.sessionSlots[prevSlotIndex ?? 0]?.saveData?.runNameText ? 50 : 60, 6, 6, 6, 6, ); const rightArrow = globalScene.add.image(0, 0, "cursor"); - rightArrow.setPosition(160, 0); + rightArrow.setPosition(160, 15); rightArrow.setName("rightArrow"); this.cursorObj.add([cursorBox, rightArrow]); this.sessionSlotsContainer.add(this.cursorObj); } const cursorPosition = cursor + this.scrollCursor; - const cursorIncrement = cursorPosition * 56; + const valueHeight = this.sessionSlots[prevSlotIndex ?? 0]?.saveData?.runNameText ? 76 : 76; //nocas + const cursorIncrement = cursorPosition * 76; if (this.sessionSlots[cursorPosition] && this.cursorObj) { const hasData = this.sessionSlots[cursorPosition].hasData; // If the session slot lacks session data, it does not move from its default, central position. // Only session slots with session data will move leftwards and have a visible arrow. if (!hasData) { - this.cursorObj.setPosition(151, 26 + cursorIncrement); + this.cursorObj.setPosition(151, 20 + cursorIncrement); this.sessionSlots[cursorPosition].setPosition(0, cursorIncrement); } else { - this.cursorObj.setPosition(145, 26 + cursorIncrement); + this.cursorObj.setPosition(145, 20 + cursorIncrement); this.sessionSlots[cursorPosition].setPosition(-6, cursorIncrement); } this.setArrowVisibility(hasData); @@ -310,7 +413,8 @@ export default class SaveSlotSelectUiHandler extends MessageUiHandler { revertSessionSlot(slotIndex: number): void { const sessionSlot = this.sessionSlots[slotIndex]; if (sessionSlot) { - sessionSlot.setPosition(0, slotIndex * 56); + const valueHeight = sessionSlot.saveData?.runNameText ? 76 : 76; + sessionSlot.setPosition(0, slotIndex * valueHeight); } } @@ -339,7 +443,7 @@ export default class SaveSlotSelectUiHandler extends MessageUiHandler { this.setCursor(this.cursor, prevSlotIndex); globalScene.tweens.add({ targets: this.sessionSlotsContainer, - y: this.sessionSlotsContainerInitialY - 56 * scrollCursor, + y: this.sessionSlotsContainerInitialY - 76 * scrollCursor, duration: fixedInt(325), ease: "Sine.easeInOut", }); @@ -373,12 +477,12 @@ export default class SaveSlotSelectUiHandler extends MessageUiHandler { class SessionSlot extends Phaser.GameObjects.Container { public slotId: number; public hasData: boolean; + private slotWindow: Phaser.GameObjects.NineSlice; private loadingLabel: Phaser.GameObjects.Text; - public saveData: SessionSaveData; constructor(slotId: number) { - super(globalScene, 0, slotId * 56); + super(globalScene, 0, slotId * 76); this.slotId = slotId; @@ -386,32 +490,42 @@ class SessionSlot extends Phaser.GameObjects.Container { } setup() { - const slotWindow = addWindow(0, 0, 304, 52); - this.add(slotWindow); + this.slotWindow = addWindow(0, 0, 304, 70); + this.add(this.slotWindow); - this.loadingLabel = addTextObject(152, 26, i18next.t("saveSlotSelectUiHandler:loading"), TextStyle.WINDOW); + this.loadingLabel = addTextObject(152, 33, i18next.t("saveSlotSelectUiHandler:loading"), TextStyle.WINDOW); this.loadingLabel.setOrigin(0.5, 0.5); this.add(this.loadingLabel); } async setupWithData(data: SessionSaveData) { + const hasName = data?.runNameText; this.remove(this.loadingLabel, true); + if (hasName) { + const nameLabel = addTextObject(8, 5, data.runNameText, TextStyle.WINDOW); + this.add(nameLabel); + } const gameModeLabel = addTextObject( 8, - 5, + hasName ? 19 : 12, `${GameMode.getModeName(data.gameMode) || i18next.t("gameMode:unkown")} - ${i18next.t("saveSlotSelectUiHandler:wave")} ${data.waveIndex}`, TextStyle.WINDOW, ); this.add(gameModeLabel); - const timestampLabel = addTextObject(8, 19, new Date(data.timestamp).toLocaleString(), TextStyle.WINDOW); + const timestampLabel = addTextObject( + 8, + hasName ? 33 : 26, + new Date(data.timestamp).toLocaleString(), + TextStyle.WINDOW, + ); this.add(timestampLabel); - const playTimeLabel = addTextObject(8, 33, getPlayTimeString(data.playTime), TextStyle.WINDOW); + const playTimeLabel = addTextObject(8, hasName ? 47 : 40, getPlayTimeString(data.playTime), TextStyle.WINDOW); this.add(playTimeLabel); - const pokemonIconsContainer = globalScene.add.container(144, 4); + const pokemonIconsContainer = globalScene.add.container(144, hasName ? 16 : 9); data.party.forEach((p: PokemonData, i: number) => { const iconContainer = globalScene.add.container(26 * i, 0); iconContainer.setScale(0.75); @@ -440,7 +554,7 @@ class SessionSlot extends Phaser.GameObjects.Container { this.add(pokemonIconsContainer); - const modifierIconsContainer = globalScene.add.container(148, 30); + const modifierIconsContainer = globalScene.add.container(148, 38); modifierIconsContainer.setScale(0.5); let visibleModifierIndex = 0; for (const m of data.modifiers) {