paddle-game/src/collision.ts
2025-03-31 16:18:59 +02:00

308 lines
No EOL
12 KiB
TypeScript

import { Ball } from './objects/ball';
import { Paddle, paddleProperties } from './objects/paddle';
import { canvas } from './canvas';
/**
* Performs swept collision detection between a ball and paddle
* Returns the time of collision (0-1) or -1 if no collision
*/
export function sweptCollision(
ball: Ball,
ballVelocityX: number,
ballVelocityY: number,
paddle: Paddle,
paddleVelocityX: number,
paddleVelocityY: number,
paddleAngularVelocity: number,
deltaTime: number
): { time: number, normalX: number, normalY: number, hitPoint: {x: number, y: number} } {
// Calculate relative velocity (ball velocity relative to paddle)
const relativeVelocityX = ballVelocityX - paddleVelocityX;
const relativeVelocityY = ballVelocityY - paddleVelocityY;
// Transform ball position to paddle's coordinate system
let relativeX = ball.x - paddle.x;
let relativeY = ball.y - paddle.y;
// Rotate to align with paddle's orientation
let rotatedX = relativeX * Math.cos(-paddle.rotation) - relativeY * Math.sin(-paddle.rotation);
let rotatedY = relativeX * Math.sin(-paddle.rotation) + relativeY * Math.cos(-paddle.rotation);
// Transform relative velocity to paddle's coordinate system
const rotatedVelocityX = relativeVelocityX * Math.cos(-paddle.rotation) - relativeVelocityY * Math.sin(-paddle.rotation);
const rotatedVelocityY = relativeVelocityX * Math.sin(-paddle.rotation) + relativeVelocityY * Math.cos(-paddle.rotation);
// Account for rotational velocity of the paddle
// Points further from the center will experience more velocity due to rotation
const rotationalVelocityX = -rotatedY * paddleAngularVelocity;
const rotationalVelocityY = rotatedX * paddleAngularVelocity;
// Complete relative velocity including rotational effects
const totalRelativeVelocityX = rotatedVelocityX - rotationalVelocityX;
const totalRelativeVelocityY = rotatedVelocityY - rotationalVelocityY;
// Calculate AABB of paddle in its own space
const halfWidth = paddleProperties.width / 2;
const halfHeight = paddleProperties.height / 2;
const paddleLeft = -halfWidth;
const paddleRight = halfWidth;
const paddleTop = -halfHeight;
const paddleBottom = halfHeight;
// Calculate expanded AABB (paddle + ball radius)
const expandedLeft = paddleLeft - ball.radius;
const expandedRight = paddleRight + ball.radius;
const expandedTop = paddleTop - ball.radius;
const expandedBottom = paddleBottom + ball.radius;
// Check for immediate overlap (ball already inside expanded AABB)
const insideX = rotatedX >= expandedLeft && rotatedX <= expandedRight;
const insideY = rotatedY >= expandedTop && rotatedY <= expandedBottom;
if (insideX && insideY) {
// Ball is already overlapping with paddle
// Find the nearest edge to push the ball out
const distToLeft = Math.abs(rotatedX - expandedLeft);
const distToRight = Math.abs(rotatedX - expandedRight);
const distToTop = Math.abs(rotatedY - expandedTop);
const distToBottom = Math.abs(rotatedY - expandedBottom);
// Find minimum distance and corresponding normal
const minDist = Math.min(distToLeft, distToRight, distToTop, distToBottom);
let normalX = 0, normalY = 0;
if (minDist === distToLeft) {
normalX = -1;
} else if (minDist === distToRight) {
normalX = 1;
} else if (minDist === distToTop) {
normalY = -1;
} else {
normalY = 1;
}
// Rotate normal back to world space
const worldNormalX = normalX * Math.cos(paddle.rotation) - normalY * Math.sin(paddle.rotation);
const worldNormalY = normalX * Math.sin(paddle.rotation) + normalY * Math.cos(paddle.rotation);
return {
time: 0,
normalX: worldNormalX,
normalY: worldNormalY,
hitPoint: { x: ball.x, y: ball.y }
};
}
// Default no collision
let normalX = 0;
let normalY = 0;
// Calculate entry and exit times for each axis
let entryTimeX, entryTimeY, exitTimeX, exitTimeY;
// X-axis collision times
if (totalRelativeVelocityX > 0) {
entryTimeX = (expandedLeft - rotatedX) / (totalRelativeVelocityX * deltaTime);
exitTimeX = (expandedRight - rotatedX) / (totalRelativeVelocityX * deltaTime);
normalX = -1;
} else if (totalRelativeVelocityX < 0) {
entryTimeX = (expandedRight - rotatedX) / (totalRelativeVelocityX * deltaTime);
exitTimeX = (expandedLeft - rotatedX) / (totalRelativeVelocityX * deltaTime);
normalX = 1;
} else {
// No relative motion in X axis
entryTimeX = rotatedX <= expandedRight && rotatedX >= expandedLeft ? 0 : Number.NEGATIVE_INFINITY;
exitTimeX = Number.POSITIVE_INFINITY;
}
// Y-axis collision times
if (totalRelativeVelocityY > 0) {
entryTimeY = (expandedTop - rotatedY) / (totalRelativeVelocityY * deltaTime);
exitTimeY = (expandedBottom - rotatedY) / (totalRelativeVelocityY * deltaTime);
normalY = -1;
} else if (totalRelativeVelocityY < 0) {
entryTimeY = (expandedBottom - rotatedY) / (totalRelativeVelocityY * deltaTime);
exitTimeY = (expandedTop - rotatedY) / (totalRelativeVelocityY * deltaTime);
normalY = 1;
} else {
// No relative motion in Y axis
entryTimeY = rotatedY <= expandedBottom && rotatedY >= expandedTop ? 0 : Number.NEGATIVE_INFINITY;
exitTimeY = Number.POSITIVE_INFINITY;
}
// Find the latest entry time and earliest exit time
const entryTime = Math.max(entryTimeX, entryTimeY);
const exitTime = Math.min(exitTimeX, exitTimeY);
// Check if there's a collision in this frame
if (entryTime > exitTime || entryTime > 1 || entryTime < 0) {
return { time: -1, normalX: 0, normalY: 0, hitPoint: { x: 0, y: 0 } };
}
// Determine which axis was hit first
if (entryTimeX > entryTimeY) {
normalY = 0;
} else {
normalX = 0;
}
// Calculate hit point in paddle's local space
const hitX = rotatedX + totalRelativeVelocityX * deltaTime * entryTime;
const hitY = rotatedY + totalRelativeVelocityY * deltaTime * entryTime;
// Rotate normal back to world space
const worldNormalX = normalX * Math.cos(paddle.rotation) - normalY * Math.sin(paddle.rotation);
const worldNormalY = normalX * Math.sin(paddle.rotation) + normalY * Math.cos(paddle.rotation);
// Convert hit point back to world space
const worldHitX = hitX * Math.cos(paddle.rotation) - hitY * Math.sin(paddle.rotation) + paddle.x;
const worldHitY = hitX * Math.sin(paddle.rotation) + hitY * Math.cos(paddle.rotation) + paddle.y;
return {
time: entryTime,
normalX: worldNormalX,
normalY: worldNormalY,
hitPoint: { x: worldHitX, y: worldHitY }
};
}
export function handleSweptCollision(
ball: Ball,
collision: { time: number, normalX: number, normalY: number, hitPoint: {x: number, y: number} },
paddle: Paddle,
paddleVelocityX: number,
paddleVelocityY: number,
paddleAngularVelocity: number,
deltaTime: number,
isLeapPressed: boolean
) {
// Check if collision data is valid before processing
if (!collision || collision.time === undefined || !collision.hitPoint) {
console.warn("Invalid collision data", collision);
return;
}
// Move ball to collision point
if (collision.time > 0) {
ball.x += ball.velocityX * deltaTime * collision.time;
ball.y += ball.velocityY * deltaTime * collision.time;
} else {
// Ball was already overlapping, move it to the hit point plus a small offset
const offset = ball.radius * 1.01;
ball.x = collision.hitPoint.x + collision.normalX * offset;
ball.y = collision.hitPoint.y + collision.normalY * offset;
}
// Calculate ball position relative to paddle center
const relativeX = ball.x - paddle.x;
const relativeY = ball.y - paddle.y;
// Calculate tangential velocity from rotation at the collision point
const tangentialVelocityX = -relativeY * paddleAngularVelocity;
const tangentialVelocityY = relativeX * paddleAngularVelocity;
// Total paddle velocity at collision point
const totalPaddleVelocityX = paddleVelocityX + tangentialVelocityX;
const totalPaddleVelocityY = paddleVelocityY + tangentialVelocityY;
// Calculate impulse direction and magnitude
const normalSpeedBefore = ball.velocityX * collision.normalX + ball.velocityY * collision.normalY;
const paddleNormalSpeed = totalPaddleVelocityX * collision.normalX + totalPaddleVelocityY * collision.normalY;
// Only bounce if ball is moving towards paddle or paddle is moving faster towards ball
if (normalSpeedBefore < paddleNormalSpeed) {
// Calculate reflection - both coefficients can be tuned
const elasticity = 1.05; // Slightly elastic collision
const friction = 0.2; // Friction coefficient for tangential component
// Split velocity into normal and tangential components
const normalVelX = normalSpeedBefore * collision.normalX;
const normalVelY = normalSpeedBefore * collision.normalY;
const tangentVelX = ball.velocityX - normalVelX;
const tangentVelY = ball.velocityY - normalVelY;
// Compute new normal velocity (applying elasticity)
let newNormalSpeed;
if (Math.abs(paddleNormalSpeed) < 0.001) {
// When paddle is stationary, just reflect with slight speed increase
newNormalSpeed = -normalSpeedBefore * elasticity;
} else {
// When paddle is moving, use the original formula
newNormalSpeed = elasticity * (paddleNormalSpeed - normalSpeedBefore) + normalSpeedBefore;
}
const newNormalVelX = newNormalSpeed * collision.normalX;
const newNormalVelY = newNormalSpeed * collision.normalY;
// Compute paddle's tangential velocity
const paddleTangentVelX = totalPaddleVelocityX - (paddleNormalSpeed * collision.normalX);
const paddleTangentVelY = totalPaddleVelocityY - (paddleNormalSpeed * collision.normalY);
// Apply friction to tangential velocity
const newTangentVelX = tangentVelX + friction * (paddleTangentVelX - tangentVelX);
const newTangentVelY = tangentVelY + friction * (paddleTangentVelY - tangentVelY);
// Combine new normal and tangential components
ball.velocityX = newNormalVelX + newTangentVelX;
ball.velocityY = newNormalVelY + newTangentVelY;
// Add paddle's momentum to the ball if using leap
if (isLeapPressed) {
const leapBoostFactor = 0.7;
const paddleSpeed = paddleProperties.moveSpeed * paddle.currentPower;
const paddleDirectionX = Math.sin(paddle.rotation);
const paddleDirectionY = -Math.cos(paddle.rotation);
ball.velocityX += paddleDirectionX * paddleSpeed * leapBoostFactor;
ball.velocityY += paddleDirectionY * paddleSpeed * leapBoostFactor;
}
}
// Slightly move the ball along the normal to prevent sticking
const minOffset = 0.1;
ball.x += collision.normalX * minOffset;
ball.y += collision.normalY * minOffset;
// Continue ball movement with remaining time if it was a standard collision
if (collision.time > 0) {
const remainingTime = 1.0 - collision.time;
ball.x += ball.velocityX * deltaTime * remainingTime;
ball.y += ball.velocityY * deltaTime * remainingTime;
}
// Add boundaries check after all position updates
// Ensure the ball stays within the canvas
ball.x = Math.max(ball.radius, Math.min(canvas.width - ball.radius, ball.x));
ball.y = Math.max(ball.radius, Math.min(canvas.height - ball.radius, ball.y));
}
export function handleWallCollisions(ball: Ball, deltaTime: number) {
// Check for wall collisions (left/right)
if (ball.x - ball.radius < 0) {
ball.x = ball.radius;
ball.velocityX = Math.abs(ball.velocityX);
} else if (ball.x + ball.radius > canvas.width) {
ball.x = canvas.width - ball.radius;
ball.velocityX = -Math.abs(ball.velocityX);
}
// Check for top wall collision
if (ball.y - ball.radius < 0) {
ball.y = ball.radius;
ball.velocityY = Math.abs(ball.velocityY);
}
// Check for bottom wall collision
if (ball.y + ball.radius > canvas.height) {
ball.y = canvas.height - ball.radius;
ball.velocityY = -Math.abs(ball.velocityY);
}
// Safeguard against NaN or Infinity values that could make the ball disappear
if (isNaN(ball.x) || isNaN(ball.y) || !isFinite(ball.x) || !isFinite(ball.y)) {
console.warn("Ball position invalid, resetting to center", {x: ball.x, y: ball.y});
ball.x = canvas.width / 2;
ball.y = canvas.height / 2;
ball.velocityX = ball.velocityX || 5;
ball.velocityY = ball.velocityY || 5;
}
}