← Race the Sun results

Race the Sun

Qwen 3.6 27b MTP · composer

Initial view of Race the Sun

Reviews & generation details

Reviews

No human review yet. Unreviewed does not mean unsuccessful.

Generation context

Imported historical artifact. Its exact original instructions and lineage are not known.

Generation prompt · exact text unavailable

See the current prompt for reference. It may differ from the instructions used to generate this artifact.

Inspect original source 63,489 bytes · SHA-256 345ca629b77c
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Race the Sun - Endless Runner</title>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #0a0a12; font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; user-select: none; }
#app { position: relative; width: 100%; height: 100%; }
canvas { display: block; width: 100%; height: 100%; }

/* HUD */
#hud { position: absolute; top: 0; left: 0; right: 0; pointer-events: none; z-index: 10; padding: 24px 32px; }
#hud-top { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 10px; }
#score-display { font-size: 42px; font-weight: 900; color: #fff; text-shadow: 0 2px 15px rgba(0,0,0,0.6), 0 0 30px rgba(255,140,80,0.2); letter-spacing: 3px; line-height: 1; }
#multiplier { color: #ffcc00; margin-right: 10px; font-size: 26px; text-shadow: 0 0 15px rgba(255,200,0,0.4); }
#distance { font-size: 22px; color: rgba(255,255,255,0.75); text-shadow: 0 2px 8px rgba(0,0,0,0.6); letter-spacing: 1px; }
#energy-bar-container { width: 220px; height: 10px; background: rgba(0,0,0,0.5); border-radius: 5px; overflow: hidden; margin-top: 8px; border: 1px solid rgba(255,255,255,0.1); }
#energy-bar { width: 100%; height: 100%; background: linear-gradient(90deg, #ff4444, #ffaa00, #44ff88); border-radius: 5px; transition: width 0.15s ease-out; }
#speed-display { position: absolute; bottom: 24px; right: 32px; font-size: 18px; color: rgba(255,255,255,0.4); text-shadow: 0 1px 4px rgba(0,0,0,0.6); letter-spacing: 1px; }

/* Title Screen */
#title-screen { position: absolute; top: 0; left: 0; right: 0; bottom: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 20; background: radial-gradient(ellipse at 50% 60%, rgba(255,140,50,0.1) 0%, transparent 70%); }
#title-screen h1 { font-size: clamp(36px, 8vw, 80px); font-weight: 900; color: #fff; text-shadow: 0 0 40px rgba(255,160,50,0.7), 0 4px 20px rgba(0,0,0,0.6); letter-spacing: clamp(4px, 1vw, 10px); margin-bottom: 8px; text-align: center; }
.subtitle { font-size: clamp(12px, 2vw, 18px); color: rgba(255,200,150,0.6); letter-spacing: clamp(3px, 0.8vw, 8px); text-transform: uppercase; margin-bottom: 40px; }
#high-score-label { font-size: 16px; color: rgba(255,255,255,0.4); margin-bottom: 30px; letter-spacing: 1px; }
#start-btn { pointer-events: all; padding: 18px 56px; font-size: 20px; font-weight: 700; letter-spacing: 5px; color: #fff; background: linear-gradient(135deg, #ff6b35, #f7c948); border: none; border-radius: 10px; cursor: pointer; text-transform: uppercase; box-shadow: 0 4px 25px rgba(255,107,53,0.4), inset 0 1px 0 rgba(255,255,255,0.2); transition: transform 0.1s ease-out, box-shadow 0.1s; }
#start-btn:hover { transform: scale(1.06); box-shadow: 0 6px 35px rgba(255,107,53,0.6), inset 0 1px 0 rgba(255,255,255,0.2); }
#start-btn:active { transform: scale(0.98); }
.controls-info { margin-top: 45px; text-align: center; color: rgba(255,255,255,0.35); font-size: 14px; line-height: 2.2; letter-spacing: 0.5px; }

/* Game Over */
#gameover-screen { position: absolute; top: 0; left: 0; right: 0; bottom: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 20; background: rgba(10,5,20,0.88); backdrop-filter: blur(4px); }
#gameover-screen h1 { font-size: clamp(36px, 7vw, 72px); font-weight: 900; color: #ff4444; text-shadow: 0 0 35px rgba(255,50,50,0.5), 0 4px 15px rgba(0,0,0,0.6); letter-spacing: clamp(4px, 1vw, 10px); margin-bottom: 35px; }
#go-stats { display: flex; flex-direction: column; gap: 14px; margin-bottom: 35px; padding: 20px 30px; background: rgba(255,255,255,0.05); border-radius: 12px; border: 1px solid rgba(255,255,255,0.08); }
.stat-row { display: flex; justify-content: space-between; width: 260px; font-size: 20px; color: rgba(255,255,255,0.7); letter-spacing: 1px; }
.stat-row span:first-child { opacity: 0.5; text-transform: uppercase; font-size: 14px; letter-spacing: 2px; }
#restart-btn { pointer-events: all; padding: 16px 48px; font-size: 18px; font-weight: 700; letter-spacing: 4px; color: #fff; background: linear-gradient(135deg, #ff6b35, #f7c948); border: none; border-radius: 10px; cursor: pointer; text-transform: uppercase; box-shadow: 0 4px 25px rgba(255,107,53,0.4), inset 0 1px 0 rgba(255,255,255,0.2); transition: transform 0.1s ease-out; }
#restart-btn:hover { transform: scale(1.06); box-shadow: 0 6px 35px rgba(255,107,53,0.6), inset 0 1px 0 rgba(255,255,255,0.2); }
#restart-btn:active { transform: scale(0.98); }

/* Animations */
@keyframes pulseGlow { 0%, 100% { opacity: 0.6; } 50% { opacity: 1; } }
</style>
</head>
<body>
<canvas id="game"></canvas>
<div id="hud" style="display:none;">
  <div id="hud-top">
    <div id="score-display"><span id="multiplier">×1</span><span id="score">0</span></div>
    <div id="distance">0 m</div>
  </div>
  <div id="energy-bar-container"><div id="energy-bar"></div></div>
  <div id="speed-display">0 km/h</div>
</div>
<div id="title-screen">
  <h1>RACE THE SUN</h1>
  <p class="subtitle">Endless Solar Runner</p>
  <div id="high-score-label">High Score: <span id="high-score-title">0</span></div>
  <button id="start-btn">LAUNCH</button>
  <div class="controls-info">
    <p><b>WASD / Arrows</b> — Move</p>
    <p><b>Space</b> — Boost</p>
    <p><b>R</b> — Restart</p>
  </div>
</div>
<div id="gameover-screen" style="display:none;">
  <h1>SUNSET</h1>
  <div id="go-stats">
    <div class="stat-row"><span>Distance:</span><span id="go-distance">0 m</span></div>
    <div class="stat-row"><span>Score:</span><span id="go-score">0</span></div>
    <div class="stat-row"><span>Best:</span><span id="go-best">0</span></div>
  </div>
  <button id="restart-btn">RELAUNCH (R)</button>
</div>
<script>
'use strict';

// ── function: mulberry32 ──
// === SEEDED PRNG (mulberry32) ===
function mulberry32(seed) {
  return function() {
    seed |= 0; seed = seed + 0x6D2B79F5 | 0;
    var t = Math.imul(seed ^ seed >>> 15, 1 | seed);
    t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
    return ((t ^ t >>> 14) >>> 0) / 4294967296;
  };
}

// ── function: hashCoord ──
function hashCoord(x, z, seed) {
  let h = (seed + Math.floor(x * 3.7) + Math.floor(z * 7.3)) | 0;
  h = ((h >> 16) ^ h) * 0x45d9f3b;
  h = ((h >> 16) ^ h) * 0x45d9f3b;
  return (h ^ (h >> 16)) >>> 0;
}

// ── function: lerp ──
// === UTILITY FUNCTIONS ===
function lerp(a, b, t) { return a + (b - a) * t; }

// ── function: clamp ──
function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }

// ── function: randRange ──
function randRange(rng, a, b) { return a + rng() * (b - a); }

