← Canyon Run results

Canyon Run

inkling · basic

◈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 24,383 bytes · SHA-256 8d431c512783
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>High-Speed Procedural Canyon Racer</title>
<style>
  @import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;900&family=Rajdhani:wght@300;700&display=swap');
  * { margin: 0; padding: 0; box-sizing: border-box; }
  html, body { width: 100%; height: 100%; overflow: hidden; background: #0a0214; font-family: 'Rajdhani', sans-serif; color: #fff; }
  #canvas-container { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; }
  #hud { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 10; }
  #multiplier { position: absolute; top: 40px; left: 50%; transform: translateX(-50%); font-family: 'Orbitron', sans-serif; font-size: 90px; font-weight: 900; color: #ffaa55; text-shadow: 0 0 30px #ff6600, 0 0 80px #ff4400; letter-spacing: -6px; line-height: 1; opacity: 0.95; }
  #score-display { position: absolute; top: 50px; right: 40px; font-size: 28px; font-weight: 300; color: #eeddcc; letter-spacing: 4px; text-shadow: 0 2px 10px rgba(0,0,0,0.6); }
  #speed-display { position: absolute; bottom: 30px; left: 40px; text-align: left; }
  #speed-display .label { font-size: 14px; text-transform: uppercase; letter-spacing: 3px; color: #aa7755; margin-bottom: 4px; }
  #speed-display .val { font-family: 'Orbitron', sans-serif; font-size: 36px; color: #ffaa88; text-shadow: 0 0 15px rgba(255,150,80,0.4); }
  #energy-bar-container { position: absolute; bottom: 30px; right: 40px; width: 300px; height: 8px; background: rgba(255,255,255,0.08); border-radius: 4px; overflow: hidden; border: 1px solid rgba(255,255,255,0.1); }
  #energy-fill { height: 100%; width: 100%; background: linear-gradient(90deg, #ffaa55, #ff4400); border-radius: 4px; transition: width 0.15s ease-out; box-shadow: 0 0 10px rgba(255,150,80,0.6); }
  #boost-meter { position: absolute; bottom: 50px; right: 40px; width: 300px; height: 4px; background: rgba(255,255,255,0.05); border-radius: 2px; overflow: hidden; }
  #boost-fill { height: 100%; width: 0%; background: linear-gradient(90deg, #44ffaa, #00ffaa); border-radius: 2px; box-shadow: 0 0 8px rgba(0,255,100,0.5); transition: width 0.15s ease-out; }
  #gameover { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(10,3,30,0.88); display: none; flex-direction: column; align-items: center; justify-content: center; z-index: 20; backdrop-filter: blur(8px); }
  #gameover h1 { font-family: 'Orbitron', sans-serif; font-size: 100px; letter-spacing: -10px; color: #ff3322; text-shadow: 0 0 60px #ff1100; margin-bottom: 10px; animation: pulse 2s infinite; }
  @keyframes pulse { 0%,100% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.05); opacity: 0.9; } }
  #gameover .stats { font-size: 26px; color: #eeddcc; margin: 6px 0; letter-spacing: 2px; }
  #gameover .stats span { color: #ffaa55; font-family: 'Orbitron', sans-serif; font-weight: 900; }
  #gameover button { margin-top: 30px; padding: 18px 50px; font-family: 'Orbitron', sans-serif; font-size: 22px; background: linear-gradient(135deg, #ffaa55, #ff6600); border: none; color: #0a0214; border-radius: 30px; cursor: pointer; box-shadow: 0 0 30px rgba(255,120,30,0.6); pointer-events: auto; transition: transform 0.1s; }
  #gameover button:hover { transform: translateY(-3px); box-shadow: 0 0 50px rgba(255,140,40,0.8); }
  #hint { position: absolute; bottom: 10px; left: 50%; transform: translateX(-50%); font-size: 12px; color: rgba(255,255,255,0.3); letter-spacing: 2px; pointer-events: none; }
</style>
</head>
<body>
<div id="canvas-container"></div>

