← back to Games Agentabrams

games/racer/NeonDrifter.aa

883 lines

<!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" />
<title>NEON DRIFTER</title>
<style>
  :root {
    --bg: #0a0e14;
    --neon: #ff2fd0;
    --cyan: #22e7ff;
    --amber: #ffd23f;
    --edge: #7a3cff;
  }
  * { box-sizing: border-box; margin: 0; padding: 0; }
  html, body {
    width: 100%; height: 100%;
    background: var(--bg);
    overflow: hidden;
    touch-action: none;
    -webkit-user-select: none;
    user-select: none;
    -webkit-tap-highlight-color: transparent;
    font-family: "Segoe UI", Helvetica, Arial, sans-serif;
  }
  #wrap {
    position: fixed;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    background: radial-gradient(ellipse at 50% 120%, #16203a 0%, #0a0e14 60%, #05070c 100%);
  }
  canvas { display: block; touch-action: none; }

  /* Touch controls */
  .touch {
    position: fixed;
    bottom: 0;
    width: 40%;
    height: 45%;
    max-height: 320px;
    z-index: 20;
    display: none;
    align-items: flex-end;
    justify-content: center;
    padding-bottom: 22px;
    -webkit-user-select: none;
    user-select: none;
  }
  .touch.left { left: 0; }
  .touch.right { right: 0; }
  .touch .btn {
    width: 92px; height: 92px;
    border-radius: 50%;
    border: 2px solid rgba(34,231,255,0.55);
    background: rgba(34,231,255,0.08);
    color: #cffcff;
    font-size: 40px;
    line-height: 88px;
    text-align: center;
    box-shadow: 0 0 18px rgba(34,231,255,0.35), inset 0 0 14px rgba(34,231,255,0.15);
    backdrop-filter: blur(2px);
  }
  .touch.active .btn {
    background: rgba(255,47,208,0.22);
    border-color: rgba(255,47,208,0.8);
    box-shadow: 0 0 24px rgba(255,47,208,0.6), inset 0 0 16px rgba(255,47,208,0.25);
  }
  @media (pointer: coarse) {
    .touch { display: flex; }
  }
</style>
</head>
<body>
  <div id="wrap">
    <canvas id="game"></canvas>
  </div>
  <div class="touch left" id="touchLeft"><div class="btn">&#8592;</div></div>
  <div class="touch right" id="touchRight"><div class="btn">&#8594;</div></div>

