← Canyon Run results

Canyon Run

longcat 2.0 · composer

◈Preview unavailable — try playing the result

Browser check: Preview unavailable or timed out. This is a technical check, not a quality review.

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 34,614 bytes · SHA-256 b536d4d3c2d4
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Canyon Run — Procedural Canyon Racer</title>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/EffectComposer.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/RenderPass.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/ShaderPass.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/MaskPass.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/UnrealBloomPass.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/shaders/CopyShader.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/shaders/LuminosityHighPassShader.js"></script>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #0a0a0f; font-family: 'Courier New', monospace; }
#game-container { position: relative; width: 100%; height: 100%; }
#game-canvas { display: block; width: 100%; height: 100%; }
.hidden { display: none !important; }
/* ── css: hud ── */
#hud {
  position: absolute; top: 0; left: 0; width: 100%; height: 100%;
  pointer-events: none; z-index: 10;
}
#hud-top {
  position: absolute; top: 24px; left: 0; right: 0;
  display: flex; justify-content: space-between; padding: 0 36px;
}
#score-display {
  font-size: 32px; font-weight: bold; color: #fff;
  text-shadow: 0 0 20px rgba(255, 140, 50, 0.8), 0 0 40px rgba(255, 100, 20, 0.4);
  letter-spacing: 2px;
}
#multiplier-display {
  font-size: 28px; font-weight: bold; color: #ffcc00;
  text-shadow: 0 0 15px rgba(255, 200, 0, 0.8);
  transition: transform 0.1s, color 0.2s;
}
#multiplier-display.pulse {
  transform: scale(1.3); color: #fff;
}
#hud-bottom {
  position: absolute; bottom: 24px; left: 36px; right: 36px;
  display: flex; gap: 24px; align-items: flex-end;
}
#energy-container, #speed-container {
  flex: 1; max-width: 300px;
}
#energy-bar, #speed-bar {
  height: 4px; border-radius: 2px; transition: width 0.15s;
}
#energy-bar {
  background: linear-gradient(90deg, #00ffaa, #00ccff);
  box-shadow: 0 0 12px rgba(0, 255, 170, 0.6);
}
#speed-bar {
  background: linear-gradient(90deg, #ff6600, #ffcc00);
  box-shadow: 0 0 12px rgba(255, 150, 0, 0.6);
}
#energy-label, #speed-label {
  font-size: 10px; color: rgba(255,255,255,0.5);
  letter-spacing: 3px; margin-top: 4px;
}
#turbo-container {
  position: absolute; bottom: 24px; right: 36px;
  width: 60px; height: 60px; text-align: center;
}
#turbo-ring {
  width: 60px; height: 60px;
}
#turbo-ring svg { width: 100%; height: 100%; transform: rotate(-90deg); }
#turbo-bg {
  fill: none; stroke: rgba(255,255,255,0.1); stroke-width: 4;
}
#turbo-fill {
  fill: none; stroke: #ff4400; stroke-width: 4;
  stroke-dasharray: 251.2; stroke-dashoffset: 251.2;
  stroke-linecap: round; transition: stroke-dashoffset 0.2s;
  filter: drop-shadow(0 0 6px rgba(255, 68, 0, 0.8));
}
#turbo-label {
  font-size: 9px; color: rgba(255,255,255,0.4);
  letter-spacing: 2px; margin-top: 2px;
}
/* ── css: screens ── */
#game-over, #start-screen {
  position: absolute; top: 0; left: 0; width: 100%; height: 100%;
  display: flex; align-items: center; justify-content: center;
  z-index: 20; background: rgba(5, 5, 15, 0.85);
  backdrop-filter: blur(4px);
}
#game-over-content, #start-content {
  text-align: center; color: #fff;
}
#game-over h1, #start-content h1 {
  font-size: 56px; letter-spacing: 8px; font-weight: bold;
  background: linear-gradient(135deg, #ff6600, #ffcc00);
  -webkit-background-clip: text; -webkit-text-fill-color: transparent;
  background-clip: text; margin-bottom: 12px;
}
#start-content h1 {
  font-size: 64px;
}
.subtitle {
  font-size: 14px; letter-spacing: 6px; color: rgba(255,255,255,0.5);
  margin-bottom: 40px;
}
#final-score {
  font-size: 48px; font-weight: bold; color: #ffcc00;
  text-shadow: 0 0 20px rgba(255, 200, 0, 0.6);
  margin: 16px 0;
}
#final-stats {
  display: flex; gap: 24px; justify-content: center;
  font-size: 12px; color: rgba(255,255,255,0.5); letter-spacing: 2px;
  margin-bottom: 32px;
}
.controls-info {
  display: flex; flex-direction: column; gap: 8px;
  font-size: 12px; color: rgba(255,255,255,0.4); letter-spacing: 2px;
  margin-bottom: 36px;
}
#restart-btn, #start-btn {
  pointer-events: all; cursor: pointer;
  background: linear-gradient(135deg, #ff6600, #ff8800);
  border: none; color: #fff; font-family: 'Courier New', monospace;
  font-size: 16px; letter-spacing: 4px; padding: 14px 48px;
  border-radius: 4px; font-weight: bold;
  transition: transform 0.15s, box-shadow 0.15s;
  box-shadow: 0 0 20px rgba(255, 100, 0, 0.4);
}
#restart-btn:hover, #start-btn:hover {
  transform: scale(1.05);
  box-shadow: 0 0 30px rgba(255, 100, 0, 0.7);
}
</style>
</head>
<body>
<div id="game-container">
  <canvas id="game-canvas"></canvas>
  <div id="hud">
    <div id="hud-top">
      <div id="score-display">0</div>
      <div id="multiplier-display">x1.0</div>
    </div>
    <div id="hud-bottom">
      <div id="energy-container">
        <div id="energy-bar"></div>
        <div id="energy-label">ENERGY</div>
      </div>
      <div id="speed-container">
        <div id="speed-bar"></div>
        <div id="speed-label">SPEED</div>
      </div>
    </div>
    <div id="turbo-container">
      <div id="turbo-ring">
        <svg viewBox="0 0 100 100">
          <circle id="turbo-bg" cx="50" cy="50" r="40"></circle>
          <circle id="turbo-fill" cx="50" cy="50" r="40"></circle>
        </svg>
      </div>
      <div id="turbo-label">TURBO</div>
    </div>
  </div>
  <div id="game-over" class="hidden">
    <div id="game-over-content">
      <h1>CRASHED</h1>
      <div id="final-score">0</div>
      <div id="final-stats">
        <span id="max-speed-stat">MAX SPEED: 0</span>
        <span id="max-multiplier-stat">MAX MULTI: x1.0</span>
      </div>
      <button id="restart-btn">RESTART</button>
    </div>
  </div>
  <div id="start-screen">
    <div id="start-content">
      <h1>CANYON RUN</h1>
      <p class="subtitle">PROCEDURAL CANYON RACER</p>
      <div class="controls-info">
        <span>← → or A/D : STEER</span>
        <span>↑ ↓ or W/S : PITCH</span>
        <span>SPACE : BOOST</span>
      </div>
      <button id="start-btn">LAUNCH</button>
    </div>
  </div>
