feat(02-01): add AI module and extend Physics for paddle2 movement and collision
- Add AI object with init(), update(), _predictBallY() methods - AI uses predictive ray-cast with wall bounce simulation to intercept ball - AI respects difficulty config (speed, reactionDelay, errorMargin) from GameConfig - Add Physics._checkPaddle2Collision() with 5-zone deflection, ball goes LEFT - Extend Physics.update() with paddle2 movement branch (2p/ai modes) - Add AI.init() to initialization block
This commit is contained in:
109
index.html
109
index.html
@@ -197,6 +197,16 @@
|
|||||||
paddle.y += dir * paddle.speed * deltaTime;
|
paddle.y += dir * paddle.speed * deltaTime;
|
||||||
paddle.y = Math.max(0, Math.min(this.height - paddle.height, paddle.y));
|
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 ---
|
// --- Move ball ---
|
||||||
ball.x += ball.vx * deltaTime;
|
ball.x += ball.vx * deltaTime;
|
||||||
ball.y += ball.vy * deltaTime;
|
ball.y += ball.vy * deltaTime;
|
||||||
@@ -221,6 +231,11 @@
|
|||||||
if (ball.vx < 0) {
|
if (ball.vx < 0) {
|
||||||
this._checkPaddleCollision(ball, paddle);
|
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) {
|
_checkPaddleCollision(ball, paddle) {
|
||||||
@@ -261,6 +276,99 @@
|
|||||||
|
|
||||||
// Push ball out of paddle to prevent double-collision next frame
|
// Push ball out of paddle to prevent double-collision next frame
|
||||||
ball.x = paddle.x + paddle.width + ball.radius + 1;
|
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();
|
Renderer.init();
|
||||||
Physics.init(Renderer.logicalWidth, Renderer.logicalHeight);
|
Physics.init(Renderer.logicalWidth, Renderer.logicalHeight);
|
||||||
Input.init();
|
Input.init();
|
||||||
|
AI.init();
|
||||||
GameLoop.start();
|
GameLoop.start();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user