← Volver a código

Breakout

Explora el código (HTML / CSS / JS) del juego

HTML

  1. <!doctype html>
  2. <html lang="es">
  3. <head>
  4. <meta charset="utf-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1">
  6. <title>Breakout</title>
  7. <link rel="stylesheet" href="css/break.css">
  8. </head>
  9. <body>
  10. <!-- Estructura del juego (canvas, HUD, botones…) -->
  11. <script src="js/break.js"></script>
  12. </body>
  13. </html>

CSS

  1. * {
  2. box-sizing: border-box;
  3. margin: 0;
  4. padding: 0;
  5. }
  6. :root {
  7. --bg: #06050c;
  8. --purple: #a55cff;
  9. --yellow: #ffd719;
  10. --green: #7cff00;
  11. --blue: #4aa3ff;
  12. --red: #ff4b5c;
  13. --text: #ffffff;
  14. --muted: rgba(255, 255, 255, 0.64);
  15. }
  16. html,
  17. body {
  18. width: 100%;
  19. min-height: 100%;
  20. overflow-x: hidden;
  21. }
  22. body {
  23. display: flex;
  24. min-width: 320px;
  25. min-height: 100svh;
  26. flex-direction: column;
  27. color: var(--text);
  28. background-color: var(--bg);
  29. background-position: center;
  30. background-size: cover;
  31. background-attachment: fixed;
  32. font-family: "Segoe UI", system-ui, -apple-system, Arial, sans-serif;
  33. }
  34. button,
  35. a {
  36. font: inherit;
  37. -webkit-tap-highlight-color: transparent;
  38. }
  39. button {
  40. cursor: pointer;
  41. }
  42. .arcade-nav {
  43. position: relative;
  44. z-index: 20;
  45. display: flex;
  46. align-items: center;
  47. justify-content: space-between;
  48. min-height: 78px;
  49. padding: 14px max(30px, 7vw);
  50. border-bottom: 1px solid rgba(255, 255, 255, 0.08);
  51. background: rgba(4, 3, 11, 0.72);
  52. backdrop-filter: blur(10px);
  53. }
  54. .arcade-brand {
  55. display: inline-flex;
  56. align-items: center;
  57. gap: 12px;
  58. color: #ffffff;
  59. text-decoration: none;
  60. font-size: 20px;
  61. font-weight: 900;
  62. letter-spacing: 0.035em;
  63. white-space: nowrap;
  64. }
  65. .brand-code {
  66. color: var(--purple);
  67. font-size: 28px;
  68. letter-spacing: -0.12em;
  69. }
  70. .arcade-nav-links {
  71. display: flex;
  72. align-items: center;
  73. gap: 3px;
  74. padding: 6px;
  75. border: 1px solid rgba(255, 255, 255, 0.11);
  76. border-radius: 999px;
  77. background: rgba(15, 12, 29, 0.78);
  78. }
  79. .arcade-nav-links a {
  80. display: inline-flex;
  81. align-items: center;
  82. gap: 8px;
  83. padding: 10px 15px;
  84. border-radius: 999px;
  85. color: rgba(255, 255, 255, 0.78);
  86. text-decoration: none;
  87. font-size: 12px;
  88. font-weight: 800;
  89. letter-spacing: 0.04em;
  90. text-transform: uppercase;
  91. }
  92. .arcade-nav-links a:hover {
  93. color: #ffffff;
  94. background: rgba(123, 92, 255, 0.18);
  95. }
  96. .arcade-nav-links i {
  97. width: 17px;
  98. color: var(--purple);
  99. text-align: center;
  100. }
  101. .break-page {
  102. width: min(1400px, 94vw);
  103. flex: 1 0 auto;
  104. margin: 0 auto;
  105. padding-bottom: 26px;
  106. }
  107. .site-footer {
  108. flex: 0 0 auto;
  109. margin-top: auto;
  110. }
  111. .game-toolbar {
  112. display: flex;
  113. align-items: center;
  114. justify-content: space-between;
  115. min-height: 56px;
  116. padding: 0 4px;
  117. }
  118. .toolbar-link {
  119. color: var(--purple);
  120. background: none;
  121. text-decoration: none;
  122. font-size: 13px;
  123. font-weight: 850;
  124. letter-spacing: 0.06em;
  125. }
  126. .toolbar-button {
  127. border: 0;
  128. }
  129. .arcade-shell {
  130. display: grid;
  131. grid-template-columns: minmax(0, 1.55fr) minmax(330px, 0.95fr);
  132. min-height: calc(100svh - 160px);
  133. overflow: hidden;
  134. border: 1px solid rgba(181, 108, 255, 0.48);
  135. border-radius: 28px;
  136. background: linear-gradient(135deg, rgba(11, 8, 25, 0.94), rgba(5, 4, 14, 0.92));
  137. box-shadow: 0 35px 100px rgba(0, 0, 0, 0.62);
  138. backdrop-filter: blur(9px);
  139. }
  140. .board-column {
  141. display: flex;
  142. min-width: 0;
  143. flex-direction: column;
  144. padding: 18px 26px 20px;
  145. border-right: 1px solid rgba(255, 255, 255, 0.08);
  146. }
  147. .break-logo {
  148. display: flex;
  149. align-items: center;
  150. justify-content: center;
  151. gap: 16px;
  152. min-height: 70px;
  153. margin: 0 auto 7px;
  154. color: var(--blue);
  155. font-family: "Courier New", monospace;
  156. font-size: clamp(32px, 3.8vw, 52px);
  157. font-weight: 1000;
  158. letter-spacing: 0.06em;
  159. text-shadow: 2px 2px #4835bc, 0 0 16px rgba(74, 163, 255, 0.48);
  160. }
  161. .brick-mark {
  162. color: var(--red);
  163. font-size: clamp(15px, 1.8vw, 25px);
  164. letter-spacing: -0.25em;
  165. text-shadow: 0 0 8px rgba(255, 75, 92, 0.8);
  166. }
  167. .brick-mark.alt {
  168. color: var(--green);
  169. text-shadow: 0 0 8px rgba(124, 255, 0, 0.7);
  170. }
  171. .canvas-shell {
  172. position: relative;
  173. width: 100%;
  174. min-height: 390px;
  175. flex: 1;
  176. overflow: hidden;
  177. border: 2px solid rgba(18, 104, 255, 0.9);
  178. border-radius: 16px;
  179. background: #03060c;
  180. box-shadow: 0 0 28px rgba(32, 70, 255, 0.22), inset 0 0 28px rgba(0, 0, 0, 0.7);
  181. }
  182. #gameCanvas {
  183. display: block;
  184. width: 100%;
  185. height: 100%;
  186. min-height: 390px;
  187. border-radius: 14px;
  188. background: #03060c;
  189. touch-action: none;
  190. }
  191. .overlay {
  192. position: absolute;
  193. inset: 0;
  194. z-index: 4;
  195. display: grid;
  196. place-items: center;
  197. padding: 20px;
  198. color: #ffffff;
  199. text-align: center;
  200. background: rgba(0, 0, 0, 0.38);
  201. backdrop-filter: blur(2px);
  202. }
  203. .overlay-countdown {
  204. font-size: clamp(55px, 7vw, 78px);
  205. font-weight: 900;
  206. pointer-events: none;
  207. }
  208. .overlay-end {
  209. background: rgba(0, 0, 0, 0.6);
  210. }
  211. .ov-title {
  212. margin-bottom: 10px;
  213. color: var(--red);
  214. font-size: clamp(32px, 4vw, 48px);
  215. font-weight: 900;
  216. }
  217. .ov-sub {
  218. color: rgba(255, 255, 255, 0.92);
  219. font-size: 16px;
  220. }
  221. .btn-play-again {
  222. min-width: 180px;
  223. min-height: 48px;
  224. margin-top: 18px;
  225. padding: 10px 18px;
  226. border: 2px solid var(--blue);
  227. border-radius: 12px;
  228. color: var(--blue);
  229. background: rgba(9, 22, 45, 0.92);
  230. font-weight: 900;
  231. }
  232. .btn-launch-mobile {
  233. display: none;
  234. position: absolute;
  235. left: 50%;
  236. bottom: 18%;
  237. z-index: 5;
  238. min-width: 180px;
  239. min-height: 48px;
  240. padding: 10px 18px;
  241. transform: translateX(-50%);
  242. border: 2px solid var(--green);
  243. border-radius: 12px;
  244. color: #091108;
  245. background: var(--green);
  246. font-weight: 900;
  247. box-shadow: 0 0 20px rgba(124, 255, 0, 0.35);
  248. }
  249. .game-help {
  250. display: flex;
  251. justify-content: center;
  252. gap: 28px;
  253. padding: 14px 5px 0;
  254. color: var(--muted);
  255. font-size: 12px;
  256. }
  257. .control-column {
  258. display: flex;
  259. min-width: 0;
  260. flex-direction: column;
  261. padding: 24px 28px 22px;
  262. }
  263. .desktop-stats,
  264. .mobile-stats {
  265. display: grid;
  266. grid-template-columns: repeat(2, 1fr);
  267. gap: 12px;
  268. }
  269. .mobile-stats {
  270. display: none;
  271. }
  272. .stat-card {
  273. display: flex;
  274. align-items: center;
  275. justify-content: center;
  276. gap: 9px;
  277. min-width: 0;
  278. padding: 12px 9px;
  279. border: 1px solid rgba(255, 255, 255, 0.14);
  280. border-radius: 16px;
  281. background: rgba(255, 255, 255, 0.035);
  282. font-size: 11px;
  283. font-weight: 700;
  284. letter-spacing: 0.04em;
  285. }
  286. .stat-card > span:last-child {
  287. display: flex;
  288. flex-direction: column;
  289. gap: 2px;
  290. }
  291. .stat-card b {
  292. color: var(--yellow);
  293. font-size: 19px;
  294. }
  295. .stat-icon {
  296. color: var(--yellow);
  297. font-size: 22px;
  298. }
  299. .stat-icon.red {
  300. color: var(--red);
  301. }
  302. .controls-block {
  303. display: grid;
  304. grid-template-columns: 1fr 175px;
  305. align-items: center;
  306. gap: 18px;
  307. margin: 30px 0 26px;
  308. }
  309. .keyboard-help h2,
  310. .score-preview h2 {
  311. margin-bottom: 17px;
  312. color: var(--purple);
  313. font-size: 15px;
  314. font-weight: 900;
  315. letter-spacing: 0.045em;
  316. }
  317. .keyboard-help p {
  318. margin: 10px 0;
  319. color: rgba(255, 255, 255, 0.7);
  320. font-size: 12px;
  321. }
  322. .keyboard-help strong {
  323. color: var(--green);
  324. }
  325. .break-controls {
  326. display: grid;
  327. grid-template-columns: repeat(3, 50px);
  328. justify-content: center;
  329. gap: 7px;
  330. }
  331. .break-controls span {
  332. display: grid;
  333. height: 50px;
  334. place-items: center;
  335. border: 1px solid rgba(165, 92, 255, 0.5);
  336. border-radius: 13px;
  337. color: var(--purple);
  338. background: #1a122d;
  339. font-size: 20px;
  340. }
  341. .break-controls .ball-key {
  342. color: #071005;
  343. background: var(--green);
  344. }
  345. .action-row {
  346. display: grid;
  347. grid-template-columns: repeat(2, 1fr);
  348. gap: 12px;
  349. }
  350. .arcade-action {
  351. min-height: 48px;
  352. border: 1px solid rgba(181, 108, 255, 0.45);
  353. border-radius: 13px;
  354. color: var(--purple);
  355. background: rgba(123, 92, 255, 0.08);
  356. font-size: 12px;
  357. font-weight: 900;
  358. }
  359. .score-preview {
  360. margin-top: 25px;
  361. }
  362. .ranking-preview {
  363. overflow: hidden;
  364. border: 1px solid rgba(255, 255, 255, 0.13);
  365. border-radius: 13px;
  366. background: rgba(255, 255, 255, 0.025);
  367. list-style: none;
  368. }
  369. .ranking-preview li {
  370. display: grid;
  371. grid-template-columns: 30px 1fr auto;
  372. gap: 8px;
  373. padding: 11px 14px;
  374. border-bottom: 1px solid rgba(255, 255, 255, 0.1);
  375. font-size: 13px;
  376. }
  377. .ranking-preview li:last-child {
  378. border-bottom: 0;
  379. }
  380. .rank-position {
  381. color: var(--purple);
  382. font-weight: 900;
  383. }
  384. .ranking-preview li:first-child .rank-position,
  385. .rank-score {
  386. color: var(--yellow);
  387. font-weight: 900;
  388. }
  389. .rank-name {
  390. overflow: hidden;
  391. text-overflow: ellipsis;
  392. white-space: nowrap;
  393. }
  394. .ranking-loading,
  395. .ranking-empty {
  396. display: block !important;
  397. color: var(--muted);
  398. text-align: center;
  399. }
  400. .btn-start-break {
  401. display: flex;
  402. align-items: center;
  403. justify-content: center;
  404. gap: 14px;
  405. width: 100%;
  406. min-height: 58px;
  407. margin-top: auto;
  408. border: 2px solid var(--blue);
  409. border-radius: 12px;
  410. color: var(--blue);
  411. background: rgba(9, 22, 45, 0.9);
  412. font-family: "Courier New", monospace;
  413. font-size: 19px;
  414. font-weight: 900;
  415. letter-spacing: 0.07em;
  416. box-shadow: 0 0 18px rgba(74, 163, 255, 0.24);
  417. }
  418. @media (max-width: 1050px) {
  419. .arcade-shell {
  420. grid-template-columns: minmax(0, 1.4fr) minmax(310px, 0.85fr);
  421. }
  422. .controls-block {
  423. grid-template-columns: 1fr;
  424. }
  425. .keyboard-help {
  426. text-align: center;
  427. }
  428. }
  429. @media (max-width: 820px) {
  430. .arcade-nav {
  431. min-height: 68px;
  432. padding: 10px 16px;
  433. }
  434. .arcade-nav-links a {
  435. padding: 9px 10px;
  436. }
  437. .arcade-nav-links a span {
  438. display: none;
  439. }
  440. .break-page {
  441. width: calc(100% - 24px);
  442. padding-bottom: 20px;
  443. }
  444. .game-toolbar {
  445. min-height: 50px;
  446. }
  447. .arcade-shell {
  448. display: block;
  449. min-height: 0;
  450. border-radius: 22px;
  451. }
  452. .board-column {
  453. padding: 18px 16px 16px;
  454. border-right: 0;
  455. }
  456. .control-column {
  457. display: none;
  458. }
  459. .break-logo {
  460. min-height: 48px;
  461. margin-bottom: 12px;
  462. font-size: clamp(27px, 8vw, 40px);
  463. }
  464. .mobile-stats {
  465. display: grid;
  466. margin-bottom: 12px;
  467. }
  468. .canvas-shell {
  469. min-height: 0;
  470. aspect-ratio: 12 / 7;
  471. flex: none;
  472. }
  473. #gameCanvas {
  474. width: 100%;
  475. height: 100%;
  476. min-height: 0;
  477. }
  478. .game-help {
  479. justify-content: space-between;
  480. gap: 10px;
  481. font-size: 10px;
  482. }
  483. .btn-launch-mobile {
  484. min-width: 150px;
  485. min-height: 44px;
  486. bottom: 14%;
  487. }
  488. }
  489. @media (max-width: 480px) {
  490. .arcade-brand {
  491. font-size: 14px;
  492. }
  493. .arcade-nav-links {
  494. padding: 4px;
  495. }
  496. .arcade-nav-links a {
  497. padding: 8px;
  498. }
  499. .break-page {
  500. width: calc(100% - 16px);
  501. }
  502. .toolbar-link {
  503. font-size: 12px;
  504. }
  505. .board-column {
  506. padding: 14px 10px 12px;
  507. }
  508. .break-logo {
  509. gap: 9px;
  510. font-size: 27px;
  511. }
  512. .mobile-stats {
  513. gap: 8px;
  514. }
  515. .stat-card {
  516. gap: 7px;
  517. padding: 10px 7px;
  518. font-size: 10px;
  519. }
  520. .stat-card b {
  521. font-size: 18px;
  522. }
  523. .ov-title {
  524. font-size: clamp(26px, 9vw, 38px);
  525. }
  526. .ov-sub {
  527. font-size: 13px;
  528. }
  529. .btn-play-again {
  530. min-width: 160px;
  531. min-height: 44px;
  532. margin-top: 13px;
  533. }
  534. }
  535. @media (max-width: 360px) {
  536. .arcade-brand span:last-child {
  537. display: none;
  538. }
  539. .break-logo {
  540. font-size: 24px;
  541. }
  542. .brick-mark {
  543. display: none;
  544. }
  545. }