// ── const: AudioSys ──
// === AUDIO SYSTEM (WebAudio Procedural) ===
const AudioSys = {
  ctx: null,
  masterGain: null,
  droneOsc: null,
  droneGain: null,
  windNode: null,
  windGain: null,

  init() {
    if (this.ctx) return;
    this.ctx = new (window.AudioContext || window.webkitAudioContext)();
    this.masterGain = this.ctx.createGain();
    this.masterGain.gain.value = 0.35;
    this.masterGain.connect(this.ctx.destination);
  },

  startDrone() {
    if (!this.ctx) return;
    // Low drone oscillator
    this.droneOsc = this.ctx.createOscillator();
    this.droneOsc.type = 'sawtooth';
    this.droneOsc.frequency.value = 55;
    this.droneGain = this.ctx.createGain();
    this.droneGain.gain.value = 0.06;
    const filter = this.ctx.createBiquadFilter();
    filter.type = 'lowpass';
    filter.frequency.value = 200;
    this.droneOsc.connect(filter);
    filter.connect(this.droneGain);
    this.droneGain.connect(this.masterGain);
    this.droneOsc.start();

    // Wind noise
    const bufSize = this.ctx.sampleRate * 2;
    const buf = this.ctx.createBuffer(1, bufSize, this.ctx.sampleRate);
    const data = buf.getChannelData(0);
    for (let i = 0; i < bufSize; i++) data[i] = Math.random() * 2 - 1;
    this.windNode = this.ctx.createBufferSource();
    this.windNode.buffer = buf;
    this.windNode.loop = true;
    this.windGain = this.ctx.createGain();
    this.windGain.gain.value = 0.03;
    const windFilter = this.ctx.createBiquadFilter();
    windFilter.type = 'bandpass';
    windFilter.frequency.value = 400;
    windFilter.Q.value = 0.5;
    this.windNode.connect(windFilter);
    windFilter.connect(this.windGain);
    this.windGain.connect(this.masterGain);
    this.windNode.start();
  },

  updateDrone(speedRatio) {
    if (!this.droneOsc || !this.windGain) return;
    const freq = lerp(55, 120, speedRatio);
    this.droneOsc.frequency.setTargetAtTime(freq, this.ctx.currentTime, 0.1);
    this.windGain.gain.setTargetAtTime(lerp(0.02, 0.08, speedRatio), this.ctx.currentTime, 0.1);
  },

  playPickup(type) {
    if (!this.ctx) return;
    const osc = this.ctx.createOscillator();
    const gain = this.ctx.createGain();
    osc.connect(gain);
    gain.connect(this.masterGain);
    const now = this.ctx.currentTime;

    if (type === 'energy') {
      osc.type = 'sine';
      osc.frequency.setValueAtTime(600, now);
      osc.frequency.exponentialRampToValueAtTime(900, now + 0.1);
      gain.gain.setValueAtTime(0.2, now);
      gain.gain.exponentialRampToValueAtTime(0.001, now + 0.15);
      osc.start(now);
      osc.stop(now + 0.15);
    } else if (type === 'score') {
      osc.type = 'triangle';
      osc.frequency.setValueAtTime(800, now);
      osc.frequency.exponentialRampToValueAtTime(1200, now + 0.06);
      gain.gain.setValueAtTime(0.15, now);
      gain.gain.exponentialRampToValueAtTime(0.001, now + 0.1);
      osc.start(now);
      osc.stop(now + 0.1);
    } else if (type === 'multiplier') {
      osc.type = 'square';
      osc.frequency.setValueAtTime(500, now);
      osc.frequency.exponentialRampToValueAtTime(1400, now + 0.2);
      gain.gain.setValueAtTime(0.1, now);
      gain.gain.exponentialRampToValueAtTime(0.001, now + 0.25);
      osc.start(now);
      osc.stop(now + 0.25);
    } else if (type === 'nearmiss') {
      osc.type = 'sine';
      osc.frequency.setValueAtTime(300, now);
      osc.frequency.exponentialRampToValueAtTime(150, now + 0.15);
      gain.gain.setValueAtTime(0.12, now);
      gain.gain.exponentialRampToValueAtTime(0.001, now + 0.2);
      osc.start(now);
      osc.stop(now + 0.2);
    } else if (type === 'crash') {
      // Noise burst for crash
      const bufSize = this.ctx.sampleRate * 0.3;
      const buf = this.ctx.createBuffer(1, bufSize, this.ctx.sampleRate);
      const data = buf.getChannelData(0);
      for (let i = 0; i < bufSize; i++) data[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / bufSize, 2);
      const noise = this.ctx.createBufferSource();
      noise.buffer = buf;
      const nGain = this.ctx.createGain();
      nGain.gain.value = 0.4;
      noise.connect(nGain);
      nGain.connect(this.masterGain);
      noise.start(now);

      // Low thud
      osc.type = 'sine';
      osc.frequency.setValueAtTime(150, now);
      osc.frequency.exponentialRampToValueAtTime(30, now + 0.3);
      gain.gain.setValueAtTime(0.3, now);
      gain.gain.exponentialRampToValueAtTime(0.001, now + 0.4);
      osc.start(now);
      osc.stop(now + 0.4);
    } else if (type === 'boost') {
      osc.type = 'sawtooth';
      osc.frequency.setValueAtTime(200, now);
      osc.frequency.exponentialRampToValueAtTime(600, now + 0.15);
      gain.gain.setValueAtTime(0.08, now);
      gain.gain.exponentialRampToValueAtTime(0.001, now + 0.2);
      osc.start(now);
      osc.stop(now + 0.2);
    }
  },

  stop() {
    if (this.droneOsc) { try { this.droneOsc.stop(); } catch(e){} this.droneOsc = null; }
    if (this.windNode) { try { this.windNode.stop(); } catch(e){} this.windNode = null; }
  },

  destroy() {
    this.stop();
    if (this.ctx) { this.ctx.close(); this.ctx = null; }
  }
};

// ── const: Particles ──
// === PARTICLE SYSTEM ===
const Particles = {
  pool: [],
  active: [],

  emit(x, y, count, config) {
    for (let i = 0; i < count; i++) {
      let p = this.pool.length > 0 ? this.pool.pop() : {};
      const spread = config.spread || Math.PI * 2;
      const angle = (config.angle !== undefined ? config.angle : 0) + (Math.random() - 0.5) * spread;
      const speed = randRange(Math.random, config.speedMin || 1, config.speedMax || 4);
      p.x = x;
      p.y = y;
      p.vx = Math.cos(angle) * speed;
      p.vy = Math.sin(angle) * speed;
      p.life = 1.0;
      p.decay = randRange(Math.random, config.decayMin || 0.02, config.decayMax || 0.05);
      p.size = randRange(Math.random, config.sizeMin || 2, config.sizeMax || 5);
      p.color = config.color || '#ffaa44';
      p.gravity = config.gravity || 0;
      this.active.push(p);
    }
  },

  update() {
    for (let i = this.active.length - 1; i >= 0; i--) {
      const p = this.active[i];
      p.x += p.vx;
      p.y += p.vy + p.gravity;
      p.life -= p.decay;
      if (p.life <= 0) {
        this.pool.push(p);
        this.active.splice(i, 1);
      }
    }
  },

  render(ctx) {
    for (const p of this.active) {
      ctx.globalAlpha = clamp(p.life, 0, 1);
      ctx.fillStyle = p.color;
      const s = p.size * p.life;
      ctx.fillRect(p.x - s/2, p.y - s/2, s, s);
    }
    ctx.globalAlpha = 1;
  },

  clear() {
    for (const p of this.active) this.pool.push(p);
    this.active.length = 0;
  }
};

// ── const: Collision ──
// === COLLISION DETECTION ===
const Collision = {
  checkPlayerVsObstacles(player, obstacles) {
    const pw = CFG.PLAYER_W / 2;
    const ph = CFG.PLAYER_H / 2;

    for (const obs of obstacles) {
      if (obs.z < -10 || obs.z > 30) continue; // only check nearby obstacles

      const ox = obs.x;
      const oy = obs.y + obs.h / 2;
      const ow = obs.w / 2;
      const oh = obs.h / 2;

      if (obs.type === 'arch') {
        // Arch: two pillars, check each separately
        const pillarW = ow * 0.2;
        // Left pillar collision
        if (this.aabbOverlap(player.x - pw, player.y - ph, player.x + pw, player.y + ph,
                              ox - ow + pillarW/2 - pillarW/2, oy - oh, ox - ow + pillarW/2 + pillarW/2, oy + oh)) {
          return obs;
        }
        // Right pillar collision
        if (this.aabbOverlap(player.x - pw, player.y - ph, player.x + pw, player.y + ph,
                              ox + ow - pillarW/2 - pillarW/2, oy - oh, ox + ow - pillarW/2 + pillarW/2, oy + oh)) {
          return obs;
        }
        // Top beam collision (only if player is high enough)
        const beamTop = oy - oh * 0.85;
        if (this.aabbOverlap(player.x - pw, player.y - ph, player.x + pw, player.y + ph,
                              ox - ow, beamTop - oh*0.15, ox + ow, beamTop + oh*0.15)) {
          return obs;
        }
      } else {
        // Standard AABB collision
        if (this.aabbOverlap(player.x - pw, player.y - ph, player.x + pw, player.y + ph,
                              ox - ow, oy - oh, ox + ow, oy + oh)) {
          return obs;
        }
      }

      // Near-miss detection
      if (!obs.nearMissed && !obs.passed) {
        const dx = Math.abs(player.x - ox);
        const dy = Math.abs(player.y - oy);
        const nearDist = (ow + pw) * 0.8;
        if (dx < nearDist && dy < (oh + ph) * 1.2 && obs.z > 5 && obs.z < 25) {
          return { ...obs, isNearMiss: true };
        }
      }
    }

    // Mark obstacles as passed
    for (const obs of obstacles) {
      if (!obs.passed && obs.z < -10) {
        obs.passed = true;
      }
    }

    return null;
  },

  aabbOverlap(x1, y1, x2, y2, x3, y3, x4, y4) {
    return x1 < x4 && x2 > x3 && y1 < y4 && y2 > y3;
  },

  checkPlayerVsPickups(player, pickups) {
    const pw = CFG.PLAYER_W / 2 + 5;
    const ph = CFG.PLAYER_H / 2 + 5;

    for (const p of pickups) {
      if (p.collected || p.z < -10 || p.z > 30) continue;

      const dx = Math.abs(player.x - p.x);
      const dy = Math.abs(player.y - (p.y + 20));

      if (dx < pw && dy < ph) {
        return p;
      }
    }
    return null;
  },
};

// ── const: Shake ──
// === SCREEN SHAKE SYSTEM ===
const Shake = {
  intensity: 0,
  decay: 0.85,
  offsetX: 0,
  offsetY: 0,

  add(amount) {
    this.intensity = Math.max(this.intensity, amount);
  },

  update() {
    if (this.intensity > 0.1) {
      this.offsetX = (Math.random() - 0.5) * this.intensity;
      this.offsetY = (Math.random() - 0.5) * this.intensity;
      this.intensity *= this.decay;
    } else {
      this.intensity = 0;
      this.offsetX = 0;
      this.offsetY = 0;
    }
  },

  getOffset() {
    return { x: this.offsetX, y: this.offsetY };
  },
};

