/* ============================================================ * debug_physics_balls_preview_match.js · 物理弹球 · 预览=实际轨迹验证 * 清空砖块后以固定角度发射,对比 simulatePath 预览点与真实球位置: * 瞄准折线必须与球实际运动线路一致(误差 < 2px)。 * 运行:node dev_test_scripts/debug/debug_physics_balls_preview_match.js * ============================================================ */ 'use strict'; const fs = require('fs'); const path = require('path'); const vm = require('vm'); const LAB = path.join(__dirname, '..', '..', 'public', 'tools', 'thought_lab', 'labs', 'physics_balls'); const ctx = { Math, console, PBAudio: { fire() {}, hit() {}, wall() {}, item() {}, breakBlock() {}, over() {}, claim() {} }, PBStorage: { getEscape() { return 50; } } }; vm.createContext(ctx); for (const f of ['pb_physics.js', 'pb_rules.js', 'pb_game.js', 'pb_render.js']) { vm.runInContext(fs.readFileSync(path.join(LAB, f), 'utf8'), ctx, { filename: f }); } const G = ctx.PBGame, RD = ctx.PBRender; let ok = true; for (const ang of [0, 0.35, -0.6, 1.0]) { const g = G.create(); G.startGame(g); g.blocks = []; g.items = []; // 清空砖块,保证对比确定 g.aimAngle = ang; const pts = RD.simulatePath(g); // 预览点:pts[k] 对应 t = k/60 s(subDt=1/240,每 4 子步采样) G.fire(g); let maxErr = 0, checked = 0; for (let f = 1; f <= 30; f++) { // 前 0.5s,每帧 4 子步与预览采样对齐 G.update(g, 1 / 60); if (!g.balls.length) break; if (f % 2) continue; if (f >= pts.length) break; const b = g.balls[0]; const err = Math.hypot(b.x - pts[f].x, b.y - pts[f].y); maxErr = Math.max(maxErr, err); checked++; } const pass = checked >= 10 && maxErr < 2; console.log((pass ? 'PASS ' : 'FAIL ') + 'angle=' + ang + ' checked=' + checked + ' maxErr=' + maxErr.toFixed(2) + 'px'); ok = pass && ok; } process.exit(ok ? 0 : 1);