332 lines
7.6 KiB
JavaScript
332 lines
7.6 KiB
JavaScript
'use strict';
|
|
|
|
const canvas = document.getElementById('pong');
|
|
const ctx = canvas.getContext('2d');
|
|
const scoreDiv = document.getElementById('score');
|
|
const restartBtn = document.getElementById('restart-btn');
|
|
const difficultySelect = document.getElementById('difficulty');
|
|
const pauseBtn = document.getElementById('pause-btn');
|
|
|
|
const w = canvas.width, h = canvas.height;
|
|
|
|
// Configuraciones por dificultad
|
|
const DIFFICULTIES = {
|
|
facil: { ballSpeed: 5.0, aiMaxSpeed: 4.0, paddleHeight: 95 },
|
|
normal: { ballSpeed: 6.5, aiMaxSpeed: 5.5, paddleHeight: 80 },
|
|
dificil: { ballSpeed: 8.0, aiMaxSpeed: 7.0, paddleHeight: 70 }
|
|
};
|
|
|
|
// Estado
|
|
let paddleHeight = DIFFICULTIES.normal.paddleHeight;
|
|
const paddleWidth = 14;
|
|
let playerY = h / 2 - paddleHeight / 2;
|
|
let aiY = h / 2 - paddleHeight / 2;
|
|
|
|
let playerScore = 0, aiScore = 0;
|
|
|
|
let ball = {
|
|
x: w / 2,
|
|
y: h / 2,
|
|
r: 12,
|
|
baseSpeed: DIFFICULTIES.normal.ballSpeed,
|
|
velX: DIFFICULTIES.normal.ballSpeed,
|
|
velY: -4.0
|
|
};
|
|
|
|
let up = false, down = false;
|
|
let running = true;
|
|
let rafId = null;
|
|
let aiMaxSpeed = DIFFICULTIES.normal.aiMaxSpeed;
|
|
|
|
// Utilidades
|
|
function clamp(v, min, max) {
|
|
return Math.max(min, Math.min(max, v));
|
|
}
|
|
|
|
function setDifficulty() {
|
|
const diff = difficultySelect && difficultySelect.value ? difficultySelect.value : 'normal';
|
|
const conf = DIFFICULTIES[diff] || DIFFICULTIES.normal;
|
|
|
|
paddleHeight = conf.paddleHeight;
|
|
aiMaxSpeed = conf.aiMaxSpeed;
|
|
ball.baseSpeed = conf.ballSpeed;
|
|
|
|
// Reposicionar palas con nueva altura
|
|
playerY = clamp(playerY, 0, h - paddleHeight);
|
|
aiY = clamp(aiY, 0, h - paddleHeight);
|
|
|
|
// Reiniciar bola con la velocidad base de la dificultad
|
|
resetBall(true);
|
|
updateScoreHUD();
|
|
}
|
|
|
|
function updateScoreHUD() {
|
|
scoreDiv.textContent = `Tú: ${playerScore} | Máquina: ${aiScore}`;
|
|
}
|
|
|
|
function drawNet() {
|
|
ctx.fillStyle = '#f2e9e4';
|
|
for (let i = 15; i < h; i += 30) {
|
|
ctx.fillRect(w / 2 - 2, i, 4, 14);
|
|
}
|
|
}
|
|
|
|
function drawScene() {
|
|
ctx.clearRect(0, 0, w, h);
|
|
|
|
// Palas
|
|
ctx.fillStyle = '#f2e9e4';
|
|
ctx.fillRect(16, playerY, paddleWidth, paddleHeight);
|
|
ctx.fillRect(w - 16 - paddleWidth, aiY, paddleWidth, paddleHeight);
|
|
|
|
// Bola
|
|
ctx.beginPath();
|
|
ctx.arc(ball.x, ball.y, ball.r, 0, Math.PI * 2);
|
|
ctx.fillStyle = '#f6c90e';
|
|
ctx.fill();
|
|
|
|
// Red central
|
|
drawNet();
|
|
|
|
// HUD
|
|
updateScoreHUD();
|
|
}
|
|
|
|
function pauseGame() {
|
|
if (!running) return;
|
|
running = false;
|
|
if (rafId) cancelAnimationFrame(rafId);
|
|
rafId = null;
|
|
if (pauseBtn) pauseBtn.textContent = 'Reanudar';
|
|
}
|
|
|
|
function resumeGame() {
|
|
if (running) return;
|
|
running = true;
|
|
if (pauseBtn) pauseBtn.textContent = 'Pausar';
|
|
rafId = requestAnimationFrame(gameLoop);
|
|
}
|
|
|
|
function togglePause() {
|
|
if (running) pauseGame();
|
|
else resumeGame();
|
|
}
|
|
|
|
function resetBall(hardReset = false) {
|
|
ball.x = w / 2;
|
|
ball.y = h / 2;
|
|
const speed = ball.baseSpeed;
|
|
ball.velX = (Math.random() > 0.5 ? speed : -speed);
|
|
ball.velY = (Math.random() - 0.5) * speed * 1.2;
|
|
|
|
if (!hardReset) {
|
|
pauseGame();
|
|
setTimeout(() => resumeGame(), 700);
|
|
}
|
|
}
|
|
|
|
function handlePlayerMovement() {
|
|
const playerSpeed = 7.0;
|
|
if (up) playerY -= playerSpeed;
|
|
if (down) playerY += playerSpeed;
|
|
playerY = clamp(playerY, 0, h - paddleHeight);
|
|
}
|
|
|
|
function predictBallYAtX(targetX) {
|
|
// Predicción simple: extrapola y con rebotes verticales
|
|
let px = ball.x;
|
|
let py = ball.y;
|
|
let vx = ball.velX;
|
|
let vy = ball.velY;
|
|
const r = ball.r;
|
|
const maxSteps = 1000;
|
|
|
|
for (let i = 0; i < maxSteps; i++) {
|
|
// Avanza
|
|
px += vx;
|
|
py += vy;
|
|
|
|
// Rebote arriba/abajo
|
|
if (py - r < 0) {
|
|
py = r;
|
|
vy = -vy;
|
|
} else if (py + r > h) {
|
|
py = h - r;
|
|
vy = -vy;
|
|
}
|
|
|
|
// Si hemos cruzado el targetX
|
|
if ((vx > 0 && px + r >= targetX) || (vx < 0 && px - r <= targetX)) {
|
|
return py;
|
|
}
|
|
|
|
// Seguridad: si vx se hace cero (no debería), rompe
|
|
if (Math.abs(vx) < 0.0001) break;
|
|
}
|
|
|
|
return py;
|
|
}
|
|
|
|
function handleAIMovement() {
|
|
// IA solo sigue cuando la bola va hacia ella, si no recentra lentamente
|
|
const aiCenter = aiY + paddleHeight / 2;
|
|
let targetY;
|
|
|
|
if (ball.velX > 0) {
|
|
// Objetivo es el centro de la pala cuando la bola llegue al borde derecho
|
|
const targetX = w - 16 - paddleWidth - ball.r;
|
|
targetY = predictBallYAtX(targetX);
|
|
} else {
|
|
// Bola alejándose: recentrar
|
|
targetY = h / 2;
|
|
}
|
|
|
|
const desired = targetY - aiCenter;
|
|
const step = clamp(desired, -aiMaxSpeed, aiMaxSpeed);
|
|
aiY += step;
|
|
aiY = clamp(aiY, 0, h - paddleHeight);
|
|
}
|
|
|
|
function collideBallWithWalls() {
|
|
// Arriba/abajo
|
|
if (ball.y - ball.r < 0) {
|
|
ball.y = ball.r;
|
|
ball.velY *= -1;
|
|
} else if (ball.y + ball.r > h) {
|
|
ball.y = h - ball.r;
|
|
ball.velY *= -1;
|
|
}
|
|
}
|
|
|
|
function collideBallWithPaddle(px, py, pw, ph, reverseXSign) {
|
|
// Chequeo de colisión círculo-rectángulo
|
|
const nearestX = clamp(ball.x, px, px + pw);
|
|
const nearestY = clamp(ball.y, py, py + ph);
|
|
const dx = ball.x - nearestX;
|
|
const dy = ball.y - nearestY;
|
|
const dist2 = dx * dx + dy * dy;
|
|
|
|
if (dist2 <= ball.r * ball.r) {
|
|
// Rebote con ángulo según punto de impacto relativo
|
|
const hitPos = ((nearestY - py) - ph / 2) / (ph / 2); // -1..1 con referencia al centro
|
|
const maxAngle = Math.PI / 3; // 60 grados
|
|
const speed = Math.max(Math.abs(ball.velX), Math.abs(ball.velY), ball.baseSpeed);
|
|
|
|
const angle = hitPos * maxAngle;
|
|
const newVX = speed * Math.cos(angle);
|
|
const newVY = speed * Math.sin(angle);
|
|
|
|
ball.velX = reverseXSign ? -Math.abs(newVX) : Math.abs(newVX);
|
|
ball.velY = newVY;
|
|
|
|
// Pequeño empujón para salir del rect
|
|
ball.x += ball.velX * 0.2;
|
|
ball.y += ball.velY * 0.2;
|
|
}
|
|
}
|
|
|
|
function checkGoals() {
|
|
// Gol IA (bola sale por izquierda)
|
|
if (ball.x + ball.r < 0) {
|
|
aiScore++;
|
|
resetBall();
|
|
return true;
|
|
}
|
|
// Gol jugador (bola sale por derecha)
|
|
if (ball.x - ball.r > w) {
|
|
playerScore++;
|
|
resetBall();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function stepBall() {
|
|
ball.x += ball.velX;
|
|
ball.y += ball.velY;
|
|
}
|
|
|
|
function gameLoop() {
|
|
if (!running) return;
|
|
|
|
handlePlayerMovement();
|
|
handleAIMovement();
|
|
|
|
stepBall();
|
|
collideBallWithWalls();
|
|
|
|
// Colisiones con palas
|
|
// Jugador (izquierda)
|
|
collideBallWithPaddle(16, playerY, paddleWidth, paddleHeight, false);
|
|
// IA (derecha)
|
|
collideBallWithPaddle(w - 16 - paddleWidth, aiY, paddleWidth, paddleHeight, true);
|
|
|
|
if (checkGoals()) {
|
|
drawScene();
|
|
return;
|
|
}
|
|
|
|
drawScene();
|
|
rafId = requestAnimationFrame(gameLoop);
|
|
}
|
|
|
|
function resetGame() {
|
|
playerScore = 0;
|
|
aiScore = 0;
|
|
playerY = h / 2 - paddleHeight / 2;
|
|
aiY = h / 2 - paddleHeight / 2;
|
|
resetBall(true);
|
|
running = true;
|
|
updateScoreHUD();
|
|
if (rafId) cancelAnimationFrame(rafId);
|
|
rafId = requestAnimationFrame(gameLoop);
|
|
}
|
|
|
|
function attachEvents() {
|
|
// Teclado (evita scroll con flechas)
|
|
window.addEventListener('keydown', (e) => {
|
|
if (e.key === 'ArrowUp') {
|
|
e.preventDefault();
|
|
up = true;
|
|
}
|
|
if (e.key === 'ArrowDown') {
|
|
e.preventDefault();
|
|
down = true;
|
|
}
|
|
});
|
|
window.addEventListener('keyup', (e) => {
|
|
if (e.key === 'ArrowUp') {
|
|
e.preventDefault();
|
|
up = false;
|
|
}
|
|
if (e.key === 'ArrowDown') {
|
|
e.preventDefault();
|
|
down = false;
|
|
}
|
|
});
|
|
|
|
// Pausa
|
|
if (pauseBtn) {
|
|
pauseBtn.addEventListener('click', togglePause);
|
|
}
|
|
|
|
// Dificultad
|
|
if (difficultySelect) {
|
|
difficultySelect.addEventListener('change', () => {
|
|
setDifficulty();
|
|
});
|
|
}
|
|
|
|
// Reinicio
|
|
restartBtn.addEventListener('click', resetGame);
|
|
}
|
|
|
|
function startGame() {
|
|
attachEvents();
|
|
setDifficulty();
|
|
drawScene();
|
|
rafId = requestAnimationFrame(gameLoop);
|
|
}
|
|
|
|
// Init
|
|
startGame(); |