Files
primal-rampage/js/ui.js
T
Primal Rampage Bot 2024ea48b5 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.
2026-08-23 04:26:58 +00:00

828 lines
31 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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 ($('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') {
desc.textContent = 'DMG ' + it.dmg + ' · ROF ' + it.rof + '/s' +
(it.pellets > 1 ? ' · ×' + it.pellets : '') + (it.pierce ? ' · PIERCE' : '') + (it.aoe ? ' · AoE' : '');
} 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')); }
function gameOver(stats, mode) {
$('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);
}
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 || [];
}
}
}
/* ---------------- 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: 'Lets 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-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-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
};
})();