/** * Physics - 物理/碰撞系统 * 处理球与墙壁、方块、道具的碰撞检测和反弹逻辑 * 支持方块圆角碰撞,与 GuideLine 使用一致的圆角几何 */ import { BLOCK_CORNER_RADIUS, BALL_RADIUS } from '../constants.js'; import { findFirstHit, reflectDirection } from './Collision.js'; /** * 检测球与墙壁碰撞 */ export function checkBallWallCollision(ball, boardWidth, boardHeight) { if (ball.x - ball.radius <= 0) return 'left'; if (ball.x + ball.radius >= boardWidth) return 'right'; if (ball.y - ball.radius <= 0) return 'top'; return null; } /** * AABB 重叠检测 */ function aabbOverlap(a, b) { return ( a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y ); } /** * 检测球心是否在方块角落区域,并返回碰撞信息 */ function checkCornerCollision(bx, by, ballRadius, rect, cr) { const corners = [ { cx: rect.x + cr, cy: rect.y + cr }, { cx: rect.x + rect.width - cr, cy: rect.y + cr }, { cx: rect.x + cr, cy: rect.y + rect.height - cr }, { cx: rect.x + rect.width - cr, cy: rect.y + rect.height - cr } ]; for (const c of corners) { const dx = bx - c.cx; const dy = by - c.cy; const distSq = dx * dx + dy * dy; const threshold = cr + ballRadius; if (distSq < threshold * threshold && distSq > 0) { const inCornerX = (bx < rect.x + cr || bx > rect.x + rect.width - cr); const inCornerY = (by < rect.y + cr || by > rect.y + rect.height - cr); if (inCornerX && inCornerY) { const dist = Math.sqrt(distSq); return { cx: c.cx, cy: c.cy, dist }; } } } return null; } /** * 检测球心是否在方块角落的"缺角"区域 */ function isInCornerGap(bx, by, rect, cr) { const inCornerX = (bx < rect.x + cr || bx > rect.x + rect.width - cr); const inCornerY = (by < rect.y + cr || by > rect.y + rect.height - cr); if (!inCornerX || !inCornerY) return false; const corners = [ { cx: rect.x + cr, cy: rect.y + cr }, { cx: rect.x + rect.width - cr, cy: rect.y + cr }, { cx: rect.x + cr, cy: rect.y + rect.height - cr }, { cx: rect.x + rect.width - cr, cy: rect.y + rect.height - cr } ]; let minDist = Infinity; for (const c of corners) { const dx = bx - c.cx; const dy = by - c.cy; const dist = Math.sqrt(dx * dx + dy * dy); if (dist < minDist) minDist = dist; } return minDist > cr; } /** * 检测球与方块碰撞(支持圆角) */ export function checkBallBlockCollision(ball, block) { const ballRect = ball.getRect(); const blockRect = block.getRect(); if (!aabbOverlap(ballRect, blockRect)) { return { hit: false, side: null, corner: null }; } const cr = BLOCK_CORNER_RADIUS; if (isInCornerGap(ball.x, ball.y, blockRect, cr + ball.radius)) { const cornerHit = checkCornerCollision(ball.x, ball.y, ball.radius, blockRect, cr); if (cornerHit) { return { hit: true, side: 'corner', corner: cornerHit }; } return { hit: false, side: null, corner: null }; } const cornerHit = checkCornerCollision(ball.x, ball.y, ball.radius, blockRect, cr); if (cornerHit) { return { hit: true, side: 'corner', corner: cornerHit }; } const overlapLeft = (ballRect.x + ballRect.width) - blockRect.x; const overlapRight = (blockRect.x + blockRect.width) - ballRect.x; const overlapTop = (ballRect.y + ballRect.height) - blockRect.y; const overlapBottom = (blockRect.y + blockRect.height) - ballRect.y; const minOverlap = Math.min(overlapLeft, overlapRight, overlapTop, overlapBottom); let side; if (minOverlap === overlapTop) side = 'top'; else if (minOverlap === overlapBottom) side = 'bottom'; else if (minOverlap === overlapLeft) side = 'left'; else side = 'right'; return { hit: true, side, corner: null }; } /** * 检测球与道具碰撞 */ export function checkBallItemCollision(ball, item) { return aabbOverlap(ball.getRect(), item.getRect()); } /** * 处理球与方块碰撞后的反弹(支持圆角) */ export function resolveBallBlockCollision(ball, side, blockRect, corner) { if (side === 'corner' && corner) { const dx = ball.x - corner.cx; const dy = ball.y - corner.cy; const len = Math.sqrt(dx * dx + dy * dy); if (len > 0) { const nx = dx / len; const ny = dy / len; const dot = ball.vx * nx + ball.vy * ny; ball.vx -= 2 * dot * nx; ball.vy -= 2 * dot * ny; ball.x = corner.cx + nx * (BLOCK_CORNER_RADIUS + ball.radius); ball.y = corner.cy + ny * (BLOCK_CORNER_RADIUS + ball.radius); } return; } if (side === 'top' || side === 'bottom') { ball.reflect('y'); } else if (side === 'left' || side === 'right') { ball.reflect('x'); } if (blockRect) { if (side === 'top') ball.y = blockRect.y - ball.radius; else if (side === 'bottom') ball.y = blockRect.y + blockRect.height + ball.radius; else if (side === 'left') ball.x = blockRect.x - ball.radius; else if (side === 'right') ball.x = blockRect.x + blockRect.width + ball.radius; } } /** * 墙壁反射 */ export function reflectWall(velocity, wall) { if (wall === 'left' || wall === 'right') { return { vx: -velocity.vx, vy: velocity.vy }; } if (wall === 'top') { return { vx: velocity.vx, vy: -velocity.vy }; } return { vx: velocity.vx, vy: velocity.vy }; } /** * 连续碰撞物理步进(与 GuideLine 使用相同的 findFirstHit) */ export function stepBallPhysics(ball, stepDist, blocks, boardWidth, boardHeight) { if (!ball.active) return []; const hitBlocks = []; let remaining = stepDist; const maxBounces = 10; for (let bounce = 0; bounce < maxBounces && remaining > 1e-6; bounce++) { const speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy); if (speed < 1e-6) break; const dx = ball.vx / speed; const dy = ball.vy / speed; const hit = findFirstHit(ball.x, ball.y, dx, dy, blocks, boardWidth, boardHeight, 0); if (!hit.surface || hit.t >= remaining) { ball.x += dx * remaining; ball.y += dy * remaining; remaining = 0; break; } ball.x = hit.x; ball.y = hit.y; remaining -= hit.t; const ref = reflectDirection(dx, dy, hit.surface, hit.cornerNormal); ball.vx = ref.dx * speed; ball.vy = ref.dy * speed; if (hit.isBlock) { for (const block of blocks) { if (block.destroyed) continue; const r = block.getRect(); const margin = BALL_RADIUS + BLOCK_CORNER_RADIUS + 2; if (ball.x >= r.x - margin && ball.x <= r.x + r.width + margin && ball.y >= r.y - margin && ball.y <= r.y + r.height + margin) { hitBlocks.push({ block }); break; } } } } if (ball.y + ball.radius >= boardHeight) { ball.y = boardHeight - ball.radius; ball.active = false; } return hitBlocks; }