Mejoras y optimizaciones en general.
This commit is contained in:
@@ -1,77 +1,196 @@
|
||||
'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');
|
||||
|
||||
const ballRadius = 8;
|
||||
let x, y, dx, dy;
|
||||
const paddleHeight = 12, paddleWidth = 75;
|
||||
let paddleX;
|
||||
const brickRowCount = 5, brickColumnCount = 7;
|
||||
// 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 = [];
|
||||
let score, gameOver;
|
||||
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++) {
|
||||
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 };
|
||||
for (let r = 0; r < brickRowCount; r++) {
|
||||
bricks[c][r] = { x: 0, y: 0, status: 1 };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reinicio y arranque de partida
|
||||
function startGame() {
|
||||
x = canvas.width/2;
|
||||
y = canvas.height-30;
|
||||
dx = 3;
|
||||
dy = -3;
|
||||
paddleX = (canvas.width-paddleWidth)/2;
|
||||
// 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 = score;
|
||||
scoreSpan.textContent = String(score);
|
||||
bestScoreSpan.textContent = String(getBest(getCurrentDifficulty()));
|
||||
gameOver = false;
|
||||
gameOverDiv.textContent = '';
|
||||
|
||||
// Preparar ladrillos (precalcular posiciones para rendimiento)
|
||||
setupBricks();
|
||||
document.addEventListener("mousemove", mouseMoveHandler);
|
||||
draw();
|
||||
precomputeBrickPositions();
|
||||
|
||||
// Listeners
|
||||
attachListeners();
|
||||
|
||||
// Animación
|
||||
if (animId) cancelAnimationFrame(animId);
|
||||
animId = requestAnimationFrame(draw);
|
||||
}
|
||||
|
||||
function mouseMoveHandler(e) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
let relativeX = e.clientX - rect.left;
|
||||
if(relativeX > 0 && relativeX < canvas.width) {
|
||||
paddleX = relativeX - paddleWidth/2;
|
||||
if(paddleX < 0) paddleX = 0;
|
||||
if(paddleX + paddleWidth > canvas.width) paddleX = canvas.width - paddleWidth;
|
||||
}
|
||||
}
|
||||
|
||||
function collisionDetection() {
|
||||
for(let c=0; c<brickColumnCount; c++) {
|
||||
for(let r=0; r<brickRowCount; r++) {
|
||||
const b = bricks[c][r];
|
||||
if(b.status == 1) {
|
||||
if(x > b.x && x < b.x+brickWidth && y > b.y && y < b.y+brickHeight) {
|
||||
dy = -dy;
|
||||
b.status = 0;
|
||||
score++;
|
||||
scoreSpan.textContent = score;
|
||||
if(score == brickRowCount*brickColumnCount) {
|
||||
gameOver = true;
|
||||
gameOverDiv.textContent = "¡Ganaste! 🏆";
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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.arc(x, y, ballRadius, 0, Math.PI * 2);
|
||||
ctx.fillStyle = "#f9d923";
|
||||
ctx.fill();
|
||||
ctx.closePath();
|
||||
@@ -79,22 +198,19 @@ function drawBall() {
|
||||
|
||||
function drawPaddle() {
|
||||
ctx.beginPath();
|
||||
ctx.rect(paddleX, canvas.height-paddleHeight-5, paddleWidth, paddleHeight);
|
||||
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 brickX = (c*(brickWidth+brickPadding))+brickOffsetLeft;
|
||||
const brickY = (r*(brickHeight+brickPadding))+brickOffsetTop;
|
||||
bricks[c][r].x = brickX;
|
||||
bricks[c][r].y = brickY;
|
||||
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(brickX, brickY, brickWidth, brickHeight);
|
||||
ctx.rect(bx, by, brickWidth, brickHeight);
|
||||
ctx.fillStyle = "#393e46";
|
||||
ctx.strokeStyle = "#f9d923";
|
||||
ctx.fill();
|
||||
@@ -105,43 +221,143 @@ function drawBricks() {
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
collisionDetection();
|
||||
|
||||
// Rebote en las paredes
|
||||
if(x + dx > canvas.width-ballRadius || x + dx < ballRadius) {
|
||||
dx = -dx;
|
||||
// Actualizar pala por teclado
|
||||
updatePaddleByKeyboard();
|
||||
|
||||
// Colisiones
|
||||
const hitBrick = collisionWithBricks();
|
||||
if (hitBrick) {
|
||||
// Si hubo victoria, el bucle se detuvo
|
||||
return;
|
||||
}
|
||||
if(y + dy < ballRadius) {
|
||||
dy = -dy;
|
||||
} else if(y + dy > canvas.height-ballRadius-paddleHeight-5) {
|
||||
// Rebote en la paleta
|
||||
if(x > paddleX && x < paddleX + paddleWidth) {
|
||||
dy = -dy;
|
||||
} else if (y + dy > canvas.height-ballRadius) {
|
||||
// GAME OVER
|
||||
gameOver = true;
|
||||
gameOverDiv.textContent = "¡Game Over! 😢";
|
||||
return;
|
||||
}
|
||||
const endOrWall = collisionWallsAndPaddle();
|
||||
if (endOrWall) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Avance de la bola
|
||||
x += dx;
|
||||
y += dy;
|
||||
|
||||
if(!gameOver) {
|
||||
requestAnimationFrame(draw);
|
||||
if (!gameOver) {
|
||||
animId = requestAnimationFrame(draw);
|
||||
}
|
||||
}
|
||||
|
||||
restartBtn.onclick = function() {
|
||||
document.removeEventListener("mousemove", mouseMoveHandler);
|
||||
startGame();
|
||||
};
|
||||
|
||||
// Inicio
|
||||
startGame();
|
Reference in New Issue
Block a user