// ── function: renderChromaticAberration ──
// === CHROMATIC ABERRATION EFFECT (improved) ===
function renderChromaticAberration(ctx, W, H, speedRatio) {
  const threshold = CFG.CHROMATIC_THRESH / CFG.MAX_SPEED;
  if (speedRatio < threshold) return;

  const intensity = clamp((speedRatio - threshold) * 10, 0, 6);

  // Edge-based chromatic aberration using composite operations
  ctx.save();
  
  // Red channel offset (left edge)
  ctx.globalCompositeOperation = 'screen';
  ctx.fillStyle = `rgba(255,0,0,${intensity * 0.04})`;
  ctx.fillRect(-intensity, -intensity * 0.3, W + intensity, H);

  // Blue channel offset (right edge)  
  ctx.fillStyle = `rgba(0,0,255,${intensity * 0.04})`;
  ctx.fillRect(intensity, -intensity * 0.3, W + intensity, H);

  ctx.restore();
}

// ── const: DamageFlash ──
// === DAMAGE FLASH ===
const DamageFlash = {
  alpha: 0,

  trigger() {
    this.alpha = 0.6;
  },

  update() {
    if (this.alpha > 0) {
      this.alpha *= 0.9;
    }
  },

  render(ctx, W, H) {
    if (this.alpha <= 0.01) return;
    ctx.fillStyle = `rgba(255,50,30,${this.alpha})`;
    ctx.fillRect(0, 0, W, H);
  },
};

// ── const: Popups ──
// === NEAR MISS TEXT POPUP ===
const Popups = {
  items: [],

  add(x, y, text, color) {
    this.items.push({ x, y, text, color, life: 1.0 });
  },

  update() {
    for (let i = this.items.length - 1; i >= 0; i--) {
      const p = this.items[i];
      p.y -= 1.5;
      p.life -= 0.02;
      if (p.life <= 0) this.items.splice(i, 1);
    }
  },

  render(ctx) {
    for (const p of this.items) {
      ctx.save();
      ctx.globalAlpha = clamp(p.life, 0, 1);
      ctx.fillStyle = p.color;
      ctx.font = 'bold 24px sans-serif';
      ctx.textAlign = 'center';
      ctx.shadowColor = p.color;
      ctx.shadowBlur = 8;
      ctx.fillText(p.text, p.x, p.y);
      ctx.restore();
    }
  },

  clear() { this.items.length = 0; },
};