JS

  1. /* =========================
  2. IDIOMAS
  3. ========================= */
  4. const es = {
  5. hintTouch: "Toca LANZAR para lanzar",
  6. hintKey: "Pulsa ESPACIO para lanzar",
  7. win: "¡Has ganado!",
  8. gameOver: "¡Game Over!",
  9. winSub: "Te has pasado el nivel 😎",
  10. loseSub: "¿Otra?",
  11. };
  12. const en = {
  13. hintTouch: "Tap LAUNCH to launch",
  14. hintKey: "Press SPACE to launch",
  15. win: "You won!",
  16. gameOver: "Game Over!",
  17. winSub: "You cleared the level 😎",
  18. loseSub: "Again?",
  19. };
  20. const T =
  21. window.APP_LOCALE === "en"
  22. ? { ...en }
  23. : { ...es };
  24. /* =========================
  25. ELEMENTOS DEL DOM
  26. ========================= */
  27. const canvas =
  28. document.getElementById("gameCanvas");
  29. const ctx =
  30. canvas.getContext("2d");
  31. const scoreValueEl =
  32. document.getElementById("scoreValue");
  33. const livesValueEl =
  34. document.getElementById("livesValue");
  35. const countdownEl =
  36. document.getElementById("countdown");
  37. const overlayEl =
  38. document.getElementById("overlay");
  39. const overlayTitleEl =
  40. document.getElementById("overlayTitle");
  41. const overlaySubEl =
  42. document.getElementById("overlaySub");
  43. const playAgainBtn =
  44. document.getElementById("playAgainBtn");
  45. const resetBtn =
  46. document.getElementById("resetBtn");
  47. const btnStartBreak =
  48. document.getElementById("btnStartBreak");
  49. const btnLaunchMobile =
  50. document.getElementById("btnLaunchMobile");
  51. /* =========================
  52. TAMAÑO DEL CANVAS
  53. ========================= */
  54. canvas.width = 720;
  55. canvas.height = 420;
  56. /* =========================
  57. FONDO DE ESTRELLAS
  58. ========================= */
  59. const stars = Array.from(
  60. {
  61. length: 90,
  62. },
  63. () => ({
  64. x: Math.random() * canvas.width,
  65. y: Math.random() * canvas.height,
  66. r: Math.random() * 1.6 + 0.2,
  67. a: Math.random() * 0.6 + 0.15,
  68. s: Math.random() * 0.4 + 0.1,
  69. })
  70. );
  71. /* =========================
  72. ESTADO DEL JUEGO
  73. ========================= */
  74. let running = false;
  75. let waitingLaunch = true;
  76. let score = 0;
  77. let lives = 3;
  78. let animationFrameId = null;
  79. let countdownInterval = null;
  80. /* =========================
  81. JUGADOR
  82. ========================= */
  83. const paddle = {
  84. w: 110,
  85. h: 14,
  86. x: canvas.width / 2 - 55,
  87. y: canvas.height - 26,
  88. speed: 7,
  89. dx: 0,
  90. };
  91. /* =========================
  92. PELOTA
  93. ========================= */
  94. const ball = {
  95. r: 8,
  96. x: canvas.width / 2,
  97. y: paddle.y - 10,
  98. vx: 4,
  99. vy: -4,
  100. stuck: true,
  101. };
  102. /* =========================
  103. CONFIGURACIÓN LADRILLOS
  104. ========================= */
  105. const bricksCfg = {
  106. cols: 9,
  107. rows: 5,
  108. w: 64,
  109. h: 18,
  110. gap: 10,
  111. top: 36,
  112. left: 30,
  113. };
  114. let bricks = [];
  115. /* =========================
  116. DETECTAR MODO TÁCTIL
  117. ========================= */
  118. function isTouchMode() {
  119. return (
  120. window
  121. .matchMedia(
  122. "(max-width: 900px)"
  123. )
  124. .matches &&
  125. window
  126. .matchMedia(
  127. "(hover: none)"
  128. )
  129. .matches &&
  130. window
  131. .matchMedia(
  132. "(pointer: coarse)"
  133. )
  134. .matches
  135. );
  136. }
  137. /* =========================
  138. BOTÓN LANZAR MÓVIL
  139. ========================= */
  140. function updateLaunchButton() {
  141. if (!btnLaunchMobile) {
  142. return;
  143. }
  144. const shouldShow =
  145. isTouchMode() &&
  146. running &&
  147. ball.stuck;
  148. btnLaunchMobile.style.display =
  149. shouldShow
  150. ? "flex"
  151. : "none";
  152. }
  153. /* =========================
  154. CREAR LADRILLOS
  155. ========================= */
  156. function makeBricks() {
  157. bricks = [];
  158. for (
  159. let row = 0;
  160. row < bricksCfg.rows;
  161. row++
  162. ) {
  163. for (
  164. let column = 0;
  165. column < bricksCfg.cols;
  166. column++
  167. ) {
  168. const x =
  169. bricksCfg.left +
  170. column *
  171. (
  172. bricksCfg.w +
  173. bricksCfg.gap
  174. );
  175. const y =
  176. bricksCfg.top +
  177. row *
  178. (
  179. bricksCfg.h +
  180. bricksCfg.gap
  181. );
  182. bricks.push({
  183. x,
  184. y,
  185. w: bricksCfg.w,
  186. h: bricksCfg.h,
  187. alive: true,
  188. row,
  189. });
  190. }
  191. }
  192. }
  193. /* =========================
  194. REINICIAR JUEGO
  195. ========================= */
  196. function resetGame() {
  197. running = false;
  198. /*
  199. * Cancela el bucle anterior para
  200. * evitar que el juego se acelere.
  201. */
  202. if (
  203. animationFrameId !== null
  204. ) {
  205. cancelAnimationFrame(
  206. animationFrameId
  207. );
  208. animationFrameId = null;
  209. }
  210. /*
  211. * Cancela la cuenta atrás anterior.
  212. */
  213. if (
  214. countdownInterval !== null
  215. ) {
  216. clearInterval(
  217. countdownInterval
  218. );
  219. countdownInterval = null;
  220. }
  221. score = 0;
  222. lives = 3;
  223. updateHUD();
  224. paddle.x =
  225. canvas.width / 2 -
  226. paddle.w / 2;
  227. paddle.dx = 0;
  228. resetBall(true);
  229. makeBricks();
  230. overlayEl.style.display =
  231. "none";
  232. waitingLaunch = true;
  233. updateLaunchButton();
  234. }
  235. /* =========================
  236. REINICIAR PELOTA
  237. ========================= */
  238. function resetBall(stuck) {
  239. ball.x =
  240. paddle.x +
  241. paddle.w / 2;
  242. ball.y =
  243. paddle.y -
  244. ball.r -
  245. 2;
  246. ball.vx =
  247. 4 *
  248. (
  249. Math.random() < 0.5
  250. ? -1
  251. : 1
  252. );
  253. ball.vy = -4;
  254. ball.stuck = stuck;
  255. updateLaunchButton();
  256. }
  257. /* =========================
  258. ACTUALIZAR MARCADOR
  259. ========================= */
  260. function updateHUD() {
  261. scoreValueEl.textContent =
  262. String(score);
  263. livesValueEl.textContent =
  264. String(lives);
  265. }
  266. /* =========================
  267. BLOQUEAR TECLAS DEL NAVEGADOR
  268. ========================= */
  269. /*
  270. * Se bloquean en window y durante la fase
  271. * de captura. De esta manera el navegador
  272. * no puede mover la página ni navegar entre
  273. * botones antes de que el juego reciba la tecla.
  274. */
  275. const gameKeys = new Set([
  276. "ArrowUp",
  277. "ArrowDown",
  278. "ArrowLeft",
  279. "ArrowRight",
  280. "Space",
  281. "KeyW",
  282. "KeyA",
  283. "KeyS",
  284. "KeyD",
  285. ]);
  286. function blockBrowserGameKeys(event) {
  287. if (gameKeys.has(event.code)) {
  288. event.preventDefault();
  289. }
  290. }
  291. window.addEventListener(
  292. "keydown",
  293. blockBrowserGameKeys,
  294. {
  295. capture: true,
  296. passive: false,
  297. }
  298. );
  299. window.addEventListener(
  300. "keyup",
  301. blockBrowserGameKeys,
  302. {
  303. capture: true,
  304. passive: false,
  305. }
  306. );
  307. /* =========================
  308. CONTROLES DE TECLADO
  309. ========================= */
  310. const keys = {
  311. left: false,
  312. right: false,
  313. };
  314. document.addEventListener(
  315. "keydown",
  316. (event) => {
  317. if (
  318. event.key === "ArrowLeft"
  319. ) {
  320. keys.left = true;
  321. }
  322. if (
  323. event.key === "ArrowRight"
  324. ) {
  325. keys.right = true;
  326. }
  327. if (
  328. event.key === " " ||
  329. event.code === "Space"
  330. ) {
  331. launchBall();
  332. }
  333. },
  334. {
  335. passive: false,
  336. }
  337. );
  338. document.addEventListener(
  339. "keyup",
  340. (event) => {
  341. if (
  342. event.key === "ArrowLeft"
  343. ) {
  344. keys.left = false;
  345. }
  346. if (
  347. event.key === "ArrowRight"
  348. ) {
  349. keys.right = false;
  350. }
  351. },
  352. {
  353. passive: false,
  354. }
  355. );
  356. /*
  357. * Evita que la pala continúe moviéndose
  358. * si el navegador pierde el foco.
  359. */
  360. window.addEventListener(
  361. "blur",
  362. () => {
  363. keys.left = false;
  364. keys.right = false;
  365. }
  366. );
  367. /* =========================
  368. CONTROLES TÁCTILES
  369. ========================= */
  370. let touchActive = false;
  371. canvas.addEventListener(
  372. "pointerdown",
  373. (event) => {
  374. event.preventDefault();
  375. touchActive = true;
  376. }
  377. );
  378. canvas.addEventListener(
  379. "pointerup",
  380. (event) => {
  381. event.preventDefault();
  382. touchActive = false;
  383. }
  384. );
  385. canvas.addEventListener(
  386. "pointercancel",
  387. (event) => {
  388. event.preventDefault();
  389. touchActive = false;
  390. }
  391. );
  392. canvas.addEventListener(
  393. "pointerleave",
  394. () => {
  395. touchActive = false;
  396. }
  397. );
  398. canvas.addEventListener(
  399. "pointermove",
  400. (event) => {
  401. if (!touchActive) {
  402. return;
  403. }
  404. event.preventDefault();
  405. const rect =
  406. canvas.getBoundingClientRect();
  407. const pointerX =
  408. (
  409. event.clientX -
  410. rect.left
  411. ) *
  412. (
  413. canvas.width /
  414. rect.width
  415. );
  416. paddle.x = clamp(
  417. pointerX -
  418. paddle.w / 2,
  419. 10,
  420. canvas.width -
  421. paddle.w -
  422. 10
  423. );
  424. if (ball.stuck) {
  425. ball.x =
  426. paddle.x +
  427. paddle.w / 2;
  428. ball.y =
  429. paddle.y -
  430. ball.r -
  431. 2;
  432. }
  433. }
  434. );
  435. /* =========================
  436. LANZAR PELOTA
  437. ========================= */
  438. btnLaunchMobile?.addEventListener(
  439. "click",
  440. (event) => {
  441. event.preventDefault();
  442. launchBall();
  443. event.currentTarget.blur();
  444. }
  445. );
  446. function launchBall() {
  447. if (!running) {
  448. return;
  449. }
  450. if (!ball.stuck) {
  451. return;
  452. }
  453. ball.stuck = false;
  454. waitingLaunch = false;
  455. updateLaunchButton();
  456. }
  457. /* =========================
  458. UTILIDADES
  459. ========================= */
  460. function clamp(
  461. value,
  462. min,
  463. max
  464. ) {
  465. return Math.max(
  466. min,
  467. Math.min(
  468. max,
  469. value
  470. )
  471. );
  472. }
  473. /* =========================
  474. LIMPIAR CANVAS
  475. ========================= */
  476. function clear() {
  477. ctx.fillStyle = "#03060c";
  478. ctx.fillRect(
  479. 0,
  480. 0,
  481. canvas.width,
  482. canvas.height
  483. );
  484. const gradient =
  485. ctx.createRadialGradient(
  486. canvas.width * 0.5,
  487. canvas.height * 0.45,
  488. 50,
  489. canvas.width * 0.5,
  490. canvas.height * 0.45,
  491. Math.max(
  492. canvas.width,
  493. canvas.height
  494. )
  495. );
  496. gradient.addColorStop(
  497. 0,
  498. "rgba(255,255,255,0.03)"
  499. );
  500. gradient.addColorStop(
  501. 1,
  502. "rgba(0,0,0,0.65)"
  503. );
  504. ctx.fillStyle = gradient;
  505. ctx.fillRect(
  506. 0,
  507. 0,
  508. canvas.width,
  509. canvas.height
  510. );
  511. }
  512. /* =========================
  513. DIBUJAR ESTRELLAS
  514. ========================= */
  515. function drawStars() {
  516. ctx.save();
  517. ctx.fillStyle = "white";
  518. for (const star of stars) {
  519. ctx.globalAlpha = star.a;
  520. ctx.beginPath();
  521. ctx.arc(
  522. star.x,
  523. star.y,
  524. star.r,
  525. 0,
  526. Math.PI * 2
  527. );
  528. ctx.fill();
  529. star.y += star.s;
  530. if (
  531. star.y >
  532. canvas.height + 2
  533. ) {
  534. star.y = -2;
  535. star.x =
  536. Math.random() *
  537. canvas.width;
  538. }
  539. }
  540. ctx.restore();
  541. }
  542. /* =========================
  543. DIBUJAR PALA
  544. ========================= */
  545. function drawPaddle() {
  546. ctx.save();
  547. ctx.shadowBlur = 14;
  548. ctx.shadowColor = "#7cff00";
  549. ctx.fillStyle = "#7cff00";
  550. roundRect(
  551. ctx,
  552. paddle.x,
  553. paddle.y,
  554. paddle.w,
  555. paddle.h,
  556. 10,
  557. true
  558. );
  559. ctx.restore();
  560. }
  561. /* =========================
  562. DIBUJAR PELOTA
  563. ========================= */
  564. function drawBall() {
  565. ctx.save();
  566. ctx.shadowBlur = 14;
  567. ctx.shadowColor = "#4aa3ff";
  568. ctx.fillStyle = "#4aa3ff";
  569. ctx.beginPath();
  570. ctx.arc(
  571. ball.x,
  572. ball.y,
  573. ball.r,
  574. 0,
  575. Math.PI * 2
  576. );
  577. ctx.fill();
  578. ctx.shadowBlur = 0;
  579. ctx.fillStyle =
  580. "rgba(255,255,255,.55)";
  581. ctx.beginPath();
  582. ctx.arc(
  583. ball.x - 2.5,
  584. ball.y - 2.5,
  585. 2,
  586. 0,
  587. Math.PI * 2
  588. );
  589. ctx.fill();
  590. ctx.restore();
  591. }
  592. /* =========================
  593. DIBUJAR LADRILLOS
  594. ========================= */
  595. function drawBricks() {
  596. ctx.save();
  597. for (const brick of bricks) {
  598. if (!brick.alive) {
  599. continue;
  600. }
  601. const palette = [
  602. "#ff3b3b",
  603. "#ffb020",
  604. "#7cff00",
  605. "#4aa3ff",
  606. "#b46bff",
  607. ];
  608. const color =
  609. palette[
  610. brick.row %
  611. palette.length
  612. ];
  613. ctx.shadowBlur = 10;
  614. ctx.shadowColor = color;
  615. ctx.fillStyle = color;
  616. roundRect(
  617. ctx,
  618. brick.x,
  619. brick.y,
  620. brick.w,
  621. brick.h,
  622. 8,
  623. true
  624. );
  625. ctx.shadowBlur = 0;
  626. ctx.globalAlpha = 0.18;
  627. ctx.fillStyle = "#ffffff";
  628. roundRect(
  629. ctx,
  630. brick.x + 2,
  631. brick.y + 2,
  632. brick.w - 4,
  633. brick.h - 4,
  634. 7,
  635. true
  636. );
  637. ctx.globalAlpha = 1;
  638. }
  639. ctx.restore();
  640. }
  641. /* =========================
  642. MENSAJE DE LANZAMIENTO
  643. ========================= */
  644. function drawHint() {
  645. if (!waitingLaunch) {
  646. return;
  647. }
  648. ctx.save();
  649. ctx.globalAlpha = 0.75;
  650. ctx.fillStyle =
  651. "rgba(255,255,255,.85)";
  652. ctx.font =
  653. "700 14px system-ui, -apple-system, Segoe UI, Arial";
  654. ctx.textAlign = "center";
  655. const message =
  656. isTouchMode()
  657. ? T.hintTouch
  658. : T.hintKey;
  659. ctx.fillText(
  660. message,
  661. canvas.width / 2,
  662. canvas.height - 56
  663. );
  664. ctx.restore();
  665. }
  666. /* =========================
  667. RECTÁNGULOS REDONDEADOS
  668. ========================= */
  669. function roundRect(
  670. context,
  671. x,
  672. y,
  673. width,
  674. height,
  675. radius,
  676. fill
  677. ) {
  678. const finalRadius =
  679. Math.min(
  680. radius,
  681. width / 2,
  682. height / 2
  683. );
  684. context.beginPath();
  685. context.moveTo(
  686. x + finalRadius,
  687. y
  688. );
  689. context.arcTo(
  690. x + width,
  691. y,
  692. x + width,
  693. y + height,
  694. finalRadius
  695. );
  696. context.arcTo(
  697. x + width,
  698. y + height,
  699. x,
  700. y + height,
  701. finalRadius
  702. );
  703. context.arcTo(
  704. x,
  705. y + height,
  706. x,
  707. y,
  708. finalRadius
  709. );
  710. context.arcTo(
  711. x,
  712. y,
  713. x + width,
  714. y,
  715. finalRadius
  716. );
  717. if (fill) {
  718. context.fill();
  719. }
  720. }
  721. /* =========================
  722. FÍSICA DEL JUEGO
  723. ========================= */
  724. function update() {
  725. if (keys.left) {
  726. paddle.dx =
  727. -paddle.speed;
  728. } else if (keys.right) {
  729. paddle.dx =
  730. paddle.speed;
  731. } else {
  732. paddle.dx = 0;
  733. }
  734. paddle.x += paddle.dx;
  735. paddle.x = clamp(
  736. paddle.x,
  737. 10,
  738. canvas.width -
  739. paddle.w -
  740. 10
  741. );
  742. if (ball.stuck) {
  743. ball.x =
  744. paddle.x +
  745. paddle.w / 2;
  746. ball.y =
  747. paddle.y -
  748. ball.r -
  749. 2;
  750. return;
  751. }
  752. ball.x += ball.vx;
  753. ball.y += ball.vy;
  754. if (
  755. ball.x -
  756. ball.r <
  757. 0
  758. ) {
  759. ball.x = ball.r;
  760. ball.vx *= -1;
  761. }
  762. if (
  763. ball.x +
  764. ball.r >
  765. canvas.width
  766. ) {
  767. ball.x =
  768. canvas.width -
  769. ball.r;
  770. ball.vx *= -1;
  771. }
  772. if (
  773. ball.y -
  774. ball.r <
  775. 0
  776. ) {
  777. ball.y = ball.r;
  778. ball.vy *= -1;
  779. }
  780. if (
  781. ball.y -
  782. ball.r >
  783. canvas.height
  784. ) {
  785. lives--;
  786. updateHUD();
  787. if (lives <= 0) {
  788. endGame(false);
  789. return;
  790. }
  791. resetBall(true);
  792. waitingLaunch = true;
  793. updateLaunchButton();
  794. return;
  795. }
  796. /* Colisión con la pala */
  797. if (
  798. ball.y + ball.r >=
  799. paddle.y &&
  800. ball.y + ball.r <=
  801. paddle.y +
  802. paddle.h &&
  803. ball.x >= paddle.x &&
  804. ball.x <=
  805. paddle.x +
  806. paddle.w &&
  807. ball.vy > 0
  808. ) {
  809. const hit =
  810. (
  811. ball.x -
  812. (
  813. paddle.x +
  814. paddle.w / 2
  815. )
  816. ) /
  817. (
  818. paddle.w / 2
  819. );
  820. ball.vx = hit * 6;
  821. ball.vy *= -1;
  822. ball.y =
  823. paddle.y -
  824. ball.r -
  825. 1;
  826. }
  827. let aliveCount = 0;
  828. /* Colisiones con los ladrillos */
  829. for (const brick of bricks) {
  830. if (!brick.alive) {
  831. continue;
  832. }
  833. aliveCount++;
  834. if (
  835. ball.x > brick.x &&
  836. ball.x <
  837. brick.x +
  838. brick.w &&
  839. ball.y > brick.y &&
  840. ball.y <
  841. brick.y +
  842. brick.h
  843. ) {
  844. brick.alive = false;
  845. score += 10;
  846. updateHUD();
  847. ball.vy *= -1;
  848. }
  849. }
  850. if (aliveCount === 0) {
  851. endGame(true);
  852. }
  853. }
  854. /* =========================
  855. BUCLE PRINCIPAL
  856. ========================= */
  857. function loop() {
  858. if (!running) {
  859. return;
  860. }
  861. clear();
  862. drawStars();
  863. drawBricks();
  864. drawPaddle();
  865. drawBall();
  866. drawHint();
  867. update();
  868. updateLaunchButton();
  869. animationFrameId =
  870. requestAnimationFrame(loop);
  871. }
  872. /* =========================
  873. FIN DE PARTIDA
  874. ========================= */
  875. function endGame(win) {
  876. running = false;
  877. overlayTitleEl.textContent =
  878. win
  879. ? T.win
  880. : T.gameOver;
  881. overlaySubEl.textContent =
  882. win
  883. ? T.winSub
  884. : T.loseSub;
  885. overlayEl.style.display =
  886. "grid";
  887. if (window.GameScores) {
  888. GameScores.submit(
  889. "break",
  890. score
  891. );
  892. }
  893. updateLaunchButton();
  894. }
  895. /* =========================
  896. CUENTA ATRÁS
  897. ========================= */
  898. function startCountdown() {
  899. running = false;
  900. if (
  901. countdownInterval !== null
  902. ) {
  903. clearInterval(
  904. countdownInterval
  905. );
  906. }
  907. let countdown = 3;
  908. countdownEl.style.display =
  909. "grid";
  910. countdownEl.textContent =
  911. String(countdown);
  912. countdownInterval =
  913. setInterval(() => {
  914. countdown--;
  915. if (countdown > 0) {
  916. countdownEl.textContent =
  917. String(countdown);
  918. } else {
  919. clearInterval(
  920. countdownInterval
  921. );
  922. countdownInterval = null;
  923. countdownEl.style.display =
  924. "none";
  925. running = true;
  926. updateLaunchButton();
  927. loop();
  928. }
  929. }, 1000);
  930. }
  931. /* =========================
  932. BOTONES
  933. ========================= */
  934. playAgainBtn.addEventListener(
  935. "click",
  936. () => {
  937. resetGame();
  938. startCountdown();
  939. playAgainBtn.blur();
  940. }
  941. );
  942. resetBtn.addEventListener(
  943. "click",
  944. () => {
  945. resetGame();
  946. startCountdown();
  947. resetBtn.blur();
  948. }
  949. );
  950. btnStartBreak?.addEventListener(
  951. "click",
  952. () => {
  953. resetGame();
  954. startCountdown();
  955. btnStartBreak.blur();
  956. }
  957. );
  958. /* =========================
  959. INICIO
  960. ========================= */
  961. resetGame();
  962. makeBricks();
  963. startCountdown();