// DOM 界面:菜单 / 关卡选择 / HUD / 结算 import { LEVELS, TYPE_LABEL, TYPE_CLASS } from './levels/index.js'; import { Save } from './save.js'; import { fmtTime } from './util.js'; const $ = s => document.querySelector(s); const $$ = s => Array.from(document.querySelectorAll(s)); const SCREENS = ['scr-title', 'scr-levels', 'scr-help', 'scr-pause', 'scr-result', 'scr-editor']; const TABS = [ ['all', '全部'], ['jump', '跳刺'], ['endur', '耐久'], ['boss', 'BOSS'], ['troll', '坑向'], ['custom', '自制'], ]; export const UI = { H: {}, // handlers filter: 'all', cur: 'scr-title', toastTimer: 0, init(handlers) { this.H = handlers; // 按钮 $$('[data-act]').forEach(btn => { btn.addEventListener('click', () => { const a = btn.dataset.act; if (this.H[a]) this.H[a](); }); }); // 分类 tabs const tabs = $('#level-tabs'); tabs.innerHTML = ''; TABS.forEach(([k, label]) => { const b = document.createElement('button'); b.className = 'tab' + (k === 'all' ? ' on' : ''); b.textContent = label; b.onclick = () => { this.filter = k; $$('#level-tabs .tab').forEach(t => t.classList.remove('on')); b.classList.add('on'); this.renderLevels(); }; tabs.appendChild(b); }); }, show(name) { SCREENS.forEach(s => $('#' + s).classList.toggle('hidden', s !== name)); this.cur = name; $('#hud').classList.toggle('hidden', name !== null && name !== ''); if (name === 'scr-levels') this.renderLevels(); }, showGame() { SCREENS.forEach(s => $('#' + s).classList.add('hidden')); this.cur = ''; $('#hud').classList.remove('hidden'); }, renderLevels() { const grid = $('#level-grid'); grid.innerHTML = ''; const customs = Save.customs().map((l, i) => ({ ...l, index: 100 + i, no: i + 1 })); const all = [...LEVELS, ...customs]; const list = all.filter(l => this.filter === 'all' ? true : l.type === this.filter); if (!list.length) { grid.innerHTML = '
这个分类下还没有关卡。去「自制关卡」造一个吧。
'; return; } list.forEach(l => { const rec = Save.get(l.id); const card = document.createElement('div'); card.className = 'lv-card' + (rec ? ' cleared' : ''); const stars = '★'.repeat(l.diff || 1) + '☆'.repeat(Math.max(0, 5 - (l.diff || 1))); card.innerHTML = `
${l.builtin === false ? 'U' + (l.no || '') : String(l.index + 1).padStart(2, '0')}
${escapeHtml(l.name || 'STAGE')}
${TYPE_LABEL[l.type] || '自制'} ${stars}
${rec ? `✔ 死亡 ${rec.deaths} · ${fmtTime(rec.time)}` : '未通关'}
${rec ? '
✔
' : ''} `; card.onclick = () => this.H.play(l); grid.appendChild(card); }); }, hud({ name, deaths, ms, apples, totalApples, hasGun, save, totalSaves }) { $('#hud-level').textContent = name || ''; $('#hud-deaths').textContent = 'DEATH ' + deaths; $('#hud-time').textContent = fmtTime(ms); const ap = $('#hud-apples'); if (totalApples > 0) { ap.style.display = ''; ap.textContent = `苹果 ${apples}/${totalApples}`; } else ap.style.display = 'none'; const sv = $('#hud-save'); if (totalSaves > 0) { sv.style.display = ''; sv.textContent = save > 0 ? `存档 ${save}/${totalSaves}` : `存档 0/${totalSaves}`; sv.classList.toggle('on', save > 0); } else sv.style.display = 'none'; }, bossBar(boss) { const bar = $('#boss-bar'); if (!boss) { bar.classList.add('hidden'); return; } bar.classList.remove('hidden'); $('#boss-name').textContent = boss.name; $('#boss-fill').style.width = Math.max(0, (boss.hp / boss.maxHp) * 100) + '%'; }, toast(text) { const el = $('#toast'); if (!text) { el.classList.remove('show'); return; } if (el.textContent !== text) el.textContent = text; el.classList.add('show'); }, result({ name, ms, deaths, apples, totalApples, better, hasNext }) { $('#r-name').textContent = name; $('#r-time').textContent = fmtTime(ms); $('#r-death').textContent = deaths; $('#r-apple').textContent = totalApples ? `${apples}/${totalApples}` : '—'; $('#r-best').textContent = better ? '★ 新纪录!' : ''; $('#scr-result').querySelector('[data-act="next"]').style.display = hasNext ? '' : 'none'; this.show('scr-result'); }, }; function escapeHtml(s) { return String(s).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); }