'use strict'; /* ============================================================ NEON SURVIVORS — save.js : localStorage persistence Meta progression (gold, permanent upgrades, unlocks) + settings. ============================================================ */ const SAVE_KEY = 'neon_survivors_v1'; const DEFAULT_SAVE = { gold: 0, meta: {}, // metaId -> rank purchased chars: ['kaito'], // unlocked character ids flags: { boss1: false, time10: false, time15: false }, totals: { kills: 0, gold: 0, best: 0, wins: 0, runs: 0, evos: 0, props: 0 }, progress: { stages: ['graveyard'], bestPerStage: {}, endlessBest: 0, stageWins: {}, gradeWins: {} }, skinOwned: {}, // 'charId:skinKey' -> 1 skinSel: {}, // charId -> skinKey lbName: '', // leaderboard display name daily: { date: '', best: 0 }, ach: {}, history: [], settings: { lang: 'vi', volMaster: 0.9, volMusic: 0.6, volSfx: 0.85, shake: true, dmgNum: true, particles: 'high', // low | med | high fps: false, autoPause: true, dev: false, // developer mode tools keybinds: { up: 'KeyW', down: 'KeyS', left: 'KeyA', right: 'KeyD' } } }; const Store = { data: null, load() { this.data = JSON.parse(JSON.stringify(DEFAULT_SAVE)); try { const raw = localStorage.getItem(SAVE_KEY); if (raw) { const parsed = JSON.parse(raw); // Deep-merge known sections so old saves survive schema additions. for (const sec of ['flags', 'totals', 'settings', 'progress', 'daily', 'ach']) { if (parsed[sec]) Object.assign(this.data[sec], parsed[sec]); } if (parsed.skinOwned) Object.assign(this.data.skinOwned, parsed.skinOwned); if (parsed.skinSel) Object.assign(this.data.skinSel, parsed.skinSel); if (!this.data.lbName && typeof parsed.lbName === 'string') this.data.lbName = parsed.lbName; if (Array.isArray(parsed.history)) this.data.history = parsed.history.slice(0, 20); if (typeof parsed.gold === 'number') this.data.gold = parsed.gold; if (parsed.meta && typeof parsed.meta === 'object') this.data.meta = parsed.meta; if (Array.isArray(parsed.chars)) this.data.chars = parsed.chars; } } catch (e) { console.warn('Save load failed', e); } return this.data; }, save() { try { localStorage.setItem(SAVE_KEY, JSON.stringify(this.data)); } catch (e) { console.warn('Save write failed', e); } }, reset() { this.data = JSON.parse(JSON.stringify(DEFAULT_SAVE)); this.save(); }, s() { return this.data.settings; }, addGold(n) { this.data.gold += n; this.data.totals.gold += Math.max(0, n); // lazy-save; frequent small writes are cheap enough here this.save(); }, spendGold(n) { if (this.data.gold < n) return false; this.data.gold -= n; this.save(); return true; }, rank(metaId) { return this.data.meta[metaId] || 0; }, unlockChar(id) { if (!this.data.chars.includes(id)) { this.data.chars.push(id); this.save(); return true; } return false; }, charUnlocked(id) { return this.data.chars.includes(id); } };