Encrypted Saves
This commit is contained in:
parent
46524c6f6b
commit
4c0e7eb77c
|
|
@ -1,3 +1,5 @@
|
|||
import { SaveManager } from '../managers/SaveManager.js';
|
||||
|
||||
export class MainMenuScene extends Phaser.Scene {
|
||||
constructor() { super('MainMenuScene'); }
|
||||
|
||||
|
|
@ -120,6 +122,9 @@ export class MainMenuScene extends Phaser.Scene {
|
|||
// Fullscreen toggle — utility button, visually distinct
|
||||
const fsY = startY + buttons.length * spacing + 20;
|
||||
this._makeFullscreenButton(width / 2, fsY);
|
||||
|
||||
// Save / Restore — subtle utility buttons, upper-left
|
||||
this._makeSaveRestoreButtons();
|
||||
}
|
||||
|
||||
_makeButton(x, y, label, callback) {
|
||||
|
|
@ -138,6 +143,121 @@ export class MainMenuScene extends Phaser.Scene {
|
|||
return { bg, text };
|
||||
}
|
||||
|
||||
// ── Save file obfuscation ─────────────────────────────────────────────────
|
||||
// XOR-cipher + Base64. Not cryptographically secure, but the exported file
|
||||
// contains no readable JSON, making casual hand-editing impractical.
|
||||
// The key lives in client code so a determined reverse-engineer could decode
|
||||
// it — the goal is to deter accidental or casual cheating, not prevent it.
|
||||
|
||||
_encodeData(obj) {
|
||||
const K = 'TE::v1::TyrantsEdge::SaveKey';
|
||||
const s = JSON.stringify(obj);
|
||||
let r = '';
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
r += String.fromCharCode(s.charCodeAt(i) ^ K.charCodeAt(i % K.length));
|
||||
}
|
||||
return btoa(r);
|
||||
}
|
||||
|
||||
_decodeData(str) {
|
||||
// Try obfuscated format first, fall back to plain JSON for legacy files.
|
||||
try {
|
||||
const K = 'TE::v1::TyrantsEdge::SaveKey';
|
||||
const s = atob(str.trim());
|
||||
let r = '';
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
r += String.fromCharCode(s.charCodeAt(i) ^ K.charCodeAt(i % K.length));
|
||||
}
|
||||
return JSON.parse(r);
|
||||
} catch {
|
||||
return JSON.parse(str); // plain JSON (old save files)
|
||||
}
|
||||
}
|
||||
|
||||
_makeSaveRestoreButtons() {
|
||||
const BW = 250, BH = 34, x = 20 + BW / 2;
|
||||
|
||||
// ── Save to Drive ──
|
||||
const saveBg = this.add.rectangle(x, 25, BW, BH, 0x000000, 0.55)
|
||||
.setInteractive({ useHandCursor: true })
|
||||
.setStrokeStyle(1, 0x336633);
|
||||
this.add.text(x, 25, '💾 Save Game to Your Drive', {
|
||||
fontSize: '14px', color: '#669966'
|
||||
}).setOrigin(0.5);
|
||||
|
||||
saveBg.on('pointerover', () => saveBg.setStrokeStyle(1, 0x55aa55));
|
||||
saveBg.on('pointerout', () => saveBg.setStrokeStyle(1, 0x336633));
|
||||
saveBg.on('pointerdown', () => {
|
||||
try {
|
||||
const data = this.registry.get('save');
|
||||
const blob = new Blob([this._encodeData(data)], { type: 'application/octet-stream' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'tyrants-edge-save.te';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
this._showStatusMsg('Save file downloaded!', '#66bb66');
|
||||
} catch (e) {
|
||||
this._showStatusMsg('Save failed.', '#bb4444');
|
||||
}
|
||||
});
|
||||
|
||||
// ── Restore from File ──
|
||||
const restoreBg = this.add.rectangle(x, 65, BW, BH, 0x000000, 0.55)
|
||||
.setInteractive({ useHandCursor: true })
|
||||
.setStrokeStyle(1, 0x334466);
|
||||
this.add.text(x, 65, '📂 Restore Game from File', {
|
||||
fontSize: '14px', color: '#6688aa'
|
||||
}).setOrigin(0.5);
|
||||
|
||||
restoreBg.on('pointerover', () => restoreBg.setStrokeStyle(1, 0x5577bb));
|
||||
restoreBg.on('pointerout', () => restoreBg.setStrokeStyle(1, 0x334466));
|
||||
restoreBg.on('pointerdown', () => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = '.te,.json,application/octet-stream,application/json';
|
||||
input.style.display = 'none';
|
||||
document.body.appendChild(input);
|
||||
|
||||
input.onchange = (e) => {
|
||||
const file = e.target.files[0];
|
||||
document.body.removeChild(input);
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (evt) => {
|
||||
try {
|
||||
const restored = this._decodeData(evt.target.result);
|
||||
// Basic sanity check
|
||||
if (typeof restored !== 'object' || !restored.collection) throw new Error();
|
||||
SaveManager.save(restored);
|
||||
this.registry.set('save', restored);
|
||||
this._showStatusMsg('Restored! Reloading...', '#66bb66');
|
||||
this.time.delayedCall(1200, () => this.scene.restart());
|
||||
} catch {
|
||||
this._showStatusMsg('Invalid save file.', '#bb4444');
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
};
|
||||
|
||||
input.click();
|
||||
});
|
||||
}
|
||||
|
||||
_showStatusMsg(msg, color) {
|
||||
if (this._statusMsg) this._statusMsg.destroy();
|
||||
this._statusMsg = this.add.text(145, 95, msg, {
|
||||
fontSize: '13px', color
|
||||
}).setOrigin(0.5, 0);
|
||||
this.time.delayedCall(3000, () => {
|
||||
if (this._statusMsg) { this._statusMsg.destroy(); this._statusMsg = null; }
|
||||
});
|
||||
}
|
||||
|
||||
_makeFullscreenButton(x, y) {
|
||||
const getLabel = () => this.scale.isFullscreen ? '⛶ Exit Fullscreen' : '⛶ Toggle Fullscreen';
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue