files
Juegos/ladrillos/script.js

363 lines
10 KiB
JavaScript

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