- (() => {
- const canvas = document.getElementById("gameCanvas");
- const ctx = canvas.getContext("2d");
- const uiScore = document.getElementById("uiScore");
- const uiStage = document.getElementById("uiStage");
- const btnPlay = document.getElementById("btnReset");
- const hintOverlay = document.getElementById("hintOverlay");
- // =========================
- // I18N (ES por defecto / EN opcional)
- // =========================
- const es = {
- ready: "Listo",
- readyHtml: `Dale a <b>Play</b> para empezar<br>
- PC: ratón + click (ráfaga) · Móvil: tap dispara · hold dispara · drag mueve`,
- gameOver: "Game Over",
- gameOverHtml: `Han llegado al tope.<br><b>Dale a Play</b> para reintentar.`,
- };
- const en = {
- ready: "Ready",
- readyHtml: `Press <b>Play</b> to start<br>
- PC: mouse + click (burst) · Mobile: tap to shoot · hold to fire · drag to move`,
- gameOver: "Game Over",
- gameOverHtml: `They reached the top.<br><b>Press Play</b> to try again.`,
- };
- const T = (window.APP_LOCALE === 'en') ? { ...en } : { ...es };
- // =========================
- // CONFIG
- // =========================
- const LOGICAL_W = 540;
- const LOGICAL_H = 960;
- const FLOOR_Y = LOGICAL_H - 110;
- // ✅ DIFICULTAD CONTROLADA (para que Stage 2 no sea infierno)
- const BASE_TICK_MS = 700; // inicio stage 1
- const STAGE_STEP_MS = 45; // cada stage empieza un pelín más rápido
- const MIN_TICK_MS = 240; // NUNCA más rápido que esto
- const ALIVE_ACCEL_MS = 280; // cuánto acelera cuando quedan pocos (0..ALIVE_ACCEL_MS)
- // ✅ Origen fijo del disparo (centro de la línea tope)
- const SHOT_ORIGIN = { x: LOGICAL_W / 2, y: FLOOR_Y - 6 };
- const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
- // =========================
- // BG (sin rutas absolutas): lee background-image del canvas
- // =========================
- function extractCssUrl(cssBg) {
- if (!cssBg || cssBg === "none") return null;
- const m = cssBg.match(/url\((['"]?)(.*?)\1\)/i);
- return m && m[2] ? m[2] : null;
- }
- const bgImg = new Image();
- const bgUrl = extractCssUrl(getComputedStyle(canvas).backgroundImage);
- if (bgUrl) bgImg.src = bgUrl;
- // =========================
- // RESIZE (usa tamaño real del canvas)
- // =========================
- function resizeCanvas() {
- const dpr = Math.max(1, Math.min(3, window.devicePixelRatio || 1));
- const r = canvas.getBoundingClientRect();
- const displayW = Math.max(1, Math.floor(r.width));
- const displayH = Math.max(1, Math.floor(r.height));
- canvas.width = Math.floor(displayW * dpr);
- canvas.height = Math.floor(displayH * dpr);
- ctx.setTransform(1, 0, 0, 1, 0, 0);
- ctx.scale(canvas.width / LOGICAL_W, canvas.height / LOGICAL_H);
- }
- function getPointerPos(clientX, clientY) {
- const r = canvas.getBoundingClientRect();
- const x = (clientX - r.left) / r.width;
- const y = (clientY - r.top) / r.height;
- return {
- x: clamp(x * LOGICAL_W, 0, LOGICAL_W),
- y: clamp(y * LOGICAL_H, 0, LOGICAL_H),
- };
- }
- // =========================
- // PIXEL ALIENS (sin imágenes)
- // =========================
- const SPRITES = [
- [
- "00111100",
- "01111110",
- "11111111",
- "11011011",
- "11111111",
- "00100100",
- "01000010",
- "10000001",
- ],
- [
- "00111100",
- "11111111",
- "10111101",
- "11111111",
- "01111110",
- "00100100",
- "01000010",
- "01000010",
- ],
- [
- "01000010",
- "00100100",
- "11111111",
- "10111101",
- "11111111",
- "00100100",
- "01000010",
- "10000001",
- ],
- ];
- function drawPixelSprite(sprite, x, y, w, h, hue) {
- const rows = sprite.length;
- const cols = sprite[0].length;
- const px = w / cols;
- const py = h / rows;
- ctx.save();
- ctx.shadowBlur = 16;
- ctx.shadowColor = `hsla(${hue},95%,65%,.7)`;
- for (let r = 0; r < rows; r++) {
- const row = sprite[r];
- for (let c = 0; c < cols; c++) {
- if (row[c] !== "1") continue;
- const rx = x + c * px;
- const ry = y + r * py;
- ctx.fillStyle = `hsla(${hue},95%,62%,.95)`;
- ctx.fillRect(rx, ry, px, py);
- ctx.fillStyle = "rgba(255,255,255,.08)";
- ctx.fillRect(rx + px * 0.18, ry + py * 0.18, px * 0.55, py * 0.55);
- }
- }
- ctx.restore();
- }
- // =========================
- // STATE
- // =========================
- const state = {
- running: false,
- gameOver: false,
- score: 0,
- stage: 1,
- cross: { x: LOGICAL_W / 2, y: LOGICAL_H * 0.55 },
- // disparo
- firing: false,
- fireRate: 9, // ráfaga
- fireCooldown: 0,
- // láser visual (recorta al impacto)
- laser: {
- t: 0,
- dur: 0.09,
- x1: SHOT_ORIGIN.x,
- y1: SHOT_ORIGIN.y,
- x2: SHOT_ORIGIN.x,
- y2: SHOT_ORIGIN.y,
- hitX: null,
- hitY: null,
- },
- // pointer
- pointerDown: false,
- pointerId: null,
- pointerType: "mouse",
- downX: 0,
- downY: 0,
- downTime: 0,
- moved: false,
- dragThreshold: 14,
- holdToAutofireMs: 140,
- bullets: [],
- particles: [],
- invaders: [],
- invCfg: {
- cols: 10,
- rows: 5,
- w: 40,
- h: 32,
- gapX: 14,
- gapY: 16,
- offsetY: 110,
- },
- // spawn progresivo
- spawn: {
- total: 0,
- revealed: 0,
- acc: 0,
- intervalMs: 70,
- },
- // movimiento clásico por ticks (sin “acumulaciones” locas)
- swarm: {
- dir: -1, // empieza derecha->izq
- stepX: 10,
- stepDown: 22,
- borderPad: 18,
- tickMs: BASE_TICK_MS,
- tickAcc: 0,
- },
- };
- // =========================
- // UI / overlay
- // =========================
- function updateUI() {
- if (uiScore) uiScore.textContent = String(state.score);
- if (uiStage) uiStage.textContent = String(state.stage);
- }
- function showOverlay(title, html) {
- if (!hintOverlay) return;
- const t = hintOverlay.querySelector(".hint-title");
- const s = hintOverlay.querySelector(".hint-sub");
- if (t) t.textContent = title;
- if (s) s.innerHTML = html;
- hintOverlay.classList.remove("hidden");
- }
- function hideOverlay() {
- if (!hintOverlay) return;
- hintOverlay.classList.add("hidden");
- }
- // =========================
- // SPAWN WAVE
- // =========================
- function spawnWave() {
- state.invaders.length = 0;
- const { cols, rows, w, h, gapX, gapY, offsetY } = state.invCfg;
- const totalW = cols * w + (cols - 1) * gapX;
- const offsetX = Math.round((LOGICAL_W - totalW) / 2);
- for (let r = 0; r < rows; r++) {
- for (let c = 0; c < cols; c++) {
- state.invaders.push({
- x: offsetX + c * (w + gapX),
- y: offsetY + r * (h + gapY),
- w, h,
- alive: true,
- active: false, // aparece poco a poco
- sprite: SPRITES[r % SPRITES.length],
- hue: 20 + r * 35,
- });
- }
- }
- state.spawn.total = state.invaders.length;
- state.spawn.revealed = 0;
- state.spawn.acc = 0;
- }
- // =========================
- // GAME FLOW
- // =========================
- function setStageStartSpeed() {
- const start = BASE_TICK_MS - (state.stage - 1) * STAGE_STEP_MS;
- state.swarm.tickMs = Math.max(MIN_TICK_MS, start);
- state.swarm.tickAcc = 0;
- }
- function resetGame() {
- state.running = false;
- state.gameOver = false;
- state.score = 0;
- state.stage = 1;
- state.cross.x = LOGICAL_W / 2;
- state.cross.y = LOGICAL_H * 0.55;
- state.firing = false;
- state.fireCooldown = 0;
- state.laser.t = 0;
- state.laser.hitX = null;
- state.laser.hitY = null;
- state.pointerDown = false;
- state.pointerId = null;
- state.moved = false;
- state.bullets.length = 0;
- state.particles.length = 0;
- state.swarm.dir = -1;
- setStageStartSpeed();
- spawnWave();
- updateUI();
- showOverlay(
- T.ready,
- T.readyHtml
- );
- }
- function startGame() {
- state.running = true;
- state.gameOver = false;
- state.firing = false;
- state.fireCooldown = 0;
- hideOverlay();
- }
- function nextStage() {
- state.stage++;
- state.score += 250;
- state.swarm.dir = -1;
- setStageStartSpeed();
- spawnWave();
- updateUI();
- }
- function doGameOver() {
- state.running = false;
- state.gameOver = true;
- state.firing = false;
- showOverlay(T.gameOver, T.gameOverHtml);
- if (window.GameScores) { GameScores.submit('invasion', state.score); }
- }
- // =========================
- // FX
- // =========================
- function boom(x, y, hue) {
- for (let i = 0; i < 14; i++) {
- const a = Math.random() * Math.PI * 2;
- const sp = 60 + Math.random() * 180;
- state.particles.push({
- x, y,
- vx: Math.cos(a) * sp,
- vy: Math.sin(a) * sp,
- life: 0.35 + Math.random() * 0.25,
- t: 0,
- hue
- });
- }
- }
- // =========================
- // SHOOT (B): sale del centro fijo y va hacia la mira
- // =========================
- function shootOnce() {
- if (!state.running || state.gameOver) return;
- const dx = state.cross.x - SHOT_ORIGIN.x;
- const dy = state.cross.y - SHOT_ORIGIN.y;
- const len = Math.max(1, Math.hypot(dx, dy));
- const speed = 900;
- const vx = (dx / len) * speed;
- const vy = (dy / len) * speed;
- state.bullets.push({
- x: SHOT_ORIGIN.x,
- y: SHOT_ORIGIN.y,
- vx,
- vy,
- r: 4.6,
- life: 1.2,
- });
- // láser: del origen a la mira, se recorta al impactar
- state.laser.t = state.laser.dur;
- state.laser.x1 = SHOT_ORIGIN.x;
- state.laser.y1 = SHOT_ORIGIN.y;
- state.laser.x2 = state.cross.x;
- state.laser.y2 = state.cross.y;
- state.laser.hitX = null;
- state.laser.hitY = null;
- }
- // =========================
- // INPUT (mouse + táctil fino)
- // =========================
- function onPointerDown(e) {
- canvas.setPointerCapture?.(e.pointerId);
- state.pointerDown = true;
- state.pointerId = e.pointerId;
- state.pointerType = e.pointerType || "mouse";
- const p = getPointerPos(e.clientX, e.clientY);
- state.cross.x = p.x;
- state.cross.y = p.y;
- state.downX = p.x;
- state.downY = p.y;
- state.downTime = performance.now();
- state.moved = false;
- // PC: mantener click = ráfaga
- if (state.pointerType === "mouse" && state.running && !state.gameOver) {
- state.firing = true;
- state.fireCooldown = 0;
- }
- e.preventDefault();
- }
- function onPointerMove(e) {
- if (!state.pointerDown || e.pointerId !== state.pointerId) return;
- const p = getPointerPos(e.clientX, e.clientY);
- const dist = Math.hypot(p.x - state.downX, p.y - state.downY);
- if (dist > state.dragThreshold) {
- state.moved = true;
- if (state.pointerType !== "mouse") state.firing = false; // drag no dispara
- }
- state.cross.x = p.x;
- state.cross.y = p.y;
- e.preventDefault();
- }
- function onPointerUp(e) {
- if (e.pointerId !== state.pointerId) return;
- const wasMoved = state.moved;
- const heldMs = performance.now() - state.downTime;
- if (state.pointerType === "mouse") {
- state.firing = false;
- } else {
- // tap corto = 1 tiro
- if (state.running && !state.gameOver && !wasMoved && heldMs < state.holdToAutofireMs) {
- shootOnce();
- }
- state.firing = false;
- }
- state.pointerDown = false;
- state.pointerId = null;
- e.preventDefault();
- }
- canvas.addEventListener("contextmenu", (e) => e.preventDefault());
- canvas.addEventListener("pointerdown", onPointerDown, { passive: false });
- canvas.addEventListener("pointermove", onPointerMove, { passive: false });
- canvas.addEventListener("pointerup", onPointerUp, { passive: false });
- canvas.addEventListener("pointercancel", onPointerUp, { passive: false });
- // =========================
- // DIFICULTAD: tick por % vivos
- // =========================
- function applyAliveBasedSpeed(aliveCount) {
- const total = state.spawn.total || 1;
- const aliveRatio = aliveCount / total; // 1 -> 0
- const accel = (1 - aliveRatio) * ALIVE_ACCEL_MS; // 0..ALIVE_ACCEL_MS
- const base = Math.max(MIN_TICK_MS, BASE_TICK_MS - (state.stage - 1) * STAGE_STEP_MS);
- state.swarm.tickMs = Math.max(MIN_TICK_MS, base - accel);
- }
- // =========================
- // UPDATE
- // =========================
- function update(dt, nowMs) {
- if (!state.running || state.gameOver) return;
- // spawn progresivo
- state.spawn.acc += dt * 1000;
- while (state.spawn.revealed < state.spawn.total && state.spawn.acc >= state.spawn.intervalMs) {
- state.spawn.acc -= state.spawn.intervalMs;
- state.invaders[state.spawn.revealed].active = true;
- state.spawn.revealed++;
- }
- // autofire táctil: mantener sin mover
- if (state.pointerDown && state.pointerType !== "mouse" && !state.moved) {
- const held = nowMs - state.downTime;
- if (held >= state.holdToAutofireMs) state.firing = true;
- }
- // ráfaga
- if (state.firing) {
- state.fireCooldown -= dt;
- if (state.fireCooldown <= 0) {
- shootOnce();
- state.fireCooldown = 1 / state.fireRate;
- }
- }
- // láser timer
- if (state.laser.t > 0) {
- state.laser.t -= dt;
- if (state.laser.t <= 0) {
- state.laser.hitX = null;
- state.laser.hitY = null;
- }
- }
- // bullets
- for (let i = state.bullets.length - 1; i >= 0; i--) {
- const b = state.bullets[i];
- b.x += b.vx * dt;
- b.y += b.vy * dt;
- b.life -= dt;
- if (b.life <= 0 || b.y < -120 || b.y > LOGICAL_H + 120 || b.x < -120 || b.x > LOGICAL_W + 120) {
- state.bullets.splice(i, 1);
- }
- }
- // particles
- for (let i = state.particles.length - 1; i >= 0; i--) {
- const p = state.particles[i];
- p.t += dt;
- p.x += p.vx * dt;
- p.y += p.vy * dt;
- p.vx *= (1 - 1.2 * dt);
- p.vy *= (1 - 1.2 * dt);
- if (p.t >= p.life) state.particles.splice(i, 1);
- }
- // movimiento invaders SOLO cuando ya aparecieron todos
- if (state.spawn.revealed >= state.spawn.total) {
- state.swarm.tickAcc += dt * 1000;
- while (state.swarm.tickAcc >= state.swarm.tickMs) {
- state.swarm.tickAcc -= state.swarm.tickMs;
- let minX = Infinity, maxX = -Infinity;
- let aliveCount = 0;
- for (const inv of state.invaders) {
- if (!inv.alive || !inv.active) continue;
- aliveCount++;
- minX = Math.min(minX, inv.x);
- maxX = Math.max(maxX, inv.x + inv.w);
- }
- if (aliveCount === 0) {
- nextStage();
- return;
- }
- // ✅ dificultad real: velocidad solo por % vivos (y base por stage)
- applyAliveBasedSpeed(aliveCount);
- const leftBound = state.swarm.borderPad;
- const rightBound = LOGICAL_W - state.swarm.borderPad;
- const nextMinX = minX + state.swarm.stepX * state.swarm.dir;
- const nextMaxX = maxX + state.swarm.stepX * state.swarm.dir;
- const hitLeft = nextMinX <= leftBound;
- const hitRight = nextMaxX >= rightBound;
- if (hitLeft || hitRight) {
- state.swarm.dir *= -1;
- for (const inv of state.invaders) {
- if (!inv.alive || !inv.active) continue;
- inv.y += state.swarm.stepDown;
- }
- } else {
- for (const inv of state.invaders) {
- if (!inv.alive || !inv.active) continue;
- inv.x += state.swarm.stepX * state.swarm.dir;
- }
- }
- // game over si tocan la línea
- for (const inv of state.invaders) {
- if (!inv.alive || !inv.active) continue;
- if (inv.y + inv.h >= FLOOR_Y) {
- doGameOver();
- return;
- }
- }
- }
- }
- // colisiones bala vs invader (impacto: desaparecer y recortar láser)
- for (let bi = state.bullets.length - 1; bi >= 0; bi--) {
- const b = state.bullets[bi];
- let hit = false;
- for (const inv of state.invaders) {
- if (!inv.alive || !inv.active) continue;
- if (
- b.x + b.r >= inv.x &&
- b.x - b.r <= inv.x + inv.w &&
- b.y + b.r >= inv.y &&
- b.y - b.r <= inv.y + inv.h
- ) {
- inv.alive = false;
- hit = true;
- state.score += 10 + Math.floor(inv.hue / 18);
- // recorta láser al impacto
- state.laser.hitX = inv.x + inv.w / 2;
- state.laser.hitY = inv.y + inv.h / 2;
- state.laser.t = Math.min(state.laser.t, 0.06);
- boom(inv.x + inv.w / 2, inv.y + inv.h / 2, inv.hue);
- break;
- }
- }
- if (hit) {
- state.bullets.splice(bi, 1);
- updateUI();
- }
- }
- }
- // =========================
- // DRAW
- // =========================
- // (Tu versión “solucionado” funciona, la dejo compatible)
- function drawBackgroundImage() {
- ctx.save();
- if (!bgUrl || !bgImg.complete || !bgImg.naturalWidth) {
- // fallback simple (sin oscurecer)
- ctx.clearRect(0, 0, LOGICAL_W, LOGICAL_H);
- ctx.restore();
- return;
- }
- const iw = bgImg.naturalWidth;
- const ih = bgImg.naturalHeight;
- const scale = Math.max(LOGICAL_W / iw, LOGICAL_H / ih);
- const w = iw * scale;
- const h = ih * scale;
- const x = (LOGICAL_W - w) / 2;
- const y = (LOGICAL_H - h) / 2;
- ctx.drawImage(bgImg, x, y, w, h);
- // 👉 aquí puedes meter tu viñeta/oscurecido si quieres (tú ya lo tienes afinado)
- ctx.restore();
- }
- function drawLaser() {
- if (state.laser.t <= 0) return;
- const k = state.laser.t / state.laser.dur;
- const x1 = state.laser.x1;
- const y1 = state.laser.y1;
- const x2 = (state.laser.hitX != null) ? state.laser.hitX : state.laser.x2;
- const y2 = (state.laser.hitY != null) ? state.laser.hitY : state.laser.y2;
- ctx.save();
- ctx.globalAlpha = 0.9 * k;
- ctx.strokeStyle = "rgba(120,255,90,.95)";
- ctx.lineWidth = 5;
- ctx.shadowBlur = 26;
- ctx.shadowColor = "rgba(120,255,90,.95)";
- ctx.beginPath();
- ctx.moveTo(x1, y1);
- ctx.lineTo(x2, y2);
- ctx.stroke();
- // núcleo
- ctx.globalAlpha = 0.95 * k;
- ctx.lineWidth = 2;
- ctx.shadowBlur = 0;
- ctx.strokeStyle = "rgba(220,255,220,.9)";
- ctx.beginPath();
- ctx.moveTo(x1, y1);
- ctx.lineTo(x2, y2);
- ctx.stroke();
- ctx.restore();
- }
- function drawTriCrosshair(nowMs) {
- const c = { x: state.cross.x, y: state.cross.y };
- const a = { x: 90, y: FLOOR_Y + 70 };
- const b = { x: LOGICAL_W - 90, y: FLOOR_Y + 70 };
- ctx.save();
- const pulse = 0.8 + 0.2 * Math.sin(nowMs * 0.01);
- ctx.strokeStyle = "rgba(255,40,60,.85)";
- ctx.lineWidth = 3;
- ctx.shadowBlur = 18;
- ctx.shadowColor = "rgba(255,40,60,.65)";
- seg(c, a);
- seg(c, b);
- node(a, 6);
- node(b, 6);
- // centro
- ctx.shadowBlur = 26;
- ctx.shadowColor = "rgba(255,40,60,.9)";
- ctx.fillStyle = "rgba(255,40,60,.95)";
- ctx.beginPath();
- ctx.arc(c.x, c.y, 6.5, 0, Math.PI * 2);
- ctx.fill();
- // cruz + aro
- ctx.shadowBlur = 0;
- ctx.strokeStyle = "rgba(255,40,60,.92)";
- ctx.lineWidth = 3.2;
- ctx.lineCap = "round";
- const size = 26;
- const gap = 10;
- line(c.x - size, c.y, c.x - gap, c.y);
- line(c.x + gap, c.y, c.x + size, c.y);
- line(c.x, c.y - size, c.x, c.y - gap);
- line(c.x, c.y + gap, c.x, c.y + size);
- ctx.globalAlpha = 0.85;
- ctx.lineWidth = 2.6;
- ctx.beginPath();
- ctx.arc(c.x, c.y, 24 + 2 * pulse, 0, Math.PI * 2);
- ctx.stroke();
- ctx.restore();
- function seg(p1, p2) {
- ctx.beginPath();
- ctx.moveTo(p1.x, p1.y);
- ctx.lineTo(p2.x, p2.y);
- ctx.stroke();
- }
- function node(p, r) {
- ctx.beginPath();
- ctx.fillStyle = "rgba(255,40,60,.95)";
- ctx.shadowBlur = 18;
- ctx.shadowColor = "rgba(255,40,60,.75)";
- ctx.arc(p.x, p.y, r, 0, Math.PI * 2);
- ctx.fill();
- }
- function line(x1, y1, x2, y2) {
- ctx.beginPath();
- ctx.moveTo(x1, y1);
- ctx.lineTo(x2, y2);
- ctx.stroke();
- }
- }
- function draw(nowMs) {
- ctx.clearRect(0, 0, LOGICAL_W, LOGICAL_H);
- drawBackgroundImage();
- // línea tope
- ctx.save();
- ctx.globalAlpha = 0.45;
- ctx.fillStyle = "rgba(255,255,255,.9)";
- ctx.fillRect(0, FLOOR_Y, LOGICAL_W, 2);
- ctx.restore();
- // invaders
- for (const inv of state.invaders) {
- if (!inv.alive || !inv.active) continue;
- drawPixelSprite(inv.sprite, inv.x, inv.y, inv.w, inv.h, inv.hue);
- }
- // bullets
- for (const b of state.bullets) {
- ctx.save();
- ctx.beginPath();
- ctx.fillStyle = "rgba(124,255,91,.95)";
- ctx.shadowBlur = 18;
- ctx.shadowColor = "rgba(124,255,91,.85)";
- ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2);
- ctx.fill();
- ctx.restore();
- }
- // láser verde
- drawLaser();
- // partículas
- for (const p of state.particles) {
- const k = 1 - (p.t / p.life);
- ctx.save();
- ctx.globalAlpha = 0.9 * k;
- ctx.fillStyle = `hsla(${p.hue}, 95%, 65%, 1)`;
- ctx.shadowBlur = 18;
- ctx.shadowColor = `hsla(${p.hue}, 95%, 65%, .9)`;
- ctx.beginPath();
- ctx.arc(p.x, p.y, 3.2 * k, 0, Math.PI * 2);
- ctx.fill();
- ctx.restore();
- }
- // mira
- drawTriCrosshair(nowMs);
- }
- // =========================
- // LOOP
- // =========================
- let lastT = performance.now();
- function tick(now) {
- const dt = Math.min(0.033, (now - lastT) / 1000);
- lastT = now;
- update(dt, now);
- draw(now);
- requestAnimationFrame(tick);
- }
- // =========================
- // PLAY BUTTON
- // =========================
- btnPlay?.addEventListener("click", () => {
- if (state.gameOver) {
- resetGame();
- startGame();
- return;
- }
- if (!state.running) {
- startGame();
- return;
- }
- });
- // =========================
- // INIT
- // =========================
- window.addEventListener("resize", resizeCanvas);
- requestAnimationFrame(() => {
- resizeCanvas();
- resetGame();
- requestAnimationFrame(tick);
- });
- })();