// ── const: Input ──
// === INPUT HANDLING ===
const Input = {
  keys: {},
  left: false, right: false, up: false, down: false, boost: false,
  prevBoost: false,

  init() {
    window.addEventListener('keydown', (e) => {
      this.keys[e.code] = true;
      if (['Space','ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(e.code)) {
        e.preventDefault();
      }
    });
    window.addEventListener('keyup', (e) => {
      this.keys[e.code] = false;
    });
  },

  update() {
    this.left = !!(this.keys['KeyA'] || this.keys['ArrowLeft']);
    this.right = !!(this.keys['KeyD'] || this.keys['ArrowRight']);
    this.up = !!(this.keys['KeyW'] || this.keys['ArrowUp']);
    this.down = !!(this.keys['KeyS'] || this.keys['ArrowDown']);
    this.boost = !!this.keys['Space'];

    // Detect boost tap (for single-use boost)
    this.boostTap = this.boost && !this.prevBoost;
    this.prevBoost = this.boost;
  },

  shouldRestart() {
    return !!(this.keys['KeyR']);
  }
};

// ── const: CFG ──
// === CONSTANTS ===
const CFG = {
  // Player
  PLAYER_W: 28,
  PLAYER_H: 16,
  MOVE_SPEED: 0.15,       // easing factor per frame (snappy)
  INERTIA_TAIL: 0.92,     // velocity retention
  MAX_LATERAL_VEL: 14,
  MAX_VERTICAL_VEL: 8,
  BANK_ANGLE: 0.35,       // radians max bank when strafing
  PITCH_LEAN: 0.2,        // forward lean on pitch

  // World
  LANE_COUNT: 5,
  LANE_WIDTH: 60,
  WORLD_HALF_W: (5 * 60) / 2,  // = 150
  FOG_DIST: 1800,
  RENDER_DIST: 2400,

  // Speed
  BASE_SPEED: 3.5,        // world units per frame at start
  SPEED_RAMP: 0.0008,     // speed increase per frame
  MAX_SPEED: 12,
  BOOST_MULT: 2.0,
  BOOST_DRAIN: 0.6,       // energy drain per boost frame

  // Energy / Sun
  ENERGY_MAX: 100,
  ENERGY_DRAIN_RATE: 0.015,   // per frame in light
  ENERGY_SHADOW_MULT: 3.0,    // multiplier when in shadow
  SUN_SINK_RATE: 0.0004,      // sun y-position decrease per frame
  SUN_START_Y: 0.6,           // normalized sky position (0=top, 1=bottom)

  // Obstacles
  OBSTACLE_MIN_GAP: 250,       // min distance between obstacle waves
  OBSTACLE_MAX_GAP: 500,
  WAVE_SIZE_MIN: 1,
  WAVE_SIZE_MAX: 4,
  SPAWN_Z: 2800,

  // Pickups
  PICKUP_CHANCE: 0.3,
  ENERGY_PICKUP_VAL: 15,
  SCORE_TOKEN_VAL: 100,
  MULTIPLIER_ORB_VAL: 1,

  // Collision
  NEAR_MISS_DIST: 40,
  COLLISION_SHAKE: 12,
  NEAR_MISS_SHAKE: 4,

  // Visuals
  SPEED_LINE_THRESH: 6,       // speed at which speed lines appear
  CHROMATIC_THRESH: 8,        // speed for chromatic aberration
};

// ── const: Player ──
// === PLAYER CRAFT ===
const Player = {
  x: 0,
  y: -40,
  vx: 0,
  vy: 0,
  bankAngle: 0,
  pitchLean: 0,
  thrustPhase: 0,

  reset() {
    this.x = 0;
    this.y = -40;
    this.vx = 0;
    this.vy = 0;
    this.bankAngle = 0;
    this.pitchLean = 0;
    this.thrustPhase = 0;
  },

  update(input, dt) {
    const targetVx = input.left ? -CFG.MAX_LATERAL_VEL : (input.right ? CFG.MAX_LATERAL_VEL : 0);
    const targetVy = input.up ? -CFG.MAX_VERTICAL_VEL : (input.down ? CFG.MAX_VERTICAL_VEL : 0);

    this.vx = lerp(this.vx, targetVx, CFG.MOVE_SPEED * dt * 60);
    this.vy = lerp(this.vy, targetVy, CFG.MOVE_SPEED * dt * 60);

    if (!input.left && !input.right) {
      this.vx *= Math.pow(CFG.INERTIA_TAIL, dt * 60);
    }
    if (!input.up && !input.down) {
      this.vy *= Math.pow(CFG.INERTIA_TAIL, dt * 60);
    }

    this.vx = clamp(this.vx, -CFG.MAX_LATERAL_VEL, CFG.MAX_LATERAL_VEL);
    this.vy = clamp(this.vy, -CFG.MAX_VERTICAL_VEL, CFG.MAX_VERTICAL_VEL);

    this.x += this.vx * dt * 60;
    this.y += this.vy * dt * 60;

    this.x = clamp(this.x, -CFG.WORLD_HALF_W - 20, CFG.WORLD_HALF_W + 20);
    this.y = clamp(this.y, -180, 40);

    const targetBank = (this.vx / CFG.MAX_LATERAL_VEL) * CFG.BANK_ANGLE;
    this.bankAngle = lerp(this.bankAngle, targetBank, 0.15 * dt * 60);

    const targetPitch = -(this.vy / CFG.MAX_VERTICAL_VEL) * CFG.PITCH_LEAN;
    this.pitchLean = lerp(this.pitchLean, targetPitch, 0.12 * dt * 60);

    this.thrustPhase += dt * 30;
  },

  render(ctx, camX, camY) {
    const sx = ctx.canvas.width / 2 + (this.x - camX) * 0.5;
    const sy = ctx.canvas.height * 0.65 + (this.y - camY) * 0.3;

    ctx.save();
    ctx.translate(sx, sy);
    ctx.rotate(this.bankAngle);

    // Engine glow (behind everything)
    const flicker = Math.sin(this.thrustPhase * 7) * 0.3 + 1;
    const thrustLen = (12 + Math.random() * 6) * flicker;

    // Outer engine glow
    ctx.shadowColor = '#ff8833';
    ctx.shadowBlur = 20;
    const outerGlow = ctx.createRadialGradient(0, 10, 0, 0, 10, 15);
    outerGlow.addColorStop(0, 'rgba(255,170,50,0.6)');
    outerGlow.addColorStop(1, 'rgba(255,80,20,0)');
    ctx.fillStyle = outerGlow;
    ctx.beginPath();
    ctx.arc(0, 10, 15, 0, Math.PI * 2);
    ctx.fill();
    ctx.shadowBlur = 0;

    // Main thrust flame
    const grad = ctx.createLinearGradient(0, 8, 0, 8 + thrustLen);
    grad.addColorStop(0, '#ffffff');
    grad.addColorStop(0.2, '#ffeecc');
    grad.addColorStop(0.5, '#ffaa44');
    grad.addColorStop(1, 'rgba(255,60,20,0)');
    ctx.fillStyle = grad;
    ctx.beginPath();
    ctx.moveTo(-7, 8);
    ctx.lineTo(7, 8);
    ctx.quadraticCurveTo(3 + Math.random() * 2, 8 + thrustLen * 0.5, Math.random() * 4 - 2, 8 + thrustLen);
    ctx.closePath();
    ctx.fill();

    // Secondary flame (inner)
    const grad2 = ctx.createLinearGradient(0, 7, 0, 7 + thrustLen * 0.6);
    grad2.addColorStop(0, 'rgba(255,255,255,0.9)');
    grad2.addColorStop(1, 'rgba(255,200,100,0)');
    ctx.fillStyle = grad2;
    ctx.beginPath();
    ctx.moveTo(-4, 7);
    ctx.lineTo(4, 7);
    ctx.quadraticCurveTo(1 + Math.random(), 7 + thrustLen * 0.3, Math.random() * 2 - 1, 7 + thrustLen * 0.6);
    ctx.closePath();
    ctx.fill();

    // Craft body
    const lean = this.pitchLean;
    
    // Shadow underneath craft
    ctx.fillStyle = 'rgba(0,0,0,0.3)';
    ctx.beginPath();
    ctx.moveTo(-2, -10 + lean * 20);
    ctx.lineTo(-16, 10);
    ctx.lineTo(16, 10);
    ctx.closePath();
    ctx.fill();

    // Main hull (dark)
    ctx.fillStyle = '#2a2a3e';
    ctx.strokeStyle = '#4a4a6e';
    ctx.lineWidth = 1.5;
    ctx.beginPath();
    ctx.moveTo(0, -16 + lean * 20);       // nose tip
    ctx.lineTo(-8, -4);                    // upper left body
    ctx.lineTo(-16, 8);                    // left wing tip
    ctx.lineTo(-10, 9);                    // left wing inner
    ctx.lineTo(-5, 7);                     // left engine mount
    ctx.lineTo(0, 8);                      // center bottom
    ctx.lineTo(5, 7);                      // right engine mount
    ctx.lineTo(10, 9);                     // right wing inner
    ctx.lineTo(16, 8);                     // right wing tip
    ctx.lineTo(8, -4);                     // upper right body
    ctx.closePath();
    ctx.fill();
    ctx.stroke();

    // Hull highlight (top surface)
    ctx.fillStyle = '#3a3a5e';
    ctx.beginPath();
    ctx.moveTo(0, -16 + lean * 20);
    ctx.lineTo(-8, -4);
    ctx.lineTo(0, -2);
    ctx.lineTo(8, -4);
    ctx.closePath();
    ctx.fill();

    // Cockpit canopy (glass)
    const cockpitGrad = ctx.createLinearGradient(0, -10 + lean * 15, 0, 2);
    cockpitGrad.addColorStop(0, 'rgba(180,220,255,0.9)');
    cockpitGrad.addColorStop(0.5, 'rgba(100,160,220,0.7)');
    cockpitGrad.addColorStop(1, 'rgba(60,100,180,0.4)');
    ctx.fillStyle = cockpitGrad;
    ctx.beginPath();
    ctx.moveTo(0, -12 + lean * 15);
    ctx.lineTo(-5, -2);
    ctx.quadraticCurveTo(0, 3, 5, -2);
    ctx.closePath();
    ctx.fill();

    // Cockpit reflection
    ctx.fillStyle = 'rgba(255,255,255,0.4)';
    ctx.beginPath();
    ctx.moveTo(-1, -10 + lean * 13);
    ctx.lineTo(-3, -4);
    ctx.lineTo(-1, -3);
    ctx.closePath();
    ctx.fill();

    // Wing accents (orange/red)
    const wingGrad = ctx.createLinearGradient(0, 6, 0, 10);
    wingGrad.addColorStop(0, '#ff6b35');
    wingGrad.addColorStop(1, '#cc4422');
    ctx.fillStyle = wingGrad;

    // Left wing accent
    ctx.beginPath();
    ctx.moveTo(-12, 7);
    ctx.lineTo(-16, 8);
    ctx.lineTo(-10, 9);
    ctx.closePath();
    ctx.fill();

    // Right wing accent
    ctx.beginPath();
    ctx.moveTo(12, 7);
    ctx.lineTo(16, 8);
    ctx.lineTo(10, 9);
    ctx.closePath();
    ctx.fill();

    // Nose tip highlight
    ctx.fillStyle = '#ffcc44';
    ctx.beginPath();
    ctx.moveTo(0, -16 + lean * 20);
    ctx.lineTo(-2, -13 + lean * 18);
    ctx.lineTo(2, -13 + lean * 18);
    ctx.closePath();
    ctx.fill();

    // Body panel lines
    ctx.strokeStyle = 'rgba(100,100,140,0.5)';
    ctx.lineWidth = 0.5;
    ctx.beginPath();
    ctx.moveTo(-6, -2);
    ctx.lineTo(-8, 6);
    ctx.stroke();
    ctx.beginPath();
    ctx.moveTo(6, -2);
    ctx.lineTo(8, 6);
    ctx.stroke();

    // Engine nozzles
    ctx.fillStyle = '#1a1a2e';
    ctx.strokeStyle = '#3a3a5e';
    ctx.lineWidth = 1;
    // Left nozzle
    ctx.beginPath();
    ctx.moveTo(-7, 6);
    ctx.lineTo(-9, 8);
    ctx.lineTo(-4, 8);
    ctx.closePath();
    ctx.fill();
    ctx.stroke();
    // Right nozzle
    ctx.beginPath();
    ctx.moveTo(7, 6);
    ctx.lineTo(9, 8);
    ctx.lineTo(4, 8);
    ctx.closePath();
    ctx.fill();
    ctx.stroke();

    ctx.restore();
  }
};

// ── const: World ──
// === WORLD & OBSTACLE GENERATION ===
const World = {
  rng: null,
  seed: 0,
  obstacles: [],
  pickups: [],
  lastSpawnZ: 0,
  groundSegments: [],
  clouds: [],

  init(seed) {
    this.seed = seed;
    this.rng = mulberry32(seed);
    this.obstacles.length = 0;
    this.pickups.length = 0;
    this.lastSpawnZ = 400;
    this.groundSegments.length = 0;

    // Generate clouds
    this.clouds.length = 0;
    for (let i = 0; i < 12; i++) {
      this.clouds.push({
        x: randRange(this.rng, -400, 400),
        y: randRange(this.rng, -80, -20),
        z: randRange(this.rng, 500, 3000),
        w: randRange(this.rng, 60, 150),
        h: randRange(this.rng, 15, 40),
        alpha: randRange(this.rng, 0.1, 0.3),
      });
    }

    // Pre-generate ground segments
    for (let z = 0; z < CFG.RENDER_DIST + 200; z += 80) {
      const h = Math.sin(z * 0.005 + seed) * 15 + Math.cos(z * 0.013) * 8;
      this.groundSegments.push({ z, height: h });
    }

    // Pre-spawn initial obstacles and pickups
    for (let i = 0; i < 20; i++) {
      this.spawnWave();
    }
  },

  spawnWave() {
    const gap = randRange(this.rng, CFG.OBSTACLE_MIN_GAP, CFG.OBSTACLE_MAX_GAP);
    const z = this.lastSpawnZ + gap;
    const waveSize = Math.floor(randRange(this.rng, CFG.WAVE_SIZE_MIN, CFG.WAVE_SIZE_MAX));

    for (let i = 0; i < waveSize; i++) {
      const laneX = randRange(this.rng, -CFG.WORLD_HALF_W + 30, CFG.WORLD_HALF_W - 30);
      const typeRoll = this.rng();
      let obsType, w, h, d;

      if (typeRoll < 0.25) {
        obsType = 'pillar';
        w = randRange(this.rng, 25, 40);
        h = randRange(this.rng, 80, 160);
        d = randRange(this.rng, 20, 35);
      } else if (typeRoll < 0.5) {
        obsType = 'wall';
        w = randRange(this.rng, 80, 140);
        h = randRange(this.rng, 40, 70);
        d = randRange(this.rng, 15, 25);
      } else if (typeRoll < 0.7) {
        obsType = 'block';
        w = randRange(this.rng, 30, 50);
        h = randRange(this.rng, 25, 40);
        d = randRange(this.rng, 20, 35);
      } else if (typeRoll < 0.85) {
        obsType = 'arch';
        w = randRange(this.rng, 60, 100);
        h = randRange(this.rng, 80, 120);
        d = randRange(this.rng, 15, 25);
      } else {
        obsType = 'spire';
        w = randRange(this.rng, 15, 25);
        h = randRange(this.rng, 100, 180);
        d = randRange(this.rng, 15, 25);
      }

      const yOff = (obsType === 'block' || obsType === 'spire') ? randRange(this.rng, -60, 30) : 0;

      this.obstacles.push({
        x: laneX, y: yOff, z: z + i * 40,
        w, h, d, type: obsType,
        passed: false, nearMissed: false,
        colorHue: randRange(this.rng, 15, 35), // warm hue range
      });

      if (this.rng() < CFG.PICKUP_CHANCE) {
        const pLaneX = randRange(this.rng, -CFG.WORLD_HALF_W + 20, CFG.WORLD_HALF_W - 20);
        const pTypeRoll = this.rng();
        let pType;
        if (pTypeRoll < 0.4) pType = 'energy';
        else if (pTypeRoll < 0.75) pType = 'score';
        else pType = 'multiplier';

        this.pickups.push({
          x: pLaneX, y: randRange(this.rng, -80, 20),
          z: z + i * 40 + 30, type: pType,
          collected: false, phase: this.rng() * Math.PI * 2,
        });
      }
    }

    this.lastSpawnZ = z;
  },

  update(speed) {
    for (const obs of this.obstacles) obs.z -= speed;
    for (const p of this.pickups) { p.z -= speed; p.phase += 0.05; }
    for (const c of this.clouds) c.z -= speed * 0.3;

    this.obstacles = this.obstacles.filter(o => o.z > -200);
    this.pickups = this.pickups.filter(p => p.z > -200 && !p.collected);

    // Recycle clouds
    for (const c of this.clouds) {
      if (c.z < -100) {
        c.z += 3500;
        c.x = randRange(Math.random, -400, 400);
      }
    }

    const furthestZ = this.obstacles.length > 0 ? Math.max(...this.obstacles.map(o => o.z)) : 0;
    if (furthestZ < CFG.RENDER_DIST) {
      this.spawnWave();
    }

    for (const seg of this.groundSegments) seg.z -= speed;
    while (this.groundSegments.length > 0 && this.groundSegments[0].z < -100) {
      const first = this.groundSegments.shift();
      const lastZ = this.groundSegments[this.groundSegments.length - 1].z;
      first.z = lastZ + 80;
      first.height = Math.sin(first.z * 0.005 + this.seed) * 15 + Math.cos(first.z * 0.013) * 8;
      this.groundSegments.push(first);
    }
  },

  getShadowFactor(playerX, playerY) {
    let shadow = 0;
    for (const obs of this.obstacles) {
      if (obs.z < 5 || obs.z > 300) continue;
      const dx = Math.abs(obs.x - playerX);
      if (dx < obs.w / 2 + 10) {
        shadow = Math.max(shadow, (obs.h * 0.5) / (obs.z * 0.3));
      }
    }
    return clamp(shadow, 0, 1);
  },

  projectToScreen(worldX, worldY, worldZ, W, H) {
    if (worldZ <= 0) return null;
    const perspective = 500 / worldZ;
    const screenX = W/2 + worldX * perspective;
    const horizonY = H * 0.42;
    const groundOffset = (H - horizonY);
    const screenY = horizonY + groundOffset * (1 - perspective) - worldY * perspective;
    return { x: screenX, y: screenY, scale: perspective };
  },

  renderSky(ctx, W, H, sunY) {
    // Multi-stop sunset gradient
    const grad = ctx.createLinearGradient(0, 0, 0, H);
    const t = clamp(sunY / 1.2, 0, 1);

    const r1 = Math.floor(lerp(35, 8, t));
    const g1 = Math.floor(lerp(25, 4, t));
    const b1 = Math.floor(lerp(65, 20, t));
    const r2 = Math.floor(lerp(120, 30, t));
    const g2 = Math.floor(lerp(60, 10, t));
    const b2 = Math.floor(lerp(80, 35, t));
    const r3 = Math.floor(lerp(200, 50, t));
    const g3 = Math.floor(lerp(100, 15, t));
    const b3 = Math.floor(lerp(60, 25, t));
    const r4 = Math.floor(lerp(255, 80, t));
    const g4 = Math.floor(lerp(160, 30, t));
    const b4 = Math.floor(lerp(60, 20, t));

    grad.addColorStop(0, `rgb(${r1},${g1},${b1})`);
    grad.addColorStop(0.35, `rgb(${r2},${g2},${b2})`);
    grad.addColorStop(0.7, `rgb(${r3},${g3},${b3})`);
    grad.addColorStop(1, `rgb(${r4},${g4},${b4})`);
    ctx.fillStyle = grad;
    ctx.fillRect(0, 0, W, H);

    // Sun glow layers
    const sunScreenY = H * (0.25 + sunY * 0.35);
    if (sunScreenY < H) {
      // Outer atmospheric glow
      const outerGlow = ctx.createRadialGradient(W/2, sunScreenY, 0, W/2, sunScreenY, 350);
      const alpha = clamp(1 - t * 0.7, 0.1, 1);
      outerGlow.addColorStop(0, `rgba(255,240,200,${alpha * 0.8})`);
      outerGlow.addColorStop(0.15, `rgba(255,200,100,${alpha * 0.4})`);
      outerGlow.addColorStop(0.4, `rgba(255,140,60,${alpha * 0.15})`);
      outerGlow.addColorStop(1, 'rgba(255,80,30,0)');
      ctx.fillStyle = outerGlow;
      ctx.fillRect(0, Math.max(0, sunScreenY - 350), W, Math.min(H, sunScreenY + 350));

      // Inner bright glow
      const innerGlow = ctx.createRadialGradient(W/2, sunScreenY, 0, W/2, sunScreenY, 120);
      innerGlow.addColorStop(0, `rgba(255,255,240,${alpha})`);
      innerGlow.addColorStop(0.3, `rgba(255,220,160,${alpha * 0.6})`);
      innerGlow.addColorStop(1, 'rgba(255,180,100,0)');
      ctx.fillStyle = innerGlow;
      ctx.fillRect(W/2 - 120, sunScreenY - 120, 240, 240);

      // Sun disc with corona
      const sunR = 30 + (1 - t) * 15;
      ctx.fillStyle = `rgba(255,245,220,${alpha})`;
      ctx.beginPath();
      ctx.arc(W/2, sunScreenY, sunR, 0, Math.PI * 2);
      ctx.fill();

      // Corona rays (animated)
      const now = performance.now() * 0.001;
      ctx.save();
      ctx.translate(W/2, sunScreenY);
      for (let i = 0; i < 16; i++) {
        const angle = (i / 16) * Math.PI * 2 + now * 0.2;
        const rayLen = sunR * (2.5 + Math.sin(now * 1.5 + i * 0.7) * 0.8);
        ctx.strokeStyle = `rgba(255,220,160,${alpha * 0.12})`;
        ctx.lineWidth = 2;
        ctx.beginPath();
        ctx.moveTo(Math.cos(angle) * sunR * 0.9, Math.sin(angle) * sunR * 0.9);
        ctx.lineTo(Math.cos(angle) * rayLen, Math.sin(angle) * rayLen);
        ctx.stroke();
      }
      ctx.restore();

      // Horizon haze band
      const hazeGrad = ctx.createLinearGradient(0, sunScreenY - 20, 0, sunScreenY + 60);
      hazeGrad.addColorStop(0, 'rgba(255,180,100,0)');
      hazeGrad.addColorStop(0.3, `rgba(255,160,80,${alpha * 0.2})`);
      hazeGrad.addColorStop(0.7, `rgba(255,140,60,${alpha * 0.15})`);
      hazeGrad.addColorStop(1, 'rgba(255,120,50,0)');
      ctx.fillStyle = hazeGrad;
      ctx.fillRect(W/2 - 300, sunScreenY - 20, 600, 80);
    }

    // Stars (appear as sun sinks)
    if (t > 0.3) {
      const starAlpha = clamp((t - 0.3) / 0.5, 0, 1);
      ctx.fillStyle = `rgba(255,255,255,${starAlpha * 0.7})`;
      for (let i = 0; i < 60; i++) {
        const sx = ((i * 7919 + this.seed) % W);
        const sy = ((i * 6271 + this.seed) % Math.floor(H * 0.45));
        const ss = 1 + (i % 3);
        const twinkle = 0.5 + Math.sin(performance.now() * 0.003 + i * 1.7) * 0.5;
        ctx.globalAlpha = starAlpha * 0.7 * twinkle;
        ctx.fillRect(sx, sy, ss, ss);
      }
      ctx.globalAlpha = 1;
    }

    // Clouds
    this.renderClouds(ctx, W, H, sunScreenY || H * 0.4, t);
  },

  renderClouds(ctx, W, H, sunY, darkness) {
    for (const cloud of this.clouds) {
      if (cloud.z < 10 || cloud.z > CFG.RENDER_DIST) continue;
      const p = this.projectToScreen(cloud.x, cloud.y + 50, cloud.z, W, H);
      if (!p || p.scale <= 0.02) continue;

      const fogFactor = clamp(1 - cloud.z / CFG.FOG_DIST, 0.05, 0.4);
      const cw = cloud.w * p.scale;
      const ch = cloud.h * p.scale;

      ctx.save();
      ctx.globalAlpha = fogFactor * (cloud.alpha + darkness * 0.1);
      
      // Cloud shape (multiple overlapping ellipses)
      const cloudGrad = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, cw * 0.5);
      cloudGrad.addColorStop(0, 'rgba(255,240,220,0.6)');
      cloudGrad.addColorStop(0.5, 'rgba(255,200,180,0.3)');
      cloudGrad.addColorStop(1, 'rgba(255,180,160,0)');
      ctx.fillStyle = cloudGrad;
      
      // Draw fluffy cloud shape
      for (let j = 0; j < 4; j++) {
        const ox = Math.sin(j * 2.3 + this.seed) * cw * 0.25;
        const oy = Math.cos(j * 1.7 + this.seed) * ch * 0.3;
        const r = cw * (0.2 + j * 0.08);
        ctx.beginPath();
        ctx.arc(p.x + ox, p.y + oy, r, 0, Math.PI * 2);
        ctx.fill();
      }

      ctx.restore();
    }
  },

  renderGround(ctx, W, H, camZ, speedRatio) {
    const horizonY = H * 0.42;

    // Ground gradient (dark purple/black)
    const groundGrad = ctx.createLinearGradient(0, horizonY, 0, H);
    groundGrad.addColorStop(0, '#1a0830');
    groundGrad.addColorStop(0.2, '#120625');
    groundGrad.addColorStop(0.5, '#0d0418');
    groundGrad.addColorStop(1, '#080210');
    ctx.fillStyle = groundGrad;
    ctx.fillRect(0, horizonY, W, H - horizonY);

    // Perspective grid lines (moving)
    const gridAlpha = 0.06 + speedRatio * 0.12;
    
    // Horizontal moving lines
    for (let i = 0; i < 35; i++) {
      const zBase = ((i * 55 + camZ * 0.5) % 1925);
      if (zBase < 15) continue;
      const perspective = 400 / zBase;
      const screenY = horizonY + (H - horizonY) * (1 - perspective);
      if (screenY > H || screenY < horizonY) continue;

      ctx.globalAlpha = gridAlpha * clamp(perspective, 0.2, 1);
      ctx.strokeStyle = 'rgba(255,140,80,1)';
      ctx.lineWidth = Math.max(0.5, perspective * 2);
      ctx.beginPath();
      ctx.moveTo(0, screenY);
      ctx.lineTo(W, screenY);
      ctx.stroke();
    }

    // Vertical lane lines converging at vanishing point
    for (let i = -CFG.LANE_COUNT; i <= CFG.LANE_COUNT; i++) {
      const worldX = i * CFG.LANE_WIDTH;
      const nearX = W/2 + worldX * 0.5;
      ctx.globalAlpha = gridAlpha * 0.4;
      ctx.strokeStyle = 'rgba(255,140,80,1)';
      ctx.lineWidth = Math.max(0.5, 1);
      ctx.beginPath();
      ctx.moveTo(W/2, horizonY);
      ctx.lineTo(nearX, H);
      ctx.stroke();
    }

    // Center lane highlight (brighter path)
    ctx.globalAlpha = gridAlpha * 0.8;
    ctx.strokeStyle = 'rgba(255,180,120,1)';
    ctx.lineWidth = Math.max(1, 2);
    ctx.beginPath();
    ctx.moveTo(W/2, horizonY);
    ctx.lineTo(W/2, H);
    ctx.stroke();

    ctx.globalAlpha = 1;

    // Terrain silhouette (mountains on edges)
    this.renderTerrainSilhouette(ctx, W, H, camZ);
  },

  renderTerrainSilhouette(ctx, W, H, camZ) {
    const horizonY = H * 0.42;
    
    // Far mountains (darker, more distant)
    ctx.fillStyle = 'rgba(12,6,25,0.8)';
    ctx.beginPath();
    ctx.moveTo(0, horizonY);
    for (let x = 0; x <= W; x += 15) {
      const worldX = (x - W/2) * 3;
      const n = Math.sin(worldX * 0.008 + camZ * 0.0005) * 40 +
                Math.cos(worldX * 0.02 + camZ * 0.001) * 20 +
                Math.sin(worldX * 0.003) * 50;
      ctx.lineTo(x, horizonY - Math.abs(n));
    }
    ctx.lineTo(W, horizonY);
    ctx.closePath();
    ctx.fill();

    // Near terrain (slightly lighter)
    ctx.fillStyle = 'rgba(18,8,32,0.9)';
    ctx.beginPath();
    ctx.moveTo(0, horizonY);
    for (let x = 0; x <= W; x += 15) {
      const worldX = (x - W/2) * 2;
      const n = Math.sin(worldX * 0.01 + camZ * 0.001) * 25 +
                Math.cos(worldX * 0.03 + camZ * 0.002) * 12 +
                Math.sin(worldX * 0.005) * 30;
      ctx.lineTo(x, horizonY - Math.abs(n));
    }
    ctx.lineTo(W, horizonY);
    ctx.closePath();
    ctx.fill();
  },

  renderObstacles(ctx, W, H, camZ) {
    const sorted = [...this.obstacles]
      .filter(o => o.z > 10 && o.z < CFG.RENDER_DIST)
      .sort((a,b) => b.z - a.z);

    for (const obs of sorted) {
      const p = this.projectToScreen(obs.x, obs.y + obs.h/2, obs.z, W, H);
      if (!p || p.scale <= 0.01) continue;

      const sw = obs.w * p.scale;
      const sh = obs.h * p.scale;
      const sd = obs.d * p.scale;
      const fogFactor = clamp(1 - obs.z / CFG.FOG_DIST, 0.15, 1);

      ctx.save();
      ctx.globalAlpha = fogFactor;

      if (obs.type === 'arch') {
        this.drawArch(ctx, p.x, p.y, sw, sh, sd, obs.colorHue);
      } else if (obs.type === 'spire') {
        this.drawSpire(ctx, p.x, p.y, sw, sh, sd, obs.colorHue);
      } else {
        this.drawObsBlock(ctx, p.x, p.y - sh/2, sw, sh, sd, obs.colorHue);
      }

      ctx.restore();
    }
  },

  drawObsBlock(ctx, x, y, w, h, d, hue) {
    const baseColor = `hsl(${hue}, 80%, 45%)`;
    const lightColor = `hsl(${hue}, 70%, 60%)`;
    const darkColor = `hsl(${hue}, 90%, 30%)`;

    // Front face
    ctx.fillStyle = baseColor;
    ctx.strokeStyle = `hsla(${hue}, 80%, 55%, 0.8)`;
    ctx.lineWidth = Math.max(1, d * 0.05);
    ctx.fillRect(x - w/2, y, w, h);
    ctx.strokeRect(x - w/2, y, w, h);

    // Top face (lighter)
    ctx.fillStyle = lightColor;
    ctx.beginPath();
    ctx.moveTo(x - w/2, y);
    ctx.lineTo(x + w/2, y);
    ctx.lineTo(x + w/2 - d*0.4, y - d*0.3);
    ctx.lineTo(x - w/2 + d*0.4, y - d*0.3);
    ctx.closePath();
    ctx.fill();
    ctx.stroke();

    // Right face (darker)
    ctx.fillStyle = darkColor;
    ctx.beginPath();
    ctx.moveTo(x + w/2, y);
    ctx.lineTo(x + w/2, y + h);
    ctx.lineTo(x + w/2 - d*0.4, y + h - d*0.3);
    ctx.lineTo(x + w/2 - d*0.4, y - d*0.3);
    ctx.closePath();
    ctx.fill();
    ctx.stroke();

    // Edge highlight
    ctx.strokeStyle = `hsla(${hue}, 90%, 75%, 0.3)`;
    ctx.lineWidth = Math.max(0.5, d * 0.02);
    ctx.beginPath();
    ctx.moveTo(x - w/2, y);
    ctx.lineTo(x + w/2, y);
    ctx.stroke();

    // Warning glow on edges (pulsing)
    const pulse = 0.3 + Math.sin(performance.now() * 0.005) * 0.15;
    ctx.shadowColor = `hsl(${hue}, 90%, 60%)`;
    ctx.shadowBlur = d * 0.3;
    ctx.strokeStyle = `hsla(${hue}, 90%, 70%, ${pulse})`;
    ctx.lineWidth = Math.max(1, d * 0.08);
    ctx.strokeRect(x - w/2, y, w, h);
    ctx.shadowBlur = 0;
  },

  drawArch(ctx, x, y, w, h, d, hue) {
    const pillarW = w * 0.18;
    
    // Left pillar
    this.drawObsBlock(ctx, x - w/2 + pillarW/2, y, pillarW, h, d, hue);
    // Right pillar  
    this.drawObsBlock(ctx, x + w/2 - pillarW/2, y, pillarW, h, d, hue);
    // Top beam
    const beamH = h * 0.12;
    this.drawObsBlock(ctx, x, y - h * 0.85, w, beamH, d, hue);

    // Arch glow (passable indicator)
    ctx.strokeStyle = `hsla(${hue + 60}, 80%, 70%, 0.2)`;
    ctx.lineWidth = Math.max(1, d * 0.1);
    ctx.setLineDash([d * 0.3, d * 0.3]);
    ctx.beginPath();
    ctx.moveTo(x - w/2 + pillarW, y + h);
    ctx.lineTo(x - w/2 + pillarW, y);
    ctx.stroke();
    ctx.beginPath();
    ctx.moveTo(x + w/2 - pillarW, y + h);
    ctx.lineTo(x + w/2 - pillarW, y);
    ctx.stroke();
    ctx.setLineDash([]);
  },

  drawSpire(ctx, x, y, w, h, d, hue) {
    // Spire: tall thin triangle shape
    const baseColor = `hsl(${hue}, 70%, 40%)`;
    const lightColor = `hsl(${hue}, 60%, 55%)`;
    const darkColor = `hsl(${hue}, 80%, 25%)`;

    // Main body (triangle)
    ctx.fillStyle = baseColor;
    ctx.strokeStyle = `hsla(${hue}, 70%, 50%, 0.8)`;
    ctx.lineWidth = Math.max(1, d * 0.05);
    ctx.beginPath();
    ctx.moveTo(x, y - h * 0.3); // tip (above center)
    ctx.lineTo(x - w/2, y + h); // bottom left
    ctx.lineTo(x + w/2, y + h); // bottom right
    ctx.closePath();
    ctx.fill();
    ctx.stroke();

    // Left face highlight
    ctx.fillStyle = lightColor;
    ctx.beginPath();
    ctx.moveTo(x, y - h * 0.3);
    ctx.lineTo(x - w/2, y + h);
    ctx.lineTo(x, y + h);
    ctx.closePath();
    ctx.fill();

    // Right face shadow
    ctx.fillStyle = darkColor;
    ctx.beginPath();
    ctx.moveTo(x, y - h * 0.3);
    ctx.lineTo(x + w/2, y + h);
    ctx.lineTo(x, y + h);
    ctx.closePath();
    ctx.fill();

    // Tip glow
    const pulse = 0.4 + Math.sin(performance.now() * 0.008) * 0.2;
    ctx.shadowColor = `hsl(${hue}, 90%, 70%)`;
    ctx.shadowBlur = d * 0.5;
    ctx.fillStyle = `hsla(${hue}, 90%, 80%, ${pulse})`;
    ctx.beginPath();
    ctx.arc(x, y - h * 0.3, Math.max(2, d * 0.15), 0, Math.PI * 2);
    ctx.fill();
    ctx.shadowBlur = 0;
  },

  renderPickups(ctx, W, H, camZ, time) {
    for (const p of this.pickups) {
      if (p.collected || p.z < 10 || p.z > CFG.RENDER_DIST) continue;

      const sp = this.projectToScreen(p.x, p.y + 25, p.z, W, H);
      if (!sp || sp.scale <= 0.01) continue;

      const fogFactor = clamp(1 - p.z / CFG.FOG_DIST, 0.3, 1);
      const bobY = Math.sin(time * 3 + p.phase) * 6 * sp.scale;
      const size = (p.type === 'multiplier' ? 14 : 10) * sp.scale;

      ctx.save();
      ctx.globalAlpha = fogFactor;
      ctx.translate(sp.x, sp.y - bobY);

      let glowColor, fillColor;
      if (p.type === 'energy') {
        glowColor = '#44ffaa';
        fillColor = '#22dd88';
      } else if (p.type === 'score') {
        glowColor = '#ffcc00';
        fillColor = '#eebb00';
      } else {
        glowColor = '#ff66ff';
        fillColor = '#ee44ee';
      }

      // Outer glow
      ctx.shadowColor = glowColor;
      ctx.shadowBlur = 20 * sp.scale;

      // Rotating diamond shape
      const rot = time * 1.5 + p.phase;
      ctx.rotate(rot);
      
      ctx.fillStyle = fillColor;
      ctx.beginPath();
      ctx.moveTo(0, -size);
      ctx.lineTo(size * 0.7, 0);
      ctx.lineTo(0, size);
      ctx.lineTo(-size * 0.7, 0);
      ctx.closePath();
      ctx.fill();

      // Inner highlight
      ctx.fillStyle = 'rgba(255,255,255,0.5)';
      ctx.beginPath();
      ctx.moveTo(0, -size * 0.6);
      ctx.lineTo(size * 0.35, 0);
      ctx.lineTo(0, size * 0.6);
      ctx.lineTo(-size * 0.35, 0);
      ctx.closePath();
      ctx.fill();

      // Border
      ctx.strokeStyle = 'rgba(255,255,255,0.7)';
      ctx.lineWidth = Math.max(1, sp.scale * 2);
      ctx.stroke();

      ctx.shadowBlur = 0;
      ctx.rotate(-rot);

      // Symbol
      ctx.fillStyle = '#fff';
      ctx.font = `bold ${Math.max(8, size * 1.2)}px sans-serif`;
      ctx.textAlign = 'center';
      ctx.textBaseline = 'middle';
      if (p.type === 'energy') ctx.fillText('\u26a1', 0, 1);
      else if (p.type === 'score') ctx.fillText('\u2726', 0, 1);
      else ctx.fillText('\u00d7' + '', 0, 1);

      ctx.restore();
    }
  },
};

