91148fe064
1. 新增物理弹球游戏全套代码,包含物理引擎、规则、渲染、游戏逻辑、存档、音效、UI 2. 新增物理弹球相关调试与单元测试脚本 3. 优化私有剪贴上传功能:增加200MB上传限制与错误提示 4. 修复复古弹球桌的光滑面防粘附与休眠逻辑
205 lines
8.1 KiB
JavaScript
205 lines
8.1 KiB
JavaScript
/* ============================================================
|
||
* 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
|
||
};
|
||
})();
|