feat: 新增物理弹球实验室及配套功能
1. 新增物理弹球游戏全套代码,包含物理引擎、规则、渲染、游戏逻辑、存档、音效、UI 2. 新增物理弹球相关调试与单元测试脚本 3. 优化私有剪贴上传功能:增加200MB上传限制与错误提示 4. 修复复古弹球桌的光滑面防粘附与休眠逻辑
This commit is contained in:
@@ -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冒烟通过,无运行时异常');
|
||||
Reference in New Issue
Block a user