Vanilla JS + Canvas 2D, zero dependencies. Gameplay: campaign waves vs 12 dino species + 3 bosses, arena mode, 7 weapons, hero talents, pets, ult/bombs, card roguelite picks. World: day/night cycle, weather fronts (rain+lightning), Blood Moon weekends, 3 act biomes (jungle -> swamp -> ashlands with lava veins). Meta: gems economy, upgrade tree, quests, bestiary, achievements, shop (weapons/heroes/skins incl. premium), revive, ad-cap system, endless scaling past wave 25, live-painted shop/bestiary icons, pro main menu, auto-aim toggle (F). Headless smoke suite included.
206 lines
6.5 KiB
JavaScript
206 lines
6.5 KiB
JavaScript
/* 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();
|
|
})();
|