<div id="hud">
  <div id="multiplier">x1.0</div>
  <div id="score-display">0</div>
  
  <div id="speed-display">
    <div class="label">VELOCITY</div>
    <div class="val" id="speed-val">60 m/s</div>
  </div>
  
  <div id="energy-bar-container"><div id="energy-fill"></div></div>
  <div id="boost-meter"><div id="boost-fill"></div></div>
</div>

<div id="gameover">
  <h1>CRASHED</h1>
  <div class="stats">FINAL SCORE: <span id="final-score">0</span></div>
  <div class="stats">MAX VELOCITY: <span id="final-speed">0</span> m/s</div>
  <div class="stats">NEAR-MISS MULTIPLIER: <span id="final-mult">1.0</span>x</div>
  <button onclick="restartGame()">RESTART FLIGHT</button>
</div>

<div id="hint">MOUSE TO BANK / PITCH &nbsp;|&nbsp; SHIFT FOR BOOST &nbsp;|&nbsp; COLLECT ORBS</div>

<script type="importmap">
{
  "imports": {
    "three": "https://unpkg.com/three@0.160.0/build/three.module.js",
    "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
  }
}
</script>

<script type="module">
import * as THREE from 'three';
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';

// --- STATE ---
let scene, camera, renderer, composer, renderPass, bloomPass, motionPass;
let clock, playerMesh, sunLight, ambientLight;
let chunks = [], particles = [];
let audioCtx, engineOsc, engineGain, nearMissGain;
let running = true;
let score = 0, multiplier = 1, energy = 100, turbo = 0, maxTurbo = 100;
let speed = 55, maxBaseSpeed = 130, speedTarget = 55;
let mouseX = window.innerWidth / 2, mouseY = window.innerHeight / 2;
let boostActive = false, shakeIntensity = 0;
let chunkLength = 50;
let pathOffset = 0;

const container = document.getElementById('canvas-container');

// --- AUDIO SETUP ---
function initAudio() {
  try {
    audioCtx = new (window.AudioContext || window.webkitAudioContext)();
    engineOsc = audioCtx.createOscillator();
    engineOsc.type = 'triangle';
    engineGain = audioCtx.createGain();
    engineOsc.connect(engineGain);
    engineGain.connect(audioCtx.destination);
    engineGain.gain.setValueAtTime(0.08, audioCtx.currentTime);
    engineOsc.frequency.setValueAtTime(180, audioCtx.currentTime);
    engineOsc.start();

    nearMissGain = audioCtx.createGain();
    nearMissGain.gain.setValueAtTime(0, audioCtx.currentTime);
  } catch(e) { console.log('Audio init failed'); }
}

function playSound(type) {
  if (!audioCtx) return;
  const osc = audioCtx.createOscillator();
  const gain = audioCtx.createGain();
  osc.connect(gain);
  gain.connect(audioCtx.destination);
  
  if (type === 'boost') {
    osc.type = 'sine';
    osc.frequency.setValueAtTime(300, audioCtx.currentTime);
    osc.frequency.linearRampToValueAtTime(900, audioCtx.currentTime + 0.4);
    gain.gain.setValueAtTime(0.15, audioCtx.currentTime);
    gain.gain.linearRampToValueAtTime(0.02, audioCtx.currentTime + 0.4);
    osc.start(); osc.stop(audioCtx.currentTime + 0.4);
  } else if (type === 'near') {
    osc.type = 'sine';
    osc.frequency.setValueAtTime(700, audioCtx.currentTime);
    osc.frequency.exponentialRampToValueAtTime(1200, audioCtx.currentTime + 0.1);
    gain.gain.setValueAtTime(0.12, audioCtx.currentTime);
    gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.15);
    osc.start(); osc.stop(audioCtx.currentTime + 0.15);
  } else if (type === 'collect') {
    osc.type = 'triangle';
    osc.frequency.setValueAtTime(1000, audioCtx.currentTime);
    osc.frequency.exponentialRampToValueAtTime(300, audioCtx.currentTime + 0.2);
    gain.gain.setValueAtTime(0.1, audioCtx.currentTime);
    gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.2);
    osc.start(); osc.stop(audioCtx.currentTime + 0.2);
  }
}

