From 00255cb09a62eb80ca4d26a28c108be89fdd3a04 Mon Sep 17 00:00:00 2001 From: Flashfyre Date: Fri, 15 Mar 2024 15:13:32 -0400 Subject: [PATCH] Fully implement save slots and ""title"" screen changes Fully implement save slots and ""title"" screen changes; fix issues with slots including clear data not working on game over and export/import not working; fix session play time not being recorded correctly --- src/battle-scene.ts | 9 +- src/modifier/modifier.ts | 18 +- src/phases.ts | 115 ++++---- src/system/game-data.ts | 152 ++++++----- src/ui/abstact-option-select-ui-handler.ts | 5 +- src/ui/loading-modal-ui-handler.ts | 2 +- src/ui/menu-ui-handler.ts | 46 +++- src/ui/save-slot-select-ui-handler.ts | 298 +++++++++++++++++++++ src/ui/starter-select-ui-handler.ts | 24 +- src/ui/ui.ts | 15 +- src/utils.ts | 12 +- 11 files changed, 548 insertions(+), 148 deletions(-) create mode 100644 src/ui/save-slot-select-ui-handler.ts diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 10387def3..bed13b3f6 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -622,7 +622,8 @@ export default class BattleScene extends Phaser.Scene { } initSession(): void { - this.sessionPlayTime = 0; + if (this.sessionPlayTime === null) + this.sessionPlayTime = 0; if (this.playTimeTimer) this.playTimeTimer.destroy(); @@ -1336,13 +1337,9 @@ export default class BattleScene extends Phaser.Scene { } } - fadeOutBgm(duration?: integer, destroy?: boolean): boolean { + fadeOutBgm(duration: integer = 500, destroy: boolean = true): boolean { if (!this.bgm) return false; - if (!duration) - duration = 500; - if (destroy === undefined) - destroy = true; const bgm = this.sound.getAllPlaying().find(bgm => bgm.key === this.bgm.key); if (bgm) { SoundFade.fadeOut(this, this.bgm, duration, destroy); diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index c0b7542fc..063ac3dce 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -24,6 +24,13 @@ export type ModifierPredicate = (modifier: Modifier) => boolean; const iconOverflowIndex = 24; +export const modifierSortFunc = (a: Modifier, b: Modifier) => { + const aId = a instanceof PokemonHeldItemModifier ? a.pokemonId : 4294967295; + const bId = b instanceof PokemonHeldItemModifier ? b.pokemonId : 4294967295; + + return aId < bId ? 1 : aId > bId ? -1 : 0; +}; + export class ModifierBar extends Phaser.GameObjects.Container { private player: boolean; private modifierCache: PersistentModifier[]; @@ -40,12 +47,7 @@ export class ModifierBar extends Phaser.GameObjects.Container { const visibleIconModifiers = modifiers.filter(m => m.isIconVisible(this.scene as BattleScene)); - visibleIconModifiers.sort((a: Modifier, b: Modifier) => { - const aId = a instanceof PokemonHeldItemModifier ? a.pokemonId : 4294967295; - const bId = b instanceof PokemonHeldItemModifier ? b.pokemonId : 4294967295; - - return aId < bId ? 1 : aId > bId ? -1 : 0; - }); + visibleIconModifiers.sort(modifierSortFunc); const thisArg = this; @@ -197,7 +199,7 @@ export abstract class PersistentModifier extends Modifier { const text = addTextObject(scene, 8, 12, this.stackCount.toString(), TextStyle.PARTY, { fontSize: '66px', color: !isStackMax ? '#f8f8f8' : maxColor }); text.setShadow(0, 0, null); - text.setStroke('#424242', 16) + text.setStroke('#424242', 16); text.setOrigin(0, 0); return text; @@ -495,6 +497,8 @@ export abstract class PokemonHeldItemModifier extends PersistentModifier { getMaxStackCount(scene: BattleScene, forThreshold?: boolean): integer { const pokemon = this.getPokemon(scene); + if (!pokemon) + return 0; if (pokemon.isPlayer() && forThreshold) return scene.getParty().map(p => this.getMaxHeldItemCount(p)).reduce((stackCount: integer, maxStackCount: integer) => Math.max(stackCount, maxStackCount), 0); return this.getMaxHeldItemCount(pokemon); diff --git a/src/phases.ts b/src/phases.ts index 887a65e06..0d41af5d8 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -51,7 +51,8 @@ import ModifierSelectUiHandler, { SHOP_OPTIONS_ROW_LIMIT } from "./ui/modifier-s import { Setting } from "./system/settings"; import { Tutorial, handleTutorial } from "./tutorial"; import { TerrainType } from "./data/terrain"; -import { OptionSelectItem } from "./ui/abstact-option-select-ui-handler"; +import { OptionSelectConfig, OptionSelectItem } from "./ui/abstact-option-select-ui-handler"; +import { SaveSlotUiMode } from "./ui/save-slot-select-ui-handler"; export class LoginPhase extends Phase { private showText: boolean; @@ -134,23 +135,19 @@ export class TitlePhase extends Phase { start(): void { super.start(); + this.scene.ui.fadeIn(250); + + this.scene.fadeOutBgm(0, false); + + this.showOptions(); + } + + showOptions(): void { const options: OptionSelectItem[] = []; if (loggedInUser?.lastSessionSlot > -1) { options.push({ label: 'Continue', - handler: () => { - this.scene.ui.setMode(Mode.MESSAGE); - this.scene.gameData.loadSession(this.scene, loggedInUser.lastSessionSlot).then((success: boolean) => { - if (success) { - this.loaded = true; - this.scene.ui.showText('Session loaded successfully.', null, () => this.end()); - } else - this.end(); - }).catch(err => { - console.error(err); - this.scene.ui.showText('Your session data could not be loaded.\nIt may be corrupted. Please reload the page.', null); - }); - } + handler: () => this.loadSaveSlot(loggedInUser.lastSessionSlot) }); } options.push({ @@ -158,16 +155,19 @@ export class TitlePhase extends Phase { handler: () => { this.scene.ui.setMode(Mode.MESSAGE); this.scene.ui.clearText(); - this.scene.sessionSlotId = 0; this.end(); } }, - /*{ + { label: 'Load', - handler: () => { - - } - },*/ + handler: () => this.scene.ui.setOverlayMode(Mode.SAVE_SLOT, SaveSlotUiMode.LOAD, + (slotId: integer) => { + if (slotId === -1) + return this.showOptions(); + this.loadSaveSlot(slotId); + } + ) + }, /*{ label: 'Daily Run', handler: () => { @@ -176,8 +176,25 @@ export class TitlePhase extends Phase { }, keepOpen: true }*/); - this.scene.ui.setMode(Mode.OPTION_SELECT, { - options: options + const config: OptionSelectConfig = { + options: options, + noCancel: true + }; + this.scene.ui.setMode(Mode.OPTION_SELECT, config); + } + + loadSaveSlot(slotId: integer): void { + this.scene.sessionSlotId = slotId; + this.scene.ui.setMode(Mode.MESSAGE); + this.scene.gameData.loadSession(this.scene, slotId).then((success: boolean) => { + if (success) { + this.loaded = true; + this.scene.ui.showText('Session loaded successfully.', null, () => this.end()); + } else + this.end(); + }).catch(err => { + console.error(err); + this.scene.ui.showText('Your session data could not be loaded.\nIt may be corrupted.', null); }); } @@ -303,28 +320,35 @@ export class SelectStarterPhase extends Phase { this.scene.playBgm('menu'); this.scene.ui.setMode(Mode.STARTER_SELECT, (starters: Starter[]) => { - const party = this.scene.getParty(); - const loadPokemonAssets: Promise[] = []; - for (let starter of starters) { - const starterProps = this.scene.gameData.getSpeciesDexAttrProps(starter.species, starter.dexAttr); - const starterFormIndex = Math.min(starterProps.formIndex, Math.max(starter.species.forms.length - 1, 0)); - const starterGender = starter.species.malePercent !== null - ? !starterProps.female ? Gender.MALE : Gender.FEMALE - : Gender.GENDERLESS; - const starterIvs = this.scene.gameData.dexData[starter.species.speciesId].ivs.slice(0); - const starterPokemon = this.scene.addPlayerPokemon(starter.species, startingLevel, starterProps.abilityIndex, starterFormIndex, starterGender, starterProps.shiny, starterIvs, starter.nature); - starterPokemon.tryPopulateMoveset(starter.moveset); - if (starter.pokerus) - starterPokemon.pokerus = true; - if (this.scene.gameMode.isSplicedOnly) - starterPokemon.generateFusionSpecies(true); - starterPokemon.setVisible(false); - party.push(starterPokemon); - loadPokemonAssets.push(starterPokemon.loadAssets()); - } - Promise.all(loadPokemonAssets).then(() => { - this.scene.ui.clearText(); - this.scene.ui.setMode(Mode.MESSAGE).then(() => { + this.scene.ui.clearText(); + this.scene.ui.setMode(Mode.SAVE_SLOT, SaveSlotUiMode.SAVE, (slotId: integer) => { + if (slotId === -1) { + this.scene.clearPhaseQueue(); + this.scene.pushPhase(new TitlePhase(this.scene)); + return this.end(); + } + this.scene.sessionSlotId = slotId; + + const party = this.scene.getParty(); + const loadPokemonAssets: Promise[] = []; + for (let starter of starters) { + const starterProps = this.scene.gameData.getSpeciesDexAttrProps(starter.species, starter.dexAttr); + const starterFormIndex = Math.min(starterProps.formIndex, Math.max(starter.species.forms.length - 1, 0)); + const starterGender = starter.species.malePercent !== null + ? !starterProps.female ? Gender.MALE : Gender.FEMALE + : Gender.GENDERLESS; + const starterIvs = this.scene.gameData.dexData[starter.species.speciesId].ivs.slice(0); + const starterPokemon = this.scene.addPlayerPokemon(starter.species, startingLevel, starterProps.abilityIndex, starterFormIndex, starterGender, starterProps.shiny, starterIvs, starter.nature); + starterPokemon.tryPopulateMoveset(starter.moveset); + if (starter.pokerus) + starterPokemon.pokerus = true; + if (this.scene.gameMode.isSplicedOnly) + starterPokemon.generateFusionSpecies(true); + starterPokemon.setVisible(false); + party.push(starterPokemon); + loadPokemonAssets.push(starterPokemon.loadAssets()); + } + Promise.all(loadPokemonAssets).then(() => { SoundFade.fadeOut(this.scene, this.scene.sound.get('menu'), 500, true); this.scene.time.delayedCall(500, () => this.scene.playBgm()); if (this.scene.gameMode.isClassic) @@ -332,6 +356,7 @@ export class SelectStarterPhase extends Phase { else this.scene.gameData.gameStats.endlessSessionsPlayed++; this.scene.newBattle(); + this.scene.sessionPlayTime = 0; this.end(); }); }); @@ -3029,7 +3054,7 @@ export class GameOverPhase extends BattlePhase { start() { super.start(); - this.scene.gameData.clearSession().then(() => { + this.scene.gameData.clearSession(this.scene.sessionSlotId).then(() => { this.scene.time.delayedCall(1000, () => { let firstClear = false; if (this.victory) { diff --git a/src/system/game-data.ts b/src/system/game-data.ts index ad785b98c..3ba087fcc 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -33,7 +33,6 @@ const saveKey = 'x0i2O7WRiANTqPmZ'; // Temporary; secure encryption is not yet n export enum GameDataType { SYSTEM, SESSION, - DAILY_SESSION, SETTINGS, TUTORIALS } @@ -44,12 +43,15 @@ export enum PlayerGender { FEMALE } -export function getDataTypeKey(dataType: GameDataType): string { +export function getDataTypeKey(dataType: GameDataType, slotId: integer = 0): string { switch (dataType) { case GameDataType.SYSTEM: return 'data'; case GameDataType.SESSION: - return 'sessionData'; + let ret = 'sessionData'; + if (slotId) + ret += slotId; + return ret; case GameDataType.SETTINGS: return 'settings'; case GameDataType.TUTORIALS: @@ -74,7 +76,7 @@ interface SystemSaveData { timestamp: integer; } -interface SessionSaveData { +export interface SessionSaveData { seed: string; playTime: integer; gameMode: GameModes; @@ -496,12 +498,38 @@ export class GameData { }); } - loadSession(scene: BattleScene, slotId: integer): Promise { + getSession(slotId: integer): Promise { return new Promise(async (resolve, reject) => { const handleSessionData = async (sessionDataStr: string) => { try { const sessionData = this.parseSessionData(sessionDataStr); + resolve(sessionData); + } catch (err) { + reject(err); + return; + } + }; + if (!bypassLogin) { + Utils.apiFetch(`savedata/get?datatype=${GameDataType.SESSION}&slot=${slotId}`) + .then(response => response.text()) + .then(async response => { + if (!response.length || response[0] !== '{') { + console.error(response); + return resolve(null); + } + + await handleSessionData(response); + }); + } else + await handleSessionData(atob(localStorage.getItem(`sessionData${slotId ? slotId : ''}`))); + }); + } + + loadSession(scene: BattleScene, slotId: integer): Promise { + return new Promise(async (resolve, reject) => { + try { + this.getSession(slotId).then(async sessionData => { console.debug(sessionData); scene.seed = sessionData.seed || scene.game.config.seed[0]; @@ -562,41 +590,27 @@ export class GameData { scene.updateModifiers(true); - // TODO: Remove if - if (battle.battleSpec !== BattleSpec.FINAL_BOSS) { - for (let enemyModifierData of sessionData.enemyModifiers) { - const modifier = enemyModifierData.toModifier(scene, modifiersModule[enemyModifierData.className]); - if (modifier) - scene.addEnemyModifier(modifier, true); - } - - scene.updateModifiers(false); + for (let enemyModifierData of sessionData.enemyModifiers) { + const modifier = enemyModifierData.toModifier(scene, modifiersModule[enemyModifierData.className]); + if (modifier) + scene.addEnemyModifier(modifier, true); } + scene.updateModifiers(false); + Promise.all(loadPokemonAssets).then(() => resolve(true)); - } catch (err) { + }).catch(err => { reject(err); return; - } - }; - - if (!bypassLogin) { - Utils.apiFetch(`savedata/get?datatype=${GameDataType.SESSION}&slot=${slotId}`) - .then(response => response.text()) - .then(async response => { - if (!response.length || response[0] !== '{') { - console.error(response); - return resolve(false); - } - - await handleSessionData(response); - }); - } else - await handleSessionData(atob(localStorage.getItem(`sessionData${slotId ? slotId : ''}`))); + }); + } catch (err) { + reject(err); + return; + } }); } - clearSession(): Promise { + clearSession(slotId: integer): Promise { return new Promise(resolve => { if (bypassLogin) { localStorage.removeItem('sessionData'); @@ -606,9 +620,9 @@ export class GameData { updateUserInfo().then(success => { if (success !== null && !success) return resolve(false); - Utils.apiFetch(`savedata/delete?datatype=${GameDataType.SESSION}`).then(response => { + Utils.apiFetch(`savedata/delete?datatype=${GameDataType.SESSION}&slot=${slotId}`).then(response => { if (response.ok) { - loggedInUser.hasGameSession = false; + loggedInUser.lastSessionSlot = -1; return resolve(true); } resolve(false); @@ -654,39 +668,47 @@ export class GameData { }) as SessionSaveData; } - public exportData(dataType: GameDataType): void { - const dataKey: string = getDataTypeKey(dataType); - const handleData = (dataStr: string) => { - switch (dataType) { - case GameDataType.SYSTEM: - dataStr = this.convertSystemDataStr(dataStr, true); - break; - } - const encryptedData = AES.encrypt(dataStr, saveKey); - const blob = new Blob([ encryptedData.toString() ], {type: 'text/json'}); - const link = document.createElement('a'); - link.href = window.URL.createObjectURL(blob); - link.download = `${dataKey}.prsv`; - link.click(); - link.remove(); - }; - if (!bypassLogin && dataType < GameDataType.SETTINGS) { - Utils.apiFetch(`savedata/get?datatype=${dataType}`) - .then(response => response.text()) - .then(response => { - if (!response.length || response[0] !== '{') { - console.error(response); - return; - } + public tryExportData(dataType: GameDataType, slotId: integer = 0): Promise { + return new Promise(resolve => { + const dataKey: string = getDataTypeKey(dataType, slotId); + const handleData = (dataStr: string) => { + switch (dataType) { + case GameDataType.SYSTEM: + dataStr = this.convertSystemDataStr(dataStr, true); + break; + } + const encryptedData = AES.encrypt(dataStr, saveKey); + const blob = new Blob([ encryptedData.toString() ], {type: 'text/json'}); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = `${dataKey}.prsv`; + link.click(); + link.remove(); + }; + if (!bypassLogin && dataType < GameDataType.SETTINGS) { + Utils.apiFetch(`savedata/get?datatype=${dataType}${dataType === GameDataType.SESSION ? `&slot=${slotId}` : ''}`) + .then(response => response.text()) + .then(response => { + if (!response.length || response[0] !== '{') { + console.error(response); + resolve(false); + return; + } - handleData(response); - }); - } else - handleData(atob(localStorage.getItem(dataKey))); + handleData(response); + resolve(true); + }); + } else { + const data = localStorage.getItem(dataKey); + if (data) + handleData(atob(data)); + resolve(!!data); + } + }); } - public importData(dataType: GameDataType): void { - const dataKey = getDataTypeKey(dataType); + public importData(dataType: GameDataType, slotId: integer = 0): void { + const dataKey = getDataTypeKey(dataType, slotId); let saveFile: any = document.getElementById('saveFile'); if (saveFile) @@ -751,7 +773,7 @@ export class GameData { updateUserInfo().then(success => { if (!success) return displayError(`Could not contact the server. Your ${dataName} data could not be imported.`); - Utils.apiPost(`savedata/update?datatype=${dataType}`, dataStr) + Utils.apiPost(`savedata/update?datatype=${dataType}${dataType === GameDataType.SESSION ? `&slot=${slotId}` : ''}`, dataStr) .then(response => response.text()) .then(error => { if (error) { diff --git a/src/ui/abstact-option-select-ui-handler.ts b/src/ui/abstact-option-select-ui-handler.ts index 4168c617b..b9186ee13 100644 --- a/src/ui/abstact-option-select-ui-handler.ts +++ b/src/ui/abstact-option-select-ui-handler.ts @@ -9,6 +9,7 @@ export interface OptionSelectConfig { yOffset?: number; options: OptionSelectItem[]; maxOptions?: integer; + noCancel?: boolean; } export interface OptionSelectItem { @@ -110,8 +111,10 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler { if (this.config?.maxOptions && this.config.options.length > this.config.maxOptions) { this.scrollCursor = (this.config.options.length - this.config.maxOptions) + 1; this.cursor = options.length - 1; - } else + } else if (!this.config?.noCancel) this.setCursor(options.length - 1); + else + return false; } const option = this.config.options[this.cursor + (this.scrollCursor - (this.scrollCursor ? 1 : 0))]; option.handler(); diff --git a/src/ui/loading-modal-ui-handler.ts b/src/ui/loading-modal-ui-handler.ts index 11c96fe4d..731348905 100644 --- a/src/ui/loading-modal-ui-handler.ts +++ b/src/ui/loading-modal-ui-handler.ts @@ -31,7 +31,7 @@ export default class LoadingModalUiHandler extends ModalUiHandler { setup(): void { super.setup(); - const label = addTextObject(this.scene, this.getWidth() / 2, this.getHeight() / 2, 'Loading...', TextStyle.WINDOW); + const label = addTextObject(this.scene, this.getWidth() / 2, this.getHeight() / 2, 'Loading…', TextStyle.WINDOW); label.setOrigin(0.5, 0.5); this.modalContainer.add(label); diff --git a/src/ui/menu-ui-handler.ts b/src/ui/menu-ui-handler.ts index 4b35a81dc..850505ac1 100644 --- a/src/ui/menu-ui-handler.ts +++ b/src/ui/menu-ui-handler.ts @@ -86,14 +86,54 @@ export default class MenuUiHandler extends MessageUiHandler { const manageDataOptions = []; + const confirmSlot = (message: string, slotFilter: (i: integer) => boolean, callback: (i: integer) => void) => { + ui.revertMode(); + ui.showText(message, null, () => { + const config: OptionSelectConfig = { + options: new Array(3).fill(null).map((_, i) => i).filter(slotFilter).map(i => { + return { + label: `Slot ${i + 1}`, + handler: () => { + callback(i); + ui.revertMode(); + ui.showText(null, 0); + } + }; + }).concat([{ + label: 'Cancel', + handler: () => { + ui.revertMode(); + ui.showText(null, 0); + } + }]), + xOffset: 98 + }; + ui.setOverlayMode(Mode.OPTION_SELECT, config); + }); + }; + manageDataOptions.push({ label: 'Import Session', - handler: () => this.scene.gameData.importData(GameDataType.SESSION), + handler: () => confirmSlot('Select a slot to import to.', () => true, slotId => this.scene.gameData.importData(GameDataType.SESSION, slotId)), keepOpen: true }); manageDataOptions.push({ label: 'Export Session', - handler: () => this.scene.gameData.exportData(GameDataType.SESSION), + handler: () => { + const dataSlots: integer[] = []; + Promise.all( + new Array(3).fill(null).map((_, i) => { + const slotId = i; + return this.scene.gameData.getSession(slotId).then(data => { + if (data) + dataSlots.push(slotId); + }) + })).then(() => { + confirmSlot('Select a slot to export from.', + i => dataSlots.indexOf(i) > -1, + slotId => this.scene.gameData.tryExportData(GameDataType.SESSION, slotId)); + }); + }, keepOpen: true }); manageDataOptions.push({ @@ -104,7 +144,7 @@ export default class MenuUiHandler extends MessageUiHandler { manageDataOptions.push( { label: 'Export Data', - handler: () => this.scene.gameData.exportData(GameDataType.SYSTEM), + handler: () => this.scene.gameData.tryExportData(GameDataType.SYSTEM), keepOpen: true }, { diff --git a/src/ui/save-slot-select-ui-handler.ts b/src/ui/save-slot-select-ui-handler.ts new file mode 100644 index 000000000..565ad0b86 --- /dev/null +++ b/src/ui/save-slot-select-ui-handler.ts @@ -0,0 +1,298 @@ +import BattleScene, { Button } from "../battle-scene"; +import { gameModes } from "../game-mode"; +import { SessionSaveData } from "../system/game-data"; +import { TextStyle, addTextObject } from "./text"; +import { Mode } from "./ui"; +import { addWindow } from "./window"; +import * as Utils from "../utils"; +import PokemonData from "../system/pokemon-data"; +import { PokemonHeldItemModifier } from "../modifier/modifier"; +import { TitlePhase } from "../phases"; +import MessageUiHandler from "./message-ui-handler"; + +const sessionSlotCount = 3; + +export enum SaveSlotUiMode { + LOAD, + SAVE +} + +export type SaveSlotSelectCallback = (cursor: integer) => void; + +export default class SaveSlotSelectUiHandler extends MessageUiHandler { + + private saveSlotSelectContainer: Phaser.GameObjects.Container; + private sessionSlotsContainer: Phaser.GameObjects.Container; + private saveSlotSelectMessageBox: Phaser.GameObjects.NineSlice; + private saveSlotSelectMessageBoxContainer: Phaser.GameObjects.Container; + private sessionSlots: SessionSlot[]; + + private uiMode: SaveSlotUiMode; + private saveSlotSelectCallback: SaveSlotSelectCallback; + + private cursorObj: Phaser.GameObjects.NineSlice; + + constructor(scene: BattleScene) { + super(scene, Mode.SAVE_SLOT); + } + + setup() { + const ui = this.getUi(); + + this.saveSlotSelectContainer = this.scene.add.container(0, 0); + this.saveSlotSelectContainer.setVisible(false); + ui.add(this.saveSlotSelectContainer); + + const loadSessionBg = this.scene.add.rectangle(0, 0, this.scene.game.canvas.width / 6, -this.scene.game.canvas.height / 6, 0x006860); + loadSessionBg.setOrigin(0, 0); + this.saveSlotSelectContainer.add(loadSessionBg); + + this.sessionSlotsContainer = this.scene.add.container(8, -this.scene.game.canvas.height / 6 + 8); + this.saveSlotSelectContainer.add(this.sessionSlotsContainer); + + this.saveSlotSelectMessageBoxContainer = this.scene.add.container(0, 0); + this.saveSlotSelectMessageBoxContainer.setVisible(false); + this.saveSlotSelectContainer.add(this.saveSlotSelectMessageBoxContainer); + + this.saveSlotSelectMessageBox = addWindow(this.scene, 1, -1, 318, 28); + this.saveSlotSelectMessageBox.setOrigin(0, 1); + this.saveSlotSelectMessageBoxContainer.add(this.saveSlotSelectMessageBox); + + this.message = addTextObject(this.scene, 8, 8, '', TextStyle.WINDOW, { maxLines: 2 }); + this.message.setOrigin(0, 0); + this.saveSlotSelectMessageBoxContainer.add(this.message); + + this.sessionSlots = []; + } + + show(args: any[]): boolean { + if ((args.length < 2 || !(args[1] instanceof Function))) + return false; + + super.show(args); + + this.uiMode = args[0] as SaveSlotUiMode;; + this.saveSlotSelectCallback = args[1] as SaveSlotSelectCallback; + + this.saveSlotSelectContainer.setVisible(true); + this.populateSessionSlots(); + this.setCursor(0); + + return true; + } + + processInput(button: Button): boolean { + const ui = this.getUi(); + + let success = false; + let error = false; + + if (button === Button.ACTION || button === Button.CANCEL) { + const originalCallback = this.saveSlotSelectCallback; + if (button === Button.ACTION) { + if (this.uiMode === SaveSlotUiMode.LOAD && !this.sessionSlots[this.cursor].hasData) + error = true; + else { + switch (this.uiMode) { + case SaveSlotUiMode.LOAD: + this.saveSlotSelectCallback = null; + originalCallback(this.cursor); + break; + case SaveSlotUiMode.SAVE: + const saveAndCallback = () => { + const originalCallback = this.saveSlotSelectCallback; + this.saveSlotSelectCallback = null; + ui.revertMode(); + ui.showText(null, 0); + ui.setMode(Mode.MESSAGE); + originalCallback(this.cursor); + }; + if (this.sessionSlots[this.cursor].hasData) { + ui.showText('Overwrite the data in the selected slot?', null, () => { + ui.setOverlayMode(Mode.CONFIRM, () => saveAndCallback(), () => { + ui.revertMode(); + ui.showText(null, 0); + }); + }); + } else + saveAndCallback(); + break; + } + success = true; + } + } else { + this.saveSlotSelectCallback = null; + originalCallback(-1); + success = true; + } + } else { + switch (button) { + case Button.UP: + success = this.setCursor(this.cursor ? this.cursor - 1 : 0); + break; + case Button.DOWN: + success = this.setCursor(this.cursor < sessionSlotCount - 1 ? this.cursor + 1 : 2); + break; + } + } + + if (success) + ui.playSelect(); + else if (error) + ui.playError(); + + return success || error; + } + + populateSessionSlots() { + for (let s = 0; s < sessionSlotCount; s++) { + const sessionSlot = new SessionSlot(this.scene, s); + sessionSlot.load(); + this.scene.add.existing(sessionSlot); + this.sessionSlotsContainer.add(sessionSlot); + this.sessionSlots.push(sessionSlot); + } + } + + showText(text: string, delay?: integer, callback?: Function, callbackDelay?: integer, prompt?: boolean, promptDelay?: integer) { + super.showText(text, delay, callback, callbackDelay, prompt, promptDelay); + + if (text?.indexOf('\n') === -1) { + this.saveSlotSelectMessageBox.setSize(318, 28); + this.message.setY(-22); + } else { + this.saveSlotSelectMessageBox.setSize(318, 42); + this.message.setY(-37); + } + + this.saveSlotSelectMessageBoxContainer.setVisible(!!text?.length); + } + + setCursor(cursor: integer): boolean { + let changed = super.setCursor(cursor); + + if (!this.cursorObj) { + this.cursorObj = this.scene.add.nineslice(0, 0, 'starter_select_cursor_highlight', null, 296, 44, 1, 1, 1, 1); + this.cursorObj.setOrigin(0, 0); + this.sessionSlotsContainer.add(this.cursorObj); + } + this.cursorObj.setPosition(4, 4 + cursor * 56); + + return changed; + } + + clear() { + super.clear(); + this.saveSlotSelectContainer.setVisible(false); + this.eraseCursor(); + this.saveSlotSelectCallback = null; + this.clearSessionSlots(); + } + + eraseCursor() { + if (this.cursorObj) + this.cursorObj.destroy(); + this.cursorObj = null; + } + + clearSessionSlots() { + this.sessionSlots.splice(0, this.sessionSlots.length); + this.sessionSlotsContainer.removeAll(true); + } +} + +class SessionSlot extends Phaser.GameObjects.Container { + public slotId: integer; + public hasData: boolean; + private loadingLabel: Phaser.GameObjects.Text; + + constructor(scene: BattleScene, slotId: integer) { + super(scene, 0, slotId * 56); + + this.slotId = slotId; + this.hasData = false; + + this.setup(); + } + + setup() { + const slotWindow = addWindow(this.scene, 0, 0, 304, 52); + this.add(slotWindow); + + this.loadingLabel = addTextObject(this.scene, 152, 26, 'Loading…', TextStyle.WINDOW); + this.loadingLabel.setOrigin(0.5, 0.5); + this.add(this.loadingLabel); + } + + async setupWithData(data: SessionSaveData) { + this.remove(this.loadingLabel, true); + + const gameModeLabel = addTextObject(this.scene, 8, 5, `${gameModes[data.gameMode].getName()} - Wave ${data.waveIndex}`, TextStyle.WINDOW); + this.add(gameModeLabel); + + const timestampLabel = addTextObject(this.scene, 8, 19, new Date(data.timestamp).toLocaleString(), TextStyle.WINDOW); + this.add(timestampLabel); + + const playTimeLabel = addTextObject(this.scene, 8, 33, Utils.getPlayTimeString(data.playTime), TextStyle.WINDOW); + this.add(playTimeLabel); + + const pokemonIconsContainer = this.scene.add.container(144, 4); + data.party.forEach((p: PokemonData, i: integer) => { + const iconContainer = this.scene.add.container(26 * i, 0); + iconContainer.setScale(0.75); + + const pokemon = p.toPokemon(this.scene); + const icon = this.scene.add.sprite(0, 0, pokemon.getIconAtlasKey(), pokemon.getIconId()); + icon.setOrigin(0, 0); + + const text = addTextObject(this.scene, 32, 20, `Lv${Utils.formatLargeNumber(pokemon.level, 1000)}`, TextStyle.PARTY, { fontSize: '54px', color: '#f8f8f8' }); + text.setShadow(0, 0, null); + text.setStroke('#424242', 14); + text.setOrigin(1, 0); + + iconContainer.add(icon); + iconContainer.add(text); + + pokemonIconsContainer.add(iconContainer); + }); + + this.add(pokemonIconsContainer); + + const modifiersModule = await import('../modifier/modifier'); + + const modifierIconsContainer = this.scene.add.container(148, 30); + modifierIconsContainer.setScale(0.5); + let visibleModifierIndex = 0; + for (let m of data.modifiers) { + const modifier = m.toModifier(this.scene, modifiersModule[m.className]); + if (modifier instanceof PokemonHeldItemModifier) + continue; + const icon = modifier.getIcon(this.scene, false); + icon.setPosition(24 * visibleModifierIndex, 0); + modifierIconsContainer.add(icon); + if (++visibleModifierIndex === 12) + break; + } + + this.add(modifierIconsContainer); + } + + load(): Promise { + return new Promise(resolve => { + this.scene.gameData.getSession(this.slotId).then(async sessionData => { + if (!sessionData) { + this.loadingLabel.setText('Empty'); + resolve(false); + return; + } + this.hasData = true; + await this.setupWithData(sessionData); + resolve(true); + }) + }); + } +} + +interface SessionSlot { + scene: BattleScene; +} \ No newline at end of file diff --git a/src/ui/starter-select-ui-handler.ts b/src/ui/starter-select-ui-handler.ts index 5a4e6d9a1..01bb11341 100644 --- a/src/ui/starter-select-ui-handler.ts +++ b/src/ui/starter-select-ui-handler.ts @@ -23,6 +23,7 @@ import { allMoves } from "../data/move"; import { Type } from "../data/type"; import { Moves } from "../data/enums/moves"; import { speciesEggMoves } from "../data/egg-moves"; +import { TitlePhase } from "../phases"; export type StarterSelectCallback = (starters: Starter[]) => void; @@ -492,6 +493,20 @@ export default class StarterSelectUiHandler extends MessageUiHandler { success = true; else error = true; + } else if (button === Button.CANCEL) { + if (this.statsMode) { + this.toggleStatsMode(false); + success = true; + } else if (this.starterCursors.length) { + this.popStarter(); + success = true; + this.updateInstructions(); + } else { + this.scene.clearPhaseQueue(); + this.scene.pushPhase(new TitlePhase(this.scene)); + this.scene.getCurrentPhase().end(); + success = true; + } } else if (this.startCursorObj.visible) { switch (button) { case Button.ACTION: @@ -658,15 +673,6 @@ export default class StarterSelectUiHandler extends MessageUiHandler { }); success = true; } - } else if (button === Button.CANCEL) { - if (this.statsMode) { - this.toggleStatsMode(false); - success = true; - } else if (this.starterCursors.length) { - this.popStarter(); - success = true; - this.updateInstructions(); - } } else { const genStarters = this.starterSelectGenIconContainers[this.getGenCursorWithScroll()].getAll().length; const rows = Math.ceil(genStarters / 9); diff --git a/src/ui/ui.ts b/src/ui/ui.ts index 30d4687f5..e483ad19b 100644 --- a/src/ui/ui.ts +++ b/src/ui/ui.ts @@ -30,6 +30,7 @@ import LoadingModalUiHandler from './loading-modal-ui-handler'; import * as Utils from "../utils"; import GameStatsUiHandler from './game-stats-ui-handler'; import AwaitableUiHandler from './awaitable-ui-handler'; +import SaveSlotSelectUiHandler from './save-slot-select-ui-handler'; export enum Mode { MESSAGE, @@ -38,7 +39,7 @@ export enum Mode { BALL, TARGET_SELECT, MODIFIER_SELECT, - //LOAD_SESSION, + SAVE_SLOT, PARTY, SUMMARY, BIOME_SELECT, @@ -60,7 +61,7 @@ export enum Mode { }; const transitionModes = [ - //Mode.LOAD_SESSION, + Mode.SAVE_SLOT, Mode.PARTY, Mode.SUMMARY, Mode.STARTER_SELECT, @@ -109,7 +110,7 @@ export default class UI extends Phaser.GameObjects.Container { new BallUiHandler(scene), new TargetSelectUiHandler(scene), new ModifierSelectUiHandler(scene), - //LoadSessionUiHandler(scene), + new SaveSlotSelectUiHandler(scene), new PartyUiHandler(scene), new SummaryUiHandler(scene), new BiomeSelectUiHandler(scene), @@ -274,10 +275,8 @@ export default class UI extends Phaser.GameObjects.Container { fadeOut(duration: integer): Promise { return new Promise(resolve => { - if (this.overlayActive) { - resolve(); - return; - } + if (this.overlayActive) + return resolve(); this.overlayActive = true; this.overlay.setAlpha(0); this.overlay.setVisible(true); @@ -293,6 +292,8 @@ export default class UI extends Phaser.GameObjects.Container { fadeIn(duration: integer): Promise { return new Promise(resolve => { + if (!this.overlayActive) + return resolve(); this.scene.tweens.add({ targets: this.overlay, alpha: 0, diff --git a/src/utils.ts b/src/utils.ts index d7dff840d..f186aee98 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -139,10 +139,10 @@ export function decToBin(input: integer): string { return bin; } -export function formatStat(stat: integer, forHp: boolean = false): string { - if (stat < (forHp ? 100000 : 1000000)) - return stat.toString(); - let ret = stat.toString(); +export function formatLargeNumber(count: integer, threshold: integer): string { + if (count < threshold) + return count.toString(); + let ret = count.toString(); let suffix = ''; switch (Math.ceil(ret.length / 3) - 1) { case 1: @@ -162,6 +162,10 @@ export function formatStat(stat: integer, forHp: boolean = false): string { return `${ret.slice(0, digits)}${decimalNumber ? `.${decimalNumber}` : ''}${suffix}`; } +export function formatStat(stat: integer, forHp: boolean = false): string { + return formatLargeNumber(stat, forHp ? 100000 : 1000000); +} + export function getEnumKeys(enumType): string[] { return Object.values(enumType).filter(v => isNaN(parseInt(v.toString()))).map(v => v.toString()); }