Add data save and load

pull/14/head
Flashfyre 2023-12-26 14:49:23 -05:00
parent 97124c2710
commit e107349a98
7 changed files with 213 additions and 52 deletions

6
package-lock.json generated
View File

@ -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",

View File

@ -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"

View File

@ -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() {

View File

@ -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 = {};

View File

@ -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);
}
}

View File

@ -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();

View File

@ -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);