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

205 lines
8.1 KiB
JavaScript
Raw 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.
/* ============================================================
* pb_rules.js · 物理弹球 Web 复刻版 · 规则层(纯函数,无 DOM)
* 场地几何 / 砖块形状与配色 / 行生成 / 道具 / 待发队列位置 / 物理参数
* ============================================================ */
var PBRules = (function () {
'use strict';
// ---- 场地逻辑坐标(竖向手机比例) ----
var FIELD = {
W: 420, H: 880,
wallL: 16, wallR: 404, // 左右侧壁 x
topY: 36, // 顶壁
checkY: 128, // 顶部虚线:砖块越线 = 游戏结束
gapHalf: 17, // 漏斗中央出口半宽(球从此落下)
arcCx: 210, arcCy: 1560, arcR: 706, // 底部大圆弧(弧顶 ≈ 854)
laneW: 26 // 侧边回收通道宽(输送带)
};
// 漏斗两斜壁:从顶角收拢到中央出口
function funnelSegs() {
return [
{ x1: FIELD.wallL, y1: FIELD.topY + 4, x2: FIELD.arcCx - FIELD.gapHalf, y2: FIELD.checkY - 10 },
{ x1: FIELD.wallR, y1: FIELD.topY + 4, x2: FIELD.arcCx + FIELD.gapHalf, y2: FIELD.checkY - 10 }
];
}
// ---- 物理参数 ----
var PHYS = {
g: 1500, e: 0.42, iterations: 16, slop: 0.4, correction: 0.55,
restV: 10, airK: 0.008, omegaK: 0.12, maxV: 3600,
sleepV: 14, sleepW: 0.5, wakeV: 90, hitV: 110,
subDt: 1 / 240
};
// ---- 球 ----
var BALL_R = 7; // 小球半径
var BIG_R = 14; // 大球半径 = 小球 2 倍(伤害也 2 倍)
var FIRE_SPEED = 980; // 发射初速
var FIRE_GAP_MS = 130; // 连发间隔
// ---- 砖块形状 ----
var SHAPES = ['circle', 'box', 'tri', 'penta'];
function shapeRadius(shape) {
if (shape === 'circle') return 17;
if (shape === 'box') return 19;
if (shape === 'tri') return 20;
return 19; // penta
}
// ---- 配色:HP 分档(复刻截图观感:黄→橙→绿→蓝→紫) ----
function hpColor(hp) {
if (hp <= 4) return hp % 2 ? '#f7c81a' : '#f7a01a';
if (hp <= 9) return '#f7a01a';
if (hp <= 29) return hp % 2 ? '#37c837' : '#8bd420';
if (hp <= 79) return '#2f7bff';
return '#a02fe0';
}
// ---- 回合 n 的新砖块 HP 区间(前期个位数,后期上百) ----
function hpRangeForTurn(n) {
var lo = Math.max(1, Math.floor(n * 0.8) - 1);
var hi = Math.max(lo + 1, n + 4);
return [lo, hi];
}
function randInt(lo, hi) { return lo + Math.floor(Math.random() * (hi - lo + 1)); }
// ---- 各形状包围圆半径(包围圆 ⊇ 实际形状,用于无重叠放置) ----
function boundRadius(shape) {
if (shape === 'circle') return 17;
if (shape === 'box') return (19 * 1.7 / 2) * Math.SQRT2; // 正方形半对角线 ≈22.8
if (shape === 'tri') return 20;
return 19; // penta 外接圆
}
// ---- 生成一行砖块(返回描述数组,game 层转刚体) ----
// 槽位随机簇团布局 + 严格无重叠放置:中心距 >= 包围圆半径和(允许相邻/点线紧贴,禁止相交)
// 放置冲突时随机偏移重试,仍冲突则放弃该块——保持随机、不整齐
// 两侧随机贴边:每边 65% 概率"封闭"(砖块贴墙,不留沿墙逃生通道),35% 留口
// avoid: 已有块 [{x,y,cr}](上一行等),新块与其也须无重叠
function makeRow(n, y, _count, avoid) {
var out = [];
var placed = [];
avoid = avoid || [];
var [lo, hi] = hpRangeForTurn(n);
var leftOpen = Math.random() < 0.35;
var rightOpen = Math.random() < 0.35;
// 封闭边:块中心距墙 21(贴边形状半径 ≤20,贴墙无缝);开放边留 34 供球通过
var xMin = FIELD.wallL + (leftOpen ? 34 : 21);
var xMax = FIELD.wallR - (rightOpen ? 34 : 21);
var usable = xMax - xMin;
var clampX = function (x) { return Math.max(FIELD.wallL + 23, Math.min(FIELD.wallR - 23, x)); };
function tryPlace(x, yy, cr, desc) {
var i, dx, dy, rr;
for (i = 0; i < placed.length; i++) {
dx = x - placed[i].x; dy = yy - placed[i].y; rr = cr + placed[i].cr;
if (dx * dx + dy * dy < rr * rr) return false; // 相交=拒绝;相等=紧贴=允许
}
for (i = 0; i < avoid.length; i++) {
dx = x - avoid[i].x; dy = yy - avoid[i].y; rr = cr + avoid[i].cr;
if (dx * dx + dy * dy < rr * rr) return false;
}
placed.push({ x: x, y: yy, cr: cr });
out.push(desc);
return true;
}
function mkBlock(x, yy, shape) {
return { shape: shape, hp: randInt(lo, hi), x: x, y: yy, angle: (Math.random() - 0.5) * 0.8 };
}
function placeBlock(x, shape, margin) {
var m = margin || 23; // 常规块留 23(旋转方块半对角 22.8 不出墙)
var cl = function (v) { return Math.max(FIELD.wallL + m, Math.min(FIELD.wallR - m, v)); };
var cr = boundRadius(shape);
x = cl(x);
var yy = y + (Math.random() - 0.5) * 10;
if (tryPlace(x, yy, cr, mkBlock(x, yy, shape))) return;
for (var k = 0; k < 8; k++) { // 冲突:随机偏移重试
var tx = cl(x + (Math.random() - 0.5) * 60);
var ty = y + (Math.random() - 0.5) * 10;
if (tryPlace(tx, ty, cr, mkBlock(tx, ty, shape))) return;
}
}
// 封闭边先放贴墙砖(只选半径 ≤20 的形状,margin 21 贴墙且旋转不出墙)
if (!leftOpen) placeBlock(xMin, ['circle', 'tri', 'penta'][randInt(0, 2)], 21);
if (!rightOpen) placeBlock(xMax, ['circle', 'tri', 'penta'][randInt(0, 2)], 21);
var segments = 4 + randInt(0, 2); // 4-6 个槽位
var segW = usable / segments;
for (var s = 0; s < segments; s++) {
var segCenter = xMin + segW * (s + 0.5);
var r = Math.random();
var nBlocks = r < 0.35 ? 0 : (r < 0.65 ? 1 : (r < 0.85 ? 2 : 3));
var blockGap = 38;
var totalW = (nBlocks - 1) * blockGap;
for (var b = 0; b < nBlocks; b++) {
placeBlock(segCenter - totalW / 2 + b * blockGap + (Math.random() - 0.5) * 8,
SHAPES[randInt(0, SHAPES.length - 1)]);
}
}
// 奖励球混排在卡片中间:锚定本行随机砖块、紧贴其旁(相邻/点线紧贴允许)
if (Math.random() < 0.6) {
var itType = Math.random() < 0.6 ? 'ball' : 'big';
for (var q = 0; q < 10; q++) {
var ix, iy;
if (placed.length && q < 8) {
var anchor = placed[randInt(0, placed.length - 1)];
var aa = Math.random() * Math.PI * 2;
var dist = anchor.cr + 12; // 中心距 = 半径和 → 紧贴
ix = clampX(anchor.x + Math.cos(aa) * dist);
iy = anchor.y + Math.sin(aa) * dist;
} else {
ix = clampX(xMin + usable * Math.random());
iy = y + (Math.random() - 0.5) * 8;
}
if (tryPlace(ix, iy, 12, { item: itType, x: ix, y: iy, angle: 0 })) break;
}
}
return out;
}
// ---- 待发球堆叠位置(漏斗口三角堆) ----
function queuePos(i) {
// 0 在出口,往上三角堆叠
var rows = [
[0],
[-9.5, 9.5],
[-19, 0, 19],
[-28.5, -9.5, 9.5, 28.5]
];
var idx = i;
for (var r = 0; r < rows.length; r++) {
if (idx < rows[r].length) {
return { x: FIELD.arcCx + rows[r][idx], y: FIELD.checkY - 22 - r * 15 };
}
idx -= rows[r].length;
}
return { x: FIELD.arcCx, y: FIELD.checkY - 22 - 4 * 15 - idx * 14 };
}
// ---- 道具 ----
var ITEMS = {
ball: { r: 12, label: '+1' }, // 球数 +1(下回合生效)
big: { r: 12, label: '大' } // 大球回合:球变大,击中 +2 / 伤害 2
};
// ---- 记录行颜色(复刻记录页交替行彩色数字) ----
var RECORD_COLORS = ['#e6c229', '#29abe2', '#b133cc', '#2955e2', '#2980e2', '#29c8e2', '#2955cc', '#33cc55', '#33cc88', '#ccaa29'];
return {
FIELD: FIELD,
funnelSegs: funnelSegs,
PHYS: PHYS,
BALL_R: BALL_R, BIG_R: BIG_R,
FIRE_SPEED: FIRE_SPEED, FIRE_GAP_MS: FIRE_GAP_MS,
SHAPES: SHAPES, shapeRadius: shapeRadius,
hpColor: hpColor, hpRangeForTurn: hpRangeForTurn,
boundRadius: boundRadius,
makeRow: makeRow, queuePos: queuePos,
ITEMS: ITEMS, RECORD_COLORS: RECORD_COLORS,
randInt: randInt
};
})();