<script>
(function () {
  "use strict";

  function boot() {
    var canvas = document.getElementById("game");
    if (!canvas) return;
    var ctx = canvas.getContext("2d");
    if (!ctx) return;

    var BEST_KEY = "abrams-racer-best";

    // ---- logical dimensions (portrait road) ----
    var W = 480, H = 800;   // logical
    var DPR = 1;

    function fitCanvas() {
      var vw = Math.max(1, window.innerWidth || document.documentElement.clientWidth || 480);
      var vh = Math.max(1, window.innerHeight || document.documentElement.clientHeight || 800);
      var targetAR = W / H;
      var winAR = vw / vh;
      var cssW, cssH;
      if (winAR > targetAR) { cssH = vh; cssW = vh * targetAR; }
      else { cssW = vw; cssH = vw / targetAR; }
      DPR = Math.min(window.devicePixelRatio || 1, 2);
      canvas.style.width = cssW + "px";
      canvas.style.height = cssH + "px";
      canvas.width = Math.round(W * DPR);
      canvas.height = Math.round(H * DPR);
      ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
    }
    window.addEventListener("resize", fitCanvas, { passive: true });
    window.addEventListener("orientationchange", fitCanvas, { passive: true });
    fitCanvas();

    // ---- road geometry ----
    var LANES = 4;
    var ROAD_MARGIN = 56;              // side margin to road edge
    var roadLeft = ROAD_MARGIN;
    var roadRight = W - ROAD_MARGIN;
    var roadW = roadRight - roadLeft;
    var laneW = roadW / LANES;

    function laneCenter(i) { return roadLeft + laneW * (i + 0.5); }

    // ---- game state ----
    var STATE_MENU = 0, STATE_PLAY = 1, STATE_OVER = 2;
    var state = STATE_MENU;

    var best = 0;
    try {
      var stored = localStorage.getItem(BEST_KEY);
      if (stored != null) { var b = parseFloat(stored); if (isFinite(b)) best = b; }
    } catch (e) {}

    var player, traffic, pickups, particles;
    var dashOffset, scroll, distance, score, speed, baseSpeed, elapsed, spawnTimer, pickTimer;
    var boost, shakeMag, newBest;

    function resetGame() {
      player = {
        x: laneCenter(1),
        y: H - 150,
        w: 46, h: 84,
        vx: 0,
        color: "#22e7ff"
      };
      traffic = [];
      pickups = [];
      particles = [];
      dashOffset = 0;
      scroll = 0;
      distance = 0;
      score = 0;
      baseSpeed = 260;      // px/sec world scroll
      speed = baseSpeed;
      elapsed = 0;
      spawnTimer = 0.6;
      pickTimer = 3.0;
      boost = 0;
      shakeMag = 0;
      newBest = false;
    }
    resetGame();

    // ---- input ----
    var keys = { left: false, right: false, up: false, down: false };
    var touchL = false, touchR = false;

    function onKeyDown(e) {
      var k = e.key;
      var handled = true;
      if (k === "ArrowLeft" || k === "a" || k === "A") keys.left = true;
      else if (k === "ArrowRight" || k === "d" || k === "D") keys.right = true;
      else if (k === "ArrowUp" || k === "w" || k === "W") keys.up = true;
      else if (k === "ArrowDown" || k === "s" || k === "S") keys.down = true;
      else if (k === " " || k === "Enter") {
        if (state === STATE_MENU) startPlay();
        else if (state === STATE_OVER) { resetGame(); startPlay(); }
      } else handled = false;

      if (handled) {
        // resume/init audio on first gesture
        ensureAudio();
        if (state === STATE_MENU && (k === "ArrowLeft" || k === "ArrowRight" || k === "a" || k === "A" || k === "d" || k === "D")) {
          startPlay();
        }
        e.preventDefault();
      }
    }
    function onKeyUp(e) {
      var k = e.key;
      if (k === "ArrowLeft" || k === "a" || k === "A") keys.left = false;
      else if (k === "ArrowRight" || k === "d" || k === "D") keys.right = false;
      else if (k === "ArrowUp" || k === "w" || k === "W") keys.up = false;
      else if (k === "ArrowDown" || k === "s" || k === "S") keys.down = false;
    }
    window.addEventListener("keydown", onKeyDown, { passive: false });
    window.addEventListener("keyup", onKeyUp, { passive: false });

    // touch buttons
    function bindTouch(el, setter) {
      if (!el) return;
      function down(e) {
        e.preventDefault();
        ensureAudio();
        el.classList.add("active");
        setter(true);
        if (state === STATE_MENU) startPlay();
        else if (state === STATE_OVER) { resetGame(); startPlay(); }
      }
      function up(e) {
        e.preventDefault();
        el.classList.remove("active");
        setter(false);
      }
      el.addEventListener("touchstart", down, { passive: false });
      el.addEventListener("touchend", up, { passive: false });
      el.addEventListener("touchcancel", up, { passive: false });
      el.addEventListener("mousedown", down);
      window.addEventListener("mouseup", up);
    }
    bindTouch(document.getElementById("touchLeft"), function (v) { touchL = v; });
    bindTouch(document.getElementById("touchRight"), function (v) { touchR = v; });

    // tap canvas to start/restart
    canvas.addEventListener("pointerdown", function (e) {
      ensureAudio();
      if (state === STATE_MENU) { e.preventDefault(); startPlay(); }
      else if (state === STATE_OVER) { e.preventDefault(); resetGame(); startPlay(); }
    });

    // prevent scroll gestures
    document.addEventListener("touchmove", function (e) { e.preventDefault(); }, { passive: false });

    // ---- audio (lazy) ----
    var actx = null, engineOsc = null, engineGain = null, engineFilter = null;
    function ensureAudio() {
      try {
        if (!actx) {
          var AC = window.AudioContext || window.webkitAudioContext;
          if (!AC) return;
          actx = new AC();
          engineGain = actx.createGain();
          engineGain.gain.value = 0.0;
          engineFilter = actx.createBiquadFilter();
          engineFilter.type = "lowpass";
          engineFilter.frequency.value = 700;
          engineOsc = actx.createOscillator();
          engineOsc.type = "sawtooth";
          engineOsc.frequency.value = 70;
          engineOsc.connect(engineFilter);
          engineFilter.connect(engineGain);
          engineGain.connect(actx.destination);
          engineOsc.start();
        }
        if (actx.state === "suspended") actx.resume();
      } catch (e) { actx = null; }
    }
    function blip(freq, dur, type, vol) {
      if (!actx) return;
      try {
        var o = actx.createOscillator();
        var g = actx.createGain();
        o.type = type || "square";
        o.frequency.value = freq;
        g.gain.value = 0.0001;
        o.connect(g); g.connect(actx.destination);
        var t = actx.currentTime;
        g.gain.setValueAtTime(0.0001, t);
        g.gain.exponentialRampToValueAtTime(vol || 0.15, t + 0.01);
        g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
        o.start(t);
        o.stop(t + dur + 0.02);
      } catch (e) {}
    }
    function crashSound() {
      if (!actx) return;
      try {
        var o = actx.createOscillator();
        var g = actx.createGain();
        o.type = "sawtooth";
        o.frequency.setValueAtTime(220, actx.currentTime);
        o.frequency.exponentialRampToValueAtTime(40, actx.currentTime + 0.45);
        g.gain.setValueAtTime(0.3, actx.currentTime);
        g.gain.exponentialRampToValueAtTime(0.0001, actx.currentTime + 0.5);
        o.connect(g); g.connect(actx.destination);
        o.start();
        o.stop(actx.currentTime + 0.55);
      } catch (e) {}
    }
    function updateEngine() {
      if (!actx || !engineGain || !engineOsc) return;
      try {
        var target = state === STATE_PLAY ? 0.05 : 0.0;
        engineGain.gain.setTargetAtTime(target, actx.currentTime, 0.1);
        var f = 55 + (speed - baseSpeed) * 0.06 + (boost > 0 ? 40 : 0);
        engineOsc.frequency.setTargetAtTime(f, actx.currentTime, 0.08);
        engineFilter.frequency.setTargetAtTime(600 + (speed - 200) * 1.2, actx.currentTime, 0.1);
      } catch (e) {}
    }

    function startPlay() {
      if (state === STATE_PLAY) return;
      state = STATE_PLAY;
      ensureAudio();
      blip(660, 0.08, "square", 0.12);
    }

    // ---- spawning ----
    var TRAFFIC_COLORS = ["#ff2fd0", "#ffd23f", "#9d5bff", "#ff5a3c", "#3cff8a"];

    function spawnTraffic() {
      var lane = Math.floor(Math.random() * LANES);
      var w = 46, h = 84;
      // occasionally a wider slow truck
      if (Math.random() < 0.18) { w = 54; h = 110; }
      var carSpeed = 60 + Math.random() * 120; // slower than world; downward relative
      traffic.push({
        lane: lane,
        x: laneCenter(lane),
        y: -h - 20,
        w: w, h: h,
        rel: carSpeed,
        color: TRAFFIC_COLORS[(Math.random() * TRAFFIC_COLORS.length) | 0]
      });
    }

    function spawnPickup() {
      var lane = Math.floor(Math.random() * LANES);
      var type = Math.random() < 0.4 ? "boost" : "coin";
      pickups.push({
        x: laneCenter(lane),
        y: -30,
        r: 14,
        type: type,
        t: 0
      });
    }

    function laneOccupiedNear(lane, y, minGap) {
      for (var i = 0; i < traffic.length; i++) {
        var t = traffic[i];
        if (t.lane === lane && Math.abs(t.y - y) < minGap) return true;
      }
      return false;
    }

    // ---- particles ----
    function addParticles(x, y, color, n, spread) {
      for (var i = 0; i < n; i++) {
        particles.push({
          x: x, y: y,
          vx: (Math.random() - 0.5) * spread,
          vy: (Math.random() - 0.5) * spread - 30,
          life: 0.4 + Math.random() * 0.5,
          age: 0,
          color: color,
          size: 2 + Math.random() * 3
        });
      }
    }

    // ---- collision ----
    function rectsOverlap(ax, ay, aw, ah, bx, by, bw, bh) {
      return Math.abs(ax - bx) * 2 < (aw + bw) && Math.abs(ay - by) * 2 < (ah + bh);
    }

    // ---- update ----
    function update(dt) {
      if (state === STATE_PLAY) {
        elapsed += dt;
        // speed ramps over time
        baseSpeed = 260 + elapsed * 9;
        if (baseSpeed > 720) baseSpeed = 720;
        var boostFactor = boost > 0 ? 1.55 : 1;
        // throttle (up/down) small modifier
        var throttle = 1;
        if (keys.up) throttle = 1.18;
        else if (keys.down) throttle = 0.72;
        speed = baseSpeed * boostFactor * throttle;

        if (boost > 0) boost -= dt;

        // steering
        var steerLeft = keys.left || touchL;
        var steerRight = keys.right || touchR;
        var accel = 900;
        if (steerLeft && !steerRight) player.vx -= accel * dt;
        else if (steerRight && !steerLeft) player.vx += accel * dt;
        else player.vx *= Math.pow(0.001, dt); // strong damping when no input
        // clamp lateral speed
        var maxVx = 420;
        if (player.vx > maxVx) player.vx = maxVx;
        if (player.vx < -maxVx) player.vx = -maxVx;
        player.x += player.vx * dt;
        // road clamp
        var half = player.w / 2 + 4;
        if (player.x < roadLeft + half) { player.x = roadLeft + half; player.vx = 0; }
        if (player.x > roadRight - half) { player.x = roadRight - half; player.vx = 0; }

        // scroll + distance
        scroll += speed * dt;
        dashOffset = (dashOffset + speed * dt) % 80;
        distance += speed * dt * 0.02; // meters-ish
        score += speed * dt * 0.05;

        // spawn traffic
        spawnTimer -= dt;
        var spawnEvery = Math.max(0.35, 1.1 - elapsed * 0.01);
        if (spawnTimer <= 0) {
          spawnTimer = spawnEvery * (0.6 + Math.random() * 0.8);
          // try a lane that isn't crowded near top
          var tries = 3;
          while (tries-- > 0) {
            var lane = Math.floor(Math.random() * LANES);
            if (!laneOccupiedNear(lane, -60, 160)) {
              var w = 46, h = 84;
              if (Math.random() < 0.18) { w = 54; h = 110; }
              traffic.push({
                lane: lane, x: laneCenter(lane), y: -h - 20,
                w: w, h: h, rel: 60 + Math.random() * 120,
                color: TRAFFIC_COLORS[(Math.random() * TRAFFIC_COLORS.length) | 0]
              });
              break;
            }
          }
        }

        // spawn pickups
        pickTimer -= dt;
        if (pickTimer <= 0) {
          pickTimer = 3.5 + Math.random() * 3.5;
          var plane = Math.floor(Math.random() * LANES);
          if (!laneOccupiedNear(plane, -40, 120)) {
            pickups.push({
              x: laneCenter(plane), y: -30, r: 14,
              type: Math.random() < 0.4 ? "boost" : "coin", t: 0
            });
          }
        }

        // move traffic (world scrolls down; traffic moves down toward player)
        for (var i = traffic.length - 1; i >= 0; i--) {
          var t = traffic[i];
          t.y += (speed - t.rel) * dt;   // relative to player
          // engine exhaust trail
          if (Math.random() < 0.3) addParticles(t.x, t.y - t.h / 2, "rgba(120,140,180,0.4)", 1, 20);
          if (t.y - t.h > H + 40) { traffic.splice(i, 1); continue; }
          // collision
          if (rectsOverlap(player.x, player.y, player.w - 8, player.h - 10, t.x, t.y, t.w - 8, t.h - 10)) {
            gameOver();
            return;
          }
        }

        // move pickups
        for (var j = pickups.length - 1; j >= 0; j--) {
          var p = pickups[j];
          p.y += speed * dt;
          p.t += dt;
          if (p.y - p.r > H + 40) { pickups.splice(j, 1); continue; }
          var dx = p.x - player.x, dy = p.y - player.y;
          if (Math.abs(dx) < (player.w / 2 + p.r) && Math.abs(dy) < (player.h / 2 + p.r)) {
            if (p.type === "boost") {
              boost = 2.2;
              score += 50;
              addParticles(p.x, p.y, "#22e7ff", 18, 220);
              blip(880, 0.14, "sawtooth", 0.16);
              blip(1320, 0.14, "sawtooth", 0.1);
            } else {
              score += 100;
              addParticles(p.x, p.y, "#ffd23f", 14, 180);
              blip(1046, 0.09, "square", 0.14);
            }
            pickups.splice(j, 1);
          }
        }

        // player exhaust / speed particles
        if (Math.random() < (boost > 0 ? 0.9 : 0.4)) {
          addParticles(player.x + (Math.random() - 0.5) * 18, player.y + player.h / 2,
            boost > 0 ? "#22e7ff" : "rgba(255,120,220,0.6)", 1, 40);
        }
      }

      // particles always update
      for (var k = particles.length - 1; k >= 0; k--) {
        var pt = particles[k];
        pt.age += dt;
        if (pt.age >= pt.life) { particles.splice(k, 1); continue; }
        pt.x += pt.vx * dt;
        pt.y += pt.vy * dt;
        pt.vy += 60 * dt;
      }

      // shake decay
      if (shakeMag > 0) { shakeMag -= dt * 40; if (shakeMag < 0) shakeMag = 0; }

      updateEngine();
    }

    function gameOver() {
      state = STATE_OVER;
      shakeMag = 18;
      addParticles(player.x, player.y, "#ff5a3c", 40, 400);
      addParticles(player.x, player.y, "#ffd23f", 30, 320);
      crashSound();
      if (distance > best) {
        best = distance;
        newBest = true;
        try { localStorage.setItem(BEST_KEY, String(Math.floor(best))); } catch (e) {}
      }
    }

    // ---- rendering ----
    function drawCar(x, y, w, h, color, isPlayer) {
      ctx.save();
      ctx.translate(x, y);
      // glow
      ctx.shadowColor = color;
      ctx.shadowBlur = isPlayer ? 22 : 14;
      // body
      ctx.fillStyle = color;
      roundRect(-w / 2, -h / 2, w, h, 10);
      ctx.fill();
      ctx.shadowBlur = 0;
      // cockpit / windows
      ctx.fillStyle = "rgba(6,10,20,0.85)";
      roundRect(-w / 2 + 7, -h / 2 + 12, w - 14, h * 0.28, 6);
      ctx.fill();
      roundRect(-w / 2 + 7, h / 2 - h * 0.32, w - 14, h * 0.20, 6);
      ctx.fill();
      // center stripe
      ctx.fillStyle = "rgba(255,255,255,0.18)";
      ctx.fillRect(-2, -h / 2 + 6, 4, h - 12);
      // headlights/taillights
      if (isPlayer) {
        ctx.fillStyle = "#fff6c8";
        ctx.fillRect(-w / 2 + 5, -h / 2 + 2, 8, 5);
        ctx.fillRect(w / 2 - 13, -h / 2 + 2, 8, 5);
      } else {
        ctx.fillStyle = "#ff3b3b";
        ctx.shadowColor = "#ff3b3b"; ctx.shadowBlur = 8;
        ctx.fillRect(-w / 2 + 5, h / 2 - 7, 8, 5);
        ctx.fillRect(w / 2 - 13, h / 2 - 7, 8, 5);
        ctx.shadowBlur = 0;
      }
      ctx.restore();
    }

    function roundRect(x, y, w, h, r) {
      if (r > w / 2) r = w / 2;
      if (r > h / 2) r = 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 draw() {
      ctx.save();
      // screen shake
      if (shakeMag > 0) {
        ctx.translate((Math.random() - 0.5) * shakeMag, (Math.random() - 0.5) * shakeMag);
      }

      // background
      ctx.fillStyle = "#05070c";
      ctx.fillRect(-40, -40, W + 80, H + 80);

      // side "grass"/gradient synth glow bands
      var g = ctx.createLinearGradient(0, 0, 0, H);
      g.addColorStop(0, "#0c1226");
      g.addColorStop(1, "#141a33");
      ctx.fillStyle = g;
      ctx.fillRect(-40, -40, W + 80, H + 80);

      // road surface
      var rg = ctx.createLinearGradient(0, 0, 0, H);
      rg.addColorStop(0, "#12141f");
      rg.addColorStop(1, "#1a1d2e");
      ctx.fillStyle = rg;
      ctx.fillRect(roadLeft, -40, roadW, H + 80);

      // glowing road edges
      drawGlowLine(roadLeft, 0, roadLeft, H, "#ff2fd0", 4);
      drawGlowLine(roadRight, 0, roadRight, H, "#22e7ff", 4);

      // moving dashed lane markers
      ctx.save();
      ctx.lineWidth = 5;
      ctx.setLineDash([32, 48]);
      ctx.lineDashOffset = -dashOffset;
      ctx.strokeStyle = "rgba(180,200,255,0.55)";
      ctx.shadowColor = "rgba(120,150,255,0.5)";
      ctx.shadowBlur = 8;
      for (var li = 1; li < LANES; li++) {
        var lx = roadLeft + laneW * li;
        ctx.beginPath();
        ctx.moveTo(lx, -40);
        ctx.lineTo(lx, H + 40);
        ctx.stroke();
      }
      ctx.restore();

      // horizon glow lines (perspective illusion) faint moving cross-lines
      ctx.save();
      ctx.strokeStyle = "rgba(122,60,255,0.10)";
      ctx.lineWidth = 2;
      for (var hy = 0; hy < H + 80; hy += 80) {
        var yy = (hy - (dashOffset % 80));
        ctx.beginPath();
        ctx.moveTo(roadLeft, yy);
        ctx.lineTo(roadRight, yy);
        ctx.stroke();
      }
      ctx.restore();

      // pickups
      for (var i = 0; i < pickups.length; i++) {
        var p = pickups[i];
        var pulse = 1 + Math.sin(p.t * 8) * 0.12;
        ctx.save();
        ctx.translate(p.x, p.y);
        ctx.scale(pulse, pulse);
        if (p.type === "boost") {
          ctx.shadowColor = "#22e7ff"; ctx.shadowBlur = 18;
          ctx.fillStyle = "#22e7ff";
          // lightning bolt
          ctx.beginPath();
          ctx.moveTo(-4, -14); ctx.lineTo(6, -2); ctx.lineTo(0, -2);
          ctx.lineTo(4, 14); ctx.lineTo(-6, 0); ctx.lineTo(0, 0);
          ctx.closePath();
          ctx.fill();
        } else {
          ctx.shadowColor = "#ffd23f"; ctx.shadowBlur = 16;
          ctx.fillStyle = "#ffd23f";
          ctx.beginPath();
          ctx.arc(0, 0, 12, 0, Math.PI * 2);
          ctx.fill();
          ctx.fillStyle = "rgba(120,80,0,0.55)";
          ctx.font = "bold 15px sans-serif";
          ctx.textAlign = "center";
          ctx.textBaseline = "middle";
          ctx.fillText("$", 0, 1);
        }
        ctx.restore();
      }

      // traffic
      for (var t = 0; t < traffic.length; t++) {
        var c = traffic[t];
        drawCar(c.x, c.y, c.w, c.h, c.color, false);
      }

      // particles
      for (var pp = 0; pp < particles.length; pp++) {
        var pt = particles[pp];
        var a = 1 - pt.age / pt.life;
        ctx.globalAlpha = a < 0 ? 0 : a;
        ctx.fillStyle = pt.color;
        ctx.fillRect(pt.x - pt.size / 2, pt.y - pt.size / 2, pt.size, pt.size);
      }
      ctx.globalAlpha = 1;

      // player
      if (state !== STATE_OVER || (Math.floor(elapsed * 10) % 2 === 0)) {
        // boost aura
        if (boost > 0) {
          ctx.save();
          ctx.globalAlpha = 0.5 + Math.sin(elapsed * 30) * 0.2;
          ctx.shadowColor = "#22e7ff"; ctx.shadowBlur = 30;
          ctx.fillStyle = "rgba(34,231,255,0.15)";
          roundRect(player.x - player.w / 2 - 6, player.y - player.h / 2 - 6, player.w + 12, player.h + 12, 12);
          ctx.fill();
          ctx.restore();
        }
        drawCar(player.x, player.y, player.w, player.h, player.color, true);
      }

      ctx.restore(); // shake

      // HUD
      drawHUD();

      // overlays
      if (state === STATE_MENU) drawMenu();
      else if (state === STATE_OVER) drawGameOver();
    }

    function drawGlowLine(x1, y1, x2, y2, color, width) {
      ctx.save();
      ctx.strokeStyle = color;
      ctx.lineWidth = width;
      ctx.shadowColor = color;
      ctx.shadowBlur = 16;
      ctx.beginPath();
      ctx.moveTo(x1, y1);
      ctx.lineTo(x2, y2);
      ctx.stroke();
      ctx.restore();
    }

    function drawHUD() {
      ctx.save();
      ctx.textBaseline = "top";
      // distance (big)
      ctx.textAlign = "left";
      ctx.fillStyle = "rgba(180,220,255,0.55)";
      ctx.font = "600 13px 'Segoe UI', sans-serif";
      ctx.fillText("DISTANCE", 18, 18);
      ctx.fillStyle = "#e8f6ff";
      ctx.shadowColor = "#22e7ff"; ctx.shadowBlur = 12;
      ctx.font = "800 30px 'Segoe UI', sans-serif";
      ctx.fillText(Math.floor(distance) + " m", 18, 32);
      ctx.shadowBlur = 0;

      // score (right)
      ctx.textAlign = "right";
      ctx.fillStyle = "rgba(255,180,240,0.55)";
      ctx.font = "600 13px 'Segoe UI', sans-serif";
      ctx.fillText("SCORE", W - 18, 18);
      ctx.fillStyle = "#ffe3f8";
      ctx.shadowColor = "#ff2fd0"; ctx.shadowBlur = 12;
      ctx.font = "800 30px 'Segoe UI', sans-serif";
      ctx.fillText(String(Math.floor(score)), W - 18, 32);
      ctx.shadowBlur = 0;

      // best (top center)
      ctx.textAlign = "center";
      ctx.fillStyle = "rgba(255,210,63,0.75)";
      ctx.font = "700 13px 'Segoe UI', sans-serif";
      ctx.fillText("BEST  " + Math.floor(best) + " m", W / 2, 22);

      // speedometer bar
      var spNorm = Math.min(1, (speed - 200) / 560);
      var barW = 160, barH = 8, barX = W / 2 - barW / 2, barY = 44;
      ctx.fillStyle = "rgba(255,255,255,0.08)";
      roundRect(barX, barY, barW, barH, 4); ctx.fill();
      var sg = ctx.createLinearGradient(barX, 0, barX + barW, 0);
      sg.addColorStop(0, "#22e7ff");
      sg.addColorStop(0.6, "#ff2fd0");
      sg.addColorStop(1, "#ff5a3c");
      ctx.fillStyle = sg;
      roundRect(barX, barY, Math.max(6, barW * spNorm), barH, 4); ctx.fill();

      if (boost > 0) {
        ctx.textAlign = "center";
        ctx.fillStyle = "#22e7ff";
        ctx.shadowColor = "#22e7ff"; ctx.shadowBlur = 14;
        ctx.font = "800 16px 'Segoe UI', sans-serif";
        ctx.fillText("⚡ BOOST", W / 2, 58);
        ctx.shadowBlur = 0;
      }
      ctx.restore();
    }

    function drawTitle(cx, cy) {
      ctx.save();
      ctx.textAlign = "center";
      ctx.textBaseline = "middle";
      // "NEON DRIFTER" uppercase, letter-spaced
      var title = "N E O N   D R I F T E R";
      ctx.font = "800 34px 'Segoe UI', sans-serif";
      ctx.fillStyle = "#22e7ff";
      ctx.shadowColor = "#ff2fd0"; ctx.shadowBlur = 24;
      ctx.fillText(title, cx, cy);
      ctx.shadowColor = "#22e7ff"; ctx.shadowBlur = 18;
      ctx.fillStyle = "#ff2fd0";
      ctx.fillText(title, cx + 1.5, cy + 1.5);
      ctx.restore();
    }

    function overlayPanel() {
      ctx.save();
      ctx.fillStyle = "rgba(5,7,12,0.72)";
      ctx.fillRect(0, 0, W, H);
      ctx.restore();
    }

    function drawMenu() {
      overlayPanel();
      drawTitle(W / 2, H * 0.32);
      ctx.save();
      ctx.textAlign = "center";
      ctx.textBaseline = "middle";
      ctx.fillStyle = "rgba(200,225,255,0.85)";
      ctx.font = "600 17px 'Segoe UI', sans-serif";
      ctx.fillText("Weave through neon traffic. Don't crash.", W / 2, H * 0.44);

      var pulse = 0.6 + Math.sin(elapsed * 4) * 0.4;
      ctx.globalAlpha = pulse;
      ctx.fillStyle = "#ffd23f";
      ctx.shadowColor = "#ffd23f"; ctx.shadowBlur = 14;
      ctx.font = "800 22px 'Segoe UI', sans-serif";
      ctx.fillText("TAP / PRESS  ← →  TO START", W / 2, H * 0.55);
      ctx.globalAlpha = 1;
      ctx.shadowBlur = 0;

      ctx.fillStyle = "rgba(160,190,230,0.6)";
      ctx.font = "500 14px 'Segoe UI', sans-serif";
      ctx.fillText("Arrows / A-D steer  •  Up/Down throttle", W / 2, H * 0.63);
      ctx.fillText("⚡ = boost   $ = coin", W / 2, H * 0.67);
      ctx.fillText("BEST  " + Math.floor(best) + " m", W / 2, H * 0.73);
      ctx.restore();
    }

    function drawGameOver() {
      overlayPanel();
      ctx.save();
      ctx.textAlign = "center";
      ctx.textBaseline = "middle";

      ctx.fillStyle = "#ff5a3c";
      ctx.shadowColor = "#ff2fd0"; ctx.shadowBlur = 22;
      ctx.font = "800 44px 'Segoe UI', sans-serif";
      ctx.fillText("CRASHED", W / 2, H * 0.30);
      ctx.shadowBlur = 0;

      if (newBest) {
        ctx.fillStyle = "#ffd23f";
        ctx.shadowColor = "#ffd23f"; ctx.shadowBlur = 16;
        ctx.font = "800 22px 'Segoe UI', sans-serif";
        ctx.fillText("★ NEW BEST ★", W / 2, H * 0.40);
        ctx.shadowBlur = 0;
      }

      ctx.fillStyle = "#e8f6ff";
      ctx.font = "700 26px 'Segoe UI', sans-serif";
      ctx.fillText(Math.floor(distance) + " m", W / 2, H * 0.47);
      ctx.fillStyle = "rgba(200,225,255,0.7)";
      ctx.font = "600 16px 'Segoe UI', sans-serif";
      ctx.fillText("SCORE  " + Math.floor(score), W / 2, H * 0.52);
      ctx.fillText("BEST  " + Math.floor(best) + " m", W / 2, H * 0.56);

      var pulse = 0.6 + Math.sin(elapsed * 5) * 0.4;
      ctx.globalAlpha = pulse;
      ctx.fillStyle = "#22e7ff";
      ctx.shadowColor = "#22e7ff"; ctx.shadowBlur = 14;
      ctx.font = "800 20px 'Segoe UI', sans-serif";
      ctx.fillText("TAP / PRESS SPACE TO RACE AGAIN", W / 2, H * 0.66);
      ctx.globalAlpha = 1;
      ctx.restore();
    }

    // ---- main loop ----
    var lastT = 0;
    function frame(ts) {
      if (!lastT) lastT = ts;
      var dt = (ts - lastT) / 1000;
      lastT = ts;
      if (dt > 0.05) dt = 0.05; // clamp big gaps
      if (dt < 0) dt = 0;

      // on game over let elapsed keep advancing for pulsing text
      if (state === STATE_OVER || state === STATE_MENU) elapsed += dt;

      try {
        update(dt);
        draw();
      } catch (e) {
        // never throw out of RAF
      }
      requestAnimationFrame(frame);
    }
    requestAnimationFrame(frame);
  }

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", boot);
  } else {
    boot();
  }
})();
</script>
</body>
</html>