/* ============================================================ * pb_ui.js · 物理弹球 Web 复刻版 · 界面 / 输入 / 主循环 * 屏幕:主菜单(直接拖拽开局) / 指南 / 暂停 / 结束 / 记录 / 设置 * 存档走 PBStorage,音效走 PBAudio。 * ============================================================ */ var PBUi = (function () { 'use strict'; var R = PBRules, F = R.FIELD; var g = null; var canvas, ctx; var lastTs = 0, globalT = 0; function $(id) { return document.getElementById(id); } function show(id) { $(id).classList.remove('hidden'); } function hide(id) { $(id).classList.add('hidden'); } function fmtSecs(s) { var m = Math.floor(s / 60), ss = Math.floor(s % 60); return (m < 10 ? '0' + m : m) + ':' + (ss < 10 ? '0' + ss : ss); } // ---- 主菜单演示背景 ---- function toIdleDemo() { PBGame.startGame(g); g.state = 'idle'; $('pb-best').textContent = '最佳: ' + PBStorage.getBest().toLocaleString(); $('pb-ball-count').textContent = PBStorage.getBalls(); if (PBStorage.canClaimDaily()) show('pb-daily'); else hide('pb-daily'); show('pb-menu'); hide('pb-hud'); hide('pb-over'); } // ---- 开局(消耗 1 球;不足时救济 +1,保证可玩) ---- function tryStart() { if (!PBStorage.spendBall()) { PBStorage.addBalls(1); PBStorage.spendBall(); } PBGame.startGame(g); lastScore = 0; hide('pb-menu'); show('pb-hud'); } function toLogical(e) { var rect = canvas.getBoundingClientRect(); return { x: (e.clientX - rect.left) / rect.width * F.W, y: (e.clientY - rect.top) / rect.height * F.H }; } function setAim(p) { var fx = F.arcCx, fy = F.checkY - 6; var dx = p.x - fx, dy = p.y - fy; if (dy < 40) dy = 40; var ang = Math.atan2(dx, dy); g.aimAngle = Math.max(-1.25, Math.min(1.25, ang)); } var downPos = null, pendingStart = false; function dist(a, b) { var dx = a.x - b.x, dy = a.y - b.y; return Math.sqrt(dx * dx + dy * dy); } function onDown(e) { PBAudio.unlock(); if (g.paused || g.state === 'over') return; var p = toLogical(e); if (g.state === 'idle') { pendingStart = true; downPos = p; return; } // 主菜单:拖拽超阈值才开局,防误触 if (g.state === 'aim' && g.queue.length) { g.aiming = true; downPos = p; setAim(p); } } function onMove(e) { var p = toLogical(e); if (pendingStart && downPos) { if (dist(p, downPos) > 14) { // 确认是拖拽才开局,并直接进入瞄准 pendingStart = false; tryStart(); g.aiming = true; setAim(p); } return; } if (g.aiming) setAim(p); } function onUp(e) { if (pendingStart) { pendingStart = false; downPos = null; return; } // 主菜单轻点 = 无操作 if (g.aiming) { g.aiming = false; var p = e && e.clientX !== undefined ? toLogical(e) : null; if (p && downPos && dist(p, downPos) < 10) { downPos = null; return; } // 轻点 = 取消瞄准,不发射 downPos = null; PBGame.fire(g); } } // ---- 结束结算 ---- function onOver() { PBStorage.addRecord(g.score, Math.floor(g.secs)); var best = PBStorage.getBest(); var isNew = g.score > best; if (isNew) PBStorage.setBest(g.score); $('pb-over-score').textContent = g.score.toLocaleString(); $('pb-over-time').textContent = '用时 ' + fmtSecs(g.secs); $('pb-over-new').style.display = isNew ? '' : 'none'; $('pb-over-best').textContent = '最佳: ' + PBStorage.getBest().toLocaleString(); show('pb-over'); hide('pb-hud'); } // ---- 记录页 ---- function renderRecords() { var list = PBStorage.getRecords(); var box = $('pb-records-list'); box.innerHTML = ''; if (!list.length) { box.innerHTML = '
暂无记录,去打一局吧
'; return; } for (var i = 0; i < list.length; i++) { var r = list[i]; var row = document.createElement('div'); row.className = 'pb-rec-row ' + (i % 2 ? 'alt' : ''); var color = R.RECORD_COLORS[i % R.RECORD_COLORS.length]; row.innerHTML = '' + (i + 1) + '.' + '' + r.date + '' + '' + r.score.toLocaleString() + '' + '' + fmtSecs(r.secs) + ''; box.appendChild(row); } } // ---- 指南演示小图 ---- function drawGuideDemo() { var c = $('pb-guide-demo'); if (!c) return; var x = c.getContext('2d'); x.clearRect(0, 0, c.width, c.height); x.strokeStyle = '#8a8a8a'; x.lineWidth = 2; x.beginPath(); x.moveTo(20, 10); x.lineTo(20, 150); x.moveTo(200, 10); x.lineTo(200, 150); x.moveTo(20, 10); x.lineTo(95, 42); x.moveTo(200, 10); x.lineTo(125, 42); x.stroke(); x.setLineDash([4, 4]); x.beginPath(); x.moveTo(20, 48); x.lineTo(200, 48); x.stroke(); x.setLineDash([]); // 待发球 x.fillStyle = '#a97a45'; [[105, 34], [117, 30], [93, 30], [111, 20]].forEach(function (p) { x.beginPath(); x.arc(p[0], p[1], 5, 0, 7); x.fill(); }); // 轨迹点 x.fillStyle = '#c89458'; for (var i = 0; i < 9; i++) { x.beginPath(); x.arc(112 + i * 8, 56 + i * 11, 3, 0, 7); x.fill(); } // 彩色砖块 var blocks = [ ['#f7c81a', 55, 95, '3'], ['#8bd420', 95, 95, '10'], ['#37c837', 130, 95, '8'], ['#f7c81a', 185, 95, '1'], ['#37c837', 90, 122, '13'], ['#2fd4c8', 125, 122, '24'], ['#f7c81a', 172, 122, '6'], ['#2f7bff', 55, 148, '80'], ['#29c8e2', 115, 148, '29'], ['#a02fe0', 185, 148, '100'] ]; blocks.forEach(function (b) { x.fillStyle = b[0]; x.fillRect(b[1] - 11, b[2] - 9, 22, 18); x.fillStyle = b[3].length > 2 ? '#fff' : '#20180a'; x.font = 'bold 10px sans-serif'; x.textAlign = 'center'; x.textBaseline = 'middle'; x.fillText(b[3], b[1], b[2] + 1); }); // 道具 x.strokeStyle = '#eee'; x.setLineDash([3, 3]); x.beginPath(); x.arc(152, 122, 8, 0, 7); x.stroke(); x.setLineDash([]); } // ---- ∞ 三色 logo ---- function drawLogo() { var c = $('pb-logo'); if (!c) return; var x = c.getContext('2d'); x.clearRect(0, 0, c.width, c.height); var colors = ['#f7c81a', '#2f7bff', '#ff2f7b']; for (var k = 0; k < colors.length; k++) { x.strokeStyle = colors[k]; x.lineWidth = 5; x.beginPath(); var off = (k - 1) * 4; for (var i = 0; i <= 200; i++) { var t = i / 200 * Math.PI * 2; var s2 = Math.sin(t) * Math.sin(t); var px = 110 + 78 * Math.cos(t) / (1 + s2); var py = 60 + (46 + off) * Math.sin(t) * Math.cos(t) / (1 + s2); if (i === 0) x.moveTo(px, py); else x.lineTo(px, py); } x.stroke(); } } function toast(msg) { var t = $('pb-toast'); t.textContent = msg; t.classList.add('show'); setTimeout(function () { t.classList.remove('show'); }, 1800); } // ---- 分数弹跳反馈 ---- var lastScore = 0; function popScore() { var el = $('pb-score'); el.classList.remove('pop'); void el.offsetWidth; el.classList.add('pop'); } // ---- 主循环 ---- function loop(ts) { var dt = Math.min(0.033, (ts - lastTs) / 1000 || 0.016); lastTs = ts; globalT += dt; var prevState = g.state; PBGame.update(g, dt); if (g.state === 'over' && prevState !== 'over') onOver(); PBRender.draw(ctx, g, globalT); // HUD if (g.state !== 'idle') { $('pb-time').textContent = '时间 ' + fmtSecs(g.secs); $('pb-score').textContent = g.score.toLocaleString(); $('pb-balls').textContent = '🥎 ' + (g.queue.length + g.balls.length + g.recycle.length); $('pb-big').classList.toggle('hidden', !g.bigTurn); if (g.score !== lastScore) { lastScore = g.score; popScore(); } } requestAnimationFrame(loop); } function bind() { canvas = $('pb-canvas'); ctx = canvas.getContext('2d'); canvas.addEventListener('pointerdown', onDown); window.addEventListener('pointermove', onMove); window.addEventListener('pointerup', onUp); $('pb-pause-btn').addEventListener('click', function () { if (g.state === 'idle' || g.state === 'over') return; g.paused = true; show('pb-pause'); }); $('pb-resume').addEventListener('click', function () { g.paused = false; hide('pb-pause'); }); $('pb-restart').addEventListener('click', function () { hide('pb-pause'); g.paused = false; tryStart(); }); $('pb-share').addEventListener('click', function () { var text = '我在「物理弹球」' + fmtSecs(g.secs) + ' 打了 ' + g.score.toLocaleString() + ' 分,来挑战我!'; try { navigator.clipboard.writeText(text).then(function () { toast('成绩已复制,去分享吧'); }, function () { toast(text); }); } catch (e) { toast(text); } }); $('pb-again').addEventListener('click', function () { hide('pb-over'); tryStart(); }); $('pb-to-menu').addEventListener('click', function () { toIdleDemo(); }); $('pb-help').addEventListener('click', function () { drawGuideDemo(); show('pb-guide'); }); $('pb-guide-ok').addEventListener('click', function () { hide('pb-guide'); PBStorage.markGuideSeen(); }); $('pb-gear').addEventListener('click', function () { $('pb-sound-toggle').checked = PBStorage.soundOn(); $('pb-escape-slider').value = PBStorage.getEscape(); $('pb-escape-val').textContent = PBStorage.getEscape(); show('pb-settings'); }); $('pb-settings-close').addEventListener('click', function () { hide('pb-settings'); }); $('pb-sound-toggle').addEventListener('change', function (e) { PBStorage.setSound(e.target.checked); toast(e.target.checked ? '音效已开启' : '音效已关闭'); }); // 逃逸强度滑块:实时存储,局内立即生效 $('pb-escape-slider').addEventListener('input', function (e) { PBStorage.setEscape(e.target.value); $('pb-escape-val').textContent = PBStorage.getEscape(); }); $('pb-side-records').addEventListener('click', function () { renderRecords(); show('pb-records'); }); $('pb-records-close').addEventListener('click', function () { hide('pb-records'); }); $('pb-daily').addEventListener('click', function () { if (PBStorage.claimDaily()) { PBAudio.claim(); toast('领取成功 +3 球'); $('pb-ball-count').textContent = PBStorage.getBalls(); hide('pb-daily'); } }); $('pb-side-balls').addEventListener('click', function () { toast('当前球数 ' + PBStorage.getBalls() + ';每局消耗 1 球,局内 +1 球道具可补充'); }); } function init() { g = PBGame.create(); bind(); drawLogo(); toIdleDemo(); if (!PBStorage.guideSeen()) { drawGuideDemo(); show('pb-guide'); } requestAnimationFrame(function (ts) { lastTs = ts; requestAnimationFrame(loop); }); } return { init: init }; })(); window.addEventListener('load', function () { PBUi.init(); });