diff --git a/index.html b/index.html
index 815cea5..905eefb 100644
--- a/index.html
+++ b/index.html
@@ -197,6 +197,16 @@
paddle.y += dir * paddle.speed * deltaTime;
paddle.y = Math.max(0, Math.min(this.height - paddle.height, paddle.y));
+ // --- Move paddle2 (human or AI) ---
+ const paddle2 = GameState.paddle2;
+ if (GameState.mode === '2p') {
+ const dir2 = Input.getVerticalInput2();
+ paddle2.y += dir2 * paddle2.speed * deltaTime;
+ paddle2.y = Math.max(0, Math.min(this.height - paddle2.height, paddle2.y));
+ } else if (GameState.mode === 'ai') {
+ AI.update(deltaTime);
+ }
+
// --- Move ball ---
ball.x += ball.vx * deltaTime;
ball.y += ball.vy * deltaTime;
@@ -221,6 +231,11 @@
if (ball.vx < 0) {
this._checkPaddleCollision(ball, paddle);
}
+
+ // --- Paddle2 collision (only when ball moving right toward paddle2) ---
+ if (ball.vx > 0) {
+ this._checkPaddle2Collision(ball, GameState.paddle2);
+ }
},
_checkPaddleCollision(ball, paddle) {
@@ -261,6 +276,99 @@
// Push ball out of paddle to prevent double-collision next frame
ball.x = paddle.x + paddle.width + ball.radius + 1;
+ },
+
+ _checkPaddle2Collision(ball, paddle) {
+ const inX = ball.x + ball.radius > paddle.x &&
+ ball.x - ball.radius < paddle.x + paddle.width;
+ const inY = ball.y + ball.radius > paddle.y &&
+ ball.y - ball.radius < paddle.y + paddle.height;
+
+ if (!inX || !inY) return;
+
+ const relativeHitPos = (ball.y - paddle.y) / paddle.height;
+ const hitZone = Math.max(0, Math.min(4, Math.floor(relativeHitPos * 5)));
+ const anglesDeg = [-60, -30, 5, 30, 60];
+ const angleRad = anglesDeg[hitZone] * Math.PI / 180;
+
+ ball.speed += GameConfig.speedIncrement;
+
+ // Ball leaves paddle2 going LEFT (negative vx)
+ ball.vx = -Math.cos(angleRad) * ball.speed;
+ ball.vy = Math.sin(angleRad) * ball.speed;
+
+ // Push ball left of paddle2 to prevent double-collision
+ ball.x = paddle.x - ball.radius - 1;
+ }
+ };
+
+ const AI = {
+ reactionElapsed: 0,
+
+ init() {
+ this.reactionElapsed = 0;
+ },
+
+ update(deltaTime) {
+ const difficulty = GameState.difficulty;
+ const config = GameConfig['AI_' + difficulty.toUpperCase()];
+ const ball = GameState.ball;
+ const paddle2 = GameState.paddle2;
+
+ // Reset reaction timer when ball is moving away (toward player side)
+ if (ball.vx < 0) {
+ this.reactionElapsed = 0;
+ return; // Ball heading toward player — AI waits
+ }
+
+ this.reactionElapsed += deltaTime;
+ if (this.reactionElapsed < config.reactionDelay) {
+ return; // Still in reaction delay window
+ }
+
+ // Predict ball y-position when it reaches paddle2 x
+ const targetY = this._predictBallY(ball, paddle2.x);
+
+ // Add difficulty-based error margin (random jitter each frame)
+ const error = (Math.random() - 0.5) * config.errorMargin * 2;
+ const aimY = targetY + error;
+
+ // Move paddle toward aim Y (clamped to field)
+ const centerY = paddle2.y + paddle2.height / 2;
+ const diff = aimY - centerY;
+
+ if (Math.abs(diff) > 3) { // Deadzone prevents jitter
+ const dir = Math.sign(diff);
+ paddle2.y += dir * config.speed * deltaTime;
+ paddle2.y = Math.max(0, Math.min(Physics.height - paddle2.height, paddle2.y));
+ }
+ },
+
+ _predictBallY(ball, targetX) {
+ let x = ball.x, y = ball.y, vx = ball.vx, vy = ball.vy;
+ const maxSteps = 500;
+ let steps = 0;
+
+ while (steps < maxSteps) {
+ if (Math.abs(x - targetX) < 1) break;
+ const timeToTarget = (targetX - x) / vx;
+ const timeStep = Math.min(0.016, Math.abs(timeToTarget));
+
+ x += vx * timeStep;
+ y += vy * timeStep;
+
+ // Wall bounces during prediction
+ if (y - ball.radius < 0) {
+ y = ball.radius;
+ vy = Math.abs(vy);
+ } else if (y + ball.radius > Physics.height) {
+ y = Physics.height - ball.radius;
+ vy = -Math.abs(vy);
+ }
+ steps++;
+ }
+
+ return y;
}
};
@@ -304,6 +412,7 @@
Renderer.init();
Physics.init(Renderer.logicalWidth, Renderer.logicalHeight);
Input.init();
+ AI.init();
GameLoop.start();