From e107349a9859fd9f96332c13bf1b608b4be7a5d1 Mon Sep 17 00:00:00 2001 From: Flashfyre Date: Tue, 26 Dec 2023 14:49:23 -0500 Subject: [PATCH] Add data save and load --- package-lock.json | 6 + package.json | 1 + src/battle-scene.ts | 2 + src/system/game-data.ts | 201 ++++++++++++++++++++++++++-------- src/ui/confirm-ui-handler.ts | 4 + src/ui/egg-list-ui-handler.ts | 4 - src/ui/menu-ui-handler.ts | 47 +++++++- 7 files changed, 213 insertions(+), 52 deletions(-) diff --git a/package-lock.json b/package-lock.json index b652dd05e08..c22159456fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.1", "dependencies": { "@material/material-color-utilities": "^0.2.7", + "crypto-js": "^4.2.0", "json-stable-stringify": "^1.1.0", "phaser": "^3.70.0", "phaser3-rex-plugins": "^1.1.84" @@ -849,6 +850,11 @@ "node": ">= 8" } }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", diff --git a/package.json b/package.json index 9a8d9980081..32b6e50921a 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ }, "dependencies": { "@material/material-color-utilities": "^0.2.7", + "crypto-js": "^4.2.0", "json-stable-stringify": "^1.1.0", "phaser": "^3.70.0", "phaser3-rex-plugins": "^1.1.84" diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 374af8b61fe..0e5e575aa17 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -373,6 +373,8 @@ export default class BattleScene extends Phaser.Scene { this.loadBgm('evolution_fanfare', 'bw/evolution_fanfare.mp3'); populateAnims(); + + //this.load.plugin('rexfilechooserplugin', 'https://raw.githubusercontent.com/rexrainbow/phaser3-rex-notes/master/dist/rexfilechooserplugin.min.js', true); } create() { diff --git a/src/system/game-data.ts b/src/system/game-data.ts index be82fd84d58..63a7a0cd871 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -17,6 +17,27 @@ import { achvs } from "./achv"; import EggData from "./egg-data"; import { Egg } from "../data/egg"; import { VoucherType, vouchers } from "./voucher"; +import { AES, enc } from "crypto-js"; +import { Mode } from "../ui/ui"; + +const saveKey = 'x0i2O7WRiANTqPmZ'; // Temporary; secure encryption is not yet necessary + +export enum GameDataType { + SYSTEM, + SESSION, + SETTINGS +} + +export function getDataTypeKey(dataType: GameDataType): string { + switch (dataType) { + case GameDataType.SYSTEM: + return 'data'; + case GameDataType.SESSION: + return 'sessionData'; + case GameDataType.SETTINGS: + return 'settings'; + } +} interface SystemSaveData { trainerId: integer; @@ -163,16 +184,7 @@ export class GameData { if (!localStorage.hasOwnProperty('data')) return false; - const data = JSON.parse(atob(localStorage.getItem('data')), (k: string, v: any) => { - if (k === 'eggs') { - const ret: EggData[] = []; - for (let e of v) - ret.push(new EggData(e)); - return ret; - } - - return k.endsWith('Attr') ? BigInt(v) : v; - }) as SystemSaveData; + const data = this.parseSystemData(atob(localStorage.getItem('data'))); console.debug(data); @@ -225,6 +237,19 @@ export class GameData { return true; } + private parseSystemData(dataStr: string): SystemSaveData { + return JSON.parse(dataStr, (k: string, v: any) => { + if (k === 'eggs') { + const ret: EggData[] = []; + for (let e of v) + ret.push(new EggData(e)); + return ret; + } + + return k.endsWith('Attr') ? BigInt(v) : v; + }) as SystemSaveData; + } + public saveSetting(setting: Setting, valueIndex: integer): boolean { let settings: object = {}; if (localStorage.hasOwnProperty('settings')) @@ -289,36 +314,8 @@ export class GameData { return resolve(false); try { - const sessionData = JSON.parse(atob(localStorage.getItem('sessionData')), (k: string, v: any) => { - /*const versions = [ scene.game.config.gameVersion, sessionData.gameVersion || '0.0.0' ]; - - if (versions[0] !== versions[1]) { - const [ versionNumbers, oldVersionNumbers ] = versions.map(ver => ver.split('.').map(v => parseInt(v))); - }*/ - - if (k === 'party' || k === 'enemyParty' || k === 'enemyField') { - const ret: PokemonData[] = []; - for (let pd of v) - ret.push(new PokemonData(pd)); - return ret; - } - - if (k === 'trainer') - return v ? new TrainerData(v) : null; - - if (k === 'modifiers' || k === 'enemyModifiers') { - const player = k === 'modifiers'; - const ret: PersistentModifierData[] = []; - for (let md of v) - ret.push(new PersistentModifierData(md, player)); - return ret; - } - - if (k === 'arena') - return new ArenaData(v); - - return v; - }) as SessionSaveData; + const sessionDataStr = atob(localStorage.getItem('sessionData')); + const sessionData = this.parseSessionData(sessionDataStr); console.debug(sessionData); @@ -346,10 +343,6 @@ export class GameData { scene.money = sessionData.money || 0; scene.updateMoneyText(); - // TODO: Remove this - if (sessionData.enemyField) - sessionData.enemyParty = sessionData.enemyField; - const battleType = sessionData.battleType || 0; const battle = scene.newBattle(sessionData.waveIndex, battleType, sessionData.trainer, battleType === BattleType.TRAINER ? trainerConfigs[sessionData.trainer.trainerType].isDouble : sessionData.enemyParty.length > 1); @@ -398,6 +391,126 @@ export class GameData { localStorage.removeItem('sessionData'); } + parseSessionData(dataStr: string): SessionSaveData { + return JSON.parse(dataStr, (k: string, v: any) => { + /*const versions = [ scene.game.config.gameVersion, sessionData.gameVersion || '0.0.0' ]; + + if (versions[0] !== versions[1]) { + const [ versionNumbers, oldVersionNumbers ] = versions.map(ver => ver.split('.').map(v => parseInt(v))); + }*/ + + if (k === 'party' || k === 'enemyParty' || k === 'enemyField') { + const ret: PokemonData[] = []; + for (let pd of v) + ret.push(new PokemonData(pd)); + return ret; + } + + if (k === 'trainer') + return v ? new TrainerData(v) : null; + + if (k === 'modifiers' || k === 'enemyModifiers') { + const player = k === 'modifiers'; + const ret: PersistentModifierData[] = []; + for (let md of v) + ret.push(new PersistentModifierData(md, player)); + return ret; + } + + if (k === 'arena') + return new ArenaData(v); + + return v; + }) as SessionSaveData; + } + + public exportData(dataType: GameDataType): void { + const dataKey: string = getDataTypeKey(dataType); + const dataStr = atob(localStorage.getItem(dataKey)); + console.log(dataStr); + 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(); + } + + public importData(dataType: GameDataType): void { + const dataKey = getDataTypeKey(dataType); + + let saveFile: any = document.getElementById('saveFile'); + if (saveFile) + saveFile.remove(); + + saveFile = document.createElement('input'); + saveFile.id = 'saveFile'; + saveFile.type = 'file'; + saveFile.accept = '.prsv'; + saveFile.style.display = 'none'; + saveFile.addEventListener('change', + e => { + let reader = new FileReader(); + + reader.onload = (_ => { + return e => { + const dataStr = AES.decrypt(e.target.result.toString(), saveKey).toString(enc.Utf8); + let valid = false; + try { + switch (dataType) { + case GameDataType.SYSTEM: + const systemData = this.parseSystemData(dataStr); + valid = !!systemData.dexData && !!systemData.timestamp; + break; + case GameDataType.SESSION: + const sessionData = this.parseSessionData(dataStr); + valid = !!sessionData.party && !!sessionData.enemyParty && !!sessionData.timestamp; + break; + case GameDataType.SETTINGS: + valid = true; + break; + } + } catch (ex) { + console.error(ex); + } + + let dataName: string; + switch (dataType) { + case GameDataType.SYSTEM: + dataName = 'save'; + break; + case GameDataType.SESSION: + dataName = 'session'; + break; + case GameDataType.SETTINGS: + dataName = 'settings'; + break; + } + + if (!valid) + return this.scene.ui.showText(`Your ${dataName} data could not be loaded. It may be corrupted.`, null, () => this.scene.ui.showText(null, 0), Utils.fixedInt(1500)); + this.scene.ui.showText(`Your ${dataName} data will be overridden and the page will reload. Proceed?`, null, () => { + this.scene.ui.setOverlayMode(Mode.CONFIRM, () => { + localStorage.setItem(dataKey, btoa(dataStr)); + window.location = window.location; + }, () => { + this.scene.ui.revertMode(); + this.scene.ui.showText(null, 0); + }, false, 98); + }); + }; + })((e.target as any).files[0]); + + reader.readAsText((e.target as any).files[0]); + } + ); + saveFile.click(); + /*(this.scene.plugins.get('rexfilechooserplugin') as FileChooserPlugin).open({ accept: '.prsv' }) + .then(result => { + });*/ + } + private initDexData(): void { const data: DexData = {}; diff --git a/src/ui/confirm-ui-handler.ts b/src/ui/confirm-ui-handler.ts index e32b38878c4..8f2c24a280f 100644 --- a/src/ui/confirm-ui-handler.ts +++ b/src/ui/confirm-ui-handler.ts @@ -28,6 +28,10 @@ export default class ConfirmUiHandler extends AbstractOptionSelectUiHandler { this.switchCheck = args.length >= 3 && args[2] as boolean; + const xOffset = (args.length >= 4 ? -args[3] as number : 0); + + this.optionSelectContainer.x = (this.scene.game.canvas.width / 6) - 1 + xOffset; + this.setCursor(this.switchCheck ? this.switchCheckCursor : 0); } } diff --git a/src/ui/egg-list-ui-handler.ts b/src/ui/egg-list-ui-handler.ts index f487eb9a025..f0c6fc2843e 100644 --- a/src/ui/egg-list-ui-handler.ts +++ b/src/ui/egg-list-ui-handler.ts @@ -117,10 +117,6 @@ export default class EggListUiHandler extends MessageUiHandler { this.setCursor(0); } - showText(text: string, delay?: integer, callback?: Function, callbackDelay?: integer, prompt?: boolean, promptDelay?: integer) { - super.showText(text, delay, callback, callbackDelay, prompt, promptDelay); - } - processInput(button: Button): boolean { const ui = this.getUi(); diff --git a/src/ui/menu-ui-handler.ts b/src/ui/menu-ui-handler.ts index da488880a39..94a22af0639 100644 --- a/src/ui/menu-ui-handler.ts +++ b/src/ui/menu-ui-handler.ts @@ -4,17 +4,24 @@ import { Mode } from "./ui"; import UiHandler from "./ui-handler"; import * as Utils from "../utils"; import { addWindow } from "./window"; +import MessageUiHandler from "./message-ui-handler"; +import { GameDataType } from "../system/game-data"; export enum MenuOptions { SETTINGS, ACHIEVEMENTS, VOUCHERS, EGG_LIST, - EGG_GACHA + EGG_GACHA, + IMPORT_SESSION, + EXPORT_SESSION, + IMPORT_DATA, + EXPORT_DATA } -export default class MenuUiHandler extends UiHandler { +export default class MenuUiHandler extends MessageUiHandler { private menuContainer: Phaser.GameObjects.Container; + private menuMessageBoxContainer: Phaser.GameObjects.Container; private menuBg: Phaser.GameObjects.NineSlice; protected optionSelectText: Phaser.GameObjects.Text; @@ -32,7 +39,7 @@ export default class MenuUiHandler extends UiHandler { this.menuContainer.setInteractive(new Phaser.Geom.Rectangle(0, 0, this.scene.game.canvas.width / 6, this.scene.game.canvas.height / 6), Phaser.Geom.Rectangle.Contains); - this.menuBg = addWindow(this.scene, (this.scene.game.canvas.width / 6) - 92, 0, 90, (this.scene.game.canvas.height / 6) - 2); + this.menuBg = addWindow(this.scene, (this.scene.game.canvas.width / 6) - 100, 0, 98, (this.scene.game.canvas.height / 6) - 2); this.menuBg.setOrigin(0, 0); this.menuContainer.add(this.menuBg); @@ -44,6 +51,23 @@ export default class MenuUiHandler extends UiHandler { ui.add(this.menuContainer); + this.menuMessageBoxContainer = this.scene.add.container(0, 130); + this.menuMessageBoxContainer.setVisible(false); + this.menuContainer.add(this.menuMessageBoxContainer); + + const menuMessageBox = addWindow(this.scene, 0, -0, 220, 48); + menuMessageBox.setOrigin(0, 0); + this.menuMessageBoxContainer.add(menuMessageBox); + + const menuMessageText = addTextObject(this.scene, 8, 8, '', TextStyle.WINDOW, { maxLines: 2 }); + menuMessageText.setWordWrapWidth(1224); + menuMessageText.setOrigin(0, 0); + this.menuMessageBoxContainer.add(menuMessageText); + + this.message = menuMessageText; + + this.menuContainer.add(this.menuMessageBoxContainer); + this.setCursor(0); this.menuContainer.setVisible(false); @@ -95,7 +119,16 @@ export default class MenuUiHandler extends UiHandler { this.scene.ui.setOverlayMode(Mode.EGG_GACHA); success = true; break; - + case MenuOptions.IMPORT_SESSION: + case MenuOptions.IMPORT_DATA: + this.scene.gameData.importData(this.cursor === MenuOptions.IMPORT_DATA ? GameDataType.SYSTEM : GameDataType.SESSION); + success = true; + break; + case MenuOptions.EXPORT_SESSION: + case MenuOptions.EXPORT_DATA: + this.scene.gameData.exportData(this.cursor === MenuOptions.EXPORT_DATA ? GameDataType.SYSTEM : GameDataType.SESSION); + success = true; + break; } } else if (button === Button.CANCEL) { success = true; @@ -122,6 +155,12 @@ export default class MenuUiHandler extends UiHandler { return true; } + showText(text: string, delay?: number, callback?: Function, callbackDelay?: number, prompt?: boolean, promptDelay?: number): void { + this.menuMessageBoxContainer.setVisible(!!text); + + super.showText(text, delay, callback, callbackDelay, prompt, promptDelay); + } + setCursor(cursor: integer): boolean { const ret = super.setCursor(cursor);