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