Physics.js 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. /**
  2. * Physics - 物理/碰撞系统
  3. * 处理球与墙壁、方块、道具的碰撞检测和反弹逻辑
  4. * 支持方块圆角碰撞,与 GuideLine 使用一致的圆角几何
  5. */
  6. import { BLOCK_CORNER_RADIUS, BALL_RADIUS } from '../constants.js';
  7. import { findFirstHit, reflectDirection } from './Collision.js';
  8. /**
  9. * 检测球与墙壁碰撞
  10. */
  11. export function checkBallWallCollision(ball, boardWidth, boardHeight) {
  12. if (ball.x - ball.radius <= 0) return 'left';
  13. if (ball.x + ball.radius >= boardWidth) return 'right';
  14. if (ball.y - ball.radius <= 0) return 'top';
  15. return null;
  16. }
  17. /**
  18. * AABB 重叠检测
  19. */
  20. function aabbOverlap(a, b) {
  21. return (
  22. a.x < b.x + b.width &&
  23. a.x + a.width > b.x &&
  24. a.y < b.y + b.height &&
  25. a.y + a.height > b.y
  26. );
  27. }
  28. /**
  29. * 检测球心是否在方块角落区域,并返回碰撞信息
  30. */
  31. function checkCornerCollision(bx, by, ballRadius, rect, cr) {
  32. const corners = [
  33. { cx: rect.x + cr, cy: rect.y + cr },
  34. { cx: rect.x + rect.width - cr, cy: rect.y + cr },
  35. { cx: rect.x + cr, cy: rect.y + rect.height - cr },
  36. { cx: rect.x + rect.width - cr, cy: rect.y + rect.height - cr }
  37. ];
  38. for (const c of corners) {
  39. const dx = bx - c.cx;
  40. const dy = by - c.cy;
  41. const distSq = dx * dx + dy * dy;
  42. const threshold = cr + ballRadius;
  43. if (distSq < threshold * threshold && distSq > 0) {
  44. const inCornerX = (bx < rect.x + cr || bx > rect.x + rect.width - cr);
  45. const inCornerY = (by < rect.y + cr || by > rect.y + rect.height - cr);
  46. if (inCornerX && inCornerY) {
  47. const dist = Math.sqrt(distSq);
  48. return { cx: c.cx, cy: c.cy, dist };
  49. }
  50. }
  51. }
  52. return null;
  53. }
  54. /**
  55. * 检测球心是否在方块角落的"缺角"区域
  56. */
  57. function isInCornerGap(bx, by, rect, cr) {
  58. const inCornerX = (bx < rect.x + cr || bx > rect.x + rect.width - cr);
  59. const inCornerY = (by < rect.y + cr || by > rect.y + rect.height - cr);
  60. if (!inCornerX || !inCornerY) return false;
  61. const corners = [
  62. { cx: rect.x + cr, cy: rect.y + cr },
  63. { cx: rect.x + rect.width - cr, cy: rect.y + cr },
  64. { cx: rect.x + cr, cy: rect.y + rect.height - cr },
  65. { cx: rect.x + rect.width - cr, cy: rect.y + rect.height - cr }
  66. ];
  67. let minDist = Infinity;
  68. for (const c of corners) {
  69. const dx = bx - c.cx;
  70. const dy = by - c.cy;
  71. const dist = Math.sqrt(dx * dx + dy * dy);
  72. if (dist < minDist) minDist = dist;
  73. }
  74. return minDist > cr;
  75. }
  76. /**
  77. * 检测球与方块碰撞(支持圆角)
  78. */
  79. export function checkBallBlockCollision(ball, block) {
  80. const ballRect = ball.getRect();
  81. const blockRect = block.getRect();
  82. if (!aabbOverlap(ballRect, blockRect)) {
  83. return { hit: false, side: null, corner: null };
  84. }
  85. const cr = BLOCK_CORNER_RADIUS;
  86. if (isInCornerGap(ball.x, ball.y, blockRect, cr + ball.radius)) {
  87. const cornerHit = checkCornerCollision(ball.x, ball.y, ball.radius, blockRect, cr);
  88. if (cornerHit) {
  89. return { hit: true, side: 'corner', corner: cornerHit };
  90. }
  91. return { hit: false, side: null, corner: null };
  92. }
  93. const cornerHit = checkCornerCollision(ball.x, ball.y, ball.radius, blockRect, cr);
  94. if (cornerHit) {
  95. return { hit: true, side: 'corner', corner: cornerHit };
  96. }
  97. const overlapLeft = (ballRect.x + ballRect.width) - blockRect.x;
  98. const overlapRight = (blockRect.x + blockRect.width) - ballRect.x;
  99. const overlapTop = (ballRect.y + ballRect.height) - blockRect.y;
  100. const overlapBottom = (blockRect.y + blockRect.height) - ballRect.y;
  101. const minOverlap = Math.min(overlapLeft, overlapRight, overlapTop, overlapBottom);
  102. let side;
  103. if (minOverlap === overlapTop) side = 'top';
  104. else if (minOverlap === overlapBottom) side = 'bottom';
  105. else if (minOverlap === overlapLeft) side = 'left';
  106. else side = 'right';
  107. return { hit: true, side, corner: null };
  108. }
  109. /**
  110. * 检测球与道具碰撞
  111. */
  112. export function checkBallItemCollision(ball, item) {
  113. return aabbOverlap(ball.getRect(), item.getRect());
  114. }
  115. /**
  116. * 处理球与方块碰撞后的反弹(支持圆角)
  117. */
  118. export function resolveBallBlockCollision(ball, side, blockRect, corner) {
  119. if (side === 'corner' && corner) {
  120. const dx = ball.x - corner.cx;
  121. const dy = ball.y - corner.cy;
  122. const len = Math.sqrt(dx * dx + dy * dy);
  123. if (len > 0) {
  124. const nx = dx / len;
  125. const ny = dy / len;
  126. const dot = ball.vx * nx + ball.vy * ny;
  127. ball.vx -= 2 * dot * nx;
  128. ball.vy -= 2 * dot * ny;
  129. ball.x = corner.cx + nx * (BLOCK_CORNER_RADIUS + ball.radius);
  130. ball.y = corner.cy + ny * (BLOCK_CORNER_RADIUS + ball.radius);
  131. }
  132. return;
  133. }
  134. if (side === 'top' || side === 'bottom') {
  135. ball.reflect('y');
  136. } else if (side === 'left' || side === 'right') {
  137. ball.reflect('x');
  138. }
  139. if (blockRect) {
  140. if (side === 'top') ball.y = blockRect.y - ball.radius;
  141. else if (side === 'bottom') ball.y = blockRect.y + blockRect.height + ball.radius;
  142. else if (side === 'left') ball.x = blockRect.x - ball.radius;
  143. else if (side === 'right') ball.x = blockRect.x + blockRect.width + ball.radius;
  144. }
  145. }
  146. /**
  147. * 墙壁反射
  148. */
  149. export function reflectWall(velocity, wall) {
  150. if (wall === 'left' || wall === 'right') {
  151. return { vx: -velocity.vx, vy: velocity.vy };
  152. }
  153. if (wall === 'top') {
  154. return { vx: velocity.vx, vy: -velocity.vy };
  155. }
  156. return { vx: velocity.vx, vy: velocity.vy };
  157. }
  158. /**
  159. * 连续碰撞物理步进(与 GuideLine 使用相同的 findFirstHit)
  160. */
  161. export function stepBallPhysics(ball, stepDist, blocks, boardWidth, boardHeight) {
  162. if (!ball.active) return [];
  163. const hitBlocks = [];
  164. let remaining = stepDist;
  165. const maxBounces = 10;
  166. for (let bounce = 0; bounce < maxBounces && remaining > 1e-6; bounce++) {
  167. const speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
  168. if (speed < 1e-6) break;
  169. const dx = ball.vx / speed;
  170. const dy = ball.vy / speed;
  171. const hit = findFirstHit(ball.x, ball.y, dx, dy, blocks, boardWidth, boardHeight, 0);
  172. if (!hit.surface || hit.t >= remaining) {
  173. ball.x += dx * remaining;
  174. ball.y += dy * remaining;
  175. remaining = 0;
  176. break;
  177. }
  178. ball.x = hit.x;
  179. ball.y = hit.y;
  180. remaining -= hit.t;
  181. const ref = reflectDirection(dx, dy, hit.surface, hit.cornerNormal);
  182. ball.vx = ref.dx * speed;
  183. ball.vy = ref.dy * speed;
  184. if (hit.isBlock) {
  185. for (const block of blocks) {
  186. if (block.destroyed) continue;
  187. const r = block.getRect();
  188. const margin = BALL_RADIUS + BLOCK_CORNER_RADIUS + 2;
  189. if (ball.x >= r.x - margin && ball.x <= r.x + r.width + margin &&
  190. ball.y >= r.y - margin && ball.y <= r.y + r.height + margin) {
  191. hitBlocks.push({ block });
  192. break;
  193. }
  194. }
  195. }
  196. }
  197. if (ball.y + ball.radius >= boardHeight) {
  198. ball.y = boardHeight - ball.radius;
  199. ball.active = false;
  200. }
  201. return hitBlocks;
  202. }