'use strict'; const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); const scoreSpan = document.getElementById('score-value'); const bestScoreSpan = document.getElementById('best-score'); const restartBtn = document.getElementById('restart-btn'); const gameOverDiv = document.getElementById('game-over-message'); const difficultySelect = document.getElementById('difficulty'); // Parámetros por dificultad const DIFFICULTIES = { easy: { ballSpeed: 2.6, paddleWidth: 95, brickRows: 4, brickCols: 6 }, normal: { ballSpeed: 3.2, paddleWidth: 80, brickRows: 5, brickCols: 7 }, hard: { ballSpeed: 4.0, paddleWidth: 70, brickRows: 6, brickCols: 8 } }; // Estado del juego let ballRadius = 8; let x = 0, y = 0, dx = 0, dy = 0; let paddleHeight = 12, paddleWidth = DIFFICULTIES.normal.paddleWidth; let paddleX = 0; let brickRowCount = DIFFICULTIES.normal.brickRows; let brickColumnCount = DIFFICULTIES.normal.brickCols; const brickWidth = 60, brickHeight = 20, brickPadding = 10, brickOffsetTop = 30, brickOffsetLeft = 30; let bricks = []; // matriz [col][row] con {x,y,status} let score = 0, gameOver = false; let animId = null; // Controles/Listeners let mouseMoveHandlerBound = null; let keyDownHandlerBound = null; let keyUpHandlerBound = null; let keysPressed = { left: false, right: false }; // Utilidades de almacenamiento function bestKey(diff) { return `breakout_best_${diff}`; } function getBest(diff) { const v = localStorage.getItem(bestKey(diff)); return v ? parseInt(v, 10) : 0; } function setBest(diff, val) { const prev = getBest(diff); if (val > prev) { localStorage.setItem(bestKey(diff), String(val)); } bestScoreSpan.textContent = String(getBest(diff)); } // Lectura dificultad actual function getCurrentDifficulty() { const v = difficultySelect && difficultySelect.value ? difficultySelect.value : 'normal'; return DIFFICULTIES[v] ? v : 'normal'; } function getCurrentConfig() { return DIFFICULTIES[getCurrentDifficulty()]; } // Inicializa ladrillos según dificultad function setupBricks() { bricks = []; for (let c = 0; c < brickColumnCount; c++) { bricks[c] = []; for (let r = 0; r < brickRowCount; r++) { bricks[c][r] = { x: 0, y: 0, status: 1 }; } } } // Reinicio y arranque de partida function startGame() { // Configuración por dificultad const conf = getCurrentConfig(); paddleWidth = conf.paddleWidth; brickRowCount = conf.brickRows; brickColumnCount = conf.brickCols; // Estado base x = canvas.width / 2; y = canvas.height - 60; const speed = conf.ballSpeed; dx = speed; // velocidad horizontal inicial dy = -speed; // velocidad vertical inicial paddleX = (canvas.width - paddleWidth) / 2; score = 0; scoreSpan.textContent = String(score); bestScoreSpan.textContent = String(getBest(getCurrentDifficulty())); gameOver = false; gameOverDiv.textContent = ''; // Preparar ladrillos (precalcular posiciones para rendimiento) setupBricks(); precomputeBrickPositions(); // Listeners attachListeners(); // Animación if (animId) cancelAnimationFrame(animId); animId = requestAnimationFrame(draw); } // Precalcular posiciones de ladrillos una vez function precomputeBrickPositions() { for (let c = 0; c < brickColumnCount; c++) { for (let r = 0; r < brickRowCount; r++) { const brickX = (c * (brickWidth + brickPadding)) + brickOffsetLeft; const brickY = (r * (brickHeight + brickPadding)) + brickOffsetTop; bricks[c][r].x = brickX; bricks[c][r].y = brickY; } } } // Listeners seguros (evitar duplicados) function attachListeners() { // Ratón if (mouseMoveHandlerBound) { document.removeEventListener('mousemove', mouseMoveHandlerBound); } mouseMoveHandlerBound = function (e) { const rect = canvas.getBoundingClientRect(); let relativeX = e.clientX - rect.left; if (relativeX > 0 && relativeX < canvas.width) { paddleX = relativeX - paddleWidth / 2; clampPaddle(); } }; document.addEventListener('mousemove', mouseMoveHandlerBound, { passive: true }); // Teclado if (keyDownHandlerBound) window.removeEventListener('keydown', keyDownHandlerBound); if (keyUpHandlerBound) window.removeEventListener('keyup', keyUpHandlerBound); keyDownHandlerBound = function (e) { if (e.key === 'ArrowLeft' || e.key === 'Left') keysPressed.left = true; if (e.key === 'ArrowRight' || e.key === 'Right') keysPressed.right = true; }; keyUpHandlerBound = function (e) { if (e.key === 'ArrowLeft' || e.key === 'Left') keysPressed.left = false; if (e.key === 'ArrowRight' || e.key === 'Right') keysPressed.right = false; }; window.addEventListener('keydown', keyDownHandlerBound); window.addEventListener('keyup', keyUpHandlerBound); // Botón reinicio restartBtn.removeEventListener('click', onRestart); restartBtn.addEventListener('click', onRestart); // Cambio de dificultad reinicia partida if (difficultySelect) { difficultySelect.removeEventListener('change', onDifficultyChange); difficultySelect.addEventListener('change', onDifficultyChange); } } function detachListeners() { if (mouseMoveHandlerBound) { document.removeEventListener('mousemove', mouseMoveHandlerBound); mouseMoveHandlerBound = null; } if (keyDownHandlerBound) { window.removeEventListener('keydown', keyDownHandlerBound); keyDownHandlerBound = null; } if (keyUpHandlerBound) { window.removeEventListener('keyup', keyUpHandlerBound); keyUpHandlerBound = null; } restartBtn.removeEventListener('click', onRestart); if (difficultySelect) difficultySelect.removeEventListener('change', onDifficultyChange); } function onRestart() { detachListeners(); startGame(); } function onDifficultyChange() { onRestart(); } function clampPaddle() { if (paddleX < 0) paddleX = 0; if (paddleX + paddleWidth > canvas.width) paddleX = canvas.width - paddleWidth; } // Dibujo de elementos function drawBall() { ctx.beginPath(); ctx.arc(x, y, ballRadius, 0, Math.PI * 2); ctx.fillStyle = "#f9d923"; ctx.fill(); ctx.closePath(); } function drawPaddle() { ctx.beginPath(); ctx.rect(paddleX, canvas.height - paddleHeight - 5, paddleWidth, paddleHeight); ctx.fillStyle = "#e94560"; ctx.fill(); ctx.closePath(); } function drawBricks() { for (let c = 0; c < brickColumnCount; c++) { for (let r = 0; r < brickRowCount; r++) { if (bricks[c][r].status === 1) { const { x: bx, y: by } = bricks[c][r]; ctx.beginPath(); ctx.rect(bx, by, brickWidth, brickHeight); ctx.fillStyle = "#393e46"; ctx.strokeStyle = "#f9d923"; ctx.fill(); ctx.stroke(); ctx.closePath(); } } } } // Control de pala por teclado (suave) function updatePaddleByKeyboard() { const conf = getCurrentConfig(); const step = Math.max(4, conf.ballSpeed * 2.2); if (keysPressed.left) { paddleX -= step; } else if (keysPressed.right) { paddleX += step; } clampPaddle(); } // Colisión círculo-rectángulo (ball vs rect) function circleRectCollision(cx, cy, radius, rx, ry, rw, rh) { // Punto más cercano del rect al círculo const nearestX = Math.max(rx, Math.min(cx, rx + rw)); const nearestY = Math.max(ry, Math.min(cy, ry + rh)); const dx = cx - nearestX; const dy = cy - nearestY; return (dx * dx + dy * dy) <= (radius * radius); } function collisionWithBricks() { for (let c = 0; c < brickColumnCount; c++) { for (let r = 0; r < brickRowCount; r++) { const b = bricks[c][r]; if (b.status !== 1) continue; if (circleRectCollision(x, y, ballRadius, b.x, b.y, brickWidth, brickHeight)) { // Determinar lado de impacto para invertir adecuadamente // Calculamos centro del brick const bxCenter = b.x + brickWidth / 2; const byCenter = b.y + brickHeight / 2; const dxCenter = x - bxCenter; const dyCenter = y - byCenter; if (Math.abs(dxCenter) > Math.abs(dyCenter)) { dx = -dx; // impacto predominantemente lateral } else { dy = -dy; // impacto predominantemente superior/inferior } b.status = 0; score++; scoreSpan.textContent = String(score); // Victoria si todos eliminados if (score === brickRowCount * brickColumnCount) { onWin(); return true; } } } } return false; } function collisionWallsAndPaddle() { // Paredes laterales if (x + dx > canvas.width - ballRadius || x + dx < ballRadius) { dx = -dx; } // Techo if (y + dy < ballRadius) { dy = -dy; } else if (y + dy > canvas.height - ballRadius - paddleHeight - 5) { // Posible contacto con la pala if (x > paddleX && x < paddleX + paddleWidth) { // Rebote con ángulo según sitio de impacto en la pala const conf = getCurrentConfig(); const hitPos = (x - (paddleX + paddleWidth / 2)) / (paddleWidth / 2); // -1..1 const maxAngle = Math.PI / 3; // 60° const angle = hitPos * maxAngle; const speed = conf.ballSpeed; dx = speed * Math.sin(angle); dy = -Math.abs(speed * Math.cos(angle)); // hacia arriba // Evitar quedarse pegado a la pala y = canvas.height - ballRadius - paddleHeight - 6; } else if (y + dy > canvas.height - ballRadius) { // Fuera por abajo: Game Over onGameOver(); return true; } } return false; } function onWin() { gameOver = true; gameOverDiv.textContent = "¡Ganaste! 🏆"; setBest(getCurrentDifficulty(), score); stopLoop(); } function onGameOver() { gameOver = true; gameOverDiv.textContent = "¡Game Over! 😢"; stopLoop(); } function stopLoop() { if (animId) cancelAnimationFrame(animId); animId = null; detachListeners(); } // Bucle principal function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); drawBricks(); drawBall(); drawPaddle(); // Actualizar pala por teclado updatePaddleByKeyboard(); // Colisiones const hitBrick = collisionWithBricks(); if (hitBrick) { // Si hubo victoria, el bucle se detuvo return; } const endOrWall = collisionWallsAndPaddle(); if (endOrWall) { return; } // Avance de la bola x += dx; y += dy; if (!gameOver) { animId = requestAnimationFrame(draw); } } // Inicio startGame();