- /* =========================
- IDIOMAS
- ========================= */
- const es = {
- hintTouch: "Toca LANZAR para lanzar",
- hintKey: "Pulsa ESPACIO para lanzar",
- win: "¡Has ganado!",
- gameOver: "¡Game Over!",
- winSub: "Te has pasado el nivel 😎",
- loseSub: "¿Otra?",
- };
- const en = {
- hintTouch: "Tap LAUNCH to launch",
- hintKey: "Press SPACE to launch",
- win: "You won!",
- gameOver: "Game Over!",
- winSub: "You cleared the level 😎",
- loseSub: "Again?",
- };
- const T =
- window.APP_LOCALE === "en"
- ? { ...en }
- : { ...es };
- /* =========================
- ELEMENTOS DEL DOM
- ========================= */
- const canvas =
- document.getElementById("gameCanvas");
- const ctx =
- canvas.getContext("2d");
- const scoreValueEl =
- document.getElementById("scoreValue");
- const livesValueEl =
- document.getElementById("livesValue");
- const countdownEl =
- document.getElementById("countdown");
- const overlayEl =
- document.getElementById("overlay");
- const overlayTitleEl =
- document.getElementById("overlayTitle");
- const overlaySubEl =
- document.getElementById("overlaySub");
- const playAgainBtn =
- document.getElementById("playAgainBtn");
- const resetBtn =
- document.getElementById("resetBtn");
- const btnStartBreak =
- document.getElementById("btnStartBreak");
- const btnLaunchMobile =
- document.getElementById("btnLaunchMobile");
- /* =========================
- TAMAÑO DEL CANVAS
- ========================= */
- canvas.width = 720;
- canvas.height = 420;
- /* =========================
- FONDO DE ESTRELLAS
- ========================= */
- const stars = Array.from(
- {
- length: 90,
- },
- () => ({
- x: Math.random() * canvas.width,
- y: Math.random() * canvas.height,
- r: Math.random() * 1.6 + 0.2,
- a: Math.random() * 0.6 + 0.15,
- s: Math.random() * 0.4 + 0.1,
- })
- );
- /* =========================
- ESTADO DEL JUEGO
- ========================= */
- let running = false;
- let waitingLaunch = true;
- let score = 0;
- let lives = 3;
- let animationFrameId = null;
- let countdownInterval = null;
- /* =========================
- JUGADOR
- ========================= */
- const paddle = {
- w: 110,
- h: 14,
- x: canvas.width / 2 - 55,
- y: canvas.height - 26,
- speed: 7,
- dx: 0,
- };
- /* =========================
- PELOTA
- ========================= */
- const ball = {
- r: 8,
- x: canvas.width / 2,
- y: paddle.y - 10,
- vx: 4,
- vy: -4,
- stuck: true,
- };
- /* =========================
- CONFIGURACIÓN LADRILLOS
- ========================= */
- const bricksCfg = {
- cols: 9,
- rows: 5,
- w: 64,
- h: 18,
- gap: 10,
- top: 36,
- left: 30,
- };
- let bricks = [];
- /* =========================
- DETECTAR MODO TÁCTIL
- ========================= */
- function isTouchMode() {
- return (
- window
- .matchMedia(
- "(max-width: 900px)"
- )
- .matches &&
- window
- .matchMedia(
- "(hover: none)"
- )
- .matches &&
- window
- .matchMedia(
- "(pointer: coarse)"
- )
- .matches
- );
- }
- /* =========================
- BOTÓN LANZAR MÓVIL
- ========================= */
- function updateLaunchButton() {
- if (!btnLaunchMobile) {
- return;
- }
- const shouldShow =
- isTouchMode() &&
- running &&
- ball.stuck;
- btnLaunchMobile.style.display =
- shouldShow
- ? "flex"
- : "none";
- }
- /* =========================
- CREAR LADRILLOS
- ========================= */
- function makeBricks() {
- bricks = [];
- for (
- let row = 0;
- row < bricksCfg.rows;
- row++
- ) {
- for (
- let column = 0;
- column < bricksCfg.cols;
- column++
- ) {
- const x =
- bricksCfg.left +
- column *
- (
- bricksCfg.w +
- bricksCfg.gap
- );
- const y =
- bricksCfg.top +
- row *
- (
- bricksCfg.h +
- bricksCfg.gap
- );
- bricks.push({
- x,
- y,
- w: bricksCfg.w,
- h: bricksCfg.h,
- alive: true,
- row,
- });
- }
- }
- }
- /* =========================
- REINICIAR JUEGO
- ========================= */
- function resetGame() {
- running = false;
- /*
- * Cancela el bucle anterior para
- * evitar que el juego se acelere.
- */
- if (
- animationFrameId !== null
- ) {
- cancelAnimationFrame(
- animationFrameId
- );
- animationFrameId = null;
- }
- /*
- * Cancela la cuenta atrás anterior.
- */
- if (
- countdownInterval !== null
- ) {
- clearInterval(
- countdownInterval
- );
- countdownInterval = null;
- }
- score = 0;
- lives = 3;
- updateHUD();
- paddle.x =
- canvas.width / 2 -
- paddle.w / 2;
- paddle.dx = 0;
- resetBall(true);
- makeBricks();
- overlayEl.style.display =
- "none";
- waitingLaunch = true;
- updateLaunchButton();
- }
- /* =========================
- REINICIAR PELOTA
- ========================= */
- function resetBall(stuck) {
- ball.x =
- paddle.x +
- paddle.w / 2;
- ball.y =
- paddle.y -
- ball.r -
- 2;
- ball.vx =
- 4 *
- (
- Math.random() < 0.5
- ? -1
- : 1
- );
- ball.vy = -4;
- ball.stuck = stuck;
- updateLaunchButton();
- }
- /* =========================
- ACTUALIZAR MARCADOR
- ========================= */
- function updateHUD() {
- scoreValueEl.textContent =
- String(score);
- livesValueEl.textContent =
- String(lives);
- }
- /* =========================
- BLOQUEAR TECLAS DEL NAVEGADOR
- ========================= */
- /*
- * Se bloquean en window y durante la fase
- * de captura. De esta manera el navegador
- * no puede mover la página ni navegar entre
- * botones antes de que el juego reciba la tecla.
- */
- const gameKeys = new Set([
- "ArrowUp",
- "ArrowDown",
- "ArrowLeft",
- "ArrowRight",
- "Space",
- "KeyW",
- "KeyA",
- "KeyS",
- "KeyD",
- ]);
- function blockBrowserGameKeys(event) {
- if (gameKeys.has(event.code)) {
- event.preventDefault();
- }
- }
- window.addEventListener(
- "keydown",
- blockBrowserGameKeys,
- {
- capture: true,
- passive: false,
- }
- );
- window.addEventListener(
- "keyup",
- blockBrowserGameKeys,
- {
- capture: true,
- passive: false,
- }
- );
- /* =========================
- CONTROLES DE TECLADO
- ========================= */
- const keys = {
- left: false,
- right: false,
- };
- document.addEventListener(
- "keydown",
- (event) => {
- if (
- event.key === "ArrowLeft"
- ) {
- keys.left = true;
- }
- if (
- event.key === "ArrowRight"
- ) {
- keys.right = true;
- }
- if (
- event.key === " " ||
- event.code === "Space"
- ) {
- launchBall();
- }
- },
- {
- passive: false,
- }
- );
- document.addEventListener(
- "keyup",
- (event) => {
- if (
- event.key === "ArrowLeft"
- ) {
- keys.left = false;
- }
- if (
- event.key === "ArrowRight"
- ) {
- keys.right = false;
- }
- },
- {
- passive: false,
- }
- );
- /*
- * Evita que la pala continúe moviéndose
- * si el navegador pierde el foco.
- */
- window.addEventListener(
- "blur",
- () => {
- keys.left = false;
- keys.right = false;
- }
- );
- /* =========================
- CONTROLES TÁCTILES
- ========================= */
- let touchActive = false;
- canvas.addEventListener(
- "pointerdown",
- (event) => {
- event.preventDefault();
- touchActive = true;
- }
- );
- canvas.addEventListener(
- "pointerup",
- (event) => {
- event.preventDefault();
- touchActive = false;
- }
- );
- canvas.addEventListener(
- "pointercancel",
- (event) => {
- event.preventDefault();
- touchActive = false;
- }
- );
- canvas.addEventListener(
- "pointerleave",
- () => {
- touchActive = false;
- }
- );
- canvas.addEventListener(
- "pointermove",
- (event) => {
- if (!touchActive) {
- return;
- }
- event.preventDefault();
- const rect =
- canvas.getBoundingClientRect();
- const pointerX =
- (
- event.clientX -
- rect.left
- ) *
- (
- canvas.width /
- rect.width
- );
- paddle.x = clamp(
- pointerX -
- paddle.w / 2,
- 10,
- canvas.width -
- paddle.w -
- 10
- );
- if (ball.stuck) {
- ball.x =
- paddle.x +
- paddle.w / 2;
- ball.y =
- paddle.y -
- ball.r -
- 2;
- }
- }
- );
- /* =========================
- LANZAR PELOTA
- ========================= */
- btnLaunchMobile?.addEventListener(
- "click",
- (event) => {
- event.preventDefault();
- launchBall();
- event.currentTarget.blur();
- }
- );
- function launchBall() {
- if (!running) {
- return;
- }
- if (!ball.stuck) {
- return;
- }
- ball.stuck = false;
- waitingLaunch = false;
- updateLaunchButton();
- }
- /* =========================
- UTILIDADES
- ========================= */
- function clamp(
- value,
- min,
- max
- ) {
- return Math.max(
- min,
- Math.min(
- max,
- value
- )
- );
- }
- /* =========================
- LIMPIAR CANVAS
- ========================= */
- function clear() {
- ctx.fillStyle = "#03060c";
- ctx.fillRect(
- 0,
- 0,
- canvas.width,
- canvas.height
- );
- const gradient =
- ctx.createRadialGradient(
- canvas.width * 0.5,
- canvas.height * 0.45,
- 50,
- canvas.width * 0.5,
- canvas.height * 0.45,
- Math.max(
- canvas.width,
- canvas.height
- )
- );
- gradient.addColorStop(
- 0,
- "rgba(255,255,255,0.03)"
- );
- gradient.addColorStop(
- 1,
- "rgba(0,0,0,0.65)"
- );
- ctx.fillStyle = gradient;
- ctx.fillRect(
- 0,
- 0,
- canvas.width,
- canvas.height
- );
- }
- /* =========================
- DIBUJAR ESTRELLAS
- ========================= */
- function drawStars() {
- ctx.save();
- ctx.fillStyle = "white";
- for (const star of stars) {
- ctx.globalAlpha = star.a;
- ctx.beginPath();
- ctx.arc(
- star.x,
- star.y,
- star.r,
- 0,
- Math.PI * 2
- );
- ctx.fill();
- star.y += star.s;
- if (
- star.y >
- canvas.height + 2
- ) {
- star.y = -2;
- star.x =
- Math.random() *
- canvas.width;
- }
- }
- ctx.restore();
- }
- /* =========================
- DIBUJAR PALA
- ========================= */
- function drawPaddle() {
- ctx.save();
- ctx.shadowBlur = 14;
- ctx.shadowColor = "#7cff00";
- ctx.fillStyle = "#7cff00";
- roundRect(
- ctx,
- paddle.x,
- paddle.y,
- paddle.w,
- paddle.h,
- 10,
- true
- );
- ctx.restore();
- }
- /* =========================
- DIBUJAR PELOTA
- ========================= */
- function drawBall() {
- ctx.save();
- ctx.shadowBlur = 14;
- ctx.shadowColor = "#4aa3ff";
- ctx.fillStyle = "#4aa3ff";
- ctx.beginPath();
- ctx.arc(
- ball.x,
- ball.y,
- ball.r,
- 0,
- Math.PI * 2
- );
- ctx.fill();
- ctx.shadowBlur = 0;
- ctx.fillStyle =
- "rgba(255,255,255,.55)";
- ctx.beginPath();
- ctx.arc(
- ball.x - 2.5,
- ball.y - 2.5,
- 2,
- 0,
- Math.PI * 2
- );
- ctx.fill();
- ctx.restore();
- }
- /* =========================
- DIBUJAR LADRILLOS
- ========================= */
- function drawBricks() {
- ctx.save();
- for (const brick of bricks) {
- if (!brick.alive) {
- continue;
- }
- const palette = [
- "#ff3b3b",
- "#ffb020",
- "#7cff00",
- "#4aa3ff",
- "#b46bff",
- ];
- const color =
- palette[
- brick.row %
- palette.length
- ];
- ctx.shadowBlur = 10;
- ctx.shadowColor = color;
- ctx.fillStyle = color;
- roundRect(
- ctx,
- brick.x,
- brick.y,
- brick.w,
- brick.h,
- 8,
- true
- );
- ctx.shadowBlur = 0;
- ctx.globalAlpha = 0.18;
- ctx.fillStyle = "#ffffff";
- roundRect(
- ctx,
- brick.x + 2,
- brick.y + 2,
- brick.w - 4,
- brick.h - 4,
- 7,
- true
- );
- ctx.globalAlpha = 1;
- }
- ctx.restore();
- }
- /* =========================
- MENSAJE DE LANZAMIENTO
- ========================= */
- function drawHint() {
- if (!waitingLaunch) {
- return;
- }
- ctx.save();
- ctx.globalAlpha = 0.75;
- ctx.fillStyle =
- "rgba(255,255,255,.85)";
- ctx.font =
- "700 14px system-ui, -apple-system, Segoe UI, Arial";
- ctx.textAlign = "center";
- const message =
- isTouchMode()
- ? T.hintTouch
- : T.hintKey;
- ctx.fillText(
- message,
- canvas.width / 2,
- canvas.height - 56
- );
- ctx.restore();
- }
- /* =========================
- RECTÁNGULOS REDONDEADOS
- ========================= */
- function roundRect(
- context,
- x,
- y,
- width,
- height,
- radius,
- fill
- ) {
- const finalRadius =
- Math.min(
- radius,
- width / 2,
- height / 2
- );
- context.beginPath();
- context.moveTo(
- x + finalRadius,
- y
- );
- context.arcTo(
- x + width,
- y,
- x + width,
- y + height,
- finalRadius
- );
- context.arcTo(
- x + width,
- y + height,
- x,
- y + height,
- finalRadius
- );
- context.arcTo(
- x,
- y + height,
- x,
- y,
- finalRadius
- );
- context.arcTo(
- x,
- y,
- x + width,
- y,
- finalRadius
- );
- if (fill) {
- context.fill();
- }
- }
- /* =========================
- FÍSICA DEL JUEGO
- ========================= */
- function update() {
- if (keys.left) {
- paddle.dx =
- -paddle.speed;
- } else if (keys.right) {
- paddle.dx =
- paddle.speed;
- } else {
- paddle.dx = 0;
- }
- paddle.x += paddle.dx;
- paddle.x = clamp(
- paddle.x,
- 10,
- canvas.width -
- paddle.w -
- 10
- );
- if (ball.stuck) {
- ball.x =
- paddle.x +
- paddle.w / 2;
- ball.y =
- paddle.y -
- ball.r -
- 2;
- return;
- }
- ball.x += ball.vx;
- ball.y += ball.vy;
- if (
- ball.x -
- ball.r <
- 0
- ) {
- ball.x = ball.r;
- ball.vx *= -1;
- }
- if (
- ball.x +
- ball.r >
- canvas.width
- ) {
- ball.x =
- canvas.width -
- ball.r;
- ball.vx *= -1;
- }
- if (
- ball.y -
- ball.r <
- 0
- ) {
- ball.y = ball.r;
- ball.vy *= -1;
- }
- if (
- ball.y -
- ball.r >
- canvas.height
- ) {
- lives--;
- updateHUD();
- if (lives <= 0) {
- endGame(false);
- return;
- }
- resetBall(true);
- waitingLaunch = true;
- updateLaunchButton();
- return;
- }
- /* Colisión con la pala */
- if (
- ball.y + ball.r >=
- paddle.y &&
- ball.y + ball.r <=
- paddle.y +
- paddle.h &&
- ball.x >= paddle.x &&
- ball.x <=
- paddle.x +
- paddle.w &&
- ball.vy > 0
- ) {
- const hit =
- (
- ball.x -
- (
- paddle.x +
- paddle.w / 2
- )
- ) /
- (
- paddle.w / 2
- );
- ball.vx = hit * 6;
- ball.vy *= -1;
- ball.y =
- paddle.y -
- ball.r -
- 1;
- }
- let aliveCount = 0;
- /* Colisiones con los ladrillos */
- for (const brick of bricks) {
- if (!brick.alive) {
- continue;
- }
- aliveCount++;
- if (
- ball.x > brick.x &&
- ball.x <
- brick.x +
- brick.w &&
- ball.y > brick.y &&
- ball.y <
- brick.y +
- brick.h
- ) {
- brick.alive = false;
- score += 10;
- updateHUD();
- ball.vy *= -1;
- }
- }
- if (aliveCount === 0) {
- endGame(true);
- }
- }
- /* =========================
- BUCLE PRINCIPAL
- ========================= */
- function loop() {
- if (!running) {
- return;
- }
- clear();
- drawStars();
- drawBricks();
- drawPaddle();
- drawBall();
- drawHint();
- update();
- updateLaunchButton();
- animationFrameId =
- requestAnimationFrame(loop);
- }
- /* =========================
- FIN DE PARTIDA
- ========================= */
- function endGame(win) {
- running = false;
- overlayTitleEl.textContent =
- win
- ? T.win
- : T.gameOver;
- overlaySubEl.textContent =
- win
- ? T.winSub
- : T.loseSub;
- overlayEl.style.display =
- "grid";
- if (window.GameScores) {
- GameScores.submit(
- "break",
- score
- );
- }
- updateLaunchButton();
- }
- /* =========================
- CUENTA ATRÁS
- ========================= */
- function startCountdown() {
- running = false;
- if (
- countdownInterval !== null
- ) {
- clearInterval(
- countdownInterval
- );
- }
- let countdown = 3;
- countdownEl.style.display =
- "grid";
- countdownEl.textContent =
- String(countdown);
- countdownInterval =
- setInterval(() => {
- countdown--;
- if (countdown > 0) {
- countdownEl.textContent =
- String(countdown);
- } else {
- clearInterval(
- countdownInterval
- );
- countdownInterval = null;
- countdownEl.style.display =
- "none";
- running = true;
- updateLaunchButton();
- loop();
- }
- }, 1000);
- }
- /* =========================
- BOTONES
- ========================= */
- playAgainBtn.addEventListener(
- "click",
- () => {
- resetGame();
- startCountdown();
- playAgainBtn.blur();
- }
- );
- resetBtn.addEventListener(
- "click",
- () => {
- resetGame();
- startCountdown();
- resetBtn.blur();
- }
- );
- btnStartBreak?.addEventListener(
- "click",
- () => {
- resetGame();
- startCountdown();
- btnStartBreak.blur();
- }
- );
- /* =========================
- INICIO
- ========================= */
- resetGame();
- makeBricks();
- startCountdown();