Primal Rampage — prehistoric survival wave shooter
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.
This commit is contained in:
+205
@@ -0,0 +1,205 @@
|
||||
/* 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();
|
||||
})();
|
||||
Reference in New Issue
Block a user