Files
gijs_pong/index.html
Dabit 46b8f7bcc7 feat(01-02): add ball physics — movement, wall bounce, zone deflection, and GameConfig
- Add GameConfig with initialBallSpeed (220px/s), speedIncrement (18px/s), paddleSpeed (400px/s)
- Add Physics.serveBall(): centers ball, sets random angle (-45 to +45 deg), random left/right dir
- Replace Physics.update() skeleton with full ball movement and wall bounce logic
- Add Physics._checkPaddleCollision() with 5-zone angle deflection and speed increment per hit
- Ball exits left/right bounds → immediate re-serve from center
- Ball pushed out of paddle on collision to prevent double-hit next frame
- Add on-canvas speed debug display (Phase 1 verification aid)
2026-03-10 14:50:41 +01:00

280 lines
9.2 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Super Pong Next Gen</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #000; overflow: hidden; width: 100vw; height: 100vh; }
#gameCanvas { display: block; background: #000; }
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<script>
const GameConfig = {
initialBallSpeed: 220, // px/s — starting speed per serve
speedIncrement: 18, // px/s added per paddle hit
paddleSpeed: 400 // px/s — Player 1 paddle movement
};
const Renderer = {
canvas: null,
ctx: null,
logicalWidth: 0,
logicalHeight: 0,
init() {
this.canvas = document.getElementById('gameCanvas');
this.resize();
window.addEventListener('resize', () => this.resize());
},
resize() {
const dpr = window.devicePixelRatio || 1;
const w = window.innerWidth;
const h = window.innerHeight;
const minAspect = 4 / 3;
const currentAspect = w / h;
// Always fill entire window; use logical coords for game
this.logicalWidth = w;
this.logicalHeight = h;
// Enforce minimum 4:3: if narrower than 4:3, treat as 4:3 height
// (window fills screen; minimum 4:3 means we never let height dominate width)
if (currentAspect < minAspect) {
// Too tall — game uses 4:3 minimum: constrain logical height
this.logicalHeight = Math.floor(w / minAspect);
}
// Step 1: Set CSS size to logical dimensions
this.canvas.style.width = this.logicalWidth + 'px';
this.canvas.style.height = this.logicalHeight + 'px';
// Step 2: Scale bitmap to device pixel ratio
this.canvas.width = Math.floor(this.logicalWidth * dpr);
this.canvas.height = Math.floor(this.logicalHeight * dpr);
// Step 3: Scale context so all draw calls use logical pixels
this.ctx = this.canvas.getContext('2d');
this.ctx.scale(dpr, dpr);
// Notify Physics of new dimensions (Physics may not exist yet)
if (typeof Physics !== 'undefined' && Physics.onResize) {
Physics.onResize(this.logicalWidth, this.logicalHeight);
}
},
clear() {
this.ctx.clearRect(0, 0, this.logicalWidth, this.logicalHeight);
this.ctx.fillStyle = '#000';
this.ctx.fillRect(0, 0, this.logicalWidth, this.logicalHeight);
},
drawRect(x, y, w, h, color) {
this.ctx.fillStyle = color || '#fff';
this.ctx.fillRect(Math.round(x), Math.round(y), Math.round(w), Math.round(h));
},
drawCircle(x, y, radius, color) {
this.ctx.beginPath();
this.ctx.arc(Math.round(x), Math.round(y), radius, 0, Math.PI * 2);
this.ctx.fillStyle = color || '#fff';
this.ctx.fill();
}
};
const GameState = {
paddle1: {
x: 0, y: 0, // Set on Physics.init()
width: 12, height: 80,
speed: 400, // Logical pixels per second
color: '#fff'
},
ball: {
x: 0, y: 0, // Set on Physics.init()
radius: 8,
vx: 0, vy: 0, // Set on Physics.init()
speed: 0, // Current scalar speed
color: '#fff'
}
};
const Input = {
keys: { w: false, s: false },
init() {
document.addEventListener('keydown', (e) => {
if (e.code === 'KeyW') { this.keys.w = true; e.preventDefault(); }
if (e.code === 'KeyS') { this.keys.s = true; e.preventDefault(); }
});
document.addEventListener('keyup', (e) => {
if (e.code === 'KeyW') this.keys.w = false;
if (e.code === 'KeyS') this.keys.s = false;
});
},
getVerticalInput() {
if (this.keys.w) return -1;
if (this.keys.s) return 1;
return 0;
}
};
const Physics = {
width: 0,
height: 0,
init(width, height) {
this.width = width;
this.height = height;
// Position paddle1 on left side, vertically centered
GameState.paddle1.x = 30;
GameState.paddle1.y = height / 2 - GameState.paddle1.height / 2;
GameState.paddle1.speed = GameConfig.paddleSpeed;
// Serve ball from center
this.serveBall();
},
onResize(width, height) {
this.width = width;
this.height = height;
},
serveBall() {
const ball = GameState.ball;
ball.x = this.width / 2;
ball.y = this.height / 2;
ball.speed = GameConfig.initialBallSpeed;
// Random vertical angle between -45 and +45 degrees, always heading right
const angle = (Math.random() * 90 - 45) * Math.PI / 180;
// Randomly serve left or right
const dir = Math.random() < 0.5 ? 1 : -1;
ball.vx = Math.cos(angle) * ball.speed * dir;
ball.vy = Math.sin(angle) * ball.speed;
},
update(deltaTime) {
const ball = GameState.ball;
const paddle = GameState.paddle1;
// --- Move paddle1 ---
const dir = Input.getVerticalInput();
paddle.y += dir * paddle.speed * deltaTime;
paddle.y = Math.max(0, Math.min(this.height - paddle.height, paddle.y));
// --- Move ball ---
ball.x += ball.vx * deltaTime;
ball.y += ball.vy * deltaTime;
// --- Wall bounce (top and bottom) ---
if (ball.y - ball.radius < 0) {
ball.y = ball.radius;
ball.vy = Math.abs(ball.vy);
}
if (ball.y + ball.radius > this.height) {
ball.y = this.height - ball.radius;
ball.vy = -Math.abs(ball.vy);
}
// --- Ball out of bounds (left or right) → reset ---
if (ball.x + ball.radius < 0 || ball.x - ball.radius > this.width) {
this.serveBall();
return;
}
// --- Paddle collision (only check when ball moving left toward paddle1) ---
if (ball.vx < 0) {
this._checkPaddleCollision(ball, paddle);
}
},
_checkPaddleCollision(ball, paddle) {
// Speed is uncapped per CONTEXT.md. Tunneling risk begins when ball travels
// more than paddle.height px/frame. At 60fps, that's paddle.height * 60 px/s.
// With height=80: tunneling risk above ~4800 px/s. Current rallies max ~800px/s.
// If Phase 5 introduces higher speeds, add substep physics here.
// AABB: check ball center against paddle bounds (with ball radius buffer)
const inX = ball.x - ball.radius < paddle.x + paddle.width &&
ball.x + ball.radius > paddle.x;
const inY = ball.y + ball.radius > paddle.y &&
ball.y - ball.radius < paddle.y + paddle.height;
if (!inX || !inY) return;
// Zone-based deflection: divide paddle into 5 zones
const relativeHitPos = (ball.y - paddle.y) / paddle.height;
const hitZone = Math.max(0, Math.min(4, Math.floor(relativeHitPos * 5)));
// Map zone to angle (degrees) — positive = downward
const anglesDeg = [
-60, // Zone 0: top edge — steeply upward
-30, // Zone 1: upper — angled upward
5, // Zone 2: center — nearly flat (slight downward to avoid infinite horizontal)
30, // Zone 3: lower — angled downward
60 // Zone 4: bottom edge — steeply downward
];
const angleRad = anglesDeg[hitZone] * Math.PI / 180;
// Increment speed
ball.speed += GameConfig.speedIncrement;
// Decompose into velocity components, always going right after hitting paddle1
ball.vx = Math.cos(angleRad) * ball.speed;
ball.vy = Math.sin(angleRad) * ball.speed;
// Push ball out of paddle to prevent double-collision next frame
ball.x = paddle.x + paddle.width + ball.radius + 1;
}
};
const GameLoop = {
stopHandle: null,
lastTime: 0,
start() {
this.lastTime = performance.now();
this.stopHandle = window.requestAnimationFrame(this.main.bind(this));
},
stop() {
if (this.stopHandle) cancelAnimationFrame(this.stopHandle);
},
main(currentTime) {
this.stopHandle = window.requestAnimationFrame(this.main.bind(this));
const deltaTime = Math.min((currentTime - this.lastTime) / 1000, 0.05); // Cap at 50ms
this.lastTime = currentTime;
Physics.update(deltaTime);
Renderer.clear();
// Debug: show ball speed (keep for Phase 1 verification)
Renderer.ctx.fillStyle = 'rgba(255,255,255,0.4)';
Renderer.ctx.font = '14px monospace';
Renderer.ctx.fillText('speed: ' + Math.round(GameState.ball.speed), 10, 20);
// Draw paddle1
const p1 = GameState.paddle1;
Renderer.drawRect(p1.x, p1.y, p1.width, p1.height, p1.color);
// Draw ball
const b = GameState.ball;
Renderer.drawCircle(b.x, b.y, b.radius, b.color);
}
};
// Initialize all modules
Renderer.init();
Physics.init(Renderer.logicalWidth, Renderer.logicalHeight);
Input.init();
GameLoop.start();
</script>
</body>
</html>