| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237 |
- /**
- * 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;
- }
|