/* 贪蛇出动 - 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;
})();