- 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
102 lines
2.3 KiB
JavaScript
102 lines
2.3 KiB
JavaScript
/* ============================================================
|
|
* 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);
|