feat: 新增物理弹球实验室及配套功能

1. 新增物理弹球游戏全套代码,包含物理引擎、规则、渲染、游戏逻辑、存档、音效、UI
2. 新增物理弹球相关调试与单元测试脚本
3. 优化私有剪贴上传功能:增加200MB上传限制与错误提示
4. 修复复古弹球桌的光滑面防粘附与休眠逻辑
This commit is contained in:
yangxiangyuan
2026-08-08 22:31:33 +08:00
parent 4492b4106a
commit 91148fe064
17 changed files with 2690 additions and 22 deletions
@@ -0,0 +1,126 @@
/* ============================================================
* debug_physics_balls_dom_smoke.js · 物理弹球 · 前端 DOM 桩冒烟
* 无浏览器环境下用最小 DOM/Canvas 桩加载全部前端脚本,
* 驱动主循环 + 模拟拖拽发射/暂停/记录等交互,捕获运行时错误。
* 运行:node dev_test_scripts/debug/debug_physics_balls_dom_smoke.js
* ============================================================ */
'use strict';
const fs = require('fs');
const path = require('path');
const vm = require('vm');
const LAB_DIR = path.join(__dirname, '..', '..', 'public', 'tools', 'thought_lab', 'labs', 'physics_balls');
const FILES = ['pb_physics.js', 'pb_rules.js', 'pb_storage.js', 'pb_audio.js', 'pb_render.js', 'pb_game.js', 'pb_ui.js'];
function makeCtx() {
const store = {};
return new Proxy(store, {
get(t, k) {
if (k === 'createRadialGradient') return () => ({ addColorStop() {} });
if (k === 'measureText') return () => ({ width: 10 });
if (k in t) return t[k];
return function () {};
},
set(t, k, v) { t[k] = v; return true; }
});
}
function makeEl(id) {
const listeners = {};
return {
id,
style: {},
dataset: {},
classList: { add() {}, remove() {}, toggle() {}, contains() { return false; } },
children: [],
_html: '', _text: '',
set innerHTML(v) { this._html = v; this.children = []; },
get innerHTML() { return this._html; },
set textContent(v) { this._text = String(v); },
get textContent() { return this._text; },
appendChild(c) { this.children.push(c); },
addEventListener(t, f) { (listeners[t] = listeners[t] || []).push(f); },
dispatch(t, ev) { (listeners[t] || []).forEach(f => f(ev || {})); },
getBoundingClientRect() { return { left: 0, top: 0, width: 420, height: 880 }; },
getContext() { return makeCtx(); },
checked: true,
width: 420, height: 880
};
}
const els = {};
function getEl(id) { if (!els[id]) els[id] = makeEl(id); return els[id]; }
let rafCb = null;
const winListeners = {};
const sandbox = {
console, Math, Date, setTimeout, clearTimeout,
JSON, localStorage: { getItem: () => null, setItem: () => {}, removeItem: () => {} },
navigator: { clipboard: { writeText: () => ({ then: (a) => a && a() }) } },
requestAnimationFrame: (cb) => { rafCb = cb; return 1; },
document: {
getElementById: getEl,
createElement: (tag) => makeEl('dyn_' + tag)
},
addEventListener: (t, f) => { (winListeners[t] = winListeners[t] || []).push(f); },
dispatchWin: (t, ev) => { (winListeners[t] || []).forEach(f => f(ev || {})); },
window: null
};
sandbox.window = sandbox;
sandbox.globalThis = sandbox;
vm.createContext(sandbox);
for (const f of FILES) {
vm.runInContext(fs.readFileSync(path.join(LAB_DIR, f), 'utf-8'), sandbox, { filename: f });
}
function frames(n, t0) {
for (let i = 0; i < n; i++) {
const cb = rafCb; rafCb = null;
if (!cb) throw new Error('rAF chain broken');
cb(t0 + i * 16.7);
}
}
console.log('== 物理弹球 DOM 冒烟 ==');
// init(load 事件不会在 vm 里触发,手动调)
sandbox.PBUi.init();
frames(30, 0);
console.log(' ✓ init + 30 帧主菜单渲染');
// 模拟拖拽发射(pointermove/up 注册在 window 级)
const canvas = getEl('pb-canvas');
canvas.dispatch('pointerdown', { clientX: 260, clientY: 600 });
frames(5, 600);
sandbox.dispatchWin('pointermove', { clientX: 300, clientY: 650 });
sandbox.dispatchWin('pointerup', {});
frames(60 * 6, 700);
console.log(' ✓ 拖拽发射 + 6s 模拟');
// 暂停 / 继续
getEl('pb-pause-btn').dispatch('click');
getEl('pb-resume').dispatch('click');
frames(60, 60 * 7 * 16.7);
console.log(' ✓ 暂停/继续');
// 记录页 / 指南 / 设置 / 每日领取
getEl('pb-side-records').dispatch('click');
getEl('pb-records-close').dispatch('click');
getEl('pb-help').dispatch('click');
getEl('pb-guide-ok').dispatch('click');
getEl('pb-gear').dispatch('click');
getEl('pb-settings-close').dispatch('click');
getEl('pb-daily').dispatch('click');
console.log(' ✓ 记录/指南/设置/每日领取交互');
// 长时模拟 60s,覆盖回合推进/上升/回收
for (let s = 0; s < 60; s++) {
if (s % 7 === 0) {
canvas.dispatch('pointerdown', { clientX: 150 + (s * 13) % 200, clientY: 620 });
sandbox.dispatchWin('pointermove', { clientX: 190 + (s * 13) % 160, clientY: 660 });
sandbox.dispatchWin('pointerup', {});
}
frames(60, (10 + s) * 1000);
}
console.log(' ✓ 60s 长时模拟(含多次发射)');
console.log('\n冒烟通过,无运行时异常');