files
Juegos/pong/script.js

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();