/* Primal Rampage — profile & persistence v2 (localStorage). v2 adds: gems (hard currency), permanent upgrades, bestiary kills, daily quests + login streak, achievements, lifetime stats, pet, tutorial flag, rewarded-ad daily cap, shake amount + quality setting. Server-backed leaderboards/accounts plug in later via US.net. */ (function () { 'use strict'; var KEY = 'neonrampage.profile.v1'; var US = (window.US = window.US || {}); var DEFAULTS = { name: 'Rookie', lang: 'en', coins: 0, bombs: 1, weapons: ['pistol'], heroes: ['recruit'], skins: ['default'], weapon: 'pistol', hero: 'recruit', skin: 'default', bestScore: 0, bestWave: 0, // --- v2 --- gems: 0, // hard currency "ngọn lửa" upgrades: {}, // id -> level bestiary: {}, // speciesId -> {kills, done} quests: { date: '', items: [], lastDay: '', streak: 0 }, achievements: {}, // id -> true stats: { totalKills: 0, totalAmber: 0, runs: 0 }, pet: null, // {name, color} once hatched tut: false, adDay: { date: '', count: 0 }, // rewarded ads watched today (cap 3) settings: { sfx: 0.8, music: 0.5, autofire: true, autoAim: false, onehit: false, shake: true, shakeAmt: 1, quality: 'high' }, board: [] // [{name,score,wave,ts}] }; function clone(o) { return JSON.parse(JSON.stringify(o)); } function isPlain(o) { return o && typeof o === 'object' && !(o instanceof Array); } // deep-merge stored data over defaults so new fields appear after updates function mergeDefaults(dst, def) { Object.keys(def).forEach(function (k) { if (!(k in dst)) dst[k] = clone(def[k]); else if (isPlain(def[k]) && isPlain(dst[k])) mergeDefaults(dst[k], def[k]); }); return dst; } var state = null; function load() { try { var raw = localStorage.getItem(KEY); state = raw ? mergeDefaults(JSON.parse(raw), DEFAULTS) : clone(DEFAULTS); state.settings = Object.assign(clone(DEFAULTS.settings), state.settings || {}); state.stats = Object.assign(clone(DEFAULTS.stats), state.stats || {}); state.quests = Object.assign(clone(DEFAULTS.quests), state.quests || {}); state.adDay = Object.assign(clone(DEFAULTS.adDay), state.adDay || {}); if (!state.weapons.length) state.weapons = ['pistol']; if (!state.heroes.length) state.heroes = ['recruit']; if (!state.skins.length) state.skins = ['default']; } catch (e) { state = clone(DEFAULTS); } return state; } US.save = { get: function () { return state || load(); }, save: function () { try { localStorage.setItem(KEY, JSON.stringify(state)); } catch (e) { /* private mode */ } }, reset: function () { state = clone(DEFAULTS); US.save.save(); }, addCoins: function (n) { state.coins = Math.max(0, (state.coins | 0) + n); US.save.save(); }, /* ---------- hard currency ---------- */ addGems: function (n) { state.gems = Math.max(0, (state.gems | 0) + n); US.save.save(); }, spendGems: function (n) { if ((state.gems | 0) < n) return false; state.gems -= n; US.save.save(); return true; }, ownWeapon: function (id) { if (state.weapons.indexOf(id) < 0) { state.weapons.push(id); US.save.save(); } }, ownHero: function (id) { if (state.heroes.indexOf(id) < 0) { state.heroes.push(id); US.save.save(); } }, ownSkin: function (id) { if (state.skins.indexOf(id) < 0) { state.skins.push(id); US.save.save(); } }, equip: function (slot, id) { state[slot] = id; US.save.save(); }, /* ---------- permanent upgrades ---------- */ upLevel: function (id) { return state.upgrades[id] | 0; }, buyUpgrade: function (id, cost) { var lv = state.upgrades[id] | 0; if ((state.coins | 0) < cost) return false; state.coins -= cost; state.upgrades[id] = lv + 1; US.save.save(); return true; }, /* ---------- bestiary ---------- */ bumpBestiary: function (speciesId, n) { var e = state.bestiary[speciesId]; if (!e) e = state.bestiary[speciesId] = { kills: 0, done: false }; e.kills += (n || 1); US.save.save(); return e; }, bestiaryDone: function (speciesId, rewardFn) { var e = state.bestiary[speciesId]; if (e && !e.done) { e.done = true; if (rewardFn) rewardFn(); US.save.save(); return true; // newly completed } return false; }, /* ---------- lifetime stats ---------- */ stat: function (k, n) { if (n === undefined) return state.stats[k] | 0; state.stats[k] = (state.stats[k] | 0) + n; US.save.save(); return state.stats[k]; }, /* ---------- achievements ---------- */ hasAch: function (id) { return !!state.achievements[id]; }, unlockAch: function (id) { if (state.achievements[id]) return false; state.achievements[id] = true; US.save.save(); return true; }, /* ---------- rewarded ads (stub; swap with real SDK later) ---------- */ ADS_DAILY_CAP: 3, canWatchAd: function () { var today = US.save.today(); if (state.adDay.date !== today) { state.adDay = { date: today, count: 0 }; } return state.adDay.count < US.save.ADS_DAILY_CAP; }, consumeAd: function () { var today = US.save.today(); if (state.adDay.date !== today) state.adDay = { date: today, count: 0 }; state.adDay.count++; US.save.save(); }, /* ---------- live-ops ---------- */ isWeekend: function () { var d = new Date().getDay(); return d === 0 || d === 6; }, /* ---------- misc ---------- */ today: function () { var d = new Date(); return d.getFullYear() + '-' + ('0' + (d.getMonth() + 1)).slice(-2) + '-' + ('0' + d.getDate()).slice(-2); }, submitRun: function (score, wave) { var isRecord = score > state.bestScore; if (isRecord) state.bestScore = Math.floor(score); if (wave > state.bestWave) state.bestWave = wave; state.board.push({ name: state.name || 'Rookie', score: Math.floor(score), wave: wave, ts: Date.now() }); state.board.sort(function (a, b) { return b.score - a.score; }); state.board = state.board.slice(0, 10); US.save.save(); // Plug-in point for a real server leaderboard: // if (US.net && US.net.submitRun) US.net.submitRun(state.board[0]); return isRecord; } }; load(); })();