Files
Toolbox/dev_test_scripts/debug/debug_physics_balls_stuck_escape.js
yangxiangyuan 91148fe064 feat: 新增物理弹球实验室及配套功能
1. 新增物理弹球游戏全套代码,包含物理引擎、规则、渲染、游戏逻辑、存档、音效、UI
2. 新增物理弹球相关调试与单元测试脚本
3. 优化私有剪贴上传功能:增加200MB上传限制与错误提示
4. 修复复古弹球桌的光滑面防粘附与休眠逻辑
2026-08-08 22:31:33 +08:00

128 lines
4.9 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ============================================================
* debug_physics_balls_stuck_escape.js · 物理弹球 · 卡球逃脱验证
* 规则:球陷缝隙 0.5s 后横向滑出缺口继续下落;禁止向上弹射
* ("必须落下后才能上去",唯一上升途径是底部输送带)。
* 验证:1) 卡球能逃脱并下落/回收 2) 上升幅度 <=120px 3) 左右方向有随机性
* 运行:node dev_test_scripts/debug/debug_physics_balls_stuck_escape.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() {} },
PBStorage: { getEscape() { return 50; } }
};
vm.createContext(ctx);
for (const f of ['pb_physics.js', 'pb_rules.js', 'pb_game.js']) {
vm.runInContext(fs.readFileSync(path.join(LAB, f), 'utf8'), ctx, { filename: f });
}
const P = ctx.PBPhysics, G = ctx.PBGame;
// 跑 8s:成功 = 被回收 或 下落到 startY+200 以下;同时记录最小 y(上升幅度)
function runScenario(setup, startY) {
const g = G.create();
g.state = 'fly';
setup(g);
let minY = 1e9, maxX = 0, minX = 1e9, outcome = 'stuck';
for (let i = 0; i < 60 * 8; i++) {
G.update(g, 1 / 60);
if (!g.balls.length) { outcome = 'recycled'; break; }
const b = g.balls[0];
minY = Math.min(minY, b.y);
maxX = Math.max(maxX, b.x);
minX = Math.min(minX, b.x);
if (b.y > startY + 200) { outcome = 'fell'; break; }
}
return { outcome, minY, spread: maxX - minX, g };
}
let ok = true;
function check(name, res, startY) {
const escaped = res.outcome !== 'stuck';
const noLaunch = res.minY > startY - 120; // 不允许向上弹射超过 120px
const pass = escaped && noLaunch;
console.log((pass ? 'PASS ' : 'FAIL ') + name +
' outcome=' + res.outcome + ' minY=' + (res.minY === 1e9 ? 'recycled' : res.minY.toFixed(1)) +
' hSpread=' + res.spread.toFixed(0));
ok = pass && ok;
}
// rest 默认 0.2:隔离"逃脱冲量本身",排除高弹块自然反弹的干扰
const mkCircle = (g, x, y, r, rest) => {
const b = P.makeCircle({ shape: 'circle', x, y, r, isStatic: true, friction: 0.35, restitution: rest === undefined ? 0.2 : rest, tag: 'block' });
b.data = { hp: 99, shape: 'circle', r };
g.blocks.push(b);
};
const mkBall = (g, x, y) => {
const ball = P.makeCircle({ shape: 'circle', x, y, r: 7, friction: 0.08, restitution: 0.95, tag: 'ball' });
ball.data = { big: false };
g.balls.push(ball);
};
// 场景1:球静止放在两圆块谷地 → 应横向滑出后下落,不上天
check('谷地卡球逃脱', runScenario((g) => {
mkCircle(g, 180, 400, 22); mkCircle(g, 224, 400, 22);
mkBall(g, 202, 381);
}, 381), 381);
// 场景2:球静止放在单块顶部 → 应被推离块顶后下落;且单接触不触发研磨(块 HP 不变)
{
const res = runScenario((g) => {
mkCircle(g, 200, 400, 22);
mkBall(g, 200, 371);
}, 371);
check('块顶停球逃脱', res, 371);
const intact = res.g.blocks.length === 1 && res.g.blocks[0].data.hp === 99;
console.log((intact ? 'PASS ' : 'FAIL ') + '单接触不研磨 hp=' + (res.g.blocks[0] ? res.g.blocks[0].data.hp : 'gone'));
ok = intact && ok;
}
// 场景5:球楔入两低血块夹角 → 研磨快速打碎砖块、留出空间后下落
{
const res = runScenario((g) => {
mkCircle(g, 180, 400, 22); mkCircle(g, 224, 400, 22);
g.blocks.forEach(b => { b.data.hp = 8; });
mkBall(g, 202, 381);
}, 381);
const ground = res.g.blocks.length === 0;
const pass = ground && res.outcome !== 'stuck';
console.log((pass ? 'PASS ' : 'FAIL ') + '夹角研磨开空间 outcome=' + res.outcome + ' blocksLeft=' + res.g.blocks.length);
ok = pass && ok;
}
// 场景3:球停底部弧面(无砖块接触)→ 1.2s 后应被回收
check('底部停球回收', runScenario((g) => {
mkBall(g, 210, 790);
}, 790), 790);
// 场景4:单接触停球逃脱,横向方向应有随机性(左右都出现)
{
const dirs = [];
for (let k = 0; k < 10; k++) {
const g = G.create();
g.state = 'fly';
mkCircle(g, 200, 400, 22);
mkBall(g, 200 + (Math.random() * 4 - 2), 371); // 真实落点总有微小偏移
let firstVx = null;
for (let i = 0; i < 60 * 3; i++) {
G.update(g, 1 / 60);
if (!g.balls.length) break;
const b = g.balls[0];
if (firstVx === null && Math.abs(b.vx) > 200) { firstVx = b.vx; break; }
}
if (firstVx !== null) dirs.push(firstVx > 0 ? 'R' : 'L');
}
const hasL = dirs.includes('L'), hasR = dirs.includes('R');
const pass = dirs.length >= 6 && hasL && hasR;
console.log((pass ? 'PASS ' : 'FAIL ') + '逃逸方向随机性 ' + dirs.join(''));
ok = pass && ok;
}
process.exit(ok ? 0 : 1);