feat(02-02): implement state machine, scoring, match-end, mode/difficulty selection
- Added mode/difficulty/restart key handlers to Input._handleKeyDown - Rewrote GameLoop.main() with full state machine (modeSelect, diffSelect, playing, scored, gameover) - Added score detection in GameLoop (ball exits left/right edge) - Added 1s pause after score then auto-serve - Added match-end check (first to 7 wins) with winner overlay - Added center divider line and real-time score display - Renders both paddles during gameplay - Removed Physics out-of-bounds auto-serve (GameLoop now owns scoring) - Removed Phase 1 debug speed display - Added pauseTime field to GameLoop
This commit is contained in:
172
index.html
172
index.html
@@ -131,6 +131,50 @@
|
|||||||
if (e.code === 'KeyS') { this.keys.s = true; e.preventDefault(); }
|
if (e.code === 'KeyS') { this.keys.s = true; e.preventDefault(); }
|
||||||
if (e.code === 'ArrowUp') { this.keys.arrowUp = true; e.preventDefault(); }
|
if (e.code === 'ArrowUp') { this.keys.arrowUp = true; e.preventDefault(); }
|
||||||
if (e.code === 'ArrowDown') { this.keys.arrowDown = true; e.preventDefault(); }
|
if (e.code === 'ArrowDown') { this.keys.arrowDown = true; e.preventDefault(); }
|
||||||
|
|
||||||
|
// Mode selection (only when in modeSelect state)
|
||||||
|
if (e.code === 'Digit1' && GameState.gameState === 'modeSelect') {
|
||||||
|
GameState.mode = 'ai';
|
||||||
|
GameState.gameState = 'diffSelect';
|
||||||
|
}
|
||||||
|
if (e.code === 'Digit2' && GameState.gameState === 'modeSelect') {
|
||||||
|
GameState.mode = '2p';
|
||||||
|
// Skip difficulty selection for 2-player — go straight to playing
|
||||||
|
GameState.gameState = 'playing';
|
||||||
|
Physics.serveBall();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Difficulty selection (only when in diffSelect state)
|
||||||
|
if (e.code === 'Digit1' && GameState.gameState === 'diffSelect') {
|
||||||
|
GameState.difficulty = 'easy';
|
||||||
|
AI.init();
|
||||||
|
GameState.gameState = 'playing';
|
||||||
|
Physics.serveBall();
|
||||||
|
}
|
||||||
|
if (e.code === 'Digit2' && GameState.gameState === 'diffSelect') {
|
||||||
|
GameState.difficulty = 'medium';
|
||||||
|
AI.init();
|
||||||
|
GameState.gameState = 'playing';
|
||||||
|
Physics.serveBall();
|
||||||
|
}
|
||||||
|
if (e.code === 'Digit3' && GameState.gameState === 'diffSelect') {
|
||||||
|
GameState.difficulty = 'hard';
|
||||||
|
AI.init();
|
||||||
|
GameState.gameState = 'playing';
|
||||||
|
Physics.serveBall();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restart (only when gameover)
|
||||||
|
if (e.code === 'KeyR' && GameState.gameState === 'gameover') {
|
||||||
|
GameState.score1 = 0;
|
||||||
|
GameState.score2 = 0;
|
||||||
|
GameState.winner = null;
|
||||||
|
GameState.mode = null;
|
||||||
|
GameState.difficulty = 'medium';
|
||||||
|
GameState.gameState = 'modeSelect';
|
||||||
|
AI.init();
|
||||||
|
Physics.init(Renderer.logicalWidth, Renderer.logicalHeight);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
this._handleKeyUp = (e) => {
|
this._handleKeyUp = (e) => {
|
||||||
if (e.code === 'KeyW') this.keys.w = false;
|
if (e.code === 'KeyW') this.keys.w = false;
|
||||||
@@ -221,12 +265,6 @@
|
|||||||
ball.vy = -Math.abs(ball.vy);
|
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) ---
|
// --- Paddle collision (only check when ball moving left toward paddle1) ---
|
||||||
if (ball.vx < 0) {
|
if (ball.vx < 0) {
|
||||||
this._checkPaddleCollision(ball, paddle);
|
this._checkPaddleCollision(ball, paddle);
|
||||||
@@ -375,6 +413,7 @@
|
|||||||
const GameLoop = {
|
const GameLoop = {
|
||||||
stopHandle: null,
|
stopHandle: null,
|
||||||
lastTime: 0,
|
lastTime: 0,
|
||||||
|
pauseTime: 0,
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
this.lastTime = performance.now();
|
this.lastTime = performance.now();
|
||||||
@@ -387,24 +426,123 @@
|
|||||||
|
|
||||||
main(currentTime) {
|
main(currentTime) {
|
||||||
this.stopHandle = window.requestAnimationFrame(this.main.bind(this));
|
this.stopHandle = window.requestAnimationFrame(this.main.bind(this));
|
||||||
const deltaTime = Math.min((currentTime - this.lastTime) / 1000, 0.05); // Cap at 50ms
|
const deltaTime = Math.min((currentTime - this.lastTime) / 1000, 0.05);
|
||||||
this.lastTime = currentTime;
|
this.lastTime = currentTime;
|
||||||
|
|
||||||
Physics.update(deltaTime);
|
const gs = GameState;
|
||||||
|
const ball = gs.ball;
|
||||||
|
|
||||||
|
// --- Physics update (only during active play) ---
|
||||||
|
if (gs.gameState === 'playing') {
|
||||||
|
Physics.update(deltaTime);
|
||||||
|
|
||||||
|
// Score detection — AFTER Physics.update(), NOT inside Physics
|
||||||
|
if (ball.x + ball.radius < 0) {
|
||||||
|
// Ball exited left edge → Player 2 (or AI) scores
|
||||||
|
gs.score2++;
|
||||||
|
gs.gameState = 'scored';
|
||||||
|
this.pauseTime = currentTime;
|
||||||
|
} else if (ball.x - ball.radius > Physics.width) {
|
||||||
|
// Ball exited right edge → Player 1 scores
|
||||||
|
gs.score1++;
|
||||||
|
gs.gameState = 'scored';
|
||||||
|
this.pauseTime = currentTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match-end check immediately after scoring
|
||||||
|
if (gs.score1 >= GameConfig.WIN_SCORE) {
|
||||||
|
gs.gameState = 'gameover';
|
||||||
|
gs.winner = 'player1';
|
||||||
|
} else if (gs.score2 >= GameConfig.WIN_SCORE) {
|
||||||
|
gs.gameState = 'gameover';
|
||||||
|
gs.winner = gs.mode === 'ai' ? 'ai' : 'player2';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Auto-serve after ~1s pause ---
|
||||||
|
if (gs.gameState === 'scored') {
|
||||||
|
const elapsed = (currentTime - this.pauseTime) / 1000;
|
||||||
|
if (elapsed >= 1.0) {
|
||||||
|
Physics.serveBall();
|
||||||
|
gs.gameState = 'playing';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Render ---
|
||||||
Renderer.clear();
|
Renderer.clear();
|
||||||
|
|
||||||
// Debug: show ball speed (keep for Phase 1 verification)
|
if (gs.gameState === 'modeSelect') {
|
||||||
Renderer.ctx.fillStyle = 'rgba(255,255,255,0.4)';
|
// Mode selection prompt
|
||||||
Renderer.ctx.font = '14px monospace';
|
Renderer.ctx.fillStyle = '#fff';
|
||||||
Renderer.ctx.fillText('speed: ' + Math.round(GameState.ball.speed), 10, 20);
|
Renderer.ctx.font = 'bold 28px monospace';
|
||||||
|
Renderer.ctx.textAlign = 'center';
|
||||||
|
Renderer.ctx.fillText('SUPER PONG NEXT GEN', Renderer.logicalWidth / 2, Renderer.logicalHeight / 2 - 60);
|
||||||
|
Renderer.ctx.font = '20px monospace';
|
||||||
|
Renderer.ctx.fillText('Press 1 — Solo vs AI', Renderer.logicalWidth / 2, Renderer.logicalHeight / 2);
|
||||||
|
Renderer.ctx.fillText('Press 2 — 2 Player Local', Renderer.logicalWidth / 2, Renderer.logicalHeight / 2 + 36);
|
||||||
|
Renderer.ctx.textAlign = 'left';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Draw paddle1
|
if (gs.gameState === 'diffSelect') {
|
||||||
const p1 = GameState.paddle1;
|
Renderer.ctx.fillStyle = '#fff';
|
||||||
|
Renderer.ctx.font = 'bold 24px monospace';
|
||||||
|
Renderer.ctx.textAlign = 'center';
|
||||||
|
Renderer.ctx.fillText('SELECT DIFFICULTY', Renderer.logicalWidth / 2, Renderer.logicalHeight / 2 - 50);
|
||||||
|
Renderer.ctx.font = '20px monospace';
|
||||||
|
Renderer.ctx.fillText('1 — Easy', Renderer.logicalWidth / 2, Renderer.logicalHeight / 2);
|
||||||
|
Renderer.ctx.fillText('2 — Medium', Renderer.logicalWidth / 2, Renderer.logicalHeight / 2 + 36);
|
||||||
|
Renderer.ctx.fillText('3 — Hard', Renderer.logicalWidth / 2, Renderer.logicalHeight / 2 + 72);
|
||||||
|
Renderer.ctx.textAlign = 'left';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Gameplay rendering (playing, scored, gameover) ---
|
||||||
|
|
||||||
|
// Paddles
|
||||||
|
const p1 = gs.paddle1;
|
||||||
|
const p2 = gs.paddle2;
|
||||||
Renderer.drawRect(p1.x, p1.y, p1.width, p1.height, p1.color);
|
Renderer.drawRect(p1.x, p1.y, p1.width, p1.height, p1.color);
|
||||||
// Draw ball
|
Renderer.drawRect(p2.x, p2.y, p2.width, p2.height, p2.color);
|
||||||
const b = GameState.ball;
|
|
||||||
Renderer.drawCircle(b.x, b.y, b.radius, b.color);
|
// Ball (stationary during scored pause)
|
||||||
|
if (gs.gameState !== 'scored') {
|
||||||
|
const b = gs.ball;
|
||||||
|
Renderer.drawCircle(b.x, b.y, b.radius, b.color);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Center divider line (dashed)
|
||||||
|
Renderer.ctx.setLineDash([10, 14]);
|
||||||
|
Renderer.ctx.strokeStyle = 'rgba(255,255,255,0.2)';
|
||||||
|
Renderer.ctx.lineWidth = 2;
|
||||||
|
Renderer.ctx.beginPath();
|
||||||
|
Renderer.ctx.moveTo(Renderer.logicalWidth / 2, 0);
|
||||||
|
Renderer.ctx.lineTo(Renderer.logicalWidth / 2, Renderer.logicalHeight);
|
||||||
|
Renderer.ctx.stroke();
|
||||||
|
Renderer.ctx.setLineDash([]);
|
||||||
|
|
||||||
|
// Scores
|
||||||
|
Renderer.ctx.fillStyle = 'rgba(255,255,255,0.8)';
|
||||||
|
Renderer.ctx.font = 'bold 48px monospace';
|
||||||
|
Renderer.ctx.textAlign = 'center';
|
||||||
|
Renderer.ctx.fillText(gs.score1, Renderer.logicalWidth / 4, 64);
|
||||||
|
Renderer.ctx.fillText(gs.score2, Renderer.logicalWidth * 3 / 4, 64);
|
||||||
|
Renderer.ctx.textAlign = 'left';
|
||||||
|
|
||||||
|
// Game over overlay
|
||||||
|
if (gs.gameState === 'gameover') {
|
||||||
|
const winnerName = gs.winner === 'player1' ? 'Player 1' :
|
||||||
|
gs.winner === 'ai' ? 'AI' : 'Player 2';
|
||||||
|
Renderer.ctx.fillStyle = 'rgba(0,0,0,0.5)';
|
||||||
|
Renderer.ctx.fillRect(0, 0, Renderer.logicalWidth, Renderer.logicalHeight);
|
||||||
|
Renderer.ctx.fillStyle = '#fff';
|
||||||
|
Renderer.ctx.font = 'bold 36px monospace';
|
||||||
|
Renderer.ctx.textAlign = 'center';
|
||||||
|
Renderer.ctx.fillText(winnerName + ' Wins!', Renderer.logicalWidth / 2, Renderer.logicalHeight / 2 - 20);
|
||||||
|
Renderer.ctx.font = '20px monospace';
|
||||||
|
Renderer.ctx.fillText('Press R to play again', Renderer.logicalWidth / 2, Renderer.logicalHeight / 2 + 24);
|
||||||
|
Renderer.ctx.textAlign = 'left';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user