</div>
<script>
'use strict';

// ── function: createAudioSystem ──
function createAudioSystem() {
  let ctx = null;
  let masterGain = null;
  let engineOsc = null;
  let engineGain = null;
  
  function init() {
    if (ctx) return;
    ctx = new (window.AudioContext || window.webkitAudioContext)();
    masterGain = ctx.createGain();
    masterGain.gain.value = 0.3;
    masterGain.connect(ctx.destination);
    
    // Engine hum
    engineOsc = ctx.createOscillator();
    engineOsc.type = 'sawtooth';
    engineOsc.frequency.value = 80;
    engineGain = ctx.createGain();
    engineGain.gain.value = 0.08;
    
    const filter = ctx.createBiquadFilter();
    filter.type = 'lowpass';
    filter.frequency.value = 200;
    
    engineOsc.connect(filter);
    filter.connect(engineGain);
    engineGain.connect(masterGain);
    engineOsc.start();
  }
  
  function updateEngine(speed, boost) {
    if (!ctx) return;
    const baseFreq = 60 + speed * 2;
    engineOsc.frequency.setTargetAtTime(baseFreq + (boost ? 80 : 0), ctx.currentTime, 0.05);
    engineGain.gain.setTargetAtTime(0.06 + speed * 0.003 + (boost ? 0.06 : 0), ctx.currentTime, 0.05);
  }
  
  function playCollect() {
    if (!ctx) return;
    const osc = ctx.createOscillator();
    const gain = ctx.createGain();
    osc.type = 'sine';
    osc.frequency.value = 880;
    gain.gain.value = 0.15;
    gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.2);
    osc.frequency.exponentialRampToValueAtTime(1760, ctx.currentTime + 0.15);
    osc.connect(gain);
    gain.connect(masterGain);
    osc.start();
    osc.stop(ctx.currentTime + 0.2);
  }
  
  function playNearMiss() {
    if (!ctx) return;
    const osc = ctx.createOscillator();
    const gain = ctx.createGain();
    osc.type = 'triangle';
    osc.frequency.value = 440;
    gain.gain.value = 0.12;
    gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.3);
    osc.frequency.exponentialRampToValueAtTime(220, ctx.currentTime + 0.25);
    osc.connect(gain);
    gain.connect(masterGain);
    osc.start();
    osc.stop(ctx.currentTime + 0.3);
  }
  
  function playCrash() {
    if (!ctx) return;
    const bufferSize = ctx.sampleRate * 0.5;
    const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
    const data = buffer.getChannelData(0);
    for (let i = 0; i < bufferSize; i++) {
      data[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / bufferSize, 2);
    }
    const source = ctx.createBufferSource();
    source.buffer = buffer;
    const gain = ctx.createGain();
    gain.gain.value = 0.3;
    source.connect(gain);
    gain.connect(masterGain);
    source.start();
  }
  
  function playBoost() {
    if (!ctx) return;
    const osc = ctx.createOscillator();
    const gain = ctx.createGain();
    osc.type = 'sawtooth';
    osc.frequency.value = 200;
    osc.frequency.exponentialRampToValueAtTime(600, ctx.currentTime + 0.15);
    gain.gain.value = 0.1;
    gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.2);
    osc.connect(gain);
    gain.connect(masterGain);
    osc.start();
    osc.stop(ctx.currentTime + 0.2);
  }
  
  return { init, updateEngine, playCollect, playNearMiss, playCrash, playBoost };
}

