Diablo2D — Shadows of Tristram: complete browser ARPG
- Isometric canvas renderer (depth-sorted, FOV/fog, additive lighting) - 3 classes x 20 skills, 4 acts x 4 floors + boss lairs, torment I-X - Diablo-style loot: rarities, affix tiers, 14 legendaries, vendor, stash - Rogue camp with 6 NPCs: Charsi/Akara/Kashya/Cain/Gheed/storage - NPC quest chain (accept -> hunt -> turn in) with rewards & gating - Procedural WebAudio SFX + generative music, EN/VI localization - Saves, settings, waypoints, hardcore mode, PWA manifest - 93-assertion headless suite + browser E2E via CDP
This commit is contained in:
+101
@@ -0,0 +1,101 @@
|
||||
/* ============================================================
|
||||
* Diablo2D — save.js : localStorage persistence layer
|
||||
* ============================================================ */
|
||||
'use strict';
|
||||
|
||||
window.D2 = window.D2 || {};
|
||||
(function (D2) {
|
||||
|
||||
const PREFIX = 'd2d.';
|
||||
const SAVE_VERSION = 3;
|
||||
|
||||
let storageOk = true;
|
||||
try {
|
||||
localStorage.setItem(PREFIX + '__probe', '1');
|
||||
localStorage.removeItem(PREFIX + '__probe');
|
||||
} catch (e) {
|
||||
storageOk = false;
|
||||
}
|
||||
|
||||
function get(key, fallback = null) {
|
||||
if (!storageOk) return fallback;
|
||||
try {
|
||||
const raw = localStorage.getItem(PREFIX + key);
|
||||
if (raw == null) return fallback;
|
||||
return JSON.parse(raw);
|
||||
} catch (e) {
|
||||
console.warn('[save] corrupt entry', key, e);
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function set(key, value) {
|
||||
if (!storageOk) return false;
|
||||
try {
|
||||
localStorage.setItem(PREFIX + key, JSON.stringify(value));
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error('[save] write failed', e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function remove(key) {
|
||||
if (!storageOk) return;
|
||||
try { localStorage.removeItem(PREFIX + key); } catch (e) {}
|
||||
}
|
||||
|
||||
function hasMeta() {
|
||||
return !!get('meta', null);
|
||||
}
|
||||
|
||||
function writeMeta(meta) {
|
||||
meta.version = SAVE_VERSION;
|
||||
meta.savedAt = Date.now();
|
||||
set('meta', meta);
|
||||
}
|
||||
|
||||
function readMeta() {
|
||||
return get('meta', null);
|
||||
}
|
||||
|
||||
function wipeAll() {
|
||||
remove('meta');
|
||||
remove('char');
|
||||
remove('world');
|
||||
remove('settings');
|
||||
}
|
||||
|
||||
/* full snapshot: meta + char + world */
|
||||
function serializeSnapshot() {
|
||||
return JSON.stringify({
|
||||
v: SAVE_VERSION,
|
||||
meta: readMeta(),
|
||||
char: get('char'),
|
||||
world: get('world'),
|
||||
});
|
||||
}
|
||||
|
||||
function restoreSnapshot(text) {
|
||||
try {
|
||||
const obj = JSON.parse(text);
|
||||
if (!obj || typeof obj !== 'object' || !obj.char) return false;
|
||||
if (obj.meta) set('meta', obj.meta);
|
||||
if (obj.char) set('char', obj.char);
|
||||
if (obj.world) set('world', obj.world);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
D2.save = {
|
||||
version: SAVE_VERSION,
|
||||
get storageOk() { return storageOk; },
|
||||
KEY: { META: 'meta', CHAR: 'char', WORLD: 'world', SETTINGS: 'settings' },
|
||||
get, set, remove,
|
||||
hasMeta, writeMeta, readMeta,
|
||||
wipeAll,
|
||||
serializeSnapshot, restoreSnapshot,
|
||||
};
|
||||
})(window.D2);
|
||||
Reference in New Issue
Block a user