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