// ── function: createPlayerCraft ──
function createPlayerCraft(scene) {
  const group = new THREE.Group();
  
  // Main body - sleek angular shape
  const bodyGeom = new THREE.ConeGeometry(0.8, 3.5, 4);
  const bodyMat = new THREE.MeshStandardMaterial({ 
    color: 0xccddee, flatShading: true, metalness: 0.7, roughness: 0.3 
  });
  const body = new THREE.Mesh(bodyGeom, bodyMat);
  body.rotation.x = Math.PI / 2;
  group.add(body);
  
  // Wings
  const wingGeom = new THREE.BufferGeometry();
  const wingVerts = new Float32Array([
    0, 0, -0.5,  -3, -0.3, 1.5,  0, 0, 1,
    0, 0, -0.5,  0, 0, 1,  3, -0.3, 1.5,
    0, 0, -0.5,  3, -0.3, 1.5,  0, 0.4, 0,
    0, 0, -0.5,  0, 0.4, 0,  -3, -0.3, 1.5,
    -3, -0.3, 1.5,  0, 0.4, 0,  0, 0, -0.5,
    3, -0.3, 1.5,  0, 0, -0.5,  0, 0.4, 0,
  ]);
  wingGeom.setAttribute('position', new THREE.BufferAttribute(wingVerts, 3));
  wingGeom.computeVertexNormals();
  const wingMat = new THREE.MeshStandardMaterial({ 
    color: 0x8899bb, flatShading: true, metalness: 0.6, roughness: 0.4, side: THREE.DoubleSide
  });
  const wings = new THREE.Mesh(wingGeom, wingMat);
  group.add(wings);
  
  // Engine glow
  const engineGeom = new THREE.SphereGeometry(0.4, 8, 8);
  const engineMat = new THREE.MeshBasicMaterial({ color: 0xff6600 });
  const engine = new THREE.Mesh(engineGeom, engineMat);
  engine.position.z = 1.8;
  engine.name = 'engine';
  group.add(engine);
  
  // Engine trail light
  const trailLight = new THREE.PointLight(0xff6600, 2, 15);
  trailLight.position.z = 2.5;
  group.add(trailLight);
  
  group.scale.set(0.7, 0.7, 0.7);
  scene.add(group);
  
  return {
    group, body, wings, engine, trailLight,
    roll: 0, pitch: 0, targetRoll: 0, targetPitch: 0
  };
}

// ── function: createTrailParticles ──
function createTrailParticles(scene, playerGroup) {
  const maxParticles = 60;
  const positions = new Float32Array(maxParticles * 3);
  const opacities = new Float32Array(maxParticles);
  const sizes = new Float32Array(maxParticles);
  
  for (let i = 0; i < maxParticles; i++) {
    positions[i*3] = 0; positions[i*3+1] = 0; positions[i*3+2] = 0;
    opacities[i] = 0; sizes[i] = 0.3;
  }
  
  const geom = new THREE.BufferGeometry();
  geom.setAttribute('position', new THREE.BufferAttribute(positions, 3));
  geom.setAttribute('opacity', new THREE.BufferAttribute(opacities, 1));
  geom.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
  
  const mat = new THREE.PointsMaterial({
    color: 0xff8800, size: 0.5, transparent: true, opacity: 0.6,
    blending: THREE.AdditiveBlending, depthWrite: false, sizeAttenuation: true
  });
  
  const particles = new THREE.Points(geom, mat);
  scene.add(particles);
  
  let idx = 0;
  
  function emit(pos, speed) {
    const i = idx % maxParticles;
    positions[i*3] = pos.x + (Math.random() - 0.5) * 0.3;
    positions[i*3+1] = pos.y + (Math.random() - 0.5) * 0.3;
    positions[i*3+2] = pos.z + 2 + Math.random() * 0.5;
    opacities[i] = 0.8;
    sizes[i] = 0.3 + speed * 0.01;
    idx++;
    geom.attributes.position.needsUpdate = true;
    geom.attributes.opacity.needsUpdate = true;
  }
  
  function update(dt) {
    for (let i = 0; i < maxParticles; i++) {
      if (opacities[i] > 0) {
        positions[i*3+2] += 35 * dt;
        opacities[i] -= 2.0 * dt;
        if (opacities[i] < 0) opacities[i] = 0;
      }
    }
    geom.attributes.position.needsUpdate = true;
    geom.attributes.opacity.needsUpdate = true;
    mat.opacity = 0.6;
  }
  
  return { emit, update, particles };
}

