🎮ArcadeLab

Neon Runner

by LaserBuilder68
581 lines19.6 KB
▶ Play
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">
  <meta name="theme-color" content="#0b1020">
  <title>Neon Runner</title>
  <style>
    html, body {
      margin: 0;
      padding: 0;
      height: 100%;
      overflow: hidden;
      background: #0b1020;
      touch-action: none;
      -webkit-tap-highlight-color: transparent;
      -webkit-user-select: none;
      user-select: none;
      overscroll-behavior: none;
      font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
    }
    #game {
      display: block;
      position: fixed;
      inset: 0;
      width: 100%;
      height: 100%;
      touch-action: none;
    }
  </style>
</head>
<body>
  <canvas id="game"></canvas>
  <script>
  (function () {
    "use strict";

    var canvas = document.getElementById("game");
    var ctx = canvas.getContext("2d");

    var W = 0, H = 0, DPR = 1, scale = 1;

    function resize() {
      DPR = Math.min(window.devicePixelRatio || 1, 2);
      W = window.innerWidth;
      H = window.innerHeight;
      canvas.width = Math.round(W * DPR);
      canvas.height = Math.round(H * DPR);
      canvas.style.width = W + "px";
      canvas.style.height = H + "px";
      ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
      scale = Math.max(0.65, Math.min(1.5, H / 720));
      player.w = 42 * scale;
      player.x = Math.max(50, Math.min(W * 0.18, 200));
      player.h = 44 * scale;
    }

    function groundY() { return H * 0.78; }

    var player = { x: 80, w: 42, h: 44, yOff: 0, vy: 0, onGround: true };

    var state = {
      running: false, over: false, t: 0, scroll: 0, speed: 0,
      score: 0, best: 0, obstacles: [], coins: [], particles: [],
      spawnTimer: 0, coinTimer: 0, shake: 0, restartLock: 0
    };

    try {
      var saved = localStorage.getItem("neonRunnerBest");
      if (saved) state.best = parseInt(saved, 10) || 0;
    } catch (e) {}

    function resetGame() {
      state.obstacles = [];
      state.coins = [];
      state.particles = [];
      state.t = 0;
      state.speed = 400 * scale;
      state.score = 0;
      state.spawnTimer = 1.1;
      state.coinTimer = 1.4;
      state.shake = 0;
      state.over = false;
      state.running = true;
      player.yOff = 0;
      player.vy = 0;
      player.onGround = true;
    }

    function gameOver() {
      state.running = false;
      state.over = true;
      state.restartLock = 0.6;
      state.shake = 16;
      explode(player.x + player.w / 2, groundY() - player.yOff - player.h / 2);
      var s = Math.floor(state.score);
      if (s > state.best) {
        state.best = s;
        try { localStorage.setItem("neonRunnerBest", String(s)); } catch (e) {}
      }
    }

    function jump() {
      if (!state.running) return;
      if (player.onGround) {
        player.vy = 1080 * scale;
        player.onGround = false;
        dust(player.x + player.w / 2, groundY(), 6);
      }
    }

    function addParticle(x, y, vx, vy, life, size, hue) {
      if (state.particles.length > 260) return;
      state.particles.push({ x: x, y: y, vx: vx, vy: vy, life: life, maxLife: life, size: size, hue: hue });
    }

    function dust(x, y, n) {
      for (var i = 0; i < n; i++) {
        var a = Math.PI + (Math.random() - 0.5) * Math.PI;
        var sp = (80 + Math.random() * 180) * scale;
        addParticle(x, y, Math.cos(a) * sp, Math.abs(Math.sin(a)) * sp * 0.8,
          0.35 + Math.random() * 0.3, (2 + Math.random() * 3) * scale, 200 + Math.random() * 60);
      }
    }

    function explode(x, y) {
      for (var i = 0; i < 44; i++) {
        var a = Math.random() * Math.PI * 2;
        var sp = (120 + Math.random() * 460) * scale;
        addParticle(x, y, Math.cos(a) * sp, Math.sin(a) * sp,
          0.5 + Math.random() * 0.7, (2 + Math.random() * 5) * scale, Math.random() * 360);
      }
    }

    function sparkle(x, y, hue) {
      for (var i = 0; i < 12; i++) {
        var a = Math.random() * Math.PI * 2;
        var sp = (60 + Math.random() * 220) * scale;
        addParticle(x, y, Math.cos(a) * sp, Math.sin(a) * sp,
          0.3 + Math.random() * 0.35, (1.5 + Math.random() * 3) * scale, hue);
      }
    }

    function updateParticles(dt) {
      for (var i = state.particles.length - 1; i >= 0; i--) {
        var p = state.particles[i];
        p.life -= dt;
        if (p.life <= 0) { state.particles.splice(i, 1); continue; }
        p.x += p.vx * dt;
        p.y += p.vy * dt;
        p.vy += 1400 * scale * dt;
        p.vx *= 0.985;
      }
    }

    function spawnObstacle() {
      var canFly = state.score > 140;
      if (canFly && Math.random() < 0.28) {
        var fw = 66 * scale, fh = 26 * scale;
        state.obstacles.push({ type: "fly", x: W + fw, w: fw, h: fh, yOff: 32 * scale, hue: Math.random() * 360 });
        return;
      }
      if (Math.random() < 0.55) {
        var sw = 32 * scale, sh = (44 + Math.random() * 24) * scale;
        state.obstacles.push({ type: "spike", x: W + sw, w: sw, h: sh, yOff: 0, hue: Math.random() * 360 });
      } else {
        var bw = (32 + Math.random() * 26) * scale, bh = (38 + Math.random() * 46) * scale;
        state.obstacles.push({ type: "block", x: W + bw, w: bw, h: bh, yOff: 0, hue: Math.random() * 360 });
      }
    }

    function spawnCoins() {
      var n = 3 + Math.floor(Math.random() * 3);
      var yOff = (58 + Math.random() * 92) * scale;
      var startX = W + 50 * scale;
      for (var i = 0; i < n; i++) {
        state.coins.push({ x: startX + i * 40 * scale, yOff: yOff, r: 10 * scale, phase: Math.random() * 6.28 });
      }
    }

    function boxOverlap(a, b) {
      return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
    }

    function playerBox() {
      var pad = 5 * scale;
      return { x: player.x + pad, y: groundY() - player.yOff - player.h + pad,
               w: player.w - pad * 2, h: player.h - pad * 2 };
    }

    function obstacleBox(o) {
      var pad = 4 * scale;
      var bottom = groundY() - o.yOff;
      return { x: o.x + pad, y: bottom - o.h + pad, w: o.w - pad * 2, h: o.h - pad * 2 };
    }

    function coinHit(c) {
      var box = playerBox();
      var cy = groundY() - c.yOff;
      var nx = Math.max(box.x, Math.min(c.x, box.x + box.w));
      var ny = Math.max(box.y, Math.min(cy, box.y + box.h));
      var dx = c.x - nx, dy = cy - ny;
      return dx * dx + dy * dy < c.r * c.r;
    }

    function update(dt) {
      if (state.restartLock > 0) state.restartLock -= dt;
      if (state.shake > 0) state.shake = Math.max(0, state.shake - dt * 34);

      if (state.running) {
        state.t += dt;
        state.speed = Math.min(1050 * scale, state.speed + 14 * scale * dt);
        state.scroll += state.speed * dt;
        state.score += state.speed * dt * 0.022;

        if (!player.onGround) {
          player.vy -= 3200 * scale * dt;
          player.yOff += player.vy * dt;
          if (player.yOff <= 0) {
            player.yOff = 0;
            player.vy = 0;
            player.onGround = true;
            dust(player.x + player.w / 2, groundY(), 5);
          }
        }

        state.spawnTimer -= dt;
        if (state.spawnTimer <= 0) {
          spawnObstacle();
          var gap = (340 + Math.random() * 330) * scale;
          state.spawnTimer = Math.max(gap / state.speed, 0.5);
        }

        state.coinTimer -= dt;
        if (state.coinTimer <= 0) {
          spawnCoins();
          state.coinTimer = 1.6 + Math.random() * 2.4;
        }

        var i;
        for (i = 0; i < state.obstacles.length; i++) state.obstacles[i].x -= state.speed * dt;
        state.obstacles = state.obstacles.filter(function (o) { return o.x + o.w > -60; });

        for (i = 0; i < state.coins.length; i++) {
          state.coins[i].x -= state.speed * dt;
          state.coins[i].phase += dt * 6;
        }
        state.coins = state.coins.filter(function (c) { return c.x + c.r > -40; });

        var pb = playerBox();
        for (i = 0; i < state.obstacles.length; i++) {
          if (boxOverlap(pb, obstacleBox(state.obstacles[i]))) { gameOver(); break; }
        }

        if (state.running) {
          for (i = state.coins.length - 1; i >= 0; i--) {
            if (coinHit(state.coins[i])) {
              var c = state.coins[i];
              sparkle(c.x, groundY() - c.yOff, (state.t * 120) % 360);
              state.score += 10;
              state.coins.splice(i, 1);
            }
          }
        }
      } else {
        state.t += dt;
        state.scroll += 70 * dt;
      }

      updateParticles(dt);
    }

    function roundRect(x, y, w, h, r) {
      r = Math.min(r, w / 2, h / 2);
      ctx.beginPath();
      ctx.moveTo(x + r, y);
      ctx.arcTo(x + w, y, x + w, y + h, r);
      ctx.arcTo(x + w, y + h, x, y + h, r);
      ctx.arcTo(x, y + h, x, y, r);
      ctx.arcTo(x, y, x + w, y, r);
      ctx.closePath();
    }

    function drawSky() {
      var hue = (state.scroll * 0.02 + state.t * 7) % 360;
      var g = ctx.createLinearGradient(0, 0, 0, H);
      g.addColorStop(0, "hsl(" + hue + ",65%,10%)");
      g.addColorStop(0.45, "hsl(" + ((hue + 45) % 360) + ",72%,24%)");
      g.addColorStop(1, "hsl(" + ((hue + 90) % 360) + ",78%,46%)");
      ctx.fillStyle = g;
      ctx.fillRect(-60, -60, W + 120, H + 120);

      var sx = W * 0.74, sy = H * 0.26, sr = 62 * scale;
      var sh = (hue + 60) % 360;
      var sg = ctx.createRadialGradient(sx, sy, 0, sx, sy, sr * 3.2);
      sg.addColorStop(0, "hsla(" + sh + ",100%,78%,0.95)");
      sg.addColorStop(0.25, "hsla(" + sh + ",100%,62%,0.35)");
      sg.addColorStop(1, "hsla(" + sh + ",100%,62%,0)");
      ctx.fillStyle = sg;
      ctx.beginPath();
      ctx.arc(sx, sy, sr * 3.2, 0, Math.PI * 2);
      ctx.fill();
      ctx.fillStyle = "hsl(" + sh + ",100%,80%)";
      ctx.beginPath();
      ctx.arc(sx, sy, sr * 0.72, 0, Math.PI * 2);
      ctx.fill();
    }

    function drawHills(factor, baseY, radius, hue, light, alpha) {
      ctx.globalAlpha = alpha;
      ctx.fillStyle = "hsl(" + hue + ",65%," + light + "%)";
      var spacing = radius * 1.45;
      var off = (state.scroll * factor) % spacing;
      for (var x = -spacing - off; x < W + spacing * 2; x += spacing) {
        ctx.beginPath();
        ctx.arc(x, baseY, radius, Math.PI, 0);
        ctx.fill();
      }
      ctx.globalAlpha = 1;
    }

    function drawGround() {
      var gy = groundY();
      var hue = (state.scroll * 0.02 + state.t * 7) % 360;
      var g = ctx.createLinearGradient(0, gy, 0, H);
      g.addColorStop(0, "hsl(" + ((hue + 200) % 360) + ",55%,16%)");
      g.addColorStop(1, "hsl(" + ((hue + 230) % 360) + ",60%,7%)");
      ctx.fillStyle = g;
      ctx.fillRect(-40, gy, W + 80, H - gy + 60);

      ctx.save();
      ctx.shadowColor = "hsl(" + ((hue + 90) % 360) + ",100%,65%)";
      ctx.shadowBlur = 20 * scale;
      ctx.fillStyle = "hsl(" + ((hue + 90) % 360) + ",100%,72%)";
      ctx.fillRect(-40, gy - 2 * scale, W + 80, 3 * scale);
      ctx.restore();

      var spacing = 82 * scale;
      var off = state.scroll % spacing;
      ctx.fillStyle = "hsla(" + ((hue + 120) % 360) + ",100%,70%,0.35)";
      for (var x = -off; x < W + spacing; x += spacing) {
        ctx.fillRect(x, gy + 16 * scale, 42 * scale, 4 * scale);
      }
    }

    function drawObstacles() {
      var gy = groundY();
      for (var i = 0; i < state.obstacles.length; i++) {
        var o = state.obstacles[i];
        var bottom = gy - o.yOff;
        var top = bottom - o.h;

        ctx.save();
        ctx.shadowColor = "hsl(" + o.hue + ",100%,60%)";
        ctx.shadowBlur = 18 * scale;

        if (o.type === "spike") {
          var g = ctx.createLinearGradient(0, top, 0, bottom);
          g.addColorStop(0, "hsl(" + o.hue + ",100%,72%)");
          g.addColorStop(1, "hsl(" + ((o.hue + 45) % 360) + ",90%,45%)");
          ctx.fillStyle = g;
          ctx.beginPath();
          ctx.moveTo(o.x, bottom);
          ctx.lineTo(o.x + o.w / 2, top);
          ctx.lineTo(o.x + o.w, bottom);
          ctx.closePath();
          ctx.fill();
        } else if (o.type === "block") {
          var g2 = ctx.createLinearGradient(o.x, top, o.x + o.w, bottom);
          g2.addColorStop(0, "hsl(" + o.hue + ",95%,68%)");
          g2.addColorStop(1, "hsl(" + ((o.hue + 50) % 360) + ",90%,42%)");
          ctx.fillStyle = g2;
          roundRect(o.x, top, o.w, o.h, 7 * scale);
          ctx.fill();
          ctx.fillStyle = "hsla(0,0%,100%,0.35)";
          roundRect(o.x + 5 * scale, top + 5 * scale, o.w - 10 * scale, 5 * scale, 3 * scale);
          ctx.fill();
        } else {
          var g3 = ctx.createLinearGradient(0, top, 0, bottom);
          g3.addColorStop(0, "hsl(" + o.hue + ",100%,75%)");
          g3.addColorStop(1, "hsl(" + ((o.hue + 60) % 360) + ",95%,50%)");
          ctx.fillStyle = g3;
          roundRect(o.x, top, o.w, o.h, o.h / 2);
          ctx.fill();
        }
        ctx.restore();
      }
    }

    function drawCoins() {
      var gy = groundY();
      for (var i = 0; i < state.coins.length; i++) {
        var c = state.coins[i];
        var cy = gy - c.yOff + Math.sin(c.phase) * 5 * scale;
        var hue = (state.t * 120 + i * 25) % 360;

        ctx.save();
        ctx.shadowColor = "hsl(" + hue + ",100%,65%)";
        ctx.shadowBlur = 18 * scale;
        var cg = ctx.createRadialGradient(c.x - 3 * scale, cy - 3 * scale, 1, c.x, cy, c.r);
        cg.addColorStop(0, "hsl(" + hue + ",100%,88%)");
        cg.addColorStop(1, "hsl(" + ((hue + 50) % 360) + ",100%,55%)");
        ctx.fillStyle = cg;
        ctx.beginPath();
        ctx.arc(c.x, cy, c.r, 0, Math.PI * 2);
        ctx.fill();
        ctx.restore();
      }
    }

    function drawPlayer() {
      var gy = groundY();
      var top = gy - player.yOff - player.h;
      var hue = (state.t * 90) % 360;

      ctx.save();
      ctx.shadowColor = "hsl(" + hue + ",100%,62%)";
      ctx.shadowBlur = 26 * scale;
      var pg = ctx.createLinearGradient(player.x, top, player.x + player.w, top + player.h);
      pg.addColorStop(0, "hsl(" + hue + ",100%,70%)");
      pg.addColorStop(1, "hsl(" + ((hue + 70) % 360) + ",100%,55%)");
      ctx.fillStyle = pg;
      roundRect(player.x, top, player.w, player.h, 12 * scale);
      ctx.fill();
      ctx.restore();

      var eyeY = top + player.h * 0.38;
      ctx.fillStyle = "#ffffff";
      ctx.beginPath();
      ctx.arc(player.x + player.w * 0.62, eyeY, 5 * scale, 0, Math.PI * 2);
      ctx.arc(player.x + player.w * 0.88, eyeY, 4.2 * scale, 0, Math.PI * 2);
      ctx.fill();
      ctx.fillStyle = "#101025";
      ctx.beginPath();
      ctx.arc(player.x + player.w * 0.66, eyeY + 1 * scale, 2.4 * scale, 0, Math.PI * 2);
      ctx.arc(player.x + player.w * 0.91, eyeY + 1 * scale, 2 * scale, 0, Math.PI * 2);
      ctx.fill();
    }

    function drawParticles() {
      for (var i = 0; i < state.particles.length; i++) {
        var p = state.particles[i];
        var a = Math.max(0, p.life / p.maxLife);
        ctx.globalAlpha = a;
        ctx.fillStyle = "hsl(" + p.hue + ",100%,68%)";
        ctx.beginPath();
        ctx.arc(p.x, p.y, p.size * (0.4 + a * 0.9), 0, Math.PI * 2);
        ctx.fill();
      }
      ctx.globalAlpha = 1;
    }

    function drawHUD() {
      var pad = Math.max(16, W * 0.05);
      var y = pad + 24 * scale;
      var fs = Math.max(20, 26 * scale);

      ctx.save();
      ctx.font = "800 " + fs + "px system-ui, sans-serif";
      ctx.textAlign = "left";
      ctx.textBaseline = "middle";
      ctx.shadowColor = "rgba(0,0,0,0.55)";
      ctx.shadowBlur = 8;
      ctx.fillStyle = "#fff";
      ctx.fillText(String(Math.floor(state.score)), pad, y);

      ctx.textAlign = "right";
      ctx.font = "600 " + (fs * 0.6) + "px system-ui, sans-serif";
      ctx.fillStyle = "rgba(255,255,255,0.75)";
      ctx.fillText("BEST " + state.best, W - pad, y);
      ctx.restore();
    }

    function drawOverlay() {
      if (state.running) return;
      ctx.save();
      ctx.fillStyle = "rgba(8,10,26,0.62)";
      ctx.fillRect(0, 0, W, H);
      var cx = W / 2, cy = H * 0.46;
      var hue = (state.t * 60) % 360;
      ctx.textAlign = "center";
      ctx.textBaseline = "middle";

      if (state.over) {
        ctx.font = "900 " + Math.max(34, 52 * scale) + "px system-ui, sans-serif";
        ctx.fillStyle = "hsl(" + hue + ",100%,68%)";
        ctx.shadowColor = "hsl(" + hue + ",100%,60%)";
        ctx.shadowBlur = 26 * scale;
        ctx.fillText("GAME OVER", cx, cy - 60 * scale);
        ctx.shadowBlur = 0;
        ctx.fillStyle = "#fff";
        ctx.font = "800 " + Math.max(26, 38 * scale) + "px system-ui, sans-serif";
        ctx.fillText("Score " + Math.floor(state.score), cx, cy + 4 * scale);
        ctx.fillStyle = "rgba(255,255,255,0.72)";
        ctx.font = "600 " + Math.max(15, 19 * scale) + "px system-ui, sans-serif";
        ctx.fillText("Best " + state.best, cx, cy + 44 * scale);
        ctx.fillStyle = "rgba(255,255,255," + (0.55 + 0.45 * Math.sin(state.t * 5)) + ")";
        ctx.font = "700 " + Math.max(16, 20 * scale) + "px system-ui, sans-serif";
        ctx.fillText("Tap or press Space to play again", cx, cy + 100 * scale);
      } else {
        ctx.font = "900 " + Math.max(36, 60 * scale) + "px system-ui, sans-serif";
        var tg = ctx.createLinearGradient(cx - 180 * scale, 0, cx + 180 * scale, 0);
        tg.addColorStop(0, "hsl(" + hue + ",100%,70%)");
        tg.addColorStop(1, "hsl(" + ((hue + 90) % 360) + ",100%,68%)");
        ctx.fillStyle = tg;
        ctx.shadowColor = "hsl(" + hue + ",100%,60%)";
        ctx.shadowBlur = 30 * scale;
        ctx.fillText("NEON RUNNER", cx, cy - 70 * scale);
        ctx.shadowBlur = 0;
        ctx.fillStyle = "#fff";
        ctx.font = "700 " + Math.max(17, 22 * scale) + "px system-ui, sans-serif";
        ctx.fillText("Tap / Space to Start", cx, cy + 10 * scale);
        ctx.fillStyle = "rgba(255,255,255,0.7)";
        ctx.font = "600 " + Math.max(14, 17 * scale) + "px system-ui, sans-serif";
        ctx.fillText("Tap = Jump   •   Desktop: Space", cx, cy + 56 * scale);
      }
      ctx.restore();
    }

    function render() {
      ctx.save();
      if (state.shake > 0.3) {
        ctx.translate((Math.random() - 0.5) * state.shake, (Math.random() - 0.5) * state.shake);
      }
      var hue = (state.scroll * 0.02 + state.t * 7) % 360;
      var gy = groundY();
      drawSky();
      drawHills(0.10, gy + 30 * scale, 160 * scale, (hue + 210) % 360, 30, 0.85);
      drawHills(0.22, gy + 20 * scale, 110 * scale, (hue + 250) % 360, 22, 0.9);
      drawHills(0.42, gy + 12 * scale, 70 * scale, (hue + 290) % 360, 15, 0.95);
      drawGround();
      drawCoins();
      drawObstacles();
      drawPlayer();
      drawParticles();
      ctx.restore();
      drawHUD();
      drawOverlay();
    }

    canvas.addEventListener("touchstart", function (e) {
      e.preventDefault();
      if (!state.running) { if (state.restartLock <= 0) resetGame(); return; }
      jump();
    }, { passive: false });

    canvas.addEventListener("mousedown", function (e) {
      e.preventDefault();
      if (!state.running) { if (state.restartLock <= 0) resetGame(); return; }
      jump();
    });

    window.addEventListener("keydown", function (e) {
      if (e.code === "Space" || e.code === "ArrowUp" || e.code === "KeyW") {
        e.preventDefault();
        if (!state.running) { if (state.restartLock <= 0) resetGame(); return; }
        jump();
      }
    });

    window.addEventListener("resize", resize);
    window.addEventListener("orientationchange", function () { setTimeout(resize, 120); });

    var last = 0;
    function loop(now) {
      if (!last) last = now;
      var dt = (now - last) / 1000;
      last = now;
      if (dt > 0.05) dt = 0.05;
      if (dt < 0) dt = 0;
      update(dt);
      render();
      requestAnimationFrame(loop);
    }

    resize();
    requestAnimationFrame(loop);
  })();
  </script>
</body>
</html>

Game Source: Neon Runner

Creator: LaserBuilder68

Libraries: none

Complexity: complex (581 lines, 19.6 KB)

The full source code is displayed above on this page.

Remix Instructions

To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: neon-runner-laserbuilder68" to link back to the original. Then publish at arcadelab.ai/publish.