Skip to content

Commit

Permalink
Add data save and load
Browse files Browse the repository at this point in the history
  • Loading branch information
Flashfyre committed Dec 26, 2023
1 parent 97124c2 commit e107349
Show file tree
Hide file tree
Showing 7 changed files with 213 additions and 52 deletions.
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 2 additions & 0 deletions src/battle-scene.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
201 changes: 157 additions & 44 deletions src/system/game-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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'))
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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);

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

Expand Down
4 changes: 4 additions & 0 deletions src/ui/confirm-ui-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand Down
4 changes: 0 additions & 4 deletions src/ui/egg-list-ui-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
47 changes: 43 additions & 4 deletions src/ui/menu-ui-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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;
Expand All @@ -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);

Expand Down

0 comments on commit e107349

Please sign in to comment.