// ── function: createTerrainSystem ──
function createTerrainSystem(scene) {
  const chunks = [];
  const chunkLength = 100;
  const visibleChunks = 4;
  const canyonWidth = 18;
  
  // Shared materials (reuse to reduce draw call overhead)
  const wallMat = new THREE.MeshStandardMaterial({ color: 0xcc6633, flatShading: true, roughness: 0.85 });
  const wallMat2 = new THREE.MeshStandardMaterial({ color: 0x994422, flatShading: true, roughness: 0.9 });
  const groundMat = new THREE.MeshStandardMaterial({ color: 0x553322, flatShading: true, roughness: 1.0 });
  const orbMat = new THREE.MeshBasicMaterial({ color: 0x00ffcc });
  
  // Shared geometries
  const groundGeom = new THREE.PlaneGeometry(canyonWidth + 30, chunkLength);
  const wallGeom = new THREE.BoxGeometry(4, 35, chunkLength);
  const orbGeom = new THREE.OctahedronGeometry(0.7, 0);
  
  function createChunk(zOffset, difficulty) {
    const group = new THREE.Group();
    group.position.z = zOffset;
    
    // Ground
    const ground = new THREE.Mesh(groundGeom, groundMat);
    ground.rotation.x = -Math.PI / 2;
    ground.position.set(0, -5, -chunkLength/2);
    group.add(ground);
    
    // Walls
    const leftWall = new THREE.Mesh(wallGeom, wallMat);
    leftWall.position.set(-canyonWidth/2 - 2, 12.5, -chunkLength/2);
    group.add(leftWall);
    
    const rightWall = new THREE.Mesh(wallGeom, wallMat2);
    rightWall.position.set(canyonWidth/2 + 2, 12.5, -chunkLength/2);
    group.add(rightWall);
    
    // Obstacles - fewer, simpler
    const obstacles = [];
    const numObstacles = Math.floor(1.5 + difficulty * 0.8);
    
    for (let i = 0; i < numObstacles; i++) {
      const z = -Math.random() * chunkLength * 0.9;
      const type = Math.random();
      
      if (type < 0.4) {
        // Monolith
        const h = 8 + Math.random() * 10;
        const w = 2.5 + Math.random() * 2;
        const monolithGeom = new THREE.BoxGeometry(w, h, w);
        const monolith = new THREE.Mesh(monolithGeom, wallMat);
        const xPos = (Math.random() - 0.5) * (canyonWidth - w - 4);
        monolith.position.set(xPos, -5 + h/2, z);
        group.add(monolith);
        obstacles.push({ pos: monolith.position, size: { x: w, y: h, z: w }, active: true });
      } else if (type < 0.7) {
        // Gate
        const gapWidth = Math.max(4, 9 - difficulty * 0.6);
        const gateH = 10;
        const gateY = 0;
        const gateGeom = new THREE.BoxGeometry(2.5, gateH, 2.5);
        
        const leftGate = new THREE.Mesh(gateGeom, wallMat2);
        leftGate.position.set(-gapWidth/2 - 1.25, gateY, z);
        group.add(leftGate);
        obstacles.push({ pos: leftGate.position, size: { x: 2.5, y: gateH, z: 2.5 }, active: true });
        
        const rightGate = new THREE.Mesh(gateGeom, wallMat2);
        rightGate.position.set(gapWidth/2 + 1.25, gateY, z);
        group.add(rightGate);
        obstacles.push({ pos: rightGate.position, size: { x: 2.5, y: gateH, z: 2.5 }, active: true });
      } else {
        // Barrier
        const bw = 5 + Math.random() * 4;
        const barrierGeom = new THREE.BoxGeometry(bw, 2.5, 2.5);
        const barrier = new THREE.Mesh(barrierGeom, wallMat);
        barrier.position.set((Math.random() - 0.5) * (canyonWidth - bw - 2), 3 + Math.random() * 3, z);
        group.add(barrier);
        obstacles.push({ pos: barrier.position, size: { x: bw, y: 2.5, z: 2.5 }, active: true });
      }
    }
    
    // Orbs
    const orbs = [];
    const numOrbs = 2 + Math.floor(Math.random() * 2);
    for (let i = 0; i < numOrbs; i++) {
      const orb = new THREE.Mesh(orbGeom, orbMat);
      const ox = (Math.random() - 0.5) * (canyonWidth - 6);
      const oy = -2 + Math.random() * 5;
      const oz = -Math.random() * chunkLength * 0.9;
      orb.position.set(ox, oy, oz);
      group.add(orb);
      
      const glowLight = new THREE.PointLight(0x00ffcc, 1.5, 8);
      glowLight.position.copy(orb.position);
      group.add(glowLight);
      
      orbs.push({ mesh: orb, light: glowLight, pos: orb.position, collected: false });
    }
    
    scene.add(group);
    return { group, obstacles, orbs, zOffset, length: chunkLength };
  }
  
  function init() {
    for (let i = 0; i < visibleChunks; i++) {
      chunks.push(createChunk(-i * chunkLength, 0));
    }
  }
  
  function update(playerZ, difficulty) {
    const lastChunk = chunks[chunks.length - 1];
    if (lastChunk && playerZ < lastChunk.zOffset + chunkLength * 2) {
      chunks.push(createChunk(lastChunk.zOffset - chunkLength, difficulty));
    }
    
    while (chunks.length > 0 && chunks[0].zOffset > playerZ + chunkLength * 2) {
      const old = chunks.shift();
      scene.remove(old.group);
      old.group.traverse(obj => {
        if (obj.geometry && obj.geometry !== groundGeom && obj.geometry !== wallGeom && obj.geometry !== orbGeom) obj.geometry.dispose();
      });
    }
  }
  
  function getAllObstacles() {
    const all = [];
    for (const chunk of chunks) {
      for (const obs of chunk.obstacles) {
        if (obs.active) {
          all.push({ pos: { x: obs.pos.x, y: obs.pos.y, z: obs.pos.z + chunk.group.position.z }, size: obs.size });
        }
      }
    }
    return all;
  }
  
  function getAllOrbs() {
    const all = [];
    for (const chunk of chunks) {
      for (const orb of chunk.orbs) {
        if (!orb.collected) {
          all.push({ pos: { x: orb.pos.x, y: orb.pos.y, z: orb.pos.z + chunk.group.position.z }, mesh: orb.mesh, light: orb.light, orbRef: orb });
        }
      }
    }
    return all;
  }
  
  return { init, update, getAllObstacles, getAllOrbs, chunks, chunkLength };
}