// ── const: SpeedLines ──
// === SPEED LINES EFFECT ===
const SpeedLines = {
  lines: [],

  init() {
    this.lines.length = 0;
    for (let i = 0; i < 40; i++) {
      this.lines.push({
        angle: Math.random() * Math.PI * 2,
        speed: randRange(Math.random, 8, 20),
        length: randRange(Math.random, 30, 100),
        offset: Math.random(),
        alpha: randRange(Math.random, 0.15, 0.4),
        width: randRange(Math.random, 0.5, 2),
      });
    }
  },

  render(ctx, W, H, speedRatio) {
    if (speedRatio < CFG.SPEED_LINE_THRESH / CFG.MAX_SPEED) return;

    const intensity = clamp((speedRatio - CFG.SPEED_LINE_THRESH / CFG.MAX_SPEED) * 3, 0, 1);
    const cx = W / 2;
    const cy = H * 0.45;

    ctx.save();
    for (const line of this.lines) {
      const dist = line.offset * Math.max(W, H) * 0.8;
      const x1 = cx + Math.cos(line.angle) * dist;
      const y1 = cy + Math.sin(line.angle) * dist;
      const x2 = x1 - Math.cos(line.angle) * line.length * intensity;
      const y2 = y1 - Math.sin(line.angle) * line.length * intensity;

      ctx.strokeStyle = `rgba(255,200,150,${line.alpha * intensity})`;
      ctx.lineWidth = line.width * intensity;
      ctx.beginPath();
      ctx.moveTo(x1, y1);
      ctx.lineTo(x2, y2);
      ctx.stroke();
    }
    ctx.restore();
  },

  update() {
    for (const line of this.lines) {
      line.offset -= line.speed * 0.008;
      if (line.offset < -0.5) line.offset += 1.5;
    }
  },
};

