Files
neon-survivors/js/save.js
T
neon-survivors-dev 796880375e NEON SURVIVORS v1.1 — full-featured bullet-heaven survivor game
Vanilla JS + Canvas, zero dependencies, offline-first PWA.

Gameplay:
- 11 weapons x8 levels + 11 evolutions (chest-based), incl. timed mines
- 13 passives, crit system with directional hit-sparks & hit-stop
- 10 characters w/ unique mods + unlock conditions, gold cosmetic skins
- 3 biomes (Neon Graveyard / Frozen Hollow / Magma Rift) each with own
  spawn tables, boss plans and music flavor; Endless mode + surges;
  4 difficulty grades; breakable crystal-lamp props
- Elite random affixes (Swift/Sturdy/Volatile), 4 bosses, win flow
- Achievements (23) w/ gold rewards, run history, daily seeded challenge

Tech:
- Cinematic canvas main-menu scene, game-feel FX suite (trails, muzzle,
  status tints, low-HP pulse), viewport culling + particle pooling
- WebAudio synth SFX + generative per-biome soundtrack
- Gamepad support, remappable keys, touch joystick, fullscreen
- i18n VI/EN, localStorage saves w/ export-import codes
- Cloudflare Workers leaderboard scaffold (KV) w/ signed submits
- Headless integrity test-suite (node test/integrity.js)
2026-08-23 07:01:22 +00:00

101 lines
3.2 KiB
JavaScript

'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); }
};