// ── function: createDebrisParticles ──
function createDebrisParticles(scene) {
  const maxParticles = 100;
  const positions = new Float32Array(maxParticles * 3);
  const velocities = new Float32Array(maxParticles * 3);
  const lifetimes = new Float32Array(maxParticles);
  
  const geom = new THREE.BufferGeometry();
  geom.setAttribute('position', new THREE.BufferAttribute(positions, 3));
  
  const mat = new THREE.PointsMaterial({
    color: 0xcc8844, size: 0.3, transparent: true, opacity: 0.7,
    blending: THREE.AdditiveBlending, depthWrite: false, sizeAttenuation: true
  });
  
  const particles = new THREE.Points(geom, mat);
  scene.add(particles);
  
  let spawnTimer = 0;
  
  function spawn(pos, speed) {
    for (let j = 0; j < 3; j++) {
      for (let i = 0; i < maxParticles; i++) {
        if (lifetimes[i] <= 0) {
          positions[i*3] = pos.x + (Math.random() - 0.5) * 8;
          positions[i*3+1] = pos.y + (Math.random() - 0.5) * 4;
          positions[i*3+2] = pos.z + (Math.random() - 0.5) * 3;
          velocities[i*3] = (Math.random() - 0.5) * 2;
          velocities[i*3+1] = (Math.random() - 0.5) * 2;
          velocities[i*3+2] = -speed * 0.3 - Math.random() * 10;
          lifetimes[i] = 0.5 + Math.random() * 0.5;
          break;
        }
      }
    }
  }
  
  function update(dt) {
    for (let i = 0; i < maxParticles; i++) {
      if (lifetimes[i] > 0) {
        positions[i*3] += velocities[i*3] * dt;
        positions[i*3+1] += velocities[i*3+1] * dt;
        positions[i*3+2] += velocities[i*3+2] * dt;
        lifetimes[i] -= dt;
        if (lifetimes[i] <= 0) {
          positions[i*3+2] = 999;
        }
      } else {
        positions[i*3+2] = 999;
      }
    }
    geom.attributes.position.needsUpdate = true;
  }
  
  return { spawn, update, particles };
}

// ── function: createHUD ──
function createHUD() {
  const scoreEl = document.getElementById('score-display');
  const multEl = document.getElementById('multiplier-display');
  const energyBar = document.getElementById('energy-bar');
  const speedBar = document.getElementById('speed-bar');
  const turboFill = document.getElementById('turbo-fill');
  const gameOverEl = document.getElementById('game-over');
  const finalScoreEl = document.getElementById('final-score');
  const maxSpeedEl = document.getElementById('max-speed-stat');
  const maxMultEl = document.getElementById('max-multiplier-stat');
  const startScreen = document.getElementById('start-screen');
  
  let lastMult = 1.0;
  
  function update(score, multiplier, energy, maxEnergy, speed, maxSpeedRef, turbo) {
    scoreEl.textContent = Math.floor(score).toLocaleString();
    
    if (Math.abs(multiplier - lastMult) > 0.1) {
      multEl.textContent = 'x' + multiplier.toFixed(1);
      multEl.classList.add('pulse');
      setTimeout(() => multEl.classList.remove('pulse'), 150);
      lastMult = multiplier;
    }
    
    const energyPct = Math.max(0, energy / maxEnergy * 100);
    energyBar.style.width = energyPct + '%';
    if (energyPct < 25) {
      energyBar.style.background = 'linear-gradient(90deg, #ff3300, #ff6600)';
      energyBar.style.boxShadow = '0 0 12px rgba(255, 50, 0, 0.8)';
    } else {
      energyBar.style.background = 'linear-gradient(90deg, #00ffaa, #00ccff)';
      energyBar.style.boxShadow = '0 0 12px rgba(0, 255, 170, 0.6)';
    }
    
    const speedPct = Math.min(100, speed / (maxSpeedRef * 1.5) * 100);
    speedBar.style.width = speedPct + '%';
    
    // Turbo ring - circumference is 2*PI*40 ≈ 251.2
    const turboPct = Math.max(0, Math.min(1, turbo / 100));
    turboFill.style.strokeDashoffset = 251.2 * (1 - turboPct);
  }
  
  function showGameOver(score, maxSpeed, maxMult) {
    finalScoreEl.textContent = Math.floor(score).toLocaleString();
    maxSpeedEl.textContent = 'MAX SPEED: ' + Math.floor(maxSpeed);
    maxMultEl.textContent = 'MAX MULTI: x' + maxMult.toFixed(1);
    gameOverEl.classList.remove('hidden');
  }
  
  function hideGameOver() {
    gameOverEl.classList.add('hidden');
  }
  
  function hideStartScreen() {
    startScreen.classList.add('hidden');
  }
  
  function showStartScreen() {
    startScreen.classList.remove('hidden');
  }
  
  return { update, showGameOver, hideGameOver, hideStartScreen, showStartScreen, startScreen, gameOverEl };
}