// ── function: renderVignette ──
// === VIGNETTE EFFECT ===
function renderVignette(ctx, W, H, intensity) {
  const grad = ctx.createRadialGradient(W/2, H/2, W * 0.3, W/2, H/2, W * 0.75);
  grad.addColorStop(0, 'rgba(0,0,0,0)');
  grad.addColorStop(1, `rgba(0,0,0,${intensity})`);
  ctx.fillStyle = grad;
  ctx.fillRect(0, 0, W, H);
}

// ── const: Game ──
// === GAME STATE ===
const Game = {
  state: 'title',
  canvas: null,
  ctx: null,
  W: 0, H: 0,
  runSeed: 0,
  distance: 0,
  score: 0,
  multiplier: 1,
  energy: 100,
  speed: 3.5,
  sunY: 0.6,
  camZ: 0,
  lastTime: 0,
  frameCount: 0,

  init() {
    this.canvas = document.getElementById('game');
    this.ctx = this.canvas.getContext('2d');
    this.resize();
    window.addEventListener('resize', () => this.resize());
    Input.init();
    SpeedLines.init();
    this.loadHighScore();
    this.updateTitleScreen();

    document.getElementById('start-btn').addEventListener('click', () => this.startRun());
    document.getElementById('restart-btn').addEventListener('click', () => this.restart());

    requestAnimationFrame((t) => this.loop(t));
  },

  resize() {
    const dpr = window.devicePixelRatio || 1;
    this.W = window.innerWidth;
    this.H = window.innerHeight;
    this.canvas.width = this.W * dpr;
    this.canvas.height = this.H * dpr;
    this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  },

  loadHighScore() {
    try { return parseInt(localStorage.getItem('rts_highscore') || '0'); } catch(e) { return 0; }
  },

  saveHighScore(score) {
    const current = this.loadHighScore();
    if (score > current) {
      try { localStorage.setItem('rts_highscore', score.toString()); } catch(e){}
    }
    return Math.max(score, current);
  },

  updateTitleScreen() {
    document.getElementById('high-score-title').textContent = this.loadHighScore().toLocaleString();
  },

  startRun() {
    AudioSys.init();
    if (AudioSys.ctx && AudioSys.ctx.state === 'suspended') AudioSys.ctx.resume();

    this.runSeed = Math.floor(Math.random() * 999999);
    this.distance = 0;
    this.score = 0;
    this.multiplier = 1;
    this.energy = CFG.ENERGY_MAX;
    this.speed = CFG.BASE_SPEED;
    this.sunY = CFG.SUN_START_Y;
    this.camZ = 0;
    this.frameCount = 0;

    Player.reset();
    World.init(this.runSeed);
    Particles.clear();
    Popups.clear();
    Shake.intensity = 0;
    DamageFlash.alpha = 0;

    AudioSys.startDrone();

    this.state = 'playing';
    document.getElementById('title-screen').style.display = 'none';
    document.getElementById('gameover-screen').style.display = 'none';
    document.getElementById('hud').style.display = 'block';
  },

  gameOver() {
    if (this.state !== 'playing') return;
    this.state = 'gameover';
    AudioSys.playPickup('crash');
    Shake.add(CFG.COLLISION_SHAKE);
    DamageFlash.trigger();

    const px = this.W / 2;
    const py = this.H * 0.65;
    Particles.emit(px, py, 40, {
      angle: Math.random() * Math.PI * 2, speedMin: 3, speedMax: 12,
      decayMin: 0.01, decayMax: 0.03, sizeMin: 3, sizeMax: 8, color: '#ff4422',
    });
    Particles.emit(px, py, 25, {
      angle: Math.random() * Math.PI * 2, speedMin: 1, speedMax: 6,
      decayMin: 0.005, decayMax: 0.02, sizeMin: 2, sizeMax: 4, color: '#ffcc44',
    });

    const highScore = this.saveHighScore(this.score);
    setTimeout(() => {
      document.getElementById('go-distance').textContent = Math.floor(this.distance) + ' m';
      document.getElementById('go-score').textContent = this.score.toLocaleString();
      document.getElementById('go-best').textContent = highScore.toLocaleString();
      document.getElementById('gameover-screen').style.display = 'flex';
    }, 500);
  },

  restart() {
    AudioSys.stop();
    this.startRun();
  },

  updateHUD() {
    document.getElementById('score').textContent = this.score.toLocaleString();
    document.getElementById('multiplier').textContent = '\u00d7' + this.multiplier;
    document.getElementById('distance').textContent = Math.floor(this.distance) + ' m';
    document.getElementById('speed-display').textContent = Math.floor(this.speed * 30) + ' km/h';

    const energyPct = (this.energy / CFG.ENERGY_MAX) * 100;
    const bar = document.getElementById('energy-bar');
    bar.style.width = energyPct + '%';

    if (energyPct < 25) {
      bar.style.background = '#ff4444';
    } else if (energyPct < 50) {
      bar.style.background = 'linear-gradient(90deg, #ff4444, #ffaa00)';
    } else {
      bar.style.background = 'linear-gradient(90deg, #ff4444, #ffaa00, #44ff88)';
    }

    if (energyPct < 20 && Math.floor(this.frameCount / 10) % 2 === 0) {
      bar.style.boxShadow = '0 0 10px rgba(255,0,0,0.8)';
    } else {
      bar.style.boxShadow = 'none';
    }
  },

  loop(timestamp) {
    const dt = Math.min((timestamp - this.lastTime) / 16.67, 3);
    this.lastTime = timestamp;
    this.frameCount++;

    Input.update();

    if (this.state === 'playing') {
      this.update(dt);
    } else if (this.state === 'gameover') {
      Particles.update();
      Shake.update();
      DamageFlash.update();
      Popups.update();
      SpeedLines.update();

      if (Input.shouldRestart()) this.restart();
    }

    this.render();
    requestAnimationFrame((t) => this.loop(t));
  },

  update(dt) {
    // Speed ramps up over time with slight curve
    const elapsed = this.frameCount / 60; // seconds
    this.speed = Math.min(CFG.MAX_SPEED, CFG.BASE_SPEED + elapsed * CFG.SPEED_RAMP * 60);

    let currentSpeed = this.speed;
    if (Input.boost && this.energy > 5) {
      currentSpeed *= CFG.BOOST_MULT;
      this.energy -= CFG.BOOST_DRAIN * dt;
      if (this.frameCount % 8 === 0) AudioSys.playPickup('boost');

      const px = this.W / 2 + (Math.random() - 0.5) * 10;
      Particles.emit(px, this.H * 0.65 + 10, 3, {
        angle: Math.PI / 2, spread: 0.8, speedMin: 4, speedMax: 8,
        decayMin: 0.03, decayMax: 0.06, sizeMin: 2, sizeMax: 5, color: '#ffaa33',
      });
    }

    Player.update(Input, dt);
    World.update(currentSpeed);

    this.distance += currentSpeed * 0.1;
    this.score += Math.floor(currentSpeed * this.multiplier * 0.5);

    // Sun sinks over time (slower for longer runs)
    this.sunY += CFG.SUN_SINK_RATE * dt;

    // Energy drain with shadow factor
    const shadowFactor = World.getShadowFactor(Player.x, Player.y);
    const drainMult = lerp(1, CFG.ENERGY_SHADOW_MULT, shadowFactor);
    this.energy -= CFG.ENERGY_DRAIN_RATE * drainMult * dt;

    if (this.sunY > 1.0) { this.gameOver(); return; }
    if (this.energy <= 0) { this.energy = 0; this.gameOver(); return; }

    // Collision detection
    const hitResult = Collision.checkPlayerVsObstacles(Player, World.obstacles);
    if (hitResult) {
      if (hitResult.isNearMiss) {
        hitResult.nearMissed = true;
        const bonus = 50 * this.multiplier;
        this.score += bonus;
        Shake.add(CFG.NEAR_MISS_SHAKE);
        AudioSys.playPickup('nearmiss');

        Popups.add(this.W / 2 + (Math.random() - 0.5) * 100, this.H * 0.48,
                   'NEAR MISS! +' + bonus, '#ffcc00');

        Particles.emit(this.W / 2, this.H * 0.6, 12, {
          speedMin: 2, speedMax: 5, decayMin: 0.03, decayMax: 0.06,
          sizeMin: 2, sizeMax: 4, color: '#ffcc00',
        });
      } else {
        this.gameOver();
        return;
      }
    }

    // Pickup collection
    const pickupHit = Collision.checkPlayerVsPickups(Player, World.pickups);
    if (pickupHit) {
      pickupHit.collected = true;
      const px = this.W / 2 + (Math.random() - 0.5) * 60;
      const py = this.H * 0.55;

      if (pickupHit.type === 'energy') {
        this.energy = Math.min(CFG.ENERGY_MAX, this.energy + CFG.ENERGY_PICKUP_VAL);
        AudioSys.playPickup('energy');
        Popups.add(px, py, '+\u26a1', '#44ffaa');
        Particles.emit(this.W / 2, this.H * 0.6, 10, {
          speedMin: 1, speedMax: 4, decayMin: 0.02, decayMax: 0.05,
          sizeMin: 2, sizeMax: 4, color: '#44ffaa',
        });
      } else if (pickupHit.type === 'score') {
        const val = CFG.SCORE_TOKEN_VAL * this.multiplier;
        this.score += val;
        AudioSys.playPickup('score');
        Popups.add(px, py, '+' + val, '#ffcc00');
        Particles.emit(this.W / 2, this.H * 0.6, 8, {
          speedMin: 1, speedMax: 4, decayMin: 0.03, decayMax: 0.06,
          sizeMin: 2, sizeMax: 5, color: '#ffcc00',
        });
      } else if (pickupHit.type === 'multiplier') {
        this.multiplier += CFG.MULTIPLIER_ORB_VAL;
        AudioSys.playPickup('multiplier');
        Popups.add(px, py, '\u00d7' + this.multiplier, '#ff66ff');
        Particles.emit(this.W / 2, this.H * 0.6, 15, {
          speedMin: 2, speedMax: 6, decayMin: 0.01, decayMax: 0.04,
          sizeMin: 3, sizeMax: 6, color: '#ff66ff',
        });
      }
    }

    // Thrust particles
    if (this.frameCount % 2 === 0) {
      Particles.emit(this.W / 2 + (Math.random() - 0.5) * 8, this.H * 0.65 + 12, 1, {
        angle: Math.PI / 2, spread: 0.4, speedMin: 1, speedMax: 3,
        decayMin: 0.04, decayMax: 0.08, sizeMin: 1, sizeMax: 3, color: '#ffaa44',
      });
    }

    Particles.update();
    Shake.update();
    DamageFlash.update();
    Popups.update();
    SpeedLines.update();

    AudioSys.updateDrone(currentSpeed / CFG.MAX_SPEED);
    this.updateHUD();
  },

  render() {
    const ctx = this.ctx;
    const W = this.W;
    const H = this.H;

    const shake = Shake.getOffset();
    ctx.save();
    ctx.translate(shake.x, shake.y);
    ctx.clearRect(-10, -10, W + 20, H + 20);

    if (this.state === 'title') {
      this.renderTitleBackground(ctx, W, H);
    } else {
      const speedRatio = this.speed / CFG.MAX_SPEED;

      // Sky and sun
      World.renderSky(ctx, W, H, this.sunY);

      // Ground with perspective grid
      World.renderGround(ctx, W, H, this.camZ, speedRatio);

      // Obstacles (sorted far to near)
      World.renderObstacles(ctx, W, H, this.camZ);

      // Pickups
      const time = performance.now() * 0.001;
      World.renderPickups(ctx, W, H, this.camZ, time);

      // Player craft
      Player.render(ctx, this.camZ, 0);

      // Particles on top
      Particles.render(ctx);

      // Speed lines at high speed
      SpeedLines.render(ctx, W, H, speedRatio);

      // Chromatic aberration at very high speed
      renderChromaticAberration(ctx, W, H, speedRatio);

      // Damage flash overlay
      DamageFlash.render(ctx, W, H);

      // Score popups
      Popups.render(ctx);

      // Shadow vignette when behind obstacles
      const shadowFactor = World.getShadowFactor(Player.x, Player.y);
      if (shadowFactor > 0.1) {
        ctx.fillStyle = 'rgba(0,0,20,' + (shadowFactor * 0.35) + ')';
        ctx.fillRect(0, 0, W, H);
      }

      // Energy warning vignette (red pulse when low)
      const energyPct = this.energy / CFG.ENERGY_MAX;
      if (energyPct < 0.3) {
        const vigAlpha = (1 - energyPct / 0.3) * 0.45 * (0.5 + Math.sin(this.frameCount * 0.12) * 0.5);
        ctx.fillStyle = 'rgba(255,0,0,' + vigAlpha + ')';
        ctx.fillRect(0, 0, W, H);
      }

      // Sun warning (orange pulse when sun is low)
      if (this.sunY > 0.7) {
        const warnAlpha = clamp((this.sunY - 0.7) / 0.3, 0, 1) * 0.5;
        ctx.fillStyle = 'rgba(255,80,0,' + (warnAlpha * (0.5 + Math.sin(this.frameCount * 0.15) * 0.5)) + ')';
        ctx.fillRect(0, 0, W, H);
      }

      // Always-on vignette for atmosphere
      renderVignette(ctx, W, H, 0.3 + speedRatio * 0.2);
    }

    ctx.restore();
  },

  renderTitleBackground(ctx, W, H) {
    const t = performance.now() * 0.0003;

    // Gradient sky
    const grad = ctx.createLinearGradient(0, 0, 0, H);
    grad.addColorStop(0, '#1a1040');
    grad.addColorStop(0.5, '#2a1535');
    grad.addColorStop(1, '#ff6b35');
    ctx.fillStyle = grad;
    ctx.fillRect(0, 0, W, H);

    // Sun on horizon
    const sunY = H * 0.4 + Math.sin(t) * 20;
    const sunGrad = ctx.createRadialGradient(W/2, sunY, 0, W/2, sunY, 150);
    sunGrad.addColorStop(0, 'rgba(255,240,200,0.9)');
    sunGrad.addColorStop(0.3, 'rgba(255,160,50,0.4)');
    sunGrad.addColorStop(1, 'rgba(255,80,20,0)');
    ctx.fillStyle = sunGrad;
    ctx.fillRect(W/2 - 150, sunY - 150, 300, 300);

    ctx.fillStyle = 'rgba(255,240,200,0.9)';
    ctx.beginPath();
    ctx.arc(W/2, sunY, 30, 0, Math.PI * 2);
    ctx.fill();

    // Ground silhouette
    const horizonY = H * 0.5;
    ctx.fillStyle = '#0a0515';
    ctx.fillRect(0, horizonY, W, H - horizonY);

    // Grid lines on ground (animated)
    ctx.strokeStyle = 'rgba(255,140,80,0.1)';
    ctx.lineWidth = 1;
    for (let i = 0; i < 20; i++) {
      const zBase = ((i * 60 + t * 100) % 1200);
      if (zBase < 20) continue;
      const perspective = 400 / zBase;
      const screenY = horizonY + (H - horizonY) * (1 - perspective);
      ctx.globalAlpha = clamp(perspective, 0.1, 0.3);
      ctx.beginPath();
      ctx.moveTo(0, screenY);
      ctx.lineTo(W, screenY);
      ctx.stroke();
    }
    ctx.globalAlpha = 1;

    // Floating geometric shapes
    for (let i = 0; i < 8; i++) {
      const x = W * ((i * 0.13 + t * 0.05) % 1);
      const y = horizonY - 40 - Math.sin(i * 2.5 + t * 2) * 60;
      ctx.strokeStyle = 'rgba(255,180,120,' + (0.1 + Math.sin(i + t) * 0.05) + ')';
      ctx.lineWidth = 1;
      const size = 15 + i * 3;
      ctx.save();
      ctx.translate(x, y);
      ctx.rotate(t + i);
      ctx.strokeRect(-size/2, -size/2, size, size);
      ctx.restore();
    }

    // Vignette on title screen too
    renderVignette(ctx, W, H, 0.4);
  },
};

// ── main ──
// === MAIN ENTRY POINT ===
Game.init();
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen3.6-27b-mtp","provider":"lmstudio","persona":"composer","sessionId":"0845489e-10a4-4553-aee4-e4c885780e17","tokensIn":2147482,"tokensOut":52914,"tokensTotal":2200396,"turns":54,"toolCalls":53,"failedToolCalls":2,"timestamp":"2026-07-16T04:31:57.074Z"} -->