/* Neon Rampage — DOM UI: menus, shop, settings, leaderboard, overlays. */
(function () {
'use strict';
var US = (window.US = window.US || {});
var $ = function (id) { return document.getElementById(id); };
var reviveTimer = null;
function show(el) { el.classList.remove('hidden'); }
function hide(el) { el.classList.add('hidden'); }
function toast(msg) {
var t = $('toast');
t.textContent = msg;
show(t);
clearTimeout(toast._h);
toast._h = setTimeout(function () { hide(t); }, 2200);
}
/* ---------------- screens ---------------- */
var SCREENS = ['menu', 'shop', 'settings', 'rank', 'howto', 'arena-setup', 'quests', 'bestiary'];
function L(o) { var sv = US.save.get(); return (o && (o[vi0()] || o.en)) || ''; }
function vi0() { return US.save.get().lang === 'vi' ? 'vi' : 'en'; }
function showScreen(name) {
SCREENS.forEach(function (s) {
var el = $('screen-' + s);
if (el) (s === name ? show : hide)(el);
});
$('hud-root').classList.toggle('in-game', !name);
refreshMenuStats();
}
US.uiShowScreen = showScreen;
function refreshMenuStats() {
var s = US.save.get();
$('menu-coins').textContent = '$' + US.util.fmt(s.coins);
if ($('menu-gems')) $('menu-gems').textContent = '🔥 ' + (s.gems | 0);
if ($('menu-rank') && US.save.seasonRank) {
var sr2 = US.save.seasonRank();
$('menu-rank').textContent = sr2.tier.icon + ' ' +
L({ en: sr2.tier.en, vi: sr2.tier.vi });
}
if ($('dot-upgrades')) {
var canUp = Object.keys(US.UPGRADES || {}).some(function (k) {
var d = US.UPGRADES[k];
return (s.upgrades[k] | 0) < d.max && s.coins >= upCost(d, s.upgrades[k] | 0);
});
$('dot-upgrades').classList.toggle('hidden', !canUp);
}
$('menu-best').textContent =
US.i18n.t('menu.bestScore') + ': ' + US.util.fmt(s.bestScore) +
' · ' + US.i18n.t('menu.bestWave') + ': ' + s.bestWave;
}
/* ---------------- shop ---------------- */
var shopTab = 'weapons';
/* ---- live-drawn icons: reuse the game's own painters as thumbnails --- */
function makePreview(w, h, drawFn) {
var wrap = document.createElement('div');
wrap.className = 'item-prev';
var cv = document.createElement('canvas');
cv.width = w; cv.height = h;
cv.style.width = w + 'px'; cv.style.height = h + 'px';
if (drawFn) {
var c2 = cv.getContext('2d');
try {
drawFn(c2, w, h);
} catch (e) {
// never leave a silent empty plate: show fallback + log the truth
console.warn('[icon] painter failed:', e && e.message, e);
c2.fillStyle = '#ffb938';
c2.font = '30px system-ui';
c2.textAlign = 'center'; c2.textBaseline = 'middle';
c2.fillText('🎨', w / 2, h / 2);
}
}
wrap.appendChild(cv);
return wrap;
}
var UP_ICON = { hp: '❤️', dmg: '🦷', speed: '💨', ult: '☀️', coin: '🧿', bombs: '💣' };
// hero bust: top-down but angled up so it reads like a portrait
function drawHeroIcon(c2, w, h, bodyCol, trimCol, kind) {
c2.save();
c2.translate(w / 2 - 46 * 1.25, h / 2 - 30 * 1.25);
c2.scale(1.25, 1.25);
US.art.drawHuman(c2, {
x: 46, y: 30, radius: 14, aim: -Math.PI / 2,
walkPhase: 0, moving: false,
color: bodyCol || '#e04b3a',
body: bodyCol || '#e04b3a', trim: trimCol || '#e8dcc4',
weaponKind: kind || 'slingshot',
hurtT: 0, fireT: 0, torch: false, scale: 1.3, seedF: 3
}, 1);
c2.restore();
}
function drawWeaponIcon(c2, w, h, kind) {
c2.save();
c2.translate(12, h / 2);
c2.scale(1.9, 1.9);
US.art.drawHeldWeapon(c2, kind, 0);
c2.restore();
}
var BEST_DRAW = {
grunt: { fn: 'drawRaptor', r: 15 },
runner: { fn: 'drawRaptor', r: 11 },
spitter: { fn: 'drawDilo', r: 14 },
tank: { fn: 'drawTriceratops', r: 21 },
stego: { fn: 'drawStego', r: 19 },
pachy: { fn: 'drawPachy', r: 14 },
splitter: { fn: 'drawPtero', r: 17, opt: false },
mini: { fn: 'drawPtero', r: 11, opt: true },
eliteGrunt: { fn: 'drawRaptor', r: 18 },
bruiser: { fn: 'drawRex', r: 25 },
artillery: { fn: 'drawSpino', r: 25 },
hydra: { fn: 'drawGiga', r: 25 }
};
function drawBeastIcon(c2, w, h, speciesId, locked) {
var spec = BEST_DRAW[speciesId];
if (!spec) return;
var def = (US.ENEMIES && (US.ENEMIES[speciesId])) ||
(speciesId === 'bruiser' ? (US.BOSSES && US.BOSSES.bruiser) : null) || {};
var col = def.color || '#5f8f3e';
var ent = {
x: w / 2, y: h / 2 + 4, radius: spec.r,
face: -Math.PI / 2, color: col, hitFlash: 0,
seed: 3, walkPhase: 0, dashing: 0, dashCd: 9,
shootCd: 1, charging: false, spawnAnim: 0
};
c2.save();
if (locked) c2.globalAlpha = 0.22;
var fn2 = US.art[spec.fn];
if (spec.opt === undefined) fn2(c2, ent, 1.23);
else fn2(c2, ent, 1.23, spec.opt);
c2.restore();
if (locked) {
c2.fillStyle = 'rgba(243,231,207,0.75)';
c2.font = 'bold 26px system-ui';
c2.textAlign = 'center'; c2.textBaseline = 'middle';
c2.fillText('?', w / 2, h / 2);
}
}
function renderShop() {
var s = US.save.get();
$('shop-coins').textContent = '$' + US.util.fmt(s.coins);
if ($('shop-gems')) $('shop-gems').textContent = '🔥 ' + (s.gems | 0);
['weapons', 'heroes', 'skins', 'upgrades'].forEach(function (t) {
$('tab-' + t).classList.toggle('active', t === shopTab);
});
var grid = $('shop-grid');
grid.innerHTML = '';
var list, ownArr, equipKey;
if (shopTab === 'upgrades') { renderUpgrades(grid); return; }
if (shopTab === 'weapons') { list = US.WEAPONS; ownArr = s.weapons; equipKey = 'weapon'; }
else if (shopTab === 'heroes') { list = US.HEROES; ownArr = s.heroes; equipKey = 'hero'; }
else { list = US.SKINS; ownArr = s.skins; equipKey = 'skin'; }
Object.keys(list).forEach(function (id) {
var it = list[id];
var owned = ownArr.indexOf(id) >= 0;
var equipped = s[equipKey] === id;
var card = document.createElement('div');
card.className = 'item' + (equipped ? ' equipped' : '');
// live-drawn thumbnail
if (shopTab === 'weapons') {
card.appendChild(makePreview(96, 50, function (c2, w2, h2) {
drawWeaponIcon(c2, w2, h2, it.kind);
}));
} else if (shopTab === 'heroes') {
card.appendChild(makePreview(96, 58, function (c2, w2, h2) {
drawHeroIcon(c2, w2, h2, it.tint || it.body || '#e04b3a', '#e8dcc4', 'slingshot');
}));
} else if (shopTab === 'skins') {
card.appendChild(makePreview(96, 58, function (c2, w2, h2) {
// bold colour disc so every paint job is unmistakable
c2.fillStyle = it.body || '#e04b3a';
c2.beginPath();
c2.arc(w2 / 2, h2 / 2 + 2, 21, 0, Math.PI * 2);
c2.fill();
c2.strokeStyle = 'rgba(0,0,0,0.4)';
c2.lineWidth = 2;
c2.stroke();
drawHeroIcon(c2, w2, h2, it.body, it.trim, (US.WEAPONS[sv.weapon] || {}).kind);
if (it.premium) {
c2.strokeStyle = 'rgba(255,210,74,0.8)';
c2.lineWidth = 2;
c2.strokeRect(1.5, 1.5, w2 - 3, h2 - 3);
}
// ground truth: sample what actually landed on the canvas
try {
var px3 = c2.getImageData(Math.round(w2 / 2), Math.round(h2 / 2) + 16, 1, 1).data;
console.log('[icon]', it.name, 'want', it.body,
'got rgb(' + px3[0] + ',' + px3[1] + ',' + px3[2] + ')');
} catch (e3) { /* tainted ctx: ignore */ }
}));
} else if (shopTab === 'upgrades') {
var up = document.createElement('div');
up.className = 'item-prev up-icon';
up.textContent = UP_ICON[it.id] || '💪';
card.appendChild(up);
}
var h = document.createElement('div');
h.className = 'item-name';
h.textContent = it.premium ? (L(it) || it.name) : it.name;
card.appendChild(h);
var desc = document.createElement('div');
desc.className = 'item-desc';
if (shopTab === 'weapons') {
var mw3 = US.save.masteryLevel ? US.save.masteryLevel(it.kind) : { level: 0, bonus: 0 };
desc.textContent = 'DMG ' + it.dmg + ' · ROF ' + it.rof + '/s' +
(it.pellets > 1 ? ' · ×' + it.pellets : '') + (it.pierce ? ' · PIERCE' : '') + (it.aoe ? ' · AoE' : '') +
(mw3.level > 0 ? ' · ⭐Lv' + mw3.level + ' (+' + Math.round(mw3.bonus * 100) + '%)' : '');
} else if (shopTab === 'heroes') {
desc.textContent = US.i18n.t('hero.statHp') + ' ' + it.hp + ' · ' +
US.i18n.t('hero.statSpeed') + ' ' + it.speed + ' · ' + it.talent;
} else if (it.premium) {
desc.textContent = L(it) || 'Legendary paint';
card.classList.add('premium');
} else {
desc.textContent = it.name === 'Default' ? 'Standard issue' : 'Chromatic skin';
}
card.appendChild(desc);
var btn = document.createElement('button');
btn.className = 'btn';
if (equipped) {
btn.textContent = US.i18n.t('shop.equipped');
btn.disabled = true;
} else if (owned) {
btn.textContent = US.i18n.t('shop.equip');
btn.onclick = function () {
US.audio.sfx('ui');
US.save.equip(equipKey, id);
renderShop();
};
} else {
if (it.premium) {
btn.innerHTML = '🔥 ' + it.gems;
btn.onclick = function () {
if (!US.save.spendGems(it.gems)) { toast(L({ en: 'Not enough flames!', vi: 'Không đủ ngọn lửa!' })); return; }
US.save.ownSkin(id);
US.save.equip(equipKey, id);
US.audio.sfx('buy');
toast(US.i18n.t('shop.bought'));
renderShop();
};
} else {
btn.textContent = US.i18n.t('shop.buy') + ' — $' + it.price;
btn.onclick = function () {
var cur = US.save.get();
if (cur.coins < it.price) { toast(US.i18n.t('toast.notEnough')); return; }
US.save.addCoins(-it.price);
if (shopTab === 'weapons') US.save.ownWeapon(id);
else if (shopTab === 'heroes') US.save.ownHero(id);
else US.save.ownSkin(id);
US.save.equip(equipKey, id);
US.audio.sfx('buy');
toast(US.i18n.t('shop.bought'));
renderShop();
};
}
}
card.appendChild(btn);
grid.appendChild(card);
});
// spare bombs row
var bombRow = $('shop-bombs');
bombRow.innerHTML = '';
var bl = document.createElement('span');
bl.className = 'bomb-label';
bl.textContent = '💣 ' + US.i18n.t('shop.bombs') + ': ' + (s.bombs | 0);
var bb = document.createElement('button');
bb.className = 'btn';
bb.textContent = US.i18n.t('shop.buy') + ' — $50';
bb.onclick = function () {
var cur = US.save.get();
if (cur.coins < 50) { toast(US.i18n.t('toast.notEnough')); return; }
US.save.addCoins(-50);
cur.bombs = (cur.bombs | 0) + 1;
US.save.save();
US.audio.sfx('buy');
renderShop();
};
bombRow.appendChild(bl);
bombRow.appendChild(bb);
}
/* ---------------- settings ---------------- */
function bindSettings() {
var s = US.save.get();
var langSel = $('set-lang');
langSel.value = s.lang;
langSel.onchange = function () {
US.save.get().lang = langSel.value;
US.save.save();
US.i18n.setLang(langSel.value);
US.i18n.apply();
};
$('set-sfx').value = s.settings.sfx * 100;
$('set-music').value = s.settings.music * 100;
$('set-autoaim').checked = !!s.settings.autoAim;
$('set-autofire').checked = !!s.settings.autofire;
$('set-onehit').checked = !!s.settings.onehit;
$('set-shake').checked = !!s.settings.shake;
$('set-name').value = s.name;
$('set-sfx').oninput = function () { US.save.get().settings.sfx = this.value / 100; US.audio.setVolumes(); US.save.save(); };
$('set-music').oninput = function () { US.save.get().settings.music = this.value / 100; US.audio.setVolumes(); US.save.save(); };
if ($('set-autoaim')) $('set-autoaim').onchange = function () { US.save.get().settings.autoAim = this.checked; US.save.save(); };
$('set-autofire').onchange = function () { US.save.get().settings.autofire = this.checked; US.save.save(); };
$('set-onehit').onchange = function () { US.save.get().settings.onehit = this.checked; US.save.save(); };
$('set-shake').onchange = function () { US.save.get().settings.shake = this.checked; US.save.save(); };
if ($('set-shakeamt')) {
var sa = $('set-shakeamt');
sa.value = Math.round((US.save.get().settings.shakeAmt == null ? 1 : US.save.get().settings.shakeAmt) * 100);
if ($('val-shakeamt')) $('val-shakeamt').textContent = sa.value + '%';
sa.oninput = function () {
US.save.get().settings.shakeAmt = this.value / 100;
US.save.save();
if ($('val-shakeamt')) $('val-shakeamt').textContent = this.value + '%';
};
}
if ($('set-quality')) {
$('set-quality').value = US.save.get().settings.quality || 'high';
$('set-quality').onchange = function () {
US.save.get().settings.quality = this.value;
US.save.save();
};
}
$('set-name').onchange = function () {
US.save.get().name = (this.value || 'Rookie').slice(0, 16);
US.save.save();
toast(US.i18n.t('toast.saved'));
};
$('set-reset').onclick = function () {
if (confirm(US.i18n.t('set.resetConfirm'))) {
US.save.reset();
location.reload();
}
};
}
/* ---------------- leaderboard ---------------- */
function renderRank() {
var s = US.save.get();
var box = $('rank-body');
box.innerHTML = '';
if (!s.board.length) {
var d = document.createElement('div');
d.className = 'empty';
d.textContent = US.i18n.t('rank.empty');
box.appendChild(d);
return;
}
s.board.forEach(function (row, i) {
var r = document.createElement('div');
r.className = 'rank-row' + (i === 0 ? ' gold' : '');
[('#' + (i + 1)), row.name, US.util.fmt(row.score), String(row.wave)].forEach(function (cell) {
var c = document.createElement('span');
c.textContent = cell;
r.appendChild(c);
});
box.appendChild(r);
});
}
/* ---------------- card pick ---------------- */
function showCardPick(cards, onPick) {
var ov = $('overlay-cards');
var wrap = $('card-row');
wrap.innerHTML = '';
$('card-hint').textContent = US.i18n.t('card.pickHint');
cards.forEach(function (c) {
var el = document.createElement('div');
el.className = 'card rarity-' + c.rarity;
var rc = document.createElement('span');
rc.className = 'card-rarity';
rc.textContent = US.i18n.t('card.rarity' + c.rarity.charAt(0).toUpperCase() + c.rarity.slice(1));
var ic = document.createElement('div');
ic.className = 'card-icon';
ic.textContent = c.icon;
var tt = document.createElement('div');
tt.className = 'card-title';
tt.textContent = c.title;
var dd = document.createElement('div');
dd.className = 'card-desc';
dd.textContent = c.desc;
el.appendChild(rc); el.appendChild(ic); el.appendChild(tt); el.appendChild(dd);
el.onclick = function () {
US.audio.sfx('card');
hide(ov);
onPick(c);
};
wrap.appendChild(el);
});
show(ov);
}
/* ---------------- pause / gameover / revive ---------------- */
function showPause() { show($('overlay-pause')); }
function hidePause() { hide($('overlay-pause')); }
var lastRunStats = null;
function buildShareCard(st) {
var cv = document.createElement('canvas');
cv.width = 520; cv.height = 280;
var c7 = cv.getContext('2d');
// dusk backdrop
var g7 = c7.createLinearGradient(0, 0, 0, 280);
g7.addColorStop(0, '#221033'); g7.addColorStop(0.55, '#b4581c'); g7.addColorStop(1, '#241206');
c7.fillStyle = g7; c7.fillRect(0, 0, 520, 280);
c7.fillStyle = 'rgba(255,217,143,0.9)';
c7.beginPath(); c7.arc(400, 90, 34, 0, Math.PI * 2); c7.fill();
// hero silhouette dot
c7.fillStyle = st.skinColor || '#e04b3a';
c7.beginPath(); c7.arc(96, 190, 26, 0, Math.PI * 2); c7.fill();
c7.strokeStyle = 'rgba(20,10,4,0.9)'; c7.lineWidth = 4; c7.stroke();
// text block
c7.fillStyle = '#fff3dd';
c7.font = 'bold 30px system-ui'; c7.textAlign = 'left';
c7.fillText('PRIMAL RAMPAGE', 150, 60);
c7.font = '16px system-ui';
c7.fillStyle = '#ffe4bd';
c7.fillText((st.name || US.save.get().name || 'Hunter') + ' · 🌍 ' + (st.biome || 'jungle'), 150, 92);
c7.font = 'bold 44px system-ui';
c7.fillStyle = '#ffd24a';
c7.fillText('Wave ' + (st.wave | 0), 150, 150);
c7.font = '18px system-ui';
c7.fillStyle = '#fff3dd';
c7.fillText('💀 ' + (st.kills | 0) + ' kills ×' + st.bestCombo + ' combo 💰' + (st.coins | 0), 150, 186);
if (st.petName) { c7.fillText('🐾 ' + st.petName, 150, 216); }
c7.font = '12px system-ui';
c7.fillStyle = 'rgba(255,233,201,0.75)';
c7.fillText('play: trycloudflare — hunt or be hunted', 150, 252);
return cv;
}
function gameOver(stats, mode) {
lastRunStats = stats;
$('over-title').textContent = US.i18n.t('over.title');
$('over-record').classList.toggle('hidden', !stats.record);
$('ov-score').textContent = US.util.fmt(stats.score);
$('ov-wave').textContent = mode === 'campaign' ? stats.wave : '—';
$('ov-kills').textContent = stats.kills;
$('ov-combo').textContent = '×' + stats.bestCombo;
$('ov-coins').textContent = '+$' + stats.coins;
setupAdButton(stats.coins | 0);
show($('overlay-over'));
}
function arenaOver(winner, g) {
$('over-title').textContent = winner.player ? US.i18n.t('over.win') : 'DEFEAT';
$('over-record').classList.add('hidden');
$('ov-score').textContent = g.kills + ' / ' + g.targetKills;
$('ov-wave').textContent = '—';
$('ov-kills').textContent = g.kills;
$('ov-combo').textContent = '×' + (g.bestCombo || 0);
$('ov-coins').textContent = '+$' + g.runCoins;
show($('overlay-over'));
}
function showRevive(cost, seconds) {
$('revive-cost').textContent = '-$' + cost;
var left = seconds;
$('revive-count').textContent = left;
show($('overlay-revive'));
clearInterval(reviveTimer);
reviveTimer = setInterval(function () {
left--;
$('revive-count').textContent = Math.max(0, left);
if (left <= 0) US.game.declineRevive();
}, 1000);
}
function hideRevive() {
clearInterval(reviveTimer);
hide($('overlay-revive'));
}
/* ---------------- permanent upgrades tab ---------------- */
function upCost(def, lv) { return def.base * (lv + 1); }
function renderUpgrades(grid) {
var sv = US.save.get();
var bombRow = $('shop-bombs');
if (bombRow) bombRow.innerHTML = '';
Object.keys(US.UPGRADES).forEach(function (id) {
var d = US.UPGRADES[id];
var lv = sv.upgrades[id] | 0;
var maxed = lv >= d.max;
var cost = upCost(d, lv);
var card = document.createElement('div');
card.className = 'item upgrade' + (maxed ? ' equipped' : '');
var h = document.createElement('div');
h.className = 'item-name';
h.textContent = L(d);
card.appendChild(h);
var pips = document.createElement('div');
pips.className = 'pips';
for (var i = 0; i < d.max; i++) {
var pip = document.createElement('span');
pip.className = 'pip' + (i < lv ? ' on' : '');
pips.appendChild(pip);
}
card.appendChild(pips);
var desc = document.createElement('div');
desc.className = 'item-desc';
desc.textContent = L({ en: d.en2 || d.per, vi: d.per });
card.appendChild(desc);
var btn = document.createElement('button');
btn.className = 'btn';
if (maxed) {
btn.textContent = US.i18n.t('shop.equipped');
btn.disabled = true;
} else {
btn.textContent = US.i18n.t('shop.buy') + ' — $' + US.util.fmt(cost);
btn.onclick = function () {
if (US.save.get().coins < cost) { toast(US.i18n.t('toast.notEnough')); return; }
US.save.buyUpgrade(id, cost);
US.audio.sfx('buy');
toast('💪 ' + L(d) + ' Lv.' + ((sv.upgrades[id] | 0)));
renderShop();
refreshMenuStats();
};
}
card.appendChild(btn);
grid.appendChild(card);
});
}
/* ---------------- daily quests + achievements ---------------- */
function questBadge() {
var sv = US.save.get();
var claimable = (sv.quests.items || []).some(function (q) {
return q.done && !q.claimed;
});
if ($('dot-quests')) $('dot-quests').classList.toggle('hidden', !claimable);
// daily hub glows when anything is waiting
var dailyReady =
!(sv.login.claimedToday && sv.login.last === US.save.today()) ||
US.save.canSpin() || sv.chest.keys > 0;
if ($('dot-daily')) $('dot-daily').classList.toggle('hidden', !dailyReady);
}
function renderQuests() {
var sv = US.save.get();
ensureUiQuests(sv);
$('quest-streak').textContent = '🔥 ' + L({
en: 'Login streak: day ' + (sv.quests.streak | 0),
vi: 'Điểm danh ngày thứ ' + (sv.quests.streak | 0)
}) + (sv.gems ? ' · 🔥 ×' + sv.gems : '');
$('h3-ach').textContent = '🏆 ' + L({ en: 'Achievements', vi: 'Thành tựu' });
var list = $('quest-list');
list.innerHTML = '';
sv.quests.items.forEach(function (q) {
var def = null;
US.QUEST_POOL.forEach(function (d0) { if (d0.id === q.defId) def = d0; });
if (!def) return;
var row = document.createElement('div');
row.className = 'item quest-row' + (q.claimed ? ' equipped' : '');
var t = document.createElement('div');
t.className = 'item-name';
t.textContent = L(def.text);
row.appendChild(t);
var bar = document.createElement('div');
bar.className = 'qbar';
var fill = document.createElement('div');
fill.className = 'qfill';
fill.style.width = Math.min(100, (q.prog / q.goal) * 100) + '%';
bar.appendChild(fill);
row.appendChild(bar);
var meta = document.createElement('div');
meta.className = 'item-desc';
meta.textContent = Math.min(q.prog, q.goal) + ' / ' + q.goal +
' · 🔥 +' + def.gem + ' $+' + def.coins;
row.appendChild(meta);
var btn = document.createElement('button');
btn.className = 'btn';
if (q.claimed) { btn.textContent = '✓'; btn.disabled = true; }
else if (q.done) {
btn.textContent = L({ en: 'Claim', vi: 'Nhận thưởng' });
btn.onclick = function () {
q.claimed = true;
US.save.addGems(def.gem);
US.save.addCoins(def.coins);
US.audio.sfx('buy');
toast('📜 +🔥' + def.gem + ' +$' + def.coins);
US.save.save();
renderQuests();
refreshMenuStats();
};
} else {
btn.textContent = '…';
btn.disabled = true;
}
row.appendChild(btn);
list.appendChild(row);
});
// achievements
var al = $('ach-list');
al.innerHTML = '';
Object.keys(US.ACHIEVEMENTS).forEach(function (id) {
var d = US.ACHIEVEMENTS[id];
var got = US.save.hasAch(id);
var el = document.createElement('div');
el.className = 'ach-row' + (got ? ' done' : '');
el.textContent = (got ? '🏆 ' : '🔒 ') + L(d.text) + ' (🔥' + d.gem + ')';
al.appendChild(el);
});
}
function ensureUiQuests(sv) {
// game.js owns the generator; nudge it via a run-less call
if (!sv.quests.items || sv.quests.items.length !== 3 || sv.quests.date !== US.save.today()) {
// replicate light version: full logic lives in game.js ensureQuests
if (US.game && US.game.ensureQuests) US.game.ensureQuests();
else {
sv.quests.items = sv.quests.items || [];
}
}
}
/* ---------------- daily hub ---------------- */
function fmtReward(rw) {
var parts = [];
if (rw.coins) parts.push('💰' + rw.coins);
if (rw.gems) parts.push('🔥' + rw.gems);
if (rw.chestKey) parts.push('🗝️×' + rw.chestKey);
if (rw.egg) parts.push('🥚');
return parts.join(' ') || '—';
}
function renderDaily() {
var sv = US.save.get();
var L2 = function (k) { return US.i18n.t(k); };
// calendar strip
var strip = document.getElementById('login-strip');
if (strip) {
strip.innerHTML = '';
var dayIdx = Math.min(6, sv.login.streak - (sv.login.claimedToday ? 0 : 1));
for (var d = 0; d < 7; d++) {
var chip = document.createElement('div');
chip.className = 'cal-day' + (d < dayIdx ? ' past' : d === dayIdx ? ' today' : '');
chip.innerHTML = '' + L2('daily.day') + ' ' + (d + 1) + '' +
fmtReward(US.LOGIN_REWARDS[d]) + '';
strip.appendChild(chip);
}
}
var btnClaim = document.getElementById('btn-claim-login');
if (btnClaim) {
var canClaim = !(sv.login.claimedToday && sv.login.last === US.save.today());
btnClaim.disabled = !canClaim;
btnClaim.textContent = canClaim ? L2('daily.claim') : L2('daily.claimed');
btnClaim.onclick = function () {
var res = US.save.claimLogin();
if (!res.ok) return;
US.audio.sfx('win');
document.getElementById('wheel-result').textContent =
'✅ ' + fmtReward(res.reward) + (res.pet ? ' 🥚 ' + res.pet.name : '');
renderDaily();
};
}
// wheel
drawWheel(document.getElementById('wheel-cv'), -Math.PI / 2);
var btnSpin = document.getElementById('btn-spin');
if (btnSpin) {
btnSpin.disabled = !US.save.canSpin();
btnSpin.textContent = US.save.canSpin() ? L2('daily.spin') : L2('daily.spun');
btnSpin.onclick = function () {
if (!US.save.canSpin()) return;
var res = US.save.spinWheel();
if (!res.ok) return;
US.audio.sfx('ui');
animateWheel(res.seg, function () {
US.audio.sfx('win');
document.getElementById('wheel-result').textContent =
'🎉 ' + fmtReward(res.reward) + (res.pet ? ' 🥚 ' + res.pet.name : '');
});
btnSpin.disabled = true;
btnSpin.textContent = L2('daily.spun');
};
}
// ---- daily challenge panel ----
var dinfo = US.save.dailyInfo();
var cm = document.getElementById('chal-mods');
if (cm) {
cm.innerHTML = dinfo.mods.map(function (mid) {
var md = US.DAILY_MOD_POOL.filter(function (m3) { return m3.id === mid; })[0] || {};
return '🎲 ' + L({ en: md.en, vi: md.vi });
}).join('
');
}
var cb = document.getElementById('chal-best');
var stCh = US.save.get().dailyChal;
if (cb) cb.textContent = (stCh.date === dinfo.date && stCh.bestScore > 0)
? L({ en: "Today's best", vi: 'Kỷ lục hôm nay' }) + ': ' + stCh.bestScore +
' · Top: ' + (stCh.board[0] ? stCh.board[0].score : '—')
: '';
var bp2 = document.getElementById('btn-play-chal');
if (bp2) bp2.onclick = function () {
US.audio.resume(); US.audio.sfx('ui');
US.game.start('campaign', { challenge: true });
showScreen(null);
};
// ---- weekly raid panel ----
var ri = US.save.raidInfo();
var pct = Math.min(100, Math.round(ri.damage / ri.pool * 100));
var fill = document.getElementById('raid-bar-fill');
if (fill) fill.style.width = pct + '%';
var rinfo = document.getElementById('raid-info');
if (rinfo) rinfo.innerHTML =
L({ en: 'Your damage', vi: 'Sát thương của bạn' }) + ': ' + ri.damage.toLocaleString() +
' / ' + ri.pool.toLocaleString();
var tiersEl = document.getElementById('raid-tiers');
if (tiersEl) {
tiersEl.innerHTML = '';
ri.tiers.forEach(function (t4, ti4) {
var el = document.createElement('div');
var claimed = ri.claimedTiers.indexOf(ti4) >= 0;
var can = !claimed && ri.damage >= t4.dmg;
el.className = 'raid-tier' + (claimed ? ' claimed' : can ? ' claimable' : '');
el.innerHTML = '🔥' + t4.gems + '
' + t4.dmg.toLocaleString();
el.onclick = function () {
if (!can) return;
var r3 = US.save.claimRaidTier(ti4);
if (r3.ok) { US.audio.sfx('win'); renderDaily(); }
};
tiersEl.appendChild(el);
});
}
var br2 = document.getElementById('btn-play-raid');
if (br2) br2.onclick = function () {
US.audio.resume(); US.audio.sfx('ui');
US.game.start('campaign', { raid: true });
showScreen(null);
};
// ---- prestige / rebirth panel ----
var pi = US.save.prestigeInfo();
var prInfo = document.getElementById('pr-info');
if (prInfo) prInfo.innerHTML =
L({ en: 'Ancient Gems', vi: 'Ngọc Xưa' }) + ': 🟣 ' + pi.ngoc + '' +
' · ' + L({ en: 'Rebirths', vi: 'Tái sinh' }) + ': ' + pi.count + '
' +
(pi.canPrestige
? L({ en: 'Rebirth now for', vi: 'Tái sinh nhận' }) + ' +' + pi.ngocGain + ''
: L({ en: 'Reach wave 50 to unlock Rebirth', vi: 'Đến wave 50 để mở Tái Sinh' }));
var perksEl = document.getElementById('pr-perks');
if (perksEl) {
perksEl.innerHTML = '';
Object.keys(pi.perkDefs).forEach(function (pk3) {
var def = pi.perkDefs[pk3];
var lv = pi.perks[pk3] || 0;
var cost = lv < def.cost.length ? def.cost[lv] : null;
var row = document.createElement('div');
row.className = 'pr-perk';
var label = pk3 === 'dmg' ? ('⚔️ +' + Math.round(def.per * 100) + '% DMG /lv')
: pk3 === 'hp' ? ('❤️ +' + def.per + ' HP /lv')
: ('💰 +' + Math.round(def.per * 100) + '% gold /lv');
row.innerHTML = '' + label + 'Lv ' + lv + '/' + def.cost.length + '';
var btn = document.createElement('button');
btn.className = 'btn sm';
btn.textContent = cost !== null ? '🟣' + cost : 'MAX';
btn.disabled = cost === null || pi.ngoc < cost;
btn.onclick = function () {
var r4 = US.save.buyPerk(pk3);
if (r4.ok) { US.audio.sfx('win'); renderDaily(); }
};
row.appendChild(btn);
perksEl.appendChild(row);
});
}
var btnReb = document.getElementById('btn-rebirth');
if (btnReb) {
btnReb.disabled = !pi.canPrestige;
btnReb.onclick = function () {
if (!pi.canPrestige) return;
var r5 = US.save.doPrestige();
if (r5.ok) {
US.audio.sfx('win');
toast('♻️ +' + r5.gained + ' ' + L({ en: 'Ancient Gems', vi: 'Ngọc Xưa' }));
renderDaily();
refreshMenuStats();
}
};
}
// chest
var info = document.getElementById('chest-info');
if (info) info.innerHTML = '🗝️ ×' + sv.chest.keys + '';
var btnChest = document.getElementById('btn-open-chest');
if (btnChest) {
var hasKey = sv.chest.keys > 0;
btnChest.disabled = !hasKey;
btnChest.textContent = hasKey ? L2('daily.open') : L2('daily.nokey');
btnChest.onclick = function () {
var res = US.save.openChest();
if (!res.ok) return;
US.audio.sfx('win');
document.getElementById('wheel-result').textContent =
'🧰 ' + fmtReward(res.reward) + (res.pet ? ' 🥚 ' + res.pet.name : '');
renderDaily();
};
}
}
function drawWheel(cv, rot) {
if (!cv || !cv.getContext) return;
var c2 = cv.getContext('2d');
var W2 = cv.width, H2 = cv.height, r = W2 / 2 - 6;
var cols = ['#c44a14', '#7a5230', '#b4581c', '#58a55c', '#9fd8ff', '#ffb938', '#b05f8f', '#ffd24a'];
c2.clearRect(0, 0, W2, H2);
c2.save();
c2.translate(W2 / 2, H2 / 2);
c2.rotate(rot || 0);
for (var i = 0; i < 8; i++) {
var a0 = i * Math.PI / 4, a1 = a0 + Math.PI / 4;
c2.beginPath(); c2.moveTo(0, 0); c2.arc(0, 0, r, a0, a1); c2.closePath();
c2.fillStyle = cols[i % cols.length]; c2.fill();
c2.strokeStyle = 'rgba(20,10,4,0.8)'; c2.lineWidth = 2; c2.stroke();
c2.save();
c2.rotate(a0 + Math.PI / 8);
c2.fillStyle = '#fff3dd';
c2.font = 'bold 11px system-ui';
c2.textAlign = 'right';
var lbl = fmtReward(US.save.WHEEL[i]);
c2.fillText(lbl, r - 10, 4);
c2.restore();
}
c2.restore();
c2.beginPath(); c2.arc(W2 / 2, H2 / 2, 16, 0, Math.PI * 2);
c2.fillStyle = '#241206'; c2.fill();
c2.strokeStyle = '#ffb938'; c2.lineWidth = 3; c2.stroke();
}
function animateWheel(targetSeg, done) {
var cv = document.getElementById('wheel-cv');
if (!cv) { if (done) done(); return; }
var start = performance.now(), dur = 2600;
var turns = 5;
var targetRot = turns * Math.PI * 2 + (Math.PI * 2 - targetSeg * Math.PI / 4 - Math.PI / 8);
(function step() {
var t = (performance.now() - start) / dur;
if (t >= 1) { drawWheel(cv, targetRot % (Math.PI * 2)); if (done) done(); return; }
var e2 = 1 - Math.pow(1 - t, 3);
drawWheel(cv, e2 * targetRot);
requestAnimationFrame(step);
})();
}
/* ---------------- bestiary ---------------- */
function renderBestiary() {
var sv = US.save.get();
var grid = $('bestiary-grid');
grid.innerHTML = '';
var keys = Object.keys(US.BESTIARY);
var doneN = 0;
keys.forEach(function (id) {
var d = US.BESTIARY[id];
var e = sv.bestiary[id] || { kills: 0, done: false };
if (e.done) doneN++;
var card = document.createElement('div');
card.className = 'item best-card' + (e.done ? ' equipped' : (e.kills > 0 ? '' : ' locked'));
var h = document.createElement('div');
h.className = 'item-name';
h.textContent = e.kills > 0 ? L(d.name) : '? ? ?';
card.appendChild(h);
card.appendChild(makePreview(104, 60, function (c2, w2, h2) {
drawBeastIcon(c2, w2, h2, id, e.kills <= 0);
if (e.done) {
c2.strokeStyle = 'rgba(255,210,74,0.85)';
c2.lineWidth = 2;
c2.strokeRect(1.5, 1.5, w2 - 3, h2 - 3);
}
}));
var lore = document.createElement('div');
lore.className = 'item-desc';
lore.textContent = e.done ? L(d.lore)
: L({ en: 'Slay ' + d.goal + ' to reveal', vi: 'Hạ ' + d.goal + ' con để mở ký sự' }) +
' (' + Math.min(e.kills, d.goal) + '/' + d.goal + ')';
card.appendChild(lore);
var rw = document.createElement('div');
rw.className = 'item-desc';
rw.style.color = '#ffb938';
rw.textContent = e.done ? '✓ 🔥' + d.gem : '🔥' + d.gem;
card.appendChild(rw);
grid.appendChild(card);
});
$('best-progress').textContent = '📖 ' + doneN + ' / ' + keys.length;
}
/* ---------------- rewarded ad stub (plug real SDK here) ---------------- */
function setupAdButton(runCoins) {
var btn = $('btn-adx2');
if (!btn) return;
if (!US.save.canWatchAd()) { btn.classList.add('hidden'); return; }
btn.classList.remove('hidden');
btn.textContent = L({
en: '▶ Watch ad — bonus +50% amber (' + runCoins + ' → ' + Math.floor(runCoins * 1.5) + ')',
vi: '▶ Xem quảng cáo — +50% hổ phách (' + runCoins + ' → ' + Math.floor(runCoins * 1.5) + ')'
});
btn.onclick = function () {
btn.disabled = true;
var left = 5;
btn.textContent = L({ en: 'Ad playing… ', vi: 'Đang phát quảng cáo… ' }) + left + 's';
var iv = setInterval(function () {
left--;
if (left > 0) { btn.textContent = L({ en: 'Ad playing… ', vi: 'Đang phát quảng cáo… ' }) + left + 's'; return; }
clearInterval(iv);
US.save.consumeAd();
var bonus = Math.floor(runCoins * 0.5);
US.save.addCoins(bonus);
btn.classList.add('hidden');
toast('💰 +' + bonus);
US.audio.sfx('buy');
}, 1000);
};
}
/* ---------------- first-run tutorial ---------------- */
var TUT_STEPS = [
{ en: 'Move with WASD / arrows (or the touch sticks).', vi: 'Di chuyển bằng WASD / mũi tên (hoặc joystick cảm ứng).' },
{ en: 'You auto-aim and auto-fire at the nearest beast.', vi: 'Bạn tự ngắm và tự bắn vào con thú gần nhất.' },
{ en: 'Kill fast to build COMBO and go ON FIRE — double damage!', vi: 'Giết nhanh để lên COMBO và BÙNG CHÁY — sát thương x2!' },
{ en: 'Grab the amber ($) they drop — spend it on weapons & permanent power.', vi: 'Nhặt hổ phách ($) rơi ra — dùng mua vũ khí & nâng cấp vĩnh viễn.' },
{ en: 'Every 5th wave a BOSS stalks in. Feel the footsteps. Good luck, chief.', vi: 'Mỗi wave thứ 5 trùm sẽ xuất hiện. Nghe tiếng bước chân đi. Chúc may mắn, thủ lĩnh.' }
];
var tutStep = -1;
function startTutorial() {
tutStep = 0;
showTutStep();
show($('overlay-tutorial'));
}
function showTutStep() {
$('tut-title').textContent = L({ en: 'Welcome to the Valley', vi: 'Chào mừng đến Thung Lũng' });
$('tut-step').textContent = L(TUT_STEPS[tutStep]);
$('btn-tut-next').textContent = tutStep >= TUT_STEPS.length - 1
? L({ en: 'Let’s hunt!', vi: 'Đi săn thôi!' })
: L({ en: 'Next', vi: 'Tiếp' });
}
function endTutorial() {
hide($('overlay-tutorial'));
US.save.get().tut = true;
US.save.save();
tutStep = -1;
}
/* ---------------- wiring ---------------- */
function init() {
US.i18n.setLang(US.save.get().lang);
US.i18n.apply();
$('btn-campaign').onclick = function () { US.audio.resume(); US.audio.sfx('ui'); US.game.start('campaign'); showScreen(null); };
$('btn-arena').onclick = function () { US.audio.sfx('ui'); showScreen('arena-setup'); };
$('btn-shop').onclick = function () { US.audio.sfx('ui'); renderShop(); showScreen('shop'); };
$('btn-settings').onclick = function () { US.audio.sfx('ui'); bindSettings(); showScreen('settings'); };
$('btn-rank').onclick = function () { US.audio.sfx('ui'); renderRank(); showScreen('rank'); };
$('btn-howto').onclick = function () { US.audio.sfx('ui'); showScreen('howto'); };
// commercialization screens — guarded so a stale HTML shell can never
// crash menu init (feature buttons simply stay hidden)
// subtle hover tick on the pro menu list
document.querySelectorAll('.btn-menu').forEach(function (b) {
b.addEventListener('mouseenter', function () {
if (US.audio && US.audio.hoverTick) US.audio.hoverTick();
});
});
if ($('btn-daily')) $('btn-daily').onclick = function () { US.audio.sfx('ui'); renderDaily(); showScreen('daily'); };
if ($('btn-share')) $('btn-share').onclick = function () {
if (!lastRunStats) return;
var card2 = buildShareCard({
name: US.save.get().name,
wave: lastRunStats.wave, kills: lastRunStats.kills,
bestCombo: lastRunStats.bestCombo, coins: lastRunStats.coins,
biome: lastRunStats.biome, petName: lastRunStats.petName,
skinColor: lastRunStats.skinColor
});
try {
var a4 = document.createElement('a');
a4.download = 'primal-rampage-wave' + (lastRunStats.wave | 0) + '.png';
a4.href = card2.toDataURL('image/png');
document.body.appendChild(a4); a4.click(); a4.remove();
toast(L({ en: '📸 Share card saved!', vi: '📸 Đã lưu thẻ chia sẻ!' }));
} catch (e9) {
toast(L({ en: '📸 Card ready!', vi: '📸 Thẻ đã sẵn sàng!' }));
}
};
if ($('btn-quests')) $('btn-quests').onclick = function () { US.audio.sfx('ui'); renderQuests(); showScreen('quests'); };
if ($('btn-bestiary')) $('btn-bestiary').onclick = function () { US.audio.sfx('ui'); renderBestiary(); showScreen('bestiary'); };
if ($('btn-upgrades')) $('btn-upgrades').onclick = function () { US.audio.sfx('ui'); shopTab = 'upgrades'; renderShop(); showScreen('shop'); };
function setTxt(id, v) { if ($(id)) $(id).textContent = v; }
setTxt('lbl-quests', L({ en: 'Quests', vi: 'Nhiệm vụ' }));
setTxt('lbl-daily', L({ en: 'Daily Hub', vi: 'Ưu đãi hôm nay' }));
setTxt('lbl-bestiary', L({ en: 'Bestiary', vi: 'Ký sự thú' }));
setTxt('lbl-upgrades', L({ en: 'Upgrades', vi: 'Nâng cấp' }));
setTxt('lbl-tabup', L({ en: 'Upgrades', vi: 'Nâng cấp' }));
setTxt('h2-quests', '📜 ' + L({ en: 'Daily Quests', vi: 'Nhiệm vụ mỗi ngày' }));
setTxt('h2-best', '📖 ' + L({ en: 'Bestiary of the Valley', vi: 'Ký sự thú thung lũng' }));
setTxt('lbl-shakeamt', L({ en: 'Screen shake', vi: 'Rung màn hình' }));
setTxt('lbl-quality', L({ en: 'Effects quality', vi: 'Chất lượng hiệu ứng' }));
document.querySelectorAll('[data-back]').forEach(function (b) {
b.onclick = function () { US.audio.sfx('ui'); showScreen('menu'); };
});
[['tab-weapons', 'weapons'], ['tab-heroes', 'heroes'], ['tab-skins', 'skins'], ['tab-upgrades', 'upgrades']].forEach(function (p) {
$(p[0]).onclick = function () { US.audio.sfx('ui'); shopTab = p[1]; renderShop(); };
});
// arena setup
$('arena-bots').onchange = function () {};
$('arena-target').onchange = function () {};
$('arena-start').onclick = function () {
US.audio.resume();
US.audio.sfx('ui');
US.game.start('arena', {
arena: {
bots: parseInt($('arena-bots').value, 10),
target: parseInt($('arena-target').value, 10)
}
});
showScreen(null);
};
// first-run tutorial (guarded for stale shells)
if ($('overlay-tutorial') && !$('overlay-tutorial').classList.contains('hidden')) hide($('overlay-tutorial'));
if (!$('btn-tut-next')) return;
$('btn-tut-next').onclick = function () {
US.audio.sfx('ui');
if (tutStep >= TUT_STEPS.length - 1) endTutorial();
else { tutStep++; showTutStep(); }
};
$('btn-tut-skip').onclick = function () { US.audio.sfx('ui'); endTutorial(); };
$('btn-tut-skip').textContent = L({ en: 'Skip', vi: 'Bỏ qua' });
if (!US.save.get().tut && !$('btn-retry').onclick) setTimeout(startTutorial, 400);
// pause
$('btn-pause').onclick = function () {
if (US.game.pause()) { US.audio.sfx('ui'); showPause(); }
};
$('btn-resume').onclick = function () {
if (US.game.resume()) { US.audio.sfx('ui'); hidePause(); }
};
$('btn-quit').onclick = function () {
// bank the run politely before leaving
var g = US.game.get();
if (g && !g.over && g.mode === 'campaign' && (g.wave > 0)) {
US.save.addCoins(g.runCoins);
US.save.submitRun(g.score, g.wave || 0);
}
US.game.stop();
US.ui.hideRevive();
hidePause();
hide($('overlay-over'));
hide($('overlay-cards'));
showScreen('menu');
};
// game over
$('btn-retry').onclick = function () {
var g = US.game.get();
var mode = g ? g.mode : 'campaign';
US.game.stop();
hide($('overlay-over'));
US.audio.sfx('ui');
if (mode === 'arena') showScreen('arena-setup');
else { US.game.start('campaign'); showScreen(null); }
};
$('btn-tomenu').onclick = function () {
US.game.stop();
hide($('overlay-over'));
showScreen('menu');
};
// revive
$('btn-revive').onclick = function () { US.audio.sfx('buy'); US.game.acceptRevive(); };
$('btn-nodecline').onclick = function () { US.game.declineRevive(); };
showScreen('menu');
}
US.ui = {
init: init,
showCardPick: showCardPick,
gameOver: gameOver,
arenaOver: arenaOver,
showRevive: showRevive,
hideRevive: hideRevive,
toast: toast,
questBadge: questBadge
};
})();