function updateEngineSound() {
  if (!audioCtx || !engineOsc) return;
  const baseFreq = 180 + speed * 6 + (boostActive ? 300 : 0);
  engineOsc.frequency.linearRampToValueAtTime(baseFreq, audioCtx.currentTime + 0.05);
  const vol = 0.06 + (speed / 200) * 0.08 + (boostActive ? 0.08 : 0);
  engineGain.gain.linearRampToValueAtTime(Math.min(vol, 0.3), audioCtx.currentTime + 0.05);
}

// --- WORLD FUNCTIONS ---
function pathX(z) {
  return Math.sin(z * 0.018) * 14 + Math.sin(z * 0.06) * 8 + Math.sin(z * 0.003) * 18;
}

function generateChunk(z) {
  const group = new THREE.Group();
  const segmentCount = Math.ceil(chunkLength / 8);
  const currentDifficulty = 1 + score / 800;
  const currentWidth = Math.max(8, 36 - score * 0.15);

  for (let i = 0; i < segmentCount; i++) {
    const localZ = i * 8;
    const worldZ = z + localZ;
    const cx = pathX(worldZ);
    const leftX = cx - currentWidth / 2;
    const rightX = cx + currentWidth / 2;

    // Wall columns with varied heights / angles for geometric aesthetic
    const h1 = 7 + Math.random() * 10;
    const h2 = 7 + Math.random() * 10;
    const wallMat = new THREE.MeshStandardMaterial({
      color: 0xcc5522 + Math.floor(Math.random() * 0x221111),
      metalness: 0.1, roughness: 0.9, flatShading: true
    });
    
    const colGeo = new THREE.BoxGeometry(2.5, h1, 1.5);
    const leftCol = new THREE.Mesh(colGeo, wallMat);
    leftCol.position.set(leftX, h1 / 2, worldZ);
    leftCol.rotation.y = Math.sin(worldZ * 0.03) * 0.3 - 0.2;
    group.add(leftCol);

    const rightCol = new THREE.Mesh(colGeo, wallMat);
    rightCol.position.set(rightX, h2 / 2, worldZ);
    rightCol.rotation.y = -Math.sin(worldZ * 0.03 + 1) * 0.3 + 0.15;
    group.add(rightCol);

    // Floating Monolith obstacles
    if (Math.random() < 0.18 * currentDifficulty && worldZ > 30) {
      const size = 2 + Math.random() * 3;
      const obsGeo = new THREE.BoxGeometry(size, 7 + Math.random() * 5, size);
      const obsMat = new THREE.MeshStandardMaterial({
        color: 0x5533aa, emissive: 0x221144, metalness: 0.3, roughness: 0.7, flatShading: true
      });
      const obs = new THREE.Mesh(obsGeo, obsMat);
      const ox = cx + (Math.random() - 0.5) * (currentWidth * 0.55);
      obs.position.set(ox, 4, worldZ);
      group.add(obs);
      group.userData.obstacles = group.userData.obstacles || [];
      group.userData.obstacles.push({ mesh: obs, x: ox, y: 4, z: worldZ, r: size * 0.7 });
    }

    // Glowing energy orbs
    if (Math.random() < 0.35 && worldZ > 20) {
      const orbGeo = new THREE.IcosahedronGeometry(0.45, 0);
      const orbMat = new THREE.MeshStandardMaterial({
        color: 0xffffaa, emissive: 0xff6600, emissiveIntensity: 3, metalness: 0.2, roughness: 0.2
      });
      const orb = new THREE.Mesh(orbGeo, orbMat);
      const ox = cx + (Math.random() - 0.5) * (currentWidth * 0.75);
      const oy = 3 + Math.random() * 5;
      orb.position.set(ox, oy, worldZ);
      group.add(orb);
      
      const pointLight = new THREE.PointLight(0xff6600, 3, 12);
      pointLight.position.set(ox, oy, worldZ);
      group.add(pointLight);

      group.userData.orbs = group.userData.orbs || [];
      group.userData.orbs.push({ mesh: orb, light: pointLight, x: ox, y: oy, z: worldZ, r: 0.7 });
    }
  }

  // Ground strip for depth reference
  const floorGeo = new THREE.PlaneGeometry(currentWidth + 4, chunkLength + 4);
  const floorMat = new THREE.MeshStandardMaterial({ color: 0x150622, metalness: 0.1, roughness: 1, side: THREE.DoubleSide });
  const floor = new THREE.Mesh(floorGeo, floorMat);
  floor.rotation.x = -Math.PI / 2;
  floor.position.set(pathX(z + chunkLength / 2), -8, z + chunkLength / 2);
  group.add(floor);

  scene.add(group);
  chunks.push({ z: z, group: group });
}

// --- INIT ---
function init() {
  scene = new THREE.Scene();
  scene.background = new THREE.Color(0x140822);
  scene.fog = new THREE.FogExp2(0x2a0a3a, 0.018);

  camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 600);
  camera.position.set(0, 6, -12);

  renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: "high-performance" });
  renderer.setSize(window.innerWidth, window.innerHeight);
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
  renderer.toneMapping = THREE.ACESFilmicToneMapping;
  renderer.outputColorSpace = THREE.SRGBColorSpace;
  renderer.shadowMap.enabled = true;
  container.appendChild(renderer.domElement);

  // Lighting: Golden Hour / Cyber-Sunset palette
  sunLight = new THREE.DirectionalLight(0xffaa55, 3);
  sunLight.position.set(40, 80, -60);
  sunLight.castShadow = true;
  scene.add(sunLight);
  
  const hemiLight = new THREE.HemisphereLight(0xff7755, 0x331144, 1.2);
  scene.add(hemiLight);
  
  ambientLight = new THREE.AmbientLight(0x442255, 0.6);
  scene.add(ambientLight);
  
  const accentLight = new THREE.PointLight(0xaa44ff, 10, 200);
  accentLight.position.set(0, 30, 0);
  scene.add(accentLight);

  // Player craft (low-poly sleek wedge)
  const shipGeo = new THREE.ConeGeometry(0.7, 3, 5);
  const shipMat = new THREE.MeshStandardMaterial({
    color: 0xdddddd, emissive: 0x441100, emissiveIntensity: 1.5, metalness: 0.9, roughness: 0.2, flatShading: true
  });
  playerMesh = new THREE.Mesh(shipGeo, shipMat);
  playerMesh.rotation.x = -Math.PI / 2;
  playerMesh.rotation.y = Math.PI; // face forward (positive Z)
  scene.add(playerMesh);

  // Post-processing
  composer = new EffectComposer(renderer);
  renderPass = new RenderPass(scene, camera);
  composer.addPass(renderPass);

  motionPass = new ShaderPass({
    uniforms: { tDiffuse: { value: null }, blurStrength: { value: 0 } },
    vertexShader: `varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`,
    fragmentShader: `
      uniform sampler2D tDiffuse;
      uniform float blurStrength;
      varying vec2 vUv;
      void main() {
        vec4 color = texture2D(tDiffuse, vUv);
        vec2 dir = vUv - 0.5;
        float samples = 10.0;
        for(float i = 0.0; i < samples; i++) {
          float f = (i + 0.5) / samples;
          float dist = blurStrength * 0.05 * f;
          color += texture2D(tDiffuse, vUv + dir * dist);
        }
        gl_FragColor = color / (samples + 1.0);
      }
    `
  });
  composer.addPass(motionPass);

  bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.6, 0.35, 0.85);
  composer.addPass(bloomPass);

  // Generate initial world
  for (let i = 0; i < 8; i++) {
    generateChunk(i * chunkLength);
  }

  clock = new THREE.Clock();

  window.addEventListener('resize', () => {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
    composer.setSize(window.innerWidth, window.innerHeight);
    bloomPass.resolution.set(window.innerWidth, window.innerHeight);
  });

  window.addEventListener('mousemove', (e) => {
    mouseX = e.clientX;
    mouseY = e.clientY;
  });

  window.addEventListener('keydown', (e) => {
    if ((e.code === 'ShiftLeft' || e.code === 'ShiftRight') && !boostActive && turbo > 10) {
      boostActive = true;
      playSound('boost');
      playerMesh.material.emissiveIntensity = 4;
    }
  });

  window.addEventListener('keyup', (e) => {
    if (e.code === 'ShiftLeft' || e.code === 'ShiftRight') {
      boostActive = false;
      playerMesh.material.emissiveIntensity = 1.5;
    }
  });

  document.addEventListener('click', () => {
    if (!audioCtx) initAudio();
  }, { once: true });

  requestAnimationFrame(animate);
}

function spawnTrail() {
  const geo = new THREE.SphereGeometry(0.15, 4, 4);
  const mat = new THREE.MeshStandardMaterial({ color: 0xffffff, emissive: 0xff4400, roughness: 1 });
  const mesh = new THREE.Mesh(geo, mat);
  mesh.position.set(
    playerMesh.position.x + (Math.random() - 0.5) * 0.4,
    playerMesh.position.y + (Math.random() - 0.5) * 0.3,
    playerMesh.position.z
  );
  scene.add(mesh);
  particles.push({
    mesh: mesh, life: 1.0,
    velocity: new THREE.Vector3(0, 0, -speed * 3)
  });
}

function spawnDebris() {
  if (Math.random() < 0.15) {
    const geo = new THREE.BoxGeometry(0.2 + Math.random() * 0.4, 0.2, 0.2 + Math.random() * 0.4);
    const mat = new THREE.MeshStandardMaterial({ color: 0x554433, roughness: 0.9 });
    const mesh = new THREE.Mesh(geo, mat);
    mesh.position.set(
      pathX(playerMesh.position.z + 40) + (Math.random() - 0.5) * 25,
      1 + Math.random() * 8,
      playerMesh.position.z + 30 + Math.random() * 50
    );
    scene.add(mesh);
    particles.push({
      mesh: mesh, life: 15.0,
      velocity: new THREE.Vector3(0, 0, -speed * 1.5)
    });
  }
}

function updateParticles(delta) {
  for (let i = particles.length - 1; i >= 0; i--) {
    const p = particles[i];
    p.mesh.position.add(p.velocity.clone().multiplyScalar(delta));
    p.life -= delta;
    if (p.life <= 0) {
      scene.remove(p.mesh);
      particles.splice(i, 1);
    }
  }
  for (let i = 0; i < 2; i++) spawnTrail();
  spawnDebris();
  updateParticles(0); // immediate cleanup of new spawns? No, just keep loop.
}

// Actually fix particle loop
function processParticles(delta) {
  for (let i = 0; i < 2; i++) {
    const geo = new THREE.SphereGeometry(0.15, 4, 4);
    const mat = new THREE.MeshStandardMaterial({ color: 0xffffff, emissive: 0xff4400, roughness: 1 });
    const mesh = new THREE.Mesh(geo, mat);
    mesh.position.copy(playerMesh.position);
    mesh.position.x += (Math.random() - 0.5) * 0.5;
    mesh.position.y += (Math.random() - 0.5) * 0.2;
    scene.add(mesh);
    particles.push({ mesh: mesh, life: 0.8 + Math.random() * 0.5, velocity: new THREE.Vector3(0, 0, -speed * 3) });
  }
  if (Math.random() < 0.2) {
    const geo = new THREE.BoxGeometry(0.3, 0.3, 0.3);
    const mat = new THREE.MeshStandardMaterial({ color: 0x665544, roughness: 0.9 });
    const mesh = new THREE.Mesh(geo, mat);
    mesh.position.set(
      pathX(playerMesh.position.z + 50) + (Math.random() - 0.5) * 30,
      3 + Math.random() * 6,
      playerMesh.position.z + 60
    );
    scene.add(mesh);
    particles.push({ mesh: mesh, life: 12, velocity: new THREE.Vector3(0, 0, -speed * 2) });
  }
  
  for (let i = particles.length - 1; i >= 0; i--) {
    const p = particles[i];
    p.mesh.position.add(p.velocity.clone().multiplyScalar(delta));
    p.life -= delta;
    if (p.life <= 0) {
      scene.remove(p.mesh);
      particles.splice(i, 1);
    }
  }
}

function updatePhysics(delta) {
  if (!running) return;

  // Mouse controls mapped to flight axes
  const mxNorm = (mouseX / window.innerWidth - 0.5) * 2;
  const myNorm = (mouseY / window.innerHeight - 0.5) * 2;

  // Roll (banking) controls lateral shift and craft roll
  const rollTarget = mxNorm * 1.5;
  const rollLerp = THREE.MathUtils.lerp;
  playerMesh.rotation.z = rollLerp(playerMesh.rotation.z, rollTarget * 0.6, 0.12);
  
  // Pitch controls acceleration (down = faster, up = slower)
  const pitchTarget = myNorm * 1.2;
  playerMesh.rotation.x = rollLerp(playerMesh.rotation.x, -Math.PI / 2 + pitchTarget * 0.5, 0.1);

  // Speed dynamics
  const pitchEffect = pitchTarget;
  speedTarget = 60 + pitchEffect * 50;
  if (boostActive) {
    speedTarget *= 2.2;
    turbo = Math.max(0, turbo - delta * 40);
  } else {
    turbo = Math.min(maxTurbo, turbo + delta * 1.5 + (pitchEffect > 0 ? delta * 2 : 0));
  }
  speed = THREE.MathUtils.lerp(speed, speedTarget, 0.06);
  speed = THREE.MathUtils.clamp(speed, 30, maxBaseSpeed + (boostActive ? 80 : 0));

  // Lateral movement from banking
  const lateralShift = rollTarget * speed * delta * 0.35;
  playerMesh.position.x += lateralShift;

  // Forward motion
  playerMesh.position.z += speed * delta;

  // Energy decay / boost consumption
  energy -= delta * (6 + speed * 0.08 + (boostActive ? 15 : 0));
  if (energy < 0) energy = 0;

  // Near-miss logic using procedural path
  const cx = pathX(playerMesh.position.z);
  const currentW = Math.max(8, 36 - score * 0.14);
  const leftX = cx - currentW / 2;
  const rightX = cx + currentW / 2;
  const dL = Math.abs(playerMesh.position.x - leftX);
  const dR = Math.abs(playerMesh.position.x - rightX);
  const nearestWallDist = Math.min(dL, dR);
  
  let nearMiss = 0;
  if (nearestWallDist < 5 && nearestWallDist > 0.4) {
    nearMiss = 1 - (nearestWallDist / 5);
    score += delta * 30 * multiplier * nearMiss;
    multiplier = Math.min(10, multiplier + nearMiss * 0.3 * delta);
    shakeIntensity = Math.min(4, shakeIntensity + nearMiss * 0.8);
    if (Math.random() < 0.15) playSound('near');
  } else {
    multiplier = Math.max(1, multiplier - delta * 0.3);
    shakeIntensity *= 0.92;
  }

  // Collision: walls
  if (nearestWallDist < 0.9) {
    triggerCrash();
    return;
  }

  // Obstacles & Orbs
  for (const chunk of chunks) {
    if (chunk.group.userData.obstacles) {
      for (const obs of chunk.group.userData.obstacles) {
        const dx = playerMesh.position.x - obs.x;
        const dz = playerMesh.position.z - obs.z;
        if (Math.abs(dx) < obs.r + 0.6 && Math.abs(dz) < obs.r + 2) {
          triggerCrash();
          return;
        }
      }
    }
    if (chunk.group.userData.orbs) {
      for (let o = chunk.group.userData.orbs.length - 1; o >= 0; o--) {
        const orbData = chunk.group.userData.orbs[o];
        const dx = playerMesh.position.x - orbData.x;
        const dy = playerMesh.position.y - orbData.y;
        const dz = playerMesh.position.z - orbData.z;
        const dist = Math.sqrt(dx*dx + dy*dy + dz*dz);
        if (dist < orbData.r + 0.6) {
          energy += 25;
          turbo += 20;
          score += 150 * multiplier;
          multiplier += 0.5;
          playSound('collect');
          chunk.group.remove(orbData.mesh);
          chunk.group.remove(orbData.light);
          chunk.group.userData.orbs.splice(o, 1);
        }
      }
    }
  }

  // Camera follow with shake
  const camOffset = new THREE.Vector3(
    THREE.MathUtils.lerp(camera.position.x, playerMesh.position.x, 0.1),
    THREE.MathUtils.lerp(camera.position.y, playerMesh.position.y + 4 + (boostActive ? 2 : 0), 0.1),
    THREE.MathUtils.lerp(camera.position.z, playerMesh.position.z - 14 - (boostActive ? 6 : 0), 0.1)
  );
  
  if (shakeIntensity > 0.1) {
    camOffset.x += (Math.random() - 0.5) * shakeIntensity * 0.08;
    camOffset.y += (Math.random() - 0.5) * shakeIntensity * 0.04;
    shakeIntensity *= 0.88;
  }
  camera.position.copy(camOffset);
  camera.lookAt(
    playerMesh.position.x,
    playerMesh.position.y,
    playerMesh.position.z + 25
  );

  // Generate new chunks as we move
  const aheadZ = Math.ceil((playerMesh.position.z + 300) / chunkLength) * chunkLength;
  const lastChunkZ = chunks[chunks.length - 1].z;
  if (aheadZ > lastChunkZ) {
    generateChunk(lastChunkZ + chunkLength);
  }

  // Cleanup old chunks (behind player)
  while (chunks.length > 0 && chunks[0].z < playerMesh.position.z - 150) {
    const old = chunks.shift();
    scene.remove(old.group);
  }

  // Update HUD
  document.getElementById('multiplier').textContent = 'x' + multiplier.toFixed(1);
  document.getElementById('score-display').textContent = Math.floor(score);
  document.getElementById('speed-val').textContent = Math.floor(speed) + ' m/s';
  document.getElementById('energy-fill').style.width = Math.max(0, energy) + '%';
  document.getElementById('boost-fill').style.width = (turbo / maxTurbo * 100) + '%';

  // Motion blur intensity based on speed & boost
  motionPass.uniforms.blurStrength.value = (speed / maxBaseSpeed) * (boostActive ? 3.5 : 1.5) + (shakeIntensity / 10);

  updateEngineSound();
}

function triggerCrash() {
  running = false;
  playSound('near');
  document.getElementById('gameover').style.display = 'flex';
  document.getElementById('final-score').textContent = Math.floor(score);
  document.getElementById('final-speed').textContent = Math.floor(speed) + ' m/s';
  document.getElementById('final-mult').textContent = multiplier.toFixed(1);
}

function restartGame() {
  window.location.reload(); // simplest reset
}

function animate() {
  requestAnimationFrame(animate);
  if (!running) {
    composer.render();
    return;
  }
  const delta = Math.min(clock.getDelta(), 0.05);
  updatePhysics(delta);
  processParticles(delta);
  composer.render();
}

window.restartGame = restartGame;

init();
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingmachines/inkling","provider":"openrouter","persona":"basic","sessionId":"b84c8afa-e009-4349-8261-d261d452e1ed","tokensIn":950,"tokensOut":20573,"tokensTotal":21523,"turns":1,"toolCalls":0,"failedToolCalls":0,"timestamp":"2026-07-18T20:28:51.195Z"} -->