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 = 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();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user