/* 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: '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-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 }; })();