// ── function: createGame ──
function createGame() {
  const canvas = document.getElementById('game-canvas');
  const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: false, powerPreference: 'high-performance' });
  
  const isMobile = window.innerWidth < 768;
  const renderScale = isMobile ? 0.6 : 0.85;
  renderer.setSize(window.innerWidth * renderScale, window.innerHeight * renderScale);
  canvas.style.width = '100%';
  canvas.style.height = '100%';
  renderer.setPixelRatio(1);
  renderer.shadowMap.enabled = false;
  renderer.toneMapping = THREE.ACESFilmicToneMapping;
  renderer.toneMappingExposure = 1.3;
  
  const scene = new THREE.Scene();
  scene.fog = new THREE.FogExp2(0x2a1538, 0.007);
  scene.background = new THREE.Color(0x2a1538);
  
  // Starfield background
  const starGeom = new THREE.BufferGeometry();
  const starCount = 200;
  const starPositions = new Float32Array(starCount * 3);
  for (let i = 0; i < starCount; i++) {
    starPositions[i*3] = (Math.random() - 0.5) * 200;
    starPositions[i*3+1] = (Math.random() - 0.5) * 100 + 20;
    starPositions[i*3+2] = -Math.random() * 200 - 20;
  }
  starGeom.setAttribute('position', new THREE.BufferAttribute(starPositions, 3));
  const starMat = new THREE.PointsMaterial({ color: 0xffaaff, size: 0.8, transparent: true, opacity: 0.6, sizeAttenuation: true });
  const stars = new THREE.Points(starGeom, starMat);
  scene.add(stars);
  
  const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 150);
  camera.position.set(0, 2, 8);
  
  // Lighting
  scene.add(new THREE.AmbientLight(0x7755aa, 1.4));
  
  const dirLight = new THREE.DirectionalLight(0xff9955, 2.5);
  dirLight.position.set(-8, 15, -5);
  scene.add(dirLight);
  
  const fillLight = new THREE.DirectionalLight(0x6688ff, 0.8);
  fillLight.position.set(10, 8, 5);
  scene.add(fillLight);
  
  const rimLight = new THREE.DirectionalLight(0xff6633, 1.0);
  rimLight.position.set(0, -5, 10);
  scene.add(rimLight);
  
  // Systems
  const audio = createAudioSystem();
  const terrain = createTerrainSystem(scene);
  const debris = createDebrisParticles(scene);
  const hud = createHUD();
  
  // Player
  const player = createPlayerCraft(scene);
  const trail = createTrailParticles(scene, player.group);
  
  // State
  let state = 'start';
  let playerX = 0, playerY = 0, playerZ = 0;
  let velX = 0, velY = 0;
  let speed = 35, baseSpeed = 35, maxSpeedReached = 35;
  let energy = 100, score = 0, multiplier = 1.0, maxMultiplier = 1.0;
  let turbo = 0, boosting = false, difficulty = 0;
  let nearMissTimer = 0, cameraShake = 0, time = 0;
  let lastTime = performance.now();
  
  const keys = {};
  window.addEventListener('keydown', e => { keys[e.code] = true; });
  window.addEventListener('keyup', e => { keys[e.code] = false; });
  
  let touchX = 0, touchY = 0, touching = false;
  canvas.addEventListener('touchstart', e => {
    touching = true;
    touchX = e.touches[0].clientX;
    touchY = e.touches[0].clientY;
    e.preventDefault();
  }, { passive: false });
  canvas.addEventListener('touchmove', e => {
    touchX = e.touches[0].clientX;
    touchY = e.touches[0].clientY;
    e.preventDefault();
  }, { passive: false });
  canvas.addEventListener('touchend', () => { touching = false; });
  
  function start() {
    audio.init();
    state = 'playing';
    playerX = 0; playerY = 0; playerZ = 0;
    velX = 0; velY = 0;
    speed = 35; baseSpeed = 35; maxSpeedReached = 35;
    energy = 100; score = 0; multiplier = 1.0; maxMultiplier = 1.0;
    turbo = 0; difficulty = 0; nearMissTimer = 0; cameraShake = 0; time = 0;
    lastTime = performance.now();
    terrain.init();
    hud.hideGameOver();
    hud.hideStartScreen();
  }
  
  function gameOver() {
    state = 'over';
    audio.playCrash();
    hud.showGameOver(score, maxSpeedReached, maxMultiplier);
  }
  
  function reset() {
    for (const chunk of terrain.chunks) {
      scene.remove(chunk.group);
      chunk.group.traverse(obj => {
        if (obj.geometry) obj.geometry.dispose();
        if (obj.material) {
          if (Array.isArray(obj.material)) obj.material.forEach(m => m.dispose());
          else obj.material.dispose();
        }
      });
    }
    terrain.chunks.length = 0;
    start();
  }
  
  function update(dt) {
    if (state !== 'playing') return;
    dt = Math.min(dt, 0.033); // Cap at ~30fps worth of dt
    
    time += dt;
    difficulty = Math.min(10, time * 0.08);
    
    let inputX = 0, inputY = 0;
    if (keys['ArrowLeft'] || keys['KeyA']) inputX -= 1;
    if (keys['ArrowRight'] || keys['KeyD']) inputX += 1;
    if (keys['ArrowUp'] || keys['KeyW']) inputY += 1;
    if (keys['ArrowDown'] || keys['KeyS']) inputY -= 1;
    
    if (touching) {
      const cx = window.innerWidth / 2, cy = window.innerHeight / 2;
      inputX = Math.max(-1, Math.min(1, (touchX - cx) / cx));
      inputY = Math.max(-1, Math.min(1, -(touchY - cy) / cy));
    }
    
    boosting = (keys['Space'] || keys['ShiftLeft']) && turbo > 10 && energy > 5;
    
    // Physics - momentum based
    velX += inputX * 50 * dt - velX * 2.5 * dt;
    velY += inputY * 20 * dt - velY * 2.5 * dt;
    playerX += velX * dt;
    playerY += velY * dt;
    
    // Canyon bounds
    if (playerX > 7) { playerX = 7; velX *= -0.3; }
    if (playerX < -7) { playerX = -7; velX *= -0.3; }
    if (playerY > 5) { playerY = 5; velY *= -0.3; }
    if (playerY < -3) { playerY = -3; velY *= -0.3; }
    
    // Speed management
    baseSpeed = 35 + difficulty * 4;
    if (boosting) {
      speed = baseSpeed * 2.2;
      turbo -= 25 * dt;
      energy -= 6 * dt;
    } else {
      speed = baseSpeed + turbo * 0.4;
      if (speed > baseSpeed * 1.1) turbo += 20 * dt;
    }
    turbo = Math.max(0, Math.min(100, turbo));
    speed = Math.min(speed, baseSpeed * 2.2);
    maxSpeedReached = Math.max(maxSpeedReached, speed);
    
    // Forward motion
    playerZ -= speed * dt;
    
    // Energy decay
    energy -= (1.5 + difficulty * 0.2) * dt;
    if (energy <= 0) { energy = 0; gameOver(); return; }
    
    // Score
    score += speed * dt * multiplier * 0.8;
    
    // Banking animation
    player.targetRoll = -velX * 0.04;
    player.targetPitch = velY * 0.03 - (boosting ? 0.1 : 0);
    player.roll += (player.targetRoll - player.roll) * 5 * dt;
    player.pitch += (player.targetPitch - player.pitch) * 5 * dt;
    
    player.group.position.set(playerX, playerY, playerZ);
    player.group.rotation.z = player.roll;
    player.group.rotation.x = player.pitch;
    
    // Engine glow scales with speed
    player.engine.scale.setScalar(0.8 + speed * 0.015 + (boosting ? 0.5 : 0));
    player.trailLight.intensity = 1.5 + speed * 0.05 + (boosting ? 3 : 0);
    
    // Particles
    trail.emit(player.group.position, speed);
    trail.update(dt);
    
    if (Math.random() < speed * 0.003) debris.spawn(player.group.position, speed);
    debris.update(dt);
    
    // Animate orbs (spin)
    const orbsData = terrain.getAllOrbs();
    for (const orb of orbsData) {
      orb.mesh.rotation.y += 3 * dt;
      orb.mesh.rotation.x += 1.5 * dt;
    }
    
    // Terrain management
    terrain.update(playerZ, difficulty);
    
    // Collision detection
    const obstacles = terrain.getAllObstacles();
    let hit = false, closestDist = Infinity;
    
    for (const obs of obstacles) {
      const ox = Math.max(obs.pos.x - obs.size.x/2, Math.min(playerX, obs.pos.x + obs.size.x/2));
      const oy = Math.max(obs.pos.y - obs.size.y/2, Math.min(playerY, obs.pos.y + obs.size.y/2));
      const oz = Math.max(obs.pos.z - obs.size.z/2, Math.min(playerZ, obs.pos.z + obs.size.z/2));
      const dx = playerX - ox, dy = playerY - oy, dz = playerZ - oz;
      const dist = Math.sqrt(dx*dx + dy*dy + dz*dz);
      if (dist < closestDist) closestDist = dist;
      if (dist < 1.2) hit = true;
    }
    
    if (hit) { gameOver(); return; }
    
    // Near-miss detection
    if (closestDist < 3.5 && closestDist > 1.2 && nearMissTimer <= 0) {
      multiplier = Math.min(5.0, multiplier + 0.2);
      maxMultiplier = Math.max(maxMultiplier, multiplier);
      turbo += 8;
      audio.playNearMiss();
      nearMissTimer = 0.3;
      cameraShake = 0.3;
    }
    nearMissTimer -= dt;
    if (multiplier > 1.0) multiplier = Math.max(1.0, multiplier - 0.3 * dt);
    
    // Orb collection
    for (const orb of orbsData) {
      const dx = playerX - orb.pos.x, dy = playerY - orb.pos.y, dz = playerZ - orb.pos.z;
      if (Math.sqrt(dx*dx + dy*dy + dz*dz) < 2.0) {
        orb.orbRef.collected = true;
        orb.mesh.visible = false;
        orb.light.visible = false;
        energy = Math.min(100, energy + 15);
        score += 100 * multiplier;
        audio.playCollect();
      }
    }
    
    // Camera follow with smoothing
    camera.position.x += (playerX * 0.4 - camera.position.x) * 3 * dt;
    camera.position.y += (playerY * 0.3 + 3 - camera.position.y) * 3 * dt;
    camera.position.z += (playerZ + 10 - camera.position.z) * 3 * dt;
    camera.lookAt(playerX * 0.3, playerY * 0.2, playerZ - 8);
    
    // Camera shake on near-miss
    if (cameraShake > 0) {
      camera.position.x += (Math.random() - 0.5) * cameraShake * 0.5;
      camera.position.y += (Math.random() - 0.5) * cameraShake * 0.3;
      cameraShake -= dt * 2;
    }
    
    // FOV widens with speed
    const targetFov = 75 + (speed - baseSpeed) * 0.5 + (boosting ? 15 : 0);
    camera.fov += (targetFov - camera.fov) * 3 * dt;
    camera.updateProjectionMatrix();
    
    // Audio engine pitch
    audio.updateEngine(speed / baseSpeed, boosting);
    
    // Update HUD
    hud.update(score, multiplier, energy, 100, speed, maxSpeedReached, turbo);
  }
  
  function loop() {
    requestAnimationFrame(loop);
    const now = performance.now();
    const dt = (now - lastTime) / 1000;
    lastTime = now;
    update(dt);
    renderer.render(scene, camera);
  }
  
  function handleResize() {
    const rs = isMobile ? 0.6 : 0.85;
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth * rs, window.innerHeight * rs);
  }
  
  window.addEventListener('resize', handleResize);
  
  return { start, reset, loop, hud };
}

// ── main ──
const game = createGame();
game.loop();

// Start button
document.getElementById('start-btn').addEventListener('click', () => {
  game.start();
});

// Restart button
document.getElementById('restart-btn').addEventListener('click', () => {
  game.reset();
});

// Keyboard start
window.addEventListener('keydown', (e) => {
  if (e.code === 'Enter' || e.code === 'Space') {
    if (game.hud.startScreen && !game.hud.startScreen.classList.contains('hidden')) {
      game.start();
    } else if (!game.hud.gameOverEl.classList.contains('hidden')) {
      game.reset();
    }
  }
});
</script>
</body>
</html>
<!-- agent-meta {"model":"meituan/longcat-2.0","provider":"openrouter","persona":"composer","sessionId":"1f9925f6-ff74-4e9a-9e75-a05611da1180","tokensIn":3244792,"tokensOut":63759,"tokensTotal":3308551,"turns":75,"toolCalls":86,"failedToolCalls":2,"timestamp":"2026-07-21T03:04:36.499Z"} -->