/* 贪蛇出动 - UI:HUD 刷新 / 道具转盘 / 复活 / 结算 / Toast 与交互详情弹窗(双模式) */ (function () { 'use strict'; window.GS = window.GS || {}; const U = GS.U; /* ---------- 道具图标:程序化 SVG ---------- */ function itemSvg(key) { switch (key) { case 'helmet': return ''; case 'rocket': return ''; case 'magnet': return ''; case 'expcard': return ''; default: return ''; } } class UI { constructor() { this.el = id => document.getElementById(id); this.statsEl = this.el('gs-hud-stats'); this.coinsEl = this.el('gs-stat-coins'); this.luckyEl = this.el('gs-stat-lucky'); this.killsEl = this.el('gs-stat-kills'); this.progEl = this.el('gs-hud-progress'); this.progFill = this.el('gs-progress-fill'); this.progText = this.el('gs-progress-text'); this.boardEl = this.el('gs-hud-board'); this.boardList = this.el('gs-board-list'); this.bestName = this.el('gs-best-name'); this.bestScore = this.el('gs-best-score'); this.rouletteEl = this.el('gs-roulette'); this.reviveEl = this.el('gs-revive'); this.resultEl = this.el('gs-result'); this.toastEl = this.el('gs-toast'); this.dlgEl = this.el('gs-dialog'); this._bulbs(); this._toastTimer = null; this._dlgTimer = null; } /* 转盘灯泡装饰 */ _bulbs() { document.querySelectorAll('.gs-bulbs').forEach(box => { const vert = box.classList.contains('gs-bulbs-left') || box.classList.contains('gs-bulbs-right'); const n = vert ? 7 : 12; for (let i = 0; i < n; i++) box.appendChild(document.createElement('i')); }); setInterval(() => { document.querySelectorAll('.gs-bulbs i').forEach((b, i) => b.classList.toggle('off', (i + Math.floor(Date.now() / 400)) % 2 === 0)); }, 400); } setUiMode(mode) { GS.CFG.uiMode = mode; } /* ---------- HUD ---------- */ showHud(v) { this.statsEl.classList.toggle('gs-hidden', !v); this.boardEl.classList.toggle('gs-hidden', !v); } updateStats(totalCoins, lucky, kills) { this.coinsEl.textContent = U.fmt(totalCoins); this.luckyEl.textContent = U.fmt(lucky); this.killsEl.textContent = U.fmt(kills); } updateProgress(score, target, visible) { this.progEl.classList.toggle('gs-hidden', !visible); if (!visible) return; this.progFill.style.width = U.clamp(score / target * 100, 0, 100) + '%'; this.progText.textContent = Math.floor(Math.min(score, target)) + '/' + target; } updateBoard(rows, me, best) { if (best) { this.bestName.textContent = best.name || '-'; this.bestScore.textContent = U.fmt(best.score || 0); } let html = ''; rows.forEach((r, i) => { const rank = i + 1; const medal = rank === 1 ? '👑' : (rank <= 3 ? ['①', '②', '③'][rank - 1] : rank); html += `
${medal} ${this._esc(r.name)} ${U.fmt(r.score)}
`; }); this.boardList.innerHTML = html; } _esc(s) { return String(s).replace(/[<>&"]/g, c => ({ '<': '<', '>': '>', '&': '&', '"': '"' }[c])); } /* ---------- 转盘 ---------- */ showRoulette(gems) { this.rouletteEl.classList.remove('gs-hidden'); this.el('gs-reroll-text').textContent = GS.CFG.rerollCost + ' 换一换'; this.updateGems(gems); } hideRoulette() { this.rouletteEl.classList.add('gs-hidden'); } updateGems(gems) { const btn = this.el('gs-reroll'); btn.disabled = gems < GS.CFG.rerollCost; } setReels(keys, selected) { document.querySelectorAll('.gs-reel').forEach((reel, i) => { reel.classList.toggle('selected', selected === i); reel.querySelector('.gs-reel-icon').innerHTML = itemSvg(keys[i]); reel.title = GS.ITEMS[keys[i]].name + ':' + GS.ITEMS[keys[i]].desc; }); } spinReels(keys) { // 滚动动画:快速切换图标后落定 const reels = document.querySelectorAll('.gs-reel'); reels.forEach((reel, i) => { const icon = reel.querySelector('.gs-reel-icon'); let n = 0; const timer = setInterval(() => { icon.innerHTML = itemSvg(U.pick(GS.ITEM_KEYS)); if (++n > 8 + i * 4) { clearInterval(timer); icon.innerHTML = itemSvg(keys[i]); } }, 70); }); } /* ---------- 复活 ---------- */ showRevive(killerName, usedFree) { this.reviveEl.classList.remove('gs-hidden'); this.el('gs-revive-killer').textContent = killerName || '对手'; const btn = this.el('gs-revive-btn'); btn.disabled = usedFree; btn.textContent = usedFree ? '已复活过' : '▶ 免费复活'; } hideRevive() { this.reviveEl.classList.add('gs-hidden'); } updateReviveCount(n, pct) { this.el('gs-revive-count').textContent = n; this.el('gs-revive-count-ring').style.setProperty('--p', (pct * 100) + '%'); } /* ---------- 结算 ---------- */ showResult(data) { this.resultEl.classList.remove('gs-hidden'); this.el('gs-result-score').textContent = U.fmt(data.score); this.el('gs-result-coins').textContent = U.fmt(data.coins); this.el('gs-result-kills').textContent = U.fmt(data.kills); this.el('gs-result-lucky').textContent = U.fmt(data.lucky); this.el('gs-result-best-num').textContent = U.fmt(data.best); } hideResult() { this.resultEl.classList.add('gs-hidden'); } /* ---------- Toast(normal 模式 / 强提示) ---------- */ toast(msg, type) { const el = this.toastEl; el.textContent = msg; el.className = type || ''; el.classList.remove('gs-hidden'); clearTimeout(this._toastTimer); this._toastTimer = setTimeout(() => el.classList.add('gs-hidden'), 2000); } /* ---------- 交互详情弹窗(debug 模式) ---------- */ showDialog(info, mode) { // mode: view=查看类(normal 降级 toast) / error=异常(双模式保留) const isDebug = GS.CFG.uiMode === 'debug'; if (!isDebug && mode === 'view') { this.toast(info.action + ' 成功', 'success'); return; } if (!isDebug && mode !== 'error') { this.toast(info.action + (info.ok ? ' 成功' : ' 失败'), info.ok ? 'success' : 'error'); return; } const body = this.el('gs-dialog-body'); body.textContent = '触发动作:' + info.action + '\n' + '时间:' + U.nowStr() + '\n' + '接口地址:' + (info.url || '-') + '\n' + 'HTTP 方法:' + (info.method || '-') + '\n' + '请求参数:' + (info.req || '-') + '\n' + '返回结果:' + (info.res || '-') + '\n' + '错误信息:' + (info.err || '-') + '\n' + '耗时:' + (info.ms != null ? info.ms + 'ms' : '-') + '\n' + '当前状态:' + (info.ok ? '成功' : '失败'); this.dlgEl.classList.remove('gs-hidden'); this._dlgInfo = info; clearTimeout(this._dlgTimer); if (mode !== 'error') this._dlgTimer = setTimeout(() => this.hideDialog(), 3000); } hideDialog() { this.dlgEl.classList.add('gs-hidden'); } /* 带交互记录的 fetch 包装(双模式) */ async trackFetch(action, url, opts) { const t0 = performance.now(); let info = { action, url, method: (opts && opts.method) || 'GET', req: opts && opts.body ? opts.body : '-' }; try { const res = await fetch(url, opts); const text = await res.text(); info.ms = Math.round(performance.now() - t0); info.res = text.slice(0, 400); info.ok = res.ok; if (!res.ok) info.err = 'HTTP ' + res.status; this.showDialog(info, res.ok ? 'view' : 'error'); return res.ok ? JSON.parse(text) : null; } catch (e) { info.ms = Math.round(performance.now() - t0); info.ok = false; info.err = String(e && e.message || e); this.showDialog(info, 'error'); return null; } } } GS.UI = UI; GS.itemSvg = itemSvg; })();