/* ============================================================ * 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 }; })();