← Canyon Run results

Canyon Run

thinkingcap qwen3.6 27b · middle

◈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 41,200 bytes · SHA-256 8cea1965c8d8
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canyon Rush</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { overflow: hidden; background: #0a0510; font-family: 'Segoe UI', system-ui, sans-serif; }
canvas { display: block; }

#hud {
  position: fixed; top: 0; left: 0; width: 100%; height: 100%;
  pointer-events: none; z-index: 10;
}

#speed-bar-container {
  position: absolute; right: 30px; bottom: 40px;
  width: 6px; height: 200px; background: rgba(255,255,255,0.1);
  border-radius: 3px; overflow: hidden;
}
#speed-bar {
  position: absolute; bottom: 0; width: 100%; height: 60%;
  background: linear-gradient(to top, #ff6a00, #ffcc00);
  border-radius: 3px; transition: height 0.1s;
}

#multiplier {
  position: absolute; right: 50px; bottom: 250px;
  font-size: 48px; font-weight: 900; color: #ffcc00;
  text-shadow: 0 0 20px rgba(255,204,0,0.6);
  opacity: 0; transition: opacity 0.3s, transform 0.1s;
}
#multiplier.visible { opacity: 1; }
#multiplier.pulse { transform: scale(1.3); }

#energy-container {
  position: absolute; left: 50%; bottom: 20px;
  transform: translateX(-50%);
  width: 300px; height: 8px; background: rgba(255,255,255,0.1);
  border-radius: 4px; overflow: hidden;
}
#energy-bar {
  height: 100%; width: 100%;
  background: linear-gradient(to right, #ff3366, #ff9900, #00ffaa);
  border-radius: 4px; transition: width 0.15s;
}

#turbo-container {
  position: absolute; left: 30px; bottom: 40px;
  width: 6px; height: 200px; background: rgba(255,255,255,0.1);
  border-radius: 3px; overflow: hidden;
}
#turbo-bar {
  position: absolute; bottom: 0; width: 100%; height: 0%;
  background: linear-gradient(to top, #00aaff, #aa44ff);
  border-radius: 3px; transition: height 0.1s;
}

#score-display {
  position: absolute; left: 50%; top: 20px;
  transform: translateX(-50%);
  font-size: 28px; font-weight: 700; color: #fff;
  text-shadow: 0 0 10px rgba(255,255,255,0.4);
  letter-spacing: 2px;
}

#game-over {
  position: fixed; top: 0; left: 0; width: 100%; height: 100%;
  background: rgba(10,5,16,0.92); display: none;
  flex-direction: column; align-items: center; justify-content: center;
  z-index: 100; pointer-events: all;
}
#game-over h1 {
  font-size: 64px; color: #ff3366; margin-bottom: 20px;
  text-shadow: 0 0 40px rgba(255,51,102,0.5);
}
#game-over .stat {
  font-size: 22px; color: #ccc; margin: 8px 0; letter-spacing: 1px;
}
#game-over .stat span { color: #ffcc00; font-weight: 700; }
#restart-btn {
  margin-top: 30px; padding: 14px 50px;
  background: linear-gradient(135deg, #ff6a00, #ee0979);
  border: none; color: #fff; font-size: 20px; font-weight: 700;
  border-radius: 8px; cursor: pointer; letter-spacing: 2px;
  transition: transform 0.15s, box-shadow 0.15s;
}
#restart-btn:hover {
  transform: scale(1.05);
  box-shadow: 0 0 30px rgba(238,9,121,0.6);
}

#start-screen {
  position: fixed; top: 0; left: 0; width: 100%; height: 100%;
  background: radial-gradient(ellipse at center, #1a0a2e 0%, #0a0510 70%);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  z-index: 200; pointer-events: all;
}
#start-screen h1 {
  font-size: 72px; color: #ffcc00; margin-bottom: 10px;
  text-shadow: 0 0 60px rgba(255,204,0,0.4);
  letter-spacing: 6px;
}
#start-screen p {
  font-size: 18px; color: #aaa; margin-bottom: 30px;
  text-align: center; line-height: 1.6;
}
#start-btn {
  padding: 16px 60px; background: linear-gradient(135deg, #ff6a00, #ee0979);
  border: none; color: #fff; font-size: 24px; font-weight: 700;
  border-radius: 8px; cursor: pointer; letter-spacing: 3px;
  transition: transform 0.15s, box-shadow 0.15s;
}
#start-btn:hover {
  transform: scale(1.05);
  box-shadow: 0 0 40px rgba(238,9,121,0.6);
}

#controls-hint {
  position: absolute; bottom: 60px; left: 50%; transform: translateX(-50%);
  font-size: 14px; color: #666; letter-spacing: 1px;
}
</style>
</head>
<body>

<div id="hud">
  <div id="score-display">0</div>
  <div id="multiplier"></div>
  <div id="speed-bar-container"><div id="speed-bar"></div></div>
  <div id="turbo-container"><div id="turbo-bar"></div></div>
  <div id="energy-container"><div id="energy-bar"></div></div>
</div>

<div id="near-miss-flash" style="position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:5;opacity:0;background:radial-gradient(ellipse at center,transparent 40%,rgba(255,170,0,0.3) 100%);transition:opacity 0.1s;"></div>
<div id="vignette" style="position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:4;background:radial-gradient(ellipse at center,transparent 50%,rgba(10,5,16,0.7) 100%);"></div>

<div id="game-over">
  <h1>CRAASHED</h1>
  <div class="stat">Final Score: <span id="final-score">0</span></div>
  <div class="stat">Max Speed: <span id="max-speed">0</span></div>
  <div class="stat">Distance: <span id="distance">0</span>m</div>
  <button id="restart-btn" onclick="restartGame()">RESTART</button>
</div>

<div id="start-screen">
  <h1>CANYON RUSH</h1>
  <p>Race through an infinite canyon at breakneck speed.<br>Avoid walls. Collect orbs. Don't run out of energy.</p>
  <button id="start-btn" onclick="startGame()">LAUNCH</button>
  <div id="controls-hint">W/S or ↑/↓ — Pitch &nbsp;|&nbsp; A/D or ←/→ — Roll &nbsp;|&nbsp; SPACE — Boost</div>
</div>

<script type="importmap">
{
  "imports": {
    "three": "https://unpkg.com/three@0.163.0/build/three.module.js",
    "three/addons/": "https://unpkg.com/three@0.163.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';

// ─── GLOBALS ──────────────────────────────────────
let scene, camera, renderer, composer, bloomPass;
let player, playerBody;
let clock = new THREE.Clock();
let gameActive = false;
let score = 0, maxSpeedVal = 0, distanceTraveled = 0;
let energy = 100, turbo = 0;
let speed = 0.8, baseSpeed = 0.8, boostSpeed = 2.5;
let multiplier = 1, nearMissTimer = 0;
let difficulty = 1;

// Physics
let velocity = new THREE.Vector3();
let angularVel = { roll: 0, pitch: 0 };
const MAX_ROLL = Math.PI * 0.4;
const MAX_PITCH = Math.PI * 0.25;

// Input
const keys = {};

// Chunk system
const CHUNK_LENGTH = 200;
const CHUNK_COUNT = 8;
let chunks = [];
let nextChunkZ = 0;

// Particles
let trailParticles = [];
let debrisParticles = [];
let speedLines = null;

// Audio
let audioCtx, engineOsc, engineGain;

// ─── AUDIO SYSTEM ─────────────────────────────────
function initAudio() {
  audioCtx = new (window.AudioContext || window.webkitAudioContext)();
  
  // Engine hum
  engineOsc = audioCtx.createOscillator();
  engineGain = audioCtx.createGain();
  const filter = audioCtx.createBiquadFilter();
  filter.type = 'lowpass';
  filter.frequency.value = 300;
  engineOsc.type = 'sawtooth';
  engineOsc.frequency.value = 60;
  engineGain.gain.value = 0.04;
  engineOsc.connect(filter);
  filter.connect(engineGain);
  engineGain.connect(audioCtx.destination);
  engineOsc.start();
}

function updateEngineSound() {
  if (!engineOsc) return;
  const t = audioCtx.currentTime;
  engineOsc.frequency.setTargetAtTime(50 + speed * 120, t, 0.1);
  engineGain.gain.setTargetAtTime(0.03 + speed * 0.04, t, 0.1);
}

function playPing() {
  if (!audioCtx) return;
  const osc = audioCtx.createOscillator();
  const gain = audioCtx.createGain();
  osc.type = 'sine';
  osc.frequency.setValueAtTime(880, audioCtx.currentTime);
  osc.frequency.exponentialRampToValueAtTime(1760, audioCtx.currentTime + 0.1);
  gain.gain.setValueAtTime(0.15, audioCtx.currentTime);
  gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.3);
  osc.connect(gain);
  gain.connect(audioCtx.destination);
  osc.start();
  osc.stop(audioCtx.currentTime + 0.3);
}

function playNearMiss() {
  if (!audioCtx) return;
  const bufferSize = audioCtx.sampleRate * 0.15;
  const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
  const data = buffer.getChannelData(0);
  for (let i = 0; i < bufferSize; i++) {
    data[i] = (Math.random() * 2 - 1) * Math.exp(-i / (bufferSize * 0.3));
  }
  const source = audioCtx.createBufferSource();
  source.buffer = buffer;
  const gain = audioCtx.createGain();
  gain.gain.value = 0.08;
  const filter = audioCtx.createBiquadFilter();
  filter.type = 'bandpass';
  filter.frequency.value = 2000;
  source.connect(filter);
  filter.connect(gain);
  gain.connect(audioCtx.destination);
  source.start();
}

function playCrash() {
  if (!audioCtx) return;
  const bufferSize = audioCtx.sampleRate * 0.8;
  const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
  const data = buffer.getChannelData(0);
  for (let i = 0; i < bufferSize; i++) {
    data[i] = (Math.random() * 2 - 1) * Math.exp(-i / (bufferSize * 0.4));
  }
  const source = audioCtx.createBufferSource();
  source.buffer = buffer;
  const gain = audioCtx.createGain();
  gain.gain.value = 0.3;
  source.connect(gain);
  gain.connect(audioCtx.destination);
  source.start();
}

// ─── INIT THREE.JS ────────────────────────────────
function init() {
  scene = new THREE.Scene();
  scene.fog = new THREE.FogExp2(0x1a0a2e, 0.003);

  camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 2000);
  
  renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: 'high-performance' });
  renderer.setSize(window.innerWidth, window.innerHeight);
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
  renderer.shadowMap.enabled = true;
  renderer.shadowMap.type = THREE.PCFSoftShadowMap;
  renderer.toneMapping = THREE.ACESFilmicToneMapping;
  renderer.toneMappingExposure = 1.2;
  document.body.appendChild(renderer.domElement);

  // Post-processing
  composer = new EffectComposer(renderer);
  composer.addPass(new RenderPass(scene, camera));
  
  bloomPass = new UnrealBloomPass(
    new THREE.Vector2(window.innerWidth, window.innerHeight),
    1.5, 0.4, 0.85
  );
  bloomPass.threshold = 0.3;
  bloomPass.strength = 1.8;
  bloomPass.radius = 0.6;
  composer.addPass(bloomPass);

  // Lighting - Golden hour / cyber sunset
  const ambientLight = new THREE.AmbientLight(0x4a2060, 0.6);
  scene.add(ambientLight);

  const sunLight = new THREE.DirectionalLight(0xff8833, 2.5);
  sunLight.position.set(100, 80, -200);
  sunLight.castShadow = true;
  sunLight.shadow.mapSize.width = 2048;
  sunLight.shadow.mapSize.height = 2048;
  sunLight.shadow.camera.near = 1;
  sunLight.shadow.camera.far = 600;
  sunLight.shadow.camera.left = -150;
  sunLight.shadow.camera.right = 150;
  sunLight.shadow.camera.top = 100;
  sunLight.shadow.camera.bottom = -100;
  scene.add(sunLight);

  const rimLight = new THREE.DirectionalLight(0x6633cc, 0.8);
  rimLight.position.set(-50, 30, 100);
  scene.add(rimLight);

  // Sky dome
  createSky();

  // Player craft
  createPlayer();

  // Initial chunks
  for (let i = 0; i < CHUNK_COUNT; i++) {
    generateChunk(nextChunkZ);
    nextChunkZ -= CHUNK_LENGTH;
  }

  // Speed lines
  createSpeedLines();

  window.addEventListener('resize', onResize);
  window.addEventListener('keydown', e => keys[e.code] = true);
  window.addEventListener('keyup', e => keys[e.code] = false);
}

function createSky() {
  const skyGeo = new THREE.SphereGeometry(900, 16, 12);
  const skyMat = new THREE.ShaderMaterial({
    side: THREE.BackSide,
    uniforms: {},
    vertexShader: `
      varying vec3 vWorldPos;
      void main() {
        vWorldPos = position;
        gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
      }
    `,
    fragmentShader: `
      varying vec3 vWorldPos;
      void main() {
        float h = normalize(vWorldPos).y * 0.5 + 0.5;
        vec3 bottom = vec3(0.6, 0.15, 0.05);
        vec3 mid = vec3(0.2, 0.08, 0.3);
        vec3 top = vec3(0.04, 0.02, 0.1);
        vec3 col = mix(bottom, mid, smoothstep(0.0, 0.5, h));
        col = mix(col, top, smoothstep(0.4, 1.0, h));
        gl_FragColor = vec4(col, 1.0);
      }
    `
  });
  scene.add(new THREE.Mesh(skyGeo, skyMat));

  // Stars
  const starCount = 500;
  const starPositions = new Float32Array(starCount * 3);
  for (let i = 0; i < starCount; i++) {
    const theta = Math.random() * Math.PI * 2;
    const phi = Math.acos(Math.random() * 0.8 + 0.2);
    const r = 850;
    starPositions[i*3] = r * Math.sin(phi) * Math.cos(theta);
    starPositions[i*3+1] = r * Math.cos(phi);
    starPositions[i*3+2] = r * Math.sin(phi) * Math.sin(theta);
  }
  const starGeo = new THREE.BufferGeometry();
  starGeo.setAttribute('position', new THREE.Float32BufferAttribute(starPositions, 3));
  const starMat = new THREE.PointsMaterial({ color: 0xffffff, size: 1.5, transparent: true, opacity: 0.6 });
  scene.add(new THREE.Points(starGeo, starMat));
}

function createPlayer() {
  player = new THREE.Group();
  
  // Main body - sleek arrow shape
  const bodyShape = new THREE.ConeGeometry(0.5, 3, 4);
  const bodyMat = new THREE.MeshStandardMaterial({ 
    color: 0xcccccc, flatShading: true, metalness: 0.8, roughness: 0.2 
  });
  playerBody = new THREE.Mesh(bodyShape, bodyMat);
  playerBody.rotation.x = Math.PI / 2;
  playerBody.castShadow = true;
  player.add(playerBody);

  // Wings
  const wingGeo = new THREE.BoxGeometry(4, 0.15, 1.2);
  const wingMat = new THREE.MeshStandardMaterial({ 
    color: 0x8844aa, flatShading: true, metalness: 0.6, roughness: 0.3 
  });
  const wings = new THREE.Mesh(wingGeo, wingMat);
  wings.position.z = 0.2;
  wings.castShadow = true;
  player.add(wings);

  // Engine glow
  const engineGeo = new THREE.SphereGeometry(0.35, 6, 4);
  const engineMat = new THREE.MeshBasicMaterial({ color: 0xff6600 });
  const engineGlow = new THREE.Mesh(engineGeo, engineMat);
  engineGlow.position.z = 1.6;
  player.add(engineGlow);

  // Engine light
  const eLight = new THREE.PointLight(0xff4400, 3, 15);
  eLight.position.z = 2;
  player.add(eLight);
  
  player.userData.engineGlow = engineGlow;
  player.userData.engineLight = eLight;

  player.position.set(0, 5, 0);
  scene.add(player);
}

// ─── CHUNK GENERATION ─────────────────────────────
function generateChunk(zPos) {
  const chunkGroup = new THREE.Group();
  chunkGroup.userData = { z: zPos, obstacles: [], orbs: [] };

  // Canyon floor
  const floorGeo = new THREE.PlaneGeometry(120, CHUNK_LENGTH);
  const floorMat = new THREE.MeshStandardMaterial({ 
    color: 0x2a1530, flatShading: true, roughness: 0.9 
  });
  const floor = new THREE.Mesh(floorGeo, floorMat);
  floor.rotation.x = -Math.PI / 2;
  floor.position.set(0, -8, zPos - CHUNK_LENGTH / 2);
  floor.receiveShadow = true;
  chunkGroup.add(floor);

  // Canyon ceiling
  const ceilGeo = new THREE.PlaneGeometry(120, CHUNK_LENGTH);
  const ceilMat = new THREE.MeshStandardMaterial({ 
    color: 0x1a0a25, flatShading: true, roughness: 0.9 
  });
  const ceiling = new THREE.Mesh(ceilGeo, ceilMat);
  ceiling.rotation.x = Math.PI / 2;
  ceiling.position.set(0, 35, zPos - CHUNK_LENGTH / 2);
  chunkGroup.add(ceiling);

  // Ground details - scattered rocks and ridges
  const rockCount = 8 + Math.floor(Math.random() * 6);
  for (let r = 0; r < rockCount; r++) {
    const rh = 1 + Math.random() * 3;
    const rw = 1 + Math.random() * 2;
    const rockGeo = new THREE.ConeGeometry(rw, rh, 4);
    const rockMat = new THREE.MeshStandardMaterial({ 
      color: new THREE.Color(0.15 + Math.random()*0.1, 0.08, 0.12), flatShading: true 
    });
    const rock = new THREE.Mesh(rockGeo, rockMat);
    rock.position.set(
      (Math.random() - 0.5) * 70,
      rh / 2 - 8,
      zPos - Math.random() * CHUNK_LENGTH
    );
    rock.rotation.y = Math.random() * Math.PI;
    chunkGroup.add(rock);
  }

  // Glowing ground lines (path markers)
  const lineCount = 4;
  for (let l = 0; l < lineCount; l++) {
    const lineGeo = new THREE.BoxGeometry(0.3, 0.1, 8 + Math.random() * 12);
    const lineMat = new THREE.MeshBasicMaterial({ 
      color: 0xff6600, transparent: true, opacity: 0.4 
    });
    const line = new THREE.Mesh(lineGeo, lineMat);
    line.position.set(
      (Math.random() - 0.5) * 50,
      -7.8,
      zPos - Math.random() * CHUNK_LENGTH
    );
    chunkGroup.add(line);
  }

  // Canyon walls with procedural variation
  const wallSegments = 12;
  for (let i = 0; i < wallSegments; i++) {
    const segZ = zPos - (i / wallSegments) * CHUNK_LENGTH;
    const widthVariation = Math.sin(i * 0.7 + zPos * 0.01) * 25 + 35;
    
    // Left wall
    createWallSegment(chunkGroup, -widthVariation, segZ, i);
    // Right wall
    createWallSegment(chunkGroup, widthVariation, segZ, i);
  }

  // Obstacles based on difficulty
  const obstacleCount = Math.floor(2 + difficulty * 1.5);
  for (let i = 0; i < obstacleCount; i++) {
    const type = Math.random();
    if (type < 0.4) {
      createMonolith(chunkGroup, zPos - Math.random() * CHUNK_LENGTH);
    } else if (type < 0.7) {
      createSqueeze(chunkGroup, zPos - Math.random() * CHUNK_LENGTH);
    } else {
      createWallPillar(chunkGroup, zPos - Math.random() * CHUNK_LENGTH);
    }
  }

  // Energy orbs
  const orbCount = Math.floor(3 + difficulty * 0.5);
  for (let i = 0; i < orbCount; i++) {
    createOrb(chunkGroup, zPos - Math.random() * CHUNK_LENGTH);
  }

  scene.add(chunkGroup);
  chunks.push(chunkGroup);
}

function createWallSegment(group, xPos, zPos, index) {
  const height = 30 + Math.sin(index * 1.3) * 15;
  const depth = CHUNK_LENGTH / wallSegmentsCount() - 2;
  
  const geo = new THREE.BoxGeometry(8, height, depth);
  const hueShift = (index * 0.07) % 1;
  const r = 0.3 + Math.sin(hueShift * Math.PI * 2) * 0.15;
  const g = 0.1 + Math.cos(hueShift * Math.PI * 1.5) * 0.08;
  const b = 0.2 + Math.sin(hueShift * Math.PI * 0.7) * 0.1;
  
  const mat = new THREE.MeshStandardMaterial({ 
    color: new THREE.Color(r, g, b), flatShading: true, roughness: 0.85 
  });
  const mesh = new THREE.Mesh(geo, mat);
  mesh.position.set(xPos, height / 2 - 8, zPos);
  mesh.castShadow = true;
  mesh.receiveShadow = true;
  
  // Add collision data
  mesh.userData.isObstacle = true;
  mesh.userData.radius = Math.max(4, height * 0.3);
  group.userData.obstacles.push(mesh);
  group.add(mesh);

  // Jagged top
  const jagCount = 3 + Math.floor(Math.random() * 3);
  for (let j = 0; j < jagCount; j++) {
    const jh = 5 + Math.random() * 12;
    const jw = 2 + Math.random() * 4;
    const jGeo = new THREE.ConeGeometry(jw, jh, 3);
    const jMesh = new THREE.Mesh(jGeo, mat.clone());
    jMesh.position.set(
      xPos + (Math.random() - 0.5) * 6,
      height / 2 - 8 + jh / 2,
      zPos + (Math.random() - 0.5) * (depth / 2)
    );
    jMesh.castShadow = true;
    group.add(jMesh);
  }
}

function wallSegmentsCount() { return 12; }

function createMonolith(group, zPos) {
  const height = 15 + Math.random() * 20;
  const width = 3 + Math.random() * 4;
  const geo = new THREE.BoxGeometry(width, height, width);
  const mat = new THREE.MeshStandardMaterial({ 
    color: 0x663399, flatShading: true, metalness: 0.5, roughness: 0.3 
  });
  const mesh = new THREE.Mesh(geo, mat);
  mesh.position.set((Math.random() - 0.5) * 40, height / 2 - 8 + Math.random() * 10, zPos);
  mesh.castShadow = true;
  mesh.userData.isObstacle = true;
  mesh.userData.radius = width * 0.7;
  group.userData.obstacles.push(mesh);
  group.add(mesh);

  // Glow edges
  const edgeGeo = new THREE.BoxGeometry(width + 0.3, height + 0.3, width + 0.3);
  const edgeMat = new THREE.MeshBasicMaterial({ 
    color: 0xaa44ff, transparent: true, opacity: 0.15, wireframe: true 
  });
  group.add(new THREE.Mesh(edgeGeo, edgeMat).translateX(mesh.position.x)
    .translateY(mesh.position.y).translateZ(mesh.position.z));
}

function createSqueeze(group, zPos) {
  // Narrow gap - two walls close together with a passage through the middle
  const gapWidth = 6 + Math.random() * 4;
  const wallHeight = 25 + Math.random() * 10;
  
  for (let side = -1; side <= 1; side += 2) {
    const geo = new THREE.BoxGeometry(3, wallHeight, 8);
    const mat = new THREE.MeshStandardMaterial({ 
      color: 0x442266, flatShading: true, roughness: 0.7 
    });
    const mesh = new THREE.Mesh(geo, mat);
    mesh.position.set(side * (gapWidth / 2 + 1), wallHeight / 2 - 8, zPos);
    mesh.castShadow = true;
    mesh.userData.isObstacle = true;
    mesh.userData.radius = 3.5;
    group.userData.obstacles.push(mesh);
    group.add(mesh);
  }

  // Top bar to force going through the gap
  const topGeo = new THREE.BoxGeometry(gapWidth + 6, 2, 8);
  const topMat = new THREE.MeshStandardMaterial({ 
    color: 0x553377, flatShading: true 
  });
  const topMesh = new THREE.Mesh(topGeo, topMat);
  topMesh.position.set(0, wallHeight - 8, zPos);
  topMesh.castShadow = true;
  topMesh.userData.isObstacle = true;
  topMesh.userData.radius = 2.5;
  group.userData.obstacles.push(topMesh);
  group.add(topMesh);
}

function createWallPillar(group, zPos) {
  const geo = new THREE.CylinderGeometry(1.5, 3, 20 + Math.random() * 15, 5);
  const mat = new THREE.MeshStandardMaterial({ 
    color: 0x774488, flatShading: true, roughness: 0.6 
  });
  const mesh = new THREE.Mesh(geo, mat);
  mesh.position.set((Math.random() - 0.5) * 30, Math.random() * 15, zPos);
  mesh.castShadow = true;
  mesh.userData.isObstacle = true;
  mesh.userData.radius = 3;
  group.userData.obstacles.push(mesh);
  group.add(mesh);
}

function createOrb(group, zPos) {
  const geo = new THREE.OctahedronGeometry(0.8, 0);
  const mat = new THREE.MeshBasicMaterial({ color: 0x00ffaa });
  const mesh = new THREE.Mesh(geo, mat);
  
  // Place orbs in interesting positions - sometimes through gaps
  const x = (Math.random() - 0.5) * 35;
  const y = Math.random() * 20 + 2;
  mesh.position.set(x, y, zPos);
  
  const orbLight = new THREE.PointLight(0x00ffaa, 1.5, 8);
  mesh.add(orbLight);
  
  mesh.userData.isOrb = true;
  group.userData.orbs.push(mesh);
  group.add(mesh);
}

// ─── PARTICLES ────────────────────────────────────
function spawnTrailParticle() {
  const geo = new THREE.SphereGeometry(0.15, 3, 2);
  const mat = new THREE.MeshBasicMaterial({ 
    color: new THREE.Color().setHSL(0.08 + Math.random() * 0.05, 1, 0.6),
    transparent: true, opacity: 0.9 
  });
  const p = new THREE.Mesh(geo, mat);
  p.position.copy(player.position).add(new THREE.Vector3(
    (Math.random() - 0.5) * 0.5,
    (Math.random() - 0.5) * 0.5,
    1.8 + Math.random() * 0.5
  ));
  p.userData.life = 1.0;
  scene.add(p);
  trailParticles.push(p);
}

function spawnDebrisParticle() {
  const geo = new THREE.TetrahedronGeometry(0.2, 0);
  const mat = new THREE.MeshBasicMaterial({ 
    color: 0xffaa44, transparent: true, opacity: 0.7 
  });
  const p = new THREE.Mesh(geo, mat);
  
  // Spawn from the sides to simulate passing by terrain
  const side = Math.random() > 0.5 ? 1 : -1;
  p.position.set(
    player.position.x + side * (3 + Math.random() * 8),
    player.position.y + (Math.random() - 0.5) * 10,
    player.position.z - 20 - Math.random() * 30
  );
  p.userData.life = 1.0;
  p.userData.vel = new THREE.Vector3(
    side * (1 + Math.random()), 
    (Math.random() - 0.5) * 0.5,
    speed * 40
  );
  scene.add(p);
  debrisParticles.push(p);
}

function updateParticles(dt) {
  // Trail particles
  for (let i = trailParticles.length - 1; i >= 0; i--) {
    const p = trailParticles[i];
    p.userData.life -= dt * 2.5;
    p.material.opacity = Math.max(0, p.userData.life);
    p.scale.setScalar(p.userData.life);
    if (p.userData.life <= 0) {
      scene.remove(p);
      p.geometry.dispose();
      p.material.dispose();
      trailParticles.splice(i, 1);
    }
  }

  // Debris particles
  for (let i = debrisParticles.length - 1; i >= 0; i--) {
    const p = debrisParticles[i];
    p.userData.life -= dt * 2;
    p.position.add(p.userData.vel.clone().multiplyScalar(dt));
    p.material.opacity = Math.max(0, p.userData.life);
    p.rotation.x += dt * 5;
    p.rotation.y += dt * 3;
    if (p.userData.life <= 0) {
      scene.remove(p);
      p.geometry.dispose();
      p.material.dispose();
      debrisParticles.splice(i, 1);
    }
  }
}

// ─── SPEED LINES ──────────────────────────────────
function createSpeedLines() {
  const count = 120;
  const positions = new Float32Array(count * 3);
  const colors = new Float32Array(count * 3);
  
  for (let i = 0; i < count; i++) {
    resetSpeedLine(i, positions, colors);
  }
  
  const geo = new THREE.BufferGeometry();
  geo.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
  geo.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
  
  const mat = new THREE.PointsMaterial({ 
    size: 0.5, vertexColors: true, transparent: true, opacity: 0.6,
    blending: THREE.AdditiveBlending, depthWrite: false 
  });
  
  speedLines = new THREE.Points(geo, mat);
  scene.add(speedLines);
}

function resetSpeedLine(i, positions, colors) {
  const angle = Math.random() * Math.PI * 2;
  const radius = 5 + Math.random() * 40;
  positions[i*3] = player.position.x + Math.cos(angle) * radius;
  positions[i*3+1] = player.position.y + (Math.random() - 0.5) * 30;
  positions[i*3+2] = player.position.z - Math.random() * 80;
  
  const brightness = 0.5 + Math.random() * 0.5;
  colors[i*3] = brightness;
  colors[i*3+1] = brightness * 0.7;
  colors[i*3+2] = brightness * 0.4;
}

function updateSpeedLines(dt) {
  if (!speedLines || !gameActive) return;
  
  const posAttr = speedLines.geometry.attributes.position;
  const colAttr = speedLines.geometry.attributes.color;
  const positions = posAttr.array;
  const colors = colAttr.array;
  
  // Only show at higher speeds
  const intensity = Math.max(0, (speed - baseSpeed * 1.2) / (boostSpeed - baseSpeed));
  speedLines.material.opacity = intensity * 0.7;
  
  for (let i = 0; i < positions.length / 3; i++) {
    // Move lines toward player (positive Z direction relative to world)
    positions[i*3+2] += speed * 60 * dt;
    
    // Reset if passed the camera
    if (positions[i*3+2] > player.position.z + 15) {
      resetSpeedLine(i, positions, colors);
    }
  }
  
  posAttr.needsUpdate = true;
}

// ─── CAMERA SHAKE ─────────────────────────────────
let shakeIntensity = 0;
function addShake(amount) { shakeIntensity = Math.min(shakeIntensity + amount, 2.0); }

// ─── PHYSICS UPDATE ──────────────────────────────
function updatePhysics(dt) {
  if (!gameActive) return;

  // Input handling
  const rollInput = (keys['ArrowLeft'] || keys['KeyA']) ? -1 : (keys['ArrowRight'] || keys['KeyD']) ? 1 : 0;
  const pitchInput = (keys['ArrowUp'] || keys['KeyW']) ? -1 : (keys['ArrowDown'] || keys['KeyS']) ? 1 : 0;

  // Roll physics with momentum
  angularVel.roll += rollInput * dt * 6.0;
  angularVel.roll *= Math.pow(0.05, dt); // damping
  
  const targetRoll = -angularVel.roll;
  player.rotation.z += (targetRoll - player.rotation.z) * dt * 8;
  player.rotation.z = THREE.MathUtils.clamp(player.rotation.z, -MAX_ROLL, MAX_ROLL);

  // Pitch physics
  angularVel.pitch += pitchInput * dt * 5.0;
  angularVel.pitch *= Math.pow(0.05, dt);
  
  const targetPitch = -angularVel.pitch;
  player.rotation.x += (targetPitch - player.rotation.x) * dt * 8;
  player.rotation.x = THREE.MathUtils.clamp(player.rotation.x, -MAX_PITCH, MAX_PITCH);

  // Movement based on roll/pitch
  const moveSpeed = speed * 35;
  velocity.x = Math.sin(player.rotation.z) * moveSpeed;
  velocity.y = -Math.sin(player.rotation.x) * moveSpeed * 0.7;
  
  // Boost
  let isBoosting = keys['Space'] && turbo > 0;
  if (isBoosting) {
    speed += dt * 3;
    turbo -= dt * 25;
    addShake(0.1);
  } else {
    speed += (baseSpeed - speed) * dt * 2;
  }
  
  // Speed cap
  const maxSpd = baseSpeed + difficulty * 0.3;
  if (!isBoosting) speed = Math.min(speed, maxSpd);
  speed = Math.max(speed, 0.5);

  // Move player forward (negative Z)
  velocity.z = -speed * 40;
  
  const moveDelta = velocity.clone().multiplyScalar(dt);
  player.position.add(moveDelta);

  // Clamp lateral position
  player.position.x = THREE.MathUtils.clamp(player.position.x, -50, 50);
  player.position.y = THREE.MathUtils.clamp(player.position.y, -4, 32);

  // Distance tracking
  distanceTraveled += Math.abs(moveDelta.z);
  
  // Max speed tracking
  if (speed > maxSpeedVal) maxSpeedVal = speed;

  // Energy decay
  energy -= dt * (1.5 + difficulty * 0.3);
  if (energy <= 0) {
    gameOver();
    return;
  }

  // Turbo build from straightaways
  if (!isBoosting && Math.abs(rollInput) < 0.5) {
    turbo += dt * 8;
  }
  turbo = Math.min(turbo, 100);

  // Difficulty scaling
  difficulty = 1 + score / 2000;

  updateEngineSound();
}

// ─── COLLISION DETECTION ──────────────────────────
let lastNearMissTime = 0;
function checkCollisions(dt) {
  if (!gameActive) return;
  
  const playerPos = player.position;
  const playerRadius = 1.5;

  for (const chunk of chunks) {
    // Check obstacles
    for (const obs of chunk.userData.obstacles) {
      if (!obs.userData.isObstacle) continue;
      
      const dx = playerPos.x - obs.position.x;
      const dy = playerPos.y - obs.position.y;
      const dz = playerPos.z - obs.position.z;
      const dist = Math.sqrt(dx*dx + dy*dy + dz*dz);
      const minDist = playerRadius + (obs.userData.radius || 2);

      // Collision
      if (dist < minDist * 0.65) {
        playCrash();
        gameOver();
        return;
      }

      // Near miss (with cooldown to prevent spamming)
      const now = clock.elapsedTime;
      if (dist < minDist * 1.8 && dist > minDist * 0.65 && now - lastNearMissTime > 0.3) {
        nearMissTimer = 0.5;
        multiplier = Math.min(multiplier + 0.5, 10);
        score += Math.floor(50 * multiplier);
        addShake(0.3);
        playNearMiss();
        lastNearMissTime = now;
        
        // Flash effect
        const flashEl = document.getElementById('near-miss-flash');
        if (flashEl) {
          flashEl.style.opacity = '1';
          setTimeout(() => flashEl.style.opacity = '0', 150);
        }
      }
    }

    // Check orbs
    for (let i = chunk.userData.orbs.length - 1; i >= 0; i--) {
      const orb = chunk.userData.orbs[i];
      if (!orb || !orb.visible) continue;
      
      const dx = playerPos.x - orb.position.x;
      const dy = playerPos.y - orb.position.y;
      const dz = playerPos.z - orb.position.z;
      const dist = Math.sqrt(dx*dx + dy*dy + dz*dz);

      if (dist < 3) {
        energy = Math.min(energy + 15, 100);
        score += Math.floor(25 * multiplier);
        playPing();
        
        scene.remove(orb);
        chunk.userData.orbs.splice(i, 1);
      }
    }
  }

  // Near miss timer decay
  if (nearMissTimer > 0) {
    nearMissTimer -= dt;
  } else {
    multiplier = Math.max(1, multiplier - 0.3 * dt);
  }

  // Score from distance
  score += speed * dt * 5 * multiplier;
}

// ─── CHUNK MANAGEMENT ─────────────────────────────
function updateChunks() {
  const playerZ = player.position.z;
  
  // Remove old chunks
  for (let i = chunks.length - 1; i >= 0; i--) {
    if (chunks[i].userData.z > playerZ + CHUNK_LENGTH * 2) {
      scene.remove(chunks[i]);
      
      // Dispose geometries and materials
      chunks[i].traverse(child => {
        if (child.geometry) child.geometry.dispose();
        if (child.material) {
          if (Array.isArray(child.material)) {
            child.material.forEach(m => m.dispose());
          } else {
            child.material.dispose();
          }
        }
      });
      
      chunks.splice(i, 1);
    }
  }

  // Generate new chunks ahead
  while (nextChunkZ > playerZ - CHUNK_LENGTH * (CHUNK_COUNT - 2)) {
    generateChunk(nextChunkZ);
    nextChunkZ -= CHUNK_LENGTH;
  }
}

// ─── CAMERA UPDATE ────────────────────────────────
function updateCamera(dt) {
  const camDist = 12 + speed * 3;
  const camHeight = 5 + speed * 2;
  
  // FOV based on speed
  const targetFov = 70 + (speed - baseSpeed) * 20;
  camera.fov += (targetFov - camera.fov) * dt * 3;
  camera.updateProjectionMatrix();

  // Camera position behind player
  const idealPos = new THREE.Vector3(
    player.position.x * 0.4,
    player.position.y + camHeight,
    player.position.z + camDist
  );
  
  camera.position.lerp(idealPos, dt * 5);
  
  // Look ahead of player
  const lookTarget = new THREE.Vector3(
    player.position.x * 0.6,
    player.position.y + 1,
    player.position.z - 30
  );
  camera.lookAt(lookTarget);

  // Camera shake
  if (shakeIntensity > 0) {
    camera.position.x += (Math.random() - 0.5) * shakeIntensity;
    camera.position.y += (Math.random() - 0.5) * shakeIntensity * 0.5;
    shakeIntensity *= Math.pow(0.1, dt);
    if (shakeIntensity < 0.01) shakeIntensity = 0;
  }
}

// ─── HUD UPDATE ───────────────────────────────────
function updateHUD() {
  document.getElementById('score-display').textContent = Math.floor(score).toLocaleString();
  
  // Speed bar
  const speedPct = ((speed - 0.5) / (boostSpeed - 0.5)) * 100;
  document.getElementById('speed-bar').style.height = Math.max(5, speedPct) + '%';
  
  // Turbo bar
  document.getElementById('turbo-bar').style.height = turbo + '%';
  
  // Energy bar
  document.getElementById('energy-bar').style.width = energy + '%';
  
  // Multiplier display
  const multEl = document.getElementById('multiplier');
  if (nearMissTimer > 0 || multiplier > 1) {
    multEl.textContent = '×' + multiplier.toFixed(1);
    multEl.classList.add('visible');
    if (nearMissTimer > 0.3) multEl.classList.add('pulse');
    else multEl.classList.remove('pulse');
  } else {
    multEl.classList.remove('visible', 'pulse');
  }

  // Energy bar color warning
  const eBar = document.getElementById('energy-bar');
  if (energy < 25) {
    eBar.style.background = '#ff3366';
    eBar.style.boxShadow = '0 0 10px #ff3366';
    
    // Red warning pulse on screen edges
    const vignette = document.getElementById('vignette');
    if (vignette && Math.sin(clock.elapsedTime * 4) > 0.5) {
      vignette.style.background = 'radial-gradient(ellipse at center,transparent 40%,rgba(120,10,10,0.8) 100%)';
    }
  } else {
    eBar.style.background = '';
    eBar.style.boxShadow = '';
    
    const vignette = document.getElementById('vignette');
    if (vignette && !keys['Space']) {
      vignette.style.background = 'radial-gradient(ellipse at center,transparent 50%,rgba(10,5,16,0.7) 100%)';
    }
  }
}

// ─── GAME OVER / START ────────────────────────────
function gameOver() {
  gameActive = false;
  playCrash();
  
  document.getElementById('final-score').textContent = Math.floor(score).toLocaleString();
  document.getElementById('max-speed').textContent = (maxSpeedVal * 100).toFixed(0) + ' km/h';
  document.getElementById('distance').textContent = Math.floor(distanceTraveled);
  document.getElementById('game-over').style.display = 'flex';
  
  if (engineOsc) {
    engineGain.gain.setTargetAtTime(0, audioCtx.currentTime, 0.1);
  }
}

window.startGame = function() {
  document.getElementById('start-screen').style.display = 'none';
  initAudio();
  gameActive = true;
  score = 0; maxSpeedVal = 0; distanceTraveled = 0;
  energy = 100; turbo = 0; speed = baseSpeed; multiplier = 1; difficulty = 1;
  nearMissTimer = 0; shakeIntensity = 0;
  
  player.position.set(0, 5, 0);
  player.rotation.set(0, 0, 0);
  velocity.set(0, 0, 0);
};

window.restartGame = function() {
  // Clear all chunks
  for (const chunk of chunks) {
    scene.remove(chunk);
    chunk.traverse(child => {
      if (child.geometry) child.geometry.dispose();
      if (child.material) {
        if (Array.isArray(child.material)) child.material.forEach(m => m.dispose());
        else child.material.dispose();
      }
    });
  }
  chunks = [];
  nextChunkZ = 0;

  // Clear particles
  for (const p of trailParticles) { scene.remove(p); p.geometry?.dispose(); p.material?.dispose(); }
  trailParticles.length = 0;
  for (const p of debrisParticles) { scene.remove(p); p.geometry?.dispose(); p.material?.dispose(); }
  debrisParticles.length = 0;

  document.getElementById('game-over').style.display = 'none';
  
  // Regenerate chunks
  for (let i = 0; i < CHUNK_COUNT; i++) {
    generateChunk(nextChunkZ);
    nextChunkZ -= CHUNK_LENGTH;
  }
  
  score = 0; maxSpeedVal = 0; distanceTraveled = 0;
  energy = 100; turbo = 0; speed = baseSpeed; multiplier = 1; difficulty = 1;
  nearMissTimer = 0; shakeIntensity = 0;
  
  player.position.set(0, 5, 0);
  player.rotation.set(0, 0, 0);
  velocity.set(0, 0, 0);
  
  // Reset visual effects
  if (player.userData.engineGlow) {
    player.userData.engineGlow.material.color.setHex(0xff6600);
  }
  const vignette = document.getElementById('vignette');
  if (vignette) {
    vignette.style.background = 'radial-gradient(ellipse at center,transparent 50%,rgba(10,5,16,0.7) 100%)';
  }
  
  gameActive = true;
};

// ─── ANIMATION LOOP ──────────────────────────────
let trailTimer = 0;
let debrisTimer = 0;

function animate() {
  requestAnimationFrame(animate);
  
  const dt = Math.min(clock.getDelta(), 0.05); // cap delta to prevent spiral
  
  if (gameActive) {
    updatePhysics(dt);
    checkCollisions(dt);
    updateChunks();
    
    // Spawn particles
    trailTimer += dt;
    debrisTimer += dt;
    if (trailTimer > 0.03) { spawnTrailParticle(); trailTimer = 0; }
    if (debrisTimer > 0.15 / Math.max(speed, 0.5)) { spawnDebrisParticle(); debrisTimer = 0; }
    
    // Animate orbs
    for (const chunk of chunks) {
      const t = clock.elapsedTime;
      for (const orb of chunk.userData.orbs) {
        if (!orb.visible) continue;
        orb.rotation.y += dt * 3;
        orb.position.y += Math.sin(t * 2 + orb.position.z) * dt * 0.5;
      }
    }

    updateParticles(dt);
    updateSpeedLines(dt);
    
    // Engine glow pulse
    if (player.userData.engineGlow) {
      const pulse = 1 + Math.sin(clock.elapsedTime * 8) * 0.2;
      player.userData.engineGlow.scale.setScalar(pulse * (0.8 + speed * 0.5));
      player.userData.engineLight.intensity = 3 + speed * 4;
      
      // Boost color shift
      const isBoostingNow = keys['Space'] && turbo > 0;
      if (isBoostingNow) {
        player.userData.engineGlow.material.color.setHex(0x8844ff);
        player.userData.engineLight.color.setHex(0x6633cc);
        
        // Boost screen effect
        const vignette = document.getElementById('vignette');
        if (vignette) {
          vignette.style.background = 'radial-gradient(ellipse at center,transparent 45%,rgba(80,20,120,0.6) 100%)';
        }
      } else {
        player.userData.engineGlow.material.color.setHex(0xff6600);
        player.userData.engineLight.color.setHex(0xff4400);
        
        const vignette = document.getElementById('vignette');
        if (vignette) {
          vignette.style.background = 'radial-gradient(ellipse at center,transparent 50%,rgba(10,5,16,0.7) 100%)';
        }
      }
    }
    updateCamera(dt);
    updateHUD();
    
    // Bloom intensity based on speed
    bloomPass.strength = 1.5 + (speed - baseSpeed) * 2;
  } else {
    // Idle camera rotation when not playing
    if (player) {
      player.rotation.y += dt * 0.3;
    }
  }

  composer.render();
}

// ─── RESIZE ───────────────────────────────────────
function onResize() {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
  composer.setSize(window.innerWidth, window.innerHeight);
}

// ─── BOOT ─────────────────────────────────────────
init();
animate();

</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingcap-qwen3.6-27b","provider":"lmstudio","persona":"middle","sessionId":"465f3e0b-3e32-45c3-b122-0ddce057d4b8","tokensIn":770529,"tokensOut":20832,"tokensTotal":791361,"turns":32,"toolCalls":31,"failedToolCalls":0,"timestamp":"2026-07-18T22:38:23.288Z"} -->