Initial commit
This commit is contained in:
		
				commit
				
					
						5c8fe77204
					
				
			
		
					 16 changed files with 5372 additions and 0 deletions
				
			
		
							
								
								
									
										26
									
								
								.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,26 @@
 | 
			
		|||
# Dependencies
 | 
			
		||||
node_modules/
 | 
			
		||||
 | 
			
		||||
# Build output
 | 
			
		||||
dist/
 | 
			
		||||
 | 
			
		||||
# Environment variables
 | 
			
		||||
.env
 | 
			
		||||
.env.local
 | 
			
		||||
 | 
			
		||||
# Logs
 | 
			
		||||
logs
 | 
			
		||||
*.log
 | 
			
		||||
npm-debug.log*
 | 
			
		||||
 | 
			
		||||
# Editor directories and files
 | 
			
		||||
.idea/
 | 
			
		||||
.vscode/
 | 
			
		||||
*.suo
 | 
			
		||||
*.ntvs*
 | 
			
		||||
*.njsproj
 | 
			
		||||
*.sln
 | 
			
		||||
*.sw?
 | 
			
		||||
 | 
			
		||||
# OS specific
 | 
			
		||||
.DS_Store 
 | 
			
		||||
							
								
								
									
										48
									
								
								README.md
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								README.md
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,48 @@
 | 
			
		|||
# TypeScript + HTML Project
 | 
			
		||||
 | 
			
		||||
A web project setup with:
 | 
			
		||||
- TypeScript configuration
 | 
			
		||||
- HTML and CSS integration
 | 
			
		||||
- Webpack for bundling and serving
 | 
			
		||||
- Development server with hot reloading
 | 
			
		||||
- VSCode configuration for debugging and development
 | 
			
		||||
 | 
			
		||||
## Getting Started
 | 
			
		||||
 | 
			
		||||
1. Install dependencies:
 | 
			
		||||
```
 | 
			
		||||
npm install
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
2. Start development server (with hot reloading):
 | 
			
		||||
```
 | 
			
		||||
npm run dev
 | 
			
		||||
```
 | 
			
		||||
This will open the application in your browser at http://localhost:9000
 | 
			
		||||
 | 
			
		||||
3. Build for production:
 | 
			
		||||
```
 | 
			
		||||
npm run build
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Project Structure
 | 
			
		||||
 | 
			
		||||
- `/src` - Source files (TypeScript, HTML, CSS)
 | 
			
		||||
- `/dist` - Compiled output (generated after build)
 | 
			
		||||
- `/.vscode` - VSCode configurations
 | 
			
		||||
- `webpack.config.js` - Webpack configuration
 | 
			
		||||
- `tsconfig.json` - TypeScript configuration
 | 
			
		||||
 | 
			
		||||
## VSCode Integration
 | 
			
		||||
 | 
			
		||||
This project includes VSCode configurations for better development experience:
 | 
			
		||||
 | 
			
		||||
- **Recommended Extensions**: Open VSCode and check the "Recommended Extensions" section
 | 
			
		||||
- **Debugging**: Launch configurations for Chrome and Firefox
 | 
			
		||||
- **Tasks**: Run build and development server directly from VSCode
 | 
			
		||||
- **Settings**: Optimized editor settings for TypeScript and web development
 | 
			
		||||
 | 
			
		||||
To start debugging:
 | 
			
		||||
1. Run the development server: `npm run dev`
 | 
			
		||||
2. Press F5 or select the debug icon in VSCode and choose a browser
 | 
			
		||||
3. Set breakpoints in your TypeScript code
 | 
			
		||||
							
								
								
									
										4389
									
								
								package-lock.json
									
										
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										4389
									
								
								package-lock.json
									
										
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										26
									
								
								package.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								package.json
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,26 @@
 | 
			
		|||
{
 | 
			
		||||
  "name": "pgame",
 | 
			
		||||
  "version": "1.0.0",
 | 
			
		||||
  "main": "dist/index.js",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "build": "webpack",
 | 
			
		||||
    "start": "webpack serve",
 | 
			
		||||
    "dev": "webpack serve",
 | 
			
		||||
    "restart": "npm run build && npm run start",
 | 
			
		||||
    "test": "echo \"Error: no test specified\" && exit 1"
 | 
			
		||||
  },
 | 
			
		||||
  "keywords": [],
 | 
			
		||||
  "author": "",
 | 
			
		||||
  "license": "ISC",
 | 
			
		||||
  "description": "",
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@types/node": "^22.13.14",
 | 
			
		||||
    "copy-webpack-plugin": "^13.0.0",
 | 
			
		||||
    "html-webpack-plugin": "^5.6.3",
 | 
			
		||||
    "ts-loader": "^9.5.2",
 | 
			
		||||
    "typescript": "^5.8.2",
 | 
			
		||||
    "webpack": "^5.98.0",
 | 
			
		||||
    "webpack-cli": "^6.0.1",
 | 
			
		||||
    "webpack-dev-server": "^5.2.1"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										15
									
								
								src/canvas.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/canvas.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,15 @@
 | 
			
		|||
export const canvas = document.getElementById("gameCanvas") as HTMLCanvasElement;
 | 
			
		||||
export const ctx = canvas.getContext("2d") as CanvasRenderingContext2D;
 | 
			
		||||
 | 
			
		||||
export function setupCanvas() {
 | 
			
		||||
  resizeCanvas();
 | 
			
		||||
 | 
			
		||||
  ctx.imageSmoothingEnabled = false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function resizeCanvas() {
 | 
			
		||||
  const rect = canvas.getBoundingClientRect();
 | 
			
		||||
 | 
			
		||||
  canvas.width = rect.width;
 | 
			
		||||
  canvas.height = rect.height;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										308
									
								
								src/collision.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										308
									
								
								src/collision.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,308 @@
 | 
			
		|||
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;
 | 
			
		||||
  }
 | 
			
		||||
} 
 | 
			
		||||
							
								
								
									
										169
									
								
								src/game.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										169
									
								
								src/game.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,169 @@
 | 
			
		|||
import { isKeyPressed } from './input';
 | 
			
		||||
import { ctx, canvas, resizeCanvas } from './canvas';
 | 
			
		||||
import { Paddle, paddleProperties, drawPaddle } from './objects/paddle';
 | 
			
		||||
import { Ball, createBall, drawBall, updateBall, ballProperties } from './objects/ball';
 | 
			
		||||
import { sweptCollision, handleSweptCollision, handleWallCollisions } from './collision';
 | 
			
		||||
import { drawPowerBar } from './gui';
 | 
			
		||||
 | 
			
		||||
let lastTime: number;
 | 
			
		||||
let paddle: Paddle;
 | 
			
		||||
let ball: Ball;
 | 
			
		||||
let prevPaddleX: number;
 | 
			
		||||
let prevPaddleY: number;
 | 
			
		||||
let prevPaddleRotation: number;
 | 
			
		||||
 | 
			
		||||
export function initGame(initialPaddle: Paddle, initialBall: Ball) {
 | 
			
		||||
  paddle = initialPaddle;
 | 
			
		||||
  ball = initialBall;
 | 
			
		||||
  lastTime = performance.now();
 | 
			
		||||
  prevPaddleX = paddle.x;
 | 
			
		||||
  prevPaddleY = paddle.y;
 | 
			
		||||
  prevPaddleRotation = paddle.rotation;
 | 
			
		||||
  requestAnimationFrame(gameLoop);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function gameLoop() {
 | 
			
		||||
  // Get delta time since last frame
 | 
			
		||||
  const currentTime = performance.now();
 | 
			
		||||
  const deltaTime = (currentTime - lastTime) / 1000; // Convert to seconds
 | 
			
		||||
  lastTime = currentTime;
 | 
			
		||||
 | 
			
		||||
  resizeCanvas();
 | 
			
		||||
 | 
			
		||||
  // Fill canvas with black background
 | 
			
		||||
  ctx.fillStyle = "black";
 | 
			
		||||
  ctx.fillRect(0, 0, canvas.width, canvas.height);
 | 
			
		||||
 | 
			
		||||
  // Calculate paddle velocity from previous position
 | 
			
		||||
  const paddleVelocityX = (paddle.x - prevPaddleX) / deltaTime;
 | 
			
		||||
  const paddleVelocityY = (paddle.y - prevPaddleY) / deltaTime;
 | 
			
		||||
  const paddleAngularVelocity = (paddle.rotation - prevPaddleRotation) / deltaTime;
 | 
			
		||||
  
 | 
			
		||||
  // Save current position for next frame
 | 
			
		||||
  prevPaddleX = paddle.x;
 | 
			
		||||
  prevPaddleY = paddle.y;
 | 
			
		||||
  prevPaddleRotation = paddle.rotation;
 | 
			
		||||
 | 
			
		||||
  // Check input states first
 | 
			
		||||
  const rotatingLeft = isKeyPressed('KeyA');
 | 
			
		||||
  const rotatingRight = isKeyPressed('KeyD');
 | 
			
		||||
  const movingForward = isKeyPressed('KeyW');
 | 
			
		||||
  const movingBackward = isKeyPressed('KeyS');
 | 
			
		||||
  const leaping = isKeyPressed('Space');
 | 
			
		||||
  const sneaking = isKeyPressed('ShiftLeft');
 | 
			
		||||
 | 
			
		||||
  const rotating = rotatingLeft || rotatingRight;
 | 
			
		||||
  const moving = movingForward || movingBackward || rotating;
 | 
			
		||||
  
 | 
			
		||||
  let currentMoveSpeed = paddleProperties.moveSpeed;
 | 
			
		||||
  let currentRotationSpeed = paddleProperties.rotationSpeed;
 | 
			
		||||
  
 | 
			
		||||
  if (sneaking) {
 | 
			
		||||
    currentMoveSpeed *= 0.5;
 | 
			
		||||
    currentRotationSpeed *= 0.5;
 | 
			
		||||
  } else if (moving && leaping) {
 | 
			
		||||
    currentMoveSpeed *= paddle.currentPower;
 | 
			
		||||
    
 | 
			
		||||
    paddle.currentPower = Math.max(1, paddle.currentPower - paddleProperties.powerDecrease * deltaTime);
 | 
			
		||||
  } else {
 | 
			
		||||
    paddle.currentPower = Math.min(paddleProperties.maxPower, paddle.currentPower + paddleProperties.powerIncrease * deltaTime);
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  if (rotating) {
 | 
			
		||||
    currentMoveSpeed /= Math.sqrt(2);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Handle rotation first
 | 
			
		||||
  if (rotating) {
 | 
			
		||||
    const rotationDirection = rotatingLeft ? -1 : 1;
 | 
			
		||||
    const rotationAmount = currentRotationSpeed * deltaTime;
 | 
			
		||||
    
 | 
			
		||||
    // Store original rotation
 | 
			
		||||
    const originalRotation = paddle.rotation;
 | 
			
		||||
    
 | 
			
		||||
    // Apply rotation
 | 
			
		||||
    paddle.rotation += rotationDirection * rotationAmount;
 | 
			
		||||
    
 | 
			
		||||
    // Calculate edge positions based on original rotation
 | 
			
		||||
    let edgeX, edgeY;
 | 
			
		||||
    
 | 
			
		||||
    // Determine which edge to fix based on rotation direction and movement direction
 | 
			
		||||
    const fixLeftEdge = (rotationDirection === -1 && !movingBackward) || 
 | 
			
		||||
                         (rotationDirection === 1 && movingBackward);
 | 
			
		||||
    
 | 
			
		||||
    if (fixLeftEdge) {
 | 
			
		||||
      // Calculate left edge position before rotation
 | 
			
		||||
      edgeX = paddle.x - (paddleProperties.width / 2) * Math.cos(originalRotation);
 | 
			
		||||
      edgeY = paddle.y - (paddleProperties.width / 2) * Math.sin(originalRotation);
 | 
			
		||||
      
 | 
			
		||||
      // Recalculate paddle position to keep left edge in place
 | 
			
		||||
      paddle.x = edgeX + (paddleProperties.width / 2) * Math.cos(paddle.rotation);
 | 
			
		||||
      paddle.y = edgeY + (paddleProperties.width / 2) * Math.sin(paddle.rotation);
 | 
			
		||||
    } else {
 | 
			
		||||
      // Calculate right edge position before rotation
 | 
			
		||||
      edgeX = paddle.x + (paddleProperties.width / 2) * Math.cos(originalRotation);
 | 
			
		||||
      edgeY = paddle.y + (paddleProperties.width / 2) * Math.sin(originalRotation);
 | 
			
		||||
      
 | 
			
		||||
      // Recalculate paddle position to keep right edge in place
 | 
			
		||||
      paddle.x = edgeX - (paddleProperties.width / 2) * Math.cos(paddle.rotation);
 | 
			
		||||
      paddle.y = edgeY - (paddleProperties.width / 2) * Math.sin(paddle.rotation);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // Handle movement after rotation
 | 
			
		||||
  if (movingForward) {
 | 
			
		||||
    paddle.x += Math.sin(paddle.rotation) * currentMoveSpeed * deltaTime;
 | 
			
		||||
    paddle.y -= Math.cos(paddle.rotation) * currentMoveSpeed * deltaTime;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  if (movingBackward) {
 | 
			
		||||
    paddle.x -= Math.sin(paddle.rotation) * currentMoveSpeed * deltaTime;
 | 
			
		||||
    paddle.y += Math.cos(paddle.rotation) * currentMoveSpeed * deltaTime;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Calculate expected paddle velocity for the current frame (prediction)
 | 
			
		||||
  const currentPaddleVelocityX = (paddle.x - prevPaddleX) / deltaTime;
 | 
			
		||||
  const currentPaddleVelocityY = (paddle.y - prevPaddleY) / deltaTime;
 | 
			
		||||
  const currentPaddleAngularVelocity = (paddle.rotation - prevPaddleRotation) / deltaTime;
 | 
			
		||||
  
 | 
			
		||||
  // Check for swept paddle collision using relative velocity
 | 
			
		||||
  const paddleCollision = sweptCollision(
 | 
			
		||||
    ball, 
 | 
			
		||||
    ball.velocityX, 
 | 
			
		||||
    ball.velocityY, 
 | 
			
		||||
    paddle, 
 | 
			
		||||
    currentPaddleVelocityX,
 | 
			
		||||
    currentPaddleVelocityY,
 | 
			
		||||
    currentPaddleAngularVelocity,
 | 
			
		||||
    deltaTime
 | 
			
		||||
  );
 | 
			
		||||
  
 | 
			
		||||
  // Handle ball movement and collisions
 | 
			
		||||
  if (paddleCollision.time >= 0) {
 | 
			
		||||
    // Ball collides with paddle in this frame
 | 
			
		||||
    handleSweptCollision(
 | 
			
		||||
      ball, 
 | 
			
		||||
      paddleCollision, 
 | 
			
		||||
      paddle,
 | 
			
		||||
      currentPaddleVelocityX,
 | 
			
		||||
      currentPaddleVelocityY,
 | 
			
		||||
      currentPaddleAngularVelocity,
 | 
			
		||||
      deltaTime,
 | 
			
		||||
      leaping
 | 
			
		||||
    );
 | 
			
		||||
  } else {
 | 
			
		||||
    // No paddle collision, update ball normally
 | 
			
		||||
    updateBall(ball, deltaTime);
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // Handle wall collisions
 | 
			
		||||
  handleWallCollisions(ball, deltaTime);
 | 
			
		||||
 | 
			
		||||
  // Draw game objects
 | 
			
		||||
  drawPaddle(ctx, paddle);
 | 
			
		||||
  drawBall(ctx, ball);
 | 
			
		||||
 | 
			
		||||
  drawPowerBar(paddle);
 | 
			
		||||
 | 
			
		||||
  requestAnimationFrame(gameLoop);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										28
									
								
								src/gui.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/gui.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,28 @@
 | 
			
		|||
import { ctx } from "./canvas";
 | 
			
		||||
 | 
			
		||||
import { canvas } from "./canvas";
 | 
			
		||||
import { Paddle, paddleProperties } from "./objects/paddle";
 | 
			
		||||
 | 
			
		||||
const barProperties = {
 | 
			
		||||
    width: 500,
 | 
			
		||||
    height: 10,
 | 
			
		||||
    x: 20,
 | 
			
		||||
    y: canvas.height - 20,
 | 
			
		||||
    color: "gray",
 | 
			
		||||
    borderColor: "white"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function drawPowerBar(paddle: Paddle) {
 | 
			
		||||
    // Draw bar background
 | 
			
		||||
    ctx.fillStyle = "gray";
 | 
			
		||||
    ctx.fillRect(barProperties.x, barProperties.y, barProperties.width, barProperties.height);
 | 
			
		||||
 | 
			
		||||
    // Draw current leap strength
 | 
			
		||||
    const normalizedStrength = (paddle.currentPower - 1) / (paddleProperties.maxPower - 1); // Convert from 1-3 range to 0-1 range
 | 
			
		||||
    ctx.fillStyle = normalizedStrength > 0.7 ? "green" : normalizedStrength > 0.3 ? "yellow" : "red";
 | 
			
		||||
    ctx.fillRect(barProperties.x, barProperties.y, barProperties.width * normalizedStrength, barProperties.height);
 | 
			
		||||
 | 
			
		||||
    // Draw bar border
 | 
			
		||||
    ctx.strokeStyle = "white";
 | 
			
		||||
    ctx.strokeRect(barProperties.x, barProperties.y, barProperties.width, barProperties.height);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										14
									
								
								src/index.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/index.html
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,14 @@
 | 
			
		|||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
<head>
 | 
			
		||||
  <meta charset="UTF-8">
 | 
			
		||||
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
			
		||||
  <title>TypeScript HTML Project</title>
 | 
			
		||||
  <link rel="stylesheet" href="styles.css">
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
  <canvas id="gameCanvas"></canvas>
 | 
			
		||||
  
 | 
			
		||||
  <script src="index.js"></script>
 | 
			
		||||
</body>
 | 
			
		||||
</html> 
 | 
			
		||||
							
								
								
									
										15
									
								
								src/index.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/index.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,15 @@
 | 
			
		|||
import { initKeyboardInput } from './input';
 | 
			
		||||
import { canvas, setupCanvas } from './canvas';
 | 
			
		||||
import { createPaddle } from './objects/paddle';
 | 
			
		||||
import { initGame } from './game';
 | 
			
		||||
import { createBall } from './objects/ball';
 | 
			
		||||
// Setup canvas and initialize input
 | 
			
		||||
setupCanvas();
 | 
			
		||||
initKeyboardInput();
 | 
			
		||||
 | 
			
		||||
// Create paddle and start game
 | 
			
		||||
const paddle = createPaddle(canvas.width / 2, canvas.height - 200, "white", "Player");
 | 
			
		||||
const ball = createBall(canvas.width / 2, canvas.height / 2);
 | 
			
		||||
ball.velocityX = 200;
 | 
			
		||||
ball.velocityY = 200;
 | 
			
		||||
initGame(paddle, ball); 
 | 
			
		||||
							
								
								
									
										45
									
								
								src/input.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/input.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,45 @@
 | 
			
		|||
/**
 | 
			
		||||
 * Keyboard input handler for the game
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
// Track pressed keys
 | 
			
		||||
const keys: Record<string, boolean> = {};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Initialize keyboard event listeners
 | 
			
		||||
 */
 | 
			
		||||
export function initKeyboardInput(): void {
 | 
			
		||||
  window.addEventListener('keydown', (e) => {
 | 
			
		||||
    keys[e.code] = true;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  window.addEventListener('keyup', (e) => {
 | 
			
		||||
    keys[e.code] = false;
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Check if a specific key is currently pressed
 | 
			
		||||
 * @param key - The key to check
 | 
			
		||||
 * @returns True if the key is pressed, false otherwise
 | 
			
		||||
 */
 | 
			
		||||
export function isKeyPressed(key: string): boolean {
 | 
			
		||||
  return !!keys[key];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Get all currently pressed keys
 | 
			
		||||
 * @returns An object containing all pressed keys
 | 
			
		||||
 */
 | 
			
		||||
export function getPressedKeys(): Record<string, boolean> {
 | 
			
		||||
  return { ...keys };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Reset all key states (set all to not pressed)
 | 
			
		||||
 */
 | 
			
		||||
export function resetKeys(): void {
 | 
			
		||||
  for (const key in keys) {
 | 
			
		||||
    keys[key] = false;
 | 
			
		||||
  }
 | 
			
		||||
} 
 | 
			
		||||
							
								
								
									
										99
									
								
								src/objects/ball.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								src/objects/ball.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,99 @@
 | 
			
		|||
export interface Ball {
 | 
			
		||||
  x: number;
 | 
			
		||||
  y: number;
 | 
			
		||||
  radius: number;
 | 
			
		||||
  color: string;
 | 
			
		||||
  velocityX: number;
 | 
			
		||||
  velocityY: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const ballProperties = {
 | 
			
		||||
  radius: 10,
 | 
			
		||||
  color: "white",
 | 
			
		||||
  friction: 0.99,
 | 
			
		||||
  predictionSteps: 0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function createBall(x: number, y: number): Ball {
 | 
			
		||||
  return {
 | 
			
		||||
    x: x,
 | 
			
		||||
    y: y,
 | 
			
		||||
    radius: ballProperties.radius,
 | 
			
		||||
    color: ballProperties.color,
 | 
			
		||||
    velocityX: 0,
 | 
			
		||||
    velocityY: 0
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function drawBall(ctx: CanvasRenderingContext2D, ball: Ball) {
 | 
			
		||||
  ctx.fillStyle = ball.color;
 | 
			
		||||
  ctx.beginPath();
 | 
			
		||||
  ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
 | 
			
		||||
  ctx.fill();
 | 
			
		||||
 | 
			
		||||
  if (ballProperties.predictionSteps > 0) {
 | 
			
		||||
    drawPredictionLine(ctx, ball);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function updateBall(ball: Ball, deltaTime: number) {
 | 
			
		||||
  // Update position based on velocity
 | 
			
		||||
  ball.x += ball.velocityX * deltaTime;
 | 
			
		||||
  ball.y += ball.velocityY * deltaTime;
 | 
			
		||||
 | 
			
		||||
  // Apply friction based on delta time
 | 
			
		||||
  ball.velocityX *= Math.pow(ballProperties.friction, deltaTime);
 | 
			
		||||
  ball.velocityY *= Math.pow(ballProperties.friction, deltaTime);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function drawPredictionLine(ctx: CanvasRenderingContext2D, ball: Ball) {
 | 
			
		||||
    const canvas = ctx.canvas;
 | 
			
		||||
    const timeStep = 0.02; // Time increment for each prediction step
 | 
			
		||||
    
 | 
			
		||||
    // Create a clone of the ball for prediction
 | 
			
		||||
    const predictedBall = {
 | 
			
		||||
      x: ball.x,
 | 
			
		||||
      y: ball.y,
 | 
			
		||||
      radius: ball.radius,
 | 
			
		||||
      velocityX: ball.velocityX,
 | 
			
		||||
      velocityY: ball.velocityY
 | 
			
		||||
    };
 | 
			
		||||
    
 | 
			
		||||
    // Set up line drawing
 | 
			
		||||
    ctx.beginPath();
 | 
			
		||||
    ctx.moveTo(ball.x, ball.y);
 | 
			
		||||
    ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)'; // Semi-transparent white
 | 
			
		||||
    ctx.setLineDash([5, 5]); // Dashed line
 | 
			
		||||
    ctx.lineWidth = 2;
 | 
			
		||||
    
 | 
			
		||||
    // Predict multiple steps of ball movement
 | 
			
		||||
    for (let i = 0; i < ballProperties.predictionSteps; i++) {
 | 
			
		||||
      // Move the predicted ball
 | 
			
		||||
      predictedBall.x += predictedBall.velocityX * timeStep;
 | 
			
		||||
      predictedBall.y += predictedBall.velocityY * timeStep;
 | 
			
		||||
      
 | 
			
		||||
      // Check for wall collisions
 | 
			
		||||
      if (predictedBall.x - predictedBall.radius < 0) {
 | 
			
		||||
        predictedBall.x = predictedBall.radius;
 | 
			
		||||
        predictedBall.velocityX = Math.abs(predictedBall.velocityX);
 | 
			
		||||
      } else if (predictedBall.x + predictedBall.radius > canvas.width) {
 | 
			
		||||
        predictedBall.x = canvas.width - predictedBall.radius;
 | 
			
		||||
        predictedBall.velocityX = -Math.abs(predictedBall.velocityX);
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      if (predictedBall.y - predictedBall.radius < 0) {
 | 
			
		||||
        predictedBall.y = predictedBall.radius;
 | 
			
		||||
        predictedBall.velocityY = Math.abs(predictedBall.velocityY);
 | 
			
		||||
      } else if (predictedBall.y + predictedBall.radius > canvas.height) {
 | 
			
		||||
        predictedBall.y = canvas.height - predictedBall.radius;
 | 
			
		||||
        predictedBall.velocityY = -Math.abs(predictedBall.velocityY);
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      // Add point to the line
 | 
			
		||||
      ctx.lineTo(predictedBall.x, predictedBall.y);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Draw the prediction line
 | 
			
		||||
    ctx.stroke();
 | 
			
		||||
    ctx.setLineDash([]); // Reset line dash
 | 
			
		||||
  }
 | 
			
		||||
							
								
								
									
										111
									
								
								src/objects/paddle.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								src/objects/paddle.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,111 @@
 | 
			
		|||
/**
 | 
			
		||||
 * Properties for the game paddle
 | 
			
		||||
 * @const paddleProperties
 | 
			
		||||
 * @property {number} width - The width of the paddle in pixels
 | 
			
		||||
 * @property {number} height - The height of the paddle in pixels
 | 
			
		||||
 * @property {number} rotationSpeed - The rotation speed of the paddle in radians per second
 | 
			
		||||
 * @property {number} moveSpeed - The movement speed of the paddle in pixels per second
 | 
			
		||||
 * @property {number} maxPower - The maximum power of the paddle
 | 
			
		||||
 * @property {number} powerIncrease - The increase in power per second
 | 
			
		||||
 * @property {number} powerDecrease - The decrease in power per second
 | 
			
		||||
 */
 | 
			
		||||
export const paddleProperties = {
 | 
			
		||||
  width: 100,
 | 
			
		||||
  height: 15,
 | 
			
		||||
  rotationSpeed: Math.PI * 3,
 | 
			
		||||
  moveSpeed: 2000,
 | 
			
		||||
  maxPower: 7,
 | 
			
		||||
  powerIncrease: 7 * 0.2,
 | 
			
		||||
  powerDecrease: 7 * 10
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Represents a paddle in the game
 | 
			
		||||
 * @interface Paddle
 | 
			
		||||
 * @property {number} x - The x-coordinate of the paddle's center
 | 
			
		||||
 * @property {number} y - The y-coordinate of the paddle's center
 | 
			
		||||
 * @property {number} rotation - The rotation angle of the paddle in radians
 | 
			
		||||
 * @property {string} color - The fill color of the paddle
 | 
			
		||||
 * @property {string} label - The label of the paddle
 | 
			
		||||
 */
 | 
			
		||||
export interface Paddle {
 | 
			
		||||
  x: number;
 | 
			
		||||
  y: number;
 | 
			
		||||
  rotation: number;
 | 
			
		||||
  currentPower: number;
 | 
			
		||||
  color: string;
 | 
			
		||||
  label: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function drawPaddle(ctx: CanvasRenderingContext2D, paddle: Paddle) {
 | 
			
		||||
  ctx.save();
 | 
			
		||||
 | 
			
		||||
    // Set up the glow effect
 | 
			
		||||
    // TODO: Make the glow effect more realistic
 | 
			
		||||
  ctx.shadowColor = 'rgb(255, 0, 255)';
 | 
			
		||||
  ctx.shadowBlur = 100 - Math.pow(paddle.currentPower / paddleProperties.maxPower, 2) * 60; // Exponential blur radius for the glow
 | 
			
		||||
  ctx.shadowOffsetX = 0; // No offset
 | 
			
		||||
  ctx.shadowOffsetY = 0; // No offset
 | 
			
		||||
  
 | 
			
		||||
  // Translate to the paddle's position
 | 
			
		||||
  ctx.translate(paddle.x, paddle.y);
 | 
			
		||||
  
 | 
			
		||||
  // For edge rotation, we need to translate to the edge before rotating
 | 
			
		||||
  // Move to the center of the paddle's top edge
 | 
			
		||||
  ctx.translate(0, -paddleProperties.height/2);
 | 
			
		||||
  
 | 
			
		||||
  // Now rotate around this edge point
 | 
			
		||||
  ctx.rotate(paddle.rotation);
 | 
			
		||||
  
 | 
			
		||||
  // Move back to draw centered on the edge point
 | 
			
		||||
  ctx.translate(0, paddleProperties.height/2);
 | 
			
		||||
  
 | 
			
		||||
  const width = paddleProperties.width;
 | 
			
		||||
  const height = paddleProperties.height;
 | 
			
		||||
  const cornerRadius = 5; // Radius for the rounded corners
 | 
			
		||||
  
 | 
			
		||||
  ctx.fillStyle = paddle.color;
 | 
			
		||||
  ctx.beginPath();
 | 
			
		||||
  
 | 
			
		||||
  // Start at top-left corner and draw top edge (flat)
 | 
			
		||||
  ctx.moveTo(-width/2, -height/2);
 | 
			
		||||
  ctx.lineTo(width/2, -height/2);
 | 
			
		||||
  
 | 
			
		||||
  // Draw right edge with straight line
 | 
			
		||||
  ctx.lineTo(width/2, height/2 - cornerRadius);
 | 
			
		||||
  
 | 
			
		||||
  // Draw bottom-right rounded corner
 | 
			
		||||
  ctx.arcTo(width/2, height/2, width/2 - cornerRadius, height/2, cornerRadius);
 | 
			
		||||
  
 | 
			
		||||
  // Draw bottom edge
 | 
			
		||||
  ctx.lineTo(-width/2 + cornerRadius, height/2);
 | 
			
		||||
  
 | 
			
		||||
  // Draw bottom-left rounded corner
 | 
			
		||||
  ctx.arcTo(-width/2, height/2, -width/2, height/2 - cornerRadius, cornerRadius);
 | 
			
		||||
  
 | 
			
		||||
  // Back to top-left
 | 
			
		||||
  ctx.lineTo(-width/2, -height/2);
 | 
			
		||||
  
 | 
			
		||||
  ctx.fill();
 | 
			
		||||
  ctx.closePath();
 | 
			
		||||
 | 
			
		||||
  // Draw the label inside the paddle
 | 
			
		||||
  ctx.fillStyle = "black"; // Set the text color
 | 
			
		||||
  ctx.font = `bold ${paddleProperties.height}px Arial`; // Set the font style
 | 
			
		||||
  ctx.textAlign = "center"; // Center the text
 | 
			
		||||
  ctx.fillText(paddle.label, 0, paddleProperties.height/2 - 3); // Draw the label at the center of the paddle
 | 
			
		||||
 | 
			
		||||
  ctx.restore();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export function createPaddle(x: number, y: number, color: string = "white", label: string = "Paddle"): Paddle {
 | 
			
		||||
  return {
 | 
			
		||||
    x: x,
 | 
			
		||||
    y: y,
 | 
			
		||||
    rotation: 0,
 | 
			
		||||
    currentPower: paddleProperties.maxPower,
 | 
			
		||||
    color: color,
 | 
			
		||||
    label: label
 | 
			
		||||
  };
 | 
			
		||||
} 
 | 
			
		||||
							
								
								
									
										20
									
								
								src/styles.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/styles.css
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,20 @@
 | 
			
		|||
html, body {
 | 
			
		||||
  margin: 0;
 | 
			
		||||
  padding: 0;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body {
 | 
			
		||||
  background: #000;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#gameCanvas {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										16
									
								
								tsconfig.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								tsconfig.json
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,16 @@
 | 
			
		|||
{
 | 
			
		||||
  "compilerOptions": {
 | 
			
		||||
    "target": "es2016",
 | 
			
		||||
    "module": "commonjs",
 | 
			
		||||
    "esModuleInterop": true,
 | 
			
		||||
    "forceConsistentCasingInFileNames": true,
 | 
			
		||||
    "strict": true,
 | 
			
		||||
    "skipLibCheck": true,
 | 
			
		||||
    "outDir": "./dist",
 | 
			
		||||
    "rootDir": "./src",
 | 
			
		||||
    "lib": ["dom", "es2016"],
 | 
			
		||||
    "sourceMap": true
 | 
			
		||||
  },
 | 
			
		||||
  "include": ["src/**/*"],
 | 
			
		||||
  "exclude": ["node_modules", "dist"]
 | 
			
		||||
} 
 | 
			
		||||
							
								
								
									
										43
									
								
								webpack.config.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								webpack.config.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,43 @@
 | 
			
		|||
const path = require("path");
 | 
			
		||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
 | 
			
		||||
const CopyWebpackPlugin = require("copy-webpack-plugin");
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
  mode: "development",
 | 
			
		||||
  entry: "./src/index.ts",
 | 
			
		||||
  devtool: "source-map",
 | 
			
		||||
  module: {
 | 
			
		||||
    rules: [
 | 
			
		||||
      {
 | 
			
		||||
        test: /\.tsx?$/,
 | 
			
		||||
        use: "ts-loader",
 | 
			
		||||
        exclude: /node_modules/,
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
  resolve: {
 | 
			
		||||
    extensions: [".tsx", ".ts", ".js"],
 | 
			
		||||
  },
 | 
			
		||||
  output: {
 | 
			
		||||
    filename: "index.js",
 | 
			
		||||
    path: path.resolve(__dirname, "dist"),
 | 
			
		||||
  },
 | 
			
		||||
  plugins: [
 | 
			
		||||
    new HtmlWebpackPlugin({
 | 
			
		||||
      template: "src/index.html",
 | 
			
		||||
    }),
 | 
			
		||||
    new CopyWebpackPlugin({
 | 
			
		||||
      patterns: [{ from: "src/styles.css", to: "styles.css" }],
 | 
			
		||||
    }),
 | 
			
		||||
  ],
 | 
			
		||||
  devServer: {
 | 
			
		||||
    static: {
 | 
			
		||||
      directory: path.join(__dirname, "dist"),
 | 
			
		||||
    },
 | 
			
		||||
    compress: true,
 | 
			
		||||
    port: 9000,
 | 
			
		||||
    hot: true,
 | 
			
		||||
    open: true,
 | 
			
		||||
    historyApiFallback: true,
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue