308 lines
No EOL
12 KiB
TypeScript
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;
|
|
}
|
|
}
|