← Canyon Run results

Canyon Run

thinkingcap qwen3.6 27b · full

◈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 40,294 bytes · SHA-256 10912e5d9491
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canyon Racer</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { overflow: hidden; background: #000; font-family: 'Segoe UI', system-ui, sans-serif; }
canvas { display: block; position: fixed; top: 0; left: 0; z-index: 1; }

#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: 8px; height: 200px; background: rgba(255,255,255,0.1);
  border-radius: 4px; overflow: hidden;
}
#speed-bar {
  position: absolute; bottom: 0; width: 100%; height: 0%;
  background: linear-gradient(to top, #ff6a00, #ee0979);
  border-radius: 4px; transition: height 0.1s;
}

#turbo-bar-container {
  position: absolute; right: 50px; bottom: 40px;
  width: 8px; height: 200px; background: rgba(255,255,255,0.1);
  border-radius: 4px; overflow: hidden;
}
#turbo-bar {
  position: absolute; bottom: 0; width: 100%; height: 0%;
  background: linear-gradient(to top, #00f2fe, #4facfe);
  border-radius: 4px; transition: height 0.1s;
}

#multiplier {
  position: absolute; right: 80px; bottom: 60px;
  font-size: 48px; font-weight: 900; color: #fff;
  text-shadow: 0 0 20px rgba(255,106,0,0.8);
  opacity: 0.3; transition: all 0.2s;
}
#multiplier.active {
  opacity: 1; transform: scale(1.2);
  text-shadow: 0 0 40px rgba(255,106,0,1), 0 0 80px rgba(238,9,121,0.6);
}

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

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

#speed-text {
  position: absolute; right: 34px; bottom: 250px;
  font-size: 14px; color: rgba(255,255,255,0.6);
  text-align: center; writing-mode: vertical-rl;
  letter-spacing: 2px;
}

#game-over {
  position: fixed; top: 0; left: 0; width: 100%; height: 100%;
  background: rgba(0,0,0,0.85); display: none;
  flex-direction: column; align-items: center; justify-content: center;
  z-index: 100; pointer-events: all;
}
#game-over h1 {
  font-size: 64px; color: #ee0979; margin-bottom: 20px;
  text-shadow: 0 0 30px rgba(238,9,121,0.5);
}
#game-over .stat {
  font-size: 24px; color: #fff; margin: 8px 0; opacity: 0.8;
}
#game-over .final-score {
  font-size: 72px; font-weight: 900; color: #ff6a00;
  text-shadow: 0 0 40px rgba(255,106,0,0.8); margin: 20px 0;
}
#restart-btn {
  margin-top: 30px; padding: 15px 50px; font-size: 20px;
  background: linear-gradient(135deg, #ff6a00, #ee0979);
  border: none; color: #fff; cursor: pointer;
  border-radius: 8px; font-weight: 700; letter-spacing: 2px;
  transition: transform 0.2s, box-shadow 0.2s;
}
#restart-btn:hover {
  transform: scale(1.05);
  box-shadow: 0 0 30px rgba(255,106,0,0.5);
}

#start-screen {
  position: fixed; top: 0; left: 0; width: 100%; height: 100%;
  background: radial-gradient(ellipse at center, #1a0a2e 0%, #0d0015 100%);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  z-index: 200; pointer-events: all;
}
#start-screen h1 {
  font-size: 72px; color: #ff6a00; margin-bottom: 10px;
  text-shadow: 0 0 40px rgba(255,106,0,0.6);
}
#start-screen .subtitle {
  font-size: 18px; color: rgba(255,255,255,0.5); margin-bottom: 40px;
  letter-spacing: 4px; text-transform: uppercase;
}
#start-btn {
  padding: 18px 60px; font-size: 22px;
  background: linear-gradient(135deg, #ff6a00, #ee0979);
  border: none; color: #fff; cursor: pointer;
  border-radius: 8px; font-weight: 700; letter-spacing: 3px;
  transition: transform 0.2s, box-shadow 0.2s;
}
#start-btn:hover {
  transform: scale(1.05);
  box-shadow: 0 0 40px rgba(255,106,0,0.5);
}
.controls-info {
  margin-top: 30px; color: rgba(255,255,255,0.4); font-size: 14px;
  text-align: center; line-height: 1.8;
}

#near-miss-flash {
  position: fixed; top: 0; left: 0; width: 100%; height: 100%;
  background: radial-gradient(ellipse at center, rgba(255,106,0,0.3), transparent);
  opacity: 0; pointer-events: none; z-index: 5;
  transition: opacity 0.1s;
}

#boost-flash {
  position: fixed; top: 0; left: 0; width: 100%; height: 100%;
  background: radial-gradient(ellipse at center, rgba(79,172,254,0.2), transparent);
  opacity: 0; pointer-events: none; z-index: 5;
}
</style>
</head>
<body>

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

<div id="near-miss-flash"></div>
<div id="boost-flash"></div>

<div id="game-over">
  <h1>CRASHED</h1>
  <div class="final-score" id="final-score">0</div>
  <div class="stat" id="final-speed">Max Speed: 0 km/h</div>
  <div class="stat" id="final-distance">Distance: 0 m</div>
  <button id="restart-btn">RESTART</button>
</div>

<div id="start-screen">
  <h1>CANYON RACER</h1>
  <div class="subtitle">High-Speed Procedural Racing</div>
  <button id="start-btn">LAUNCH</button>
  <div class="controls-info">
    WASD / Arrow Keys — Steer<br>
    SHIFT — Boost &nbsp;|&nbsp; SPACE — Brake
  </div>
</div>

<script type="importmap">
{
  "imports": {
    "three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
    "three/addons/": "https://cdn.jsdelivr.net/npm/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';

// ─── AUDIO ENGINE ──────────────────────────────────────
class AudioEngine {
  constructor() {
    this.ctx = null;
    this.engineOsc = null;
    this.engineGain = null;
    this.initialized = false;
  }

  init() {
    if (this.initialized) return;
    this.ctx = new (window.AudioContext || window.webkitAudioContext)();
    
    // Engine hum
    this.engineOsc = this.ctx.createOscillator();
    this.engineGain = this.ctx.createGain();
    const engineFilter = this.ctx.createBiquadFilter();
    engineFilter.type = 'lowpass';
    engineFilter.frequency.value = 300;
    
    this.engineOsc.type = 'sawtooth';
    this.engineOsc.frequency.value = 60;
    this.engineGain.gain.value = 0.08;
    
    this.engineOsc.connect(engineFilter);
    engineFilter.connect(this.engineGain);
    this.engineGain.connect(this.ctx.destination);
    this.engineOsc.start();
    
    this.initialized = true;
  }

  setEnginePitch(speedRatio) {
    if (!this.initialized) return;
    const freq = 50 + speedRatio * 120;
    this.engineOsc.frequency.setTargetAtTime(freq, this.ctx.currentTime, 0.05);
    this.engineGain.gain.setTargetAtTime(0.04 + speedRatio * 0.08, this.ctx.currentTime, 0.05);
  }

  playCollect() {
    if (!this.initialized) return;
    const osc = this.ctx.createOscillator();
    const gain = this.ctx.createGain();
    osc.type = 'sine';
    osc.frequency.setValueAtTime(880, this.ctx.currentTime);
    osc.frequency.exponentialRampToValueAtTime(1760, this.ctx.currentTime + 0.1);
    gain.gain.setValueAtTime(0.15, this.ctx.currentTime);
    gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.2);
    osc.connect(gain);
    gain.connect(this.ctx.destination);
    osc.start();
    osc.stop(this.ctx.currentTime + 0.2);
  }

  playNearMiss() {
    if (!this.initialized) return;
    const bufferSize = this.ctx.sampleRate * 0.15;
    const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.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 = this.ctx.createBufferSource();
    source.buffer = buffer;
    const filter = this.ctx.createBiquadFilter();
    filter.type = 'bandpass';
    filter.frequency.value = 2000;
    filter.Q.value = 2;
    const gain = this.ctx.createGain();
    gain.gain.setValueAtTime(0.12, this.ctx.currentTime);
    source.connect(filter);
    filter.connect(gain);
    gain.connect(this.ctx.destination);
    source.start();
  }

  playCrash() {
    if (!this.initialized) return;
    const bufferSize = this.ctx.sampleRate * 0.5;
    const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.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 = this.ctx.createBufferSource();
    source.buffer = buffer;
    const gain = this.ctx.createGain();
    gain.gain.setValueAtTime(0.3, this.ctx.currentTime);
    source.connect(gain);
    gain.connect(this.ctx.destination);
    source.start();

    const osc = this.ctx.createOscillator();
    const g2 = this.ctx.createGain();
    osc.type = 'sawtooth';
    osc.frequency.setValueAtTime(100, this.ctx.currentTime);
    osc.frequency.exponentialRampToValueAtTime(30, this.ctx.currentTime + 0.4);
    g2.gain.setValueAtTime(0.2, this.ctx.currentTime);
    g2.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.5);
    osc.connect(g2);
    g2.connect(this.ctx.destination);
    osc.start();
    osc.stop(this.ctx.currentTime + 0.5);
  }

  playBoost() {
    if (!this.initialized) return;
    const osc = this.ctx.createOscillator();
    const gain = this.ctx.createGain();
    osc.type = 'sine';
    osc.frequency.setValueAtTime(200, this.ctx.currentTime);
    osc.frequency.exponentialRampToValueAtTime(600, this.ctx.currentTime + 0.3);
    gain.gain.setValueAtTime(0.1, this.ctx.currentTime);
    gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.4);
    osc.connect(gain);
    gain.connect(this.ctx.destination);
    osc.start();
    osc.stop(this.ctx.currentTime + 0.4);
  }

  stop() {
    if (this.engineOsc) {
      try { this.engineOsc.stop(); } catch(e) {}
    }
  }
}

// ─── GAME STATE ────────────────────────────────────────
const CHUNK_SIZE = 200;
const RENDER_DIST = 8;
const PLAYER_RADIUS = 1.5;
const BASE_SPEED = 40;
const MAX_SPEED = 120;
const BOOST_SPEED = 180;

let scene, camera, renderer, composer, bloomPass, motionBlurPass;
let player, playerBody;
let chunks = new Map();
let orbs = [];
let particles = [];
let trailParticles = [];
let debrisParticles = [];

// Game state
const game = {
  running: false,
  speed: BASE_SPEED,
  targetSpeed: BASE_SPEED,
  energy: 100,
  score: 0,
  multiplier: 1,
  turbo: 0,
  boosting: false,
  maxSpeed: 0,
  distance: 0,
  difficulty: 1,
  shakeAmount: 0,
  nearMissTimer: 0,
  chunkZ: 0,
};

// Player physics
const playerState = {
  x: 0, y: 5, z: 0,
  vx: 0, vy: 0, vz: 0,
  roll: 0, pitch: 0, yaw: 0,
  targetRoll: 0,
};

// Input
const keys = {};

// ─── INITIALIZATION ────────────────────────────────────
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);
  const renderPass = new RenderPass(scene, camera);
  composer.addPass(renderPass);

  bloomPass = new UnrealBloomPass(
    new THREE.Vector2(window.innerWidth, window.innerHeight),
    1.5, 0.4, 0.85
  );
  bloomPass.threshold = 0.2;
  bloomPass.strength = 1.2;
  bloomPass.radius = 0.8;
  composer.addPass(bloomPass);

  // Motion blur shader pass
  const motionBlurShader = {
    uniforms: {
      tDiffuse: { value: null },
      blendFactor: { value: 0.15 }
    },
    vertexShader: `varying vec2 vUv; void main(){ vUv=uv; gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.); }`,
    fragmentShader: `
      uniform sampler2D tDiffuse; uniform float blendFactor; varying vec2 vUv;
      void main(){
        vec4 c = texture2D(tDiffuse, vUv);
        vec2 dir = vec2(0.0, -blendFactor * 0.01);
        vec4 sum = vec4(0.);
        for(int i=0; i<5; i++){
          sum += texture2D(tDiffuse, vUv + dir * float(i));
        }
        gl_FragColor = (c + sum * 0.5) / 1.5;
      }`
  };
  motionBlurPass = new ShaderPass(motionBlurShader);
  composer.addPass(motionBlurPass);

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

  const sunLight = new THREE.DirectionalLight(0xff8c42, 2.0);
  sunLight.position.set(-100, 80, -200);
  sunLight.castShadow = true;
  sunLight.shadow.mapSize.set(2048, 2048);
  sunLight.shadow.camera.near = 1;
  sunLight.shadow.camera.far = 500;
  sunLight.shadow.camera.left = -100;
  sunLight.shadow.camera.right = 100;
  sunLight.shadow.camera.top = 100;
  sunLight.shadow.camera.bottom = -100;
  scene.add(sunLight);

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

  const hemiLight = new THREE.HemisphereLight(0xff7744, 0x2d1b69, 0.4);
  scene.add(hemiLight);

  // Skybox gradient via a large sphere
  createSky();

  // Create player craft
  createPlayer();

  // Generate initial chunks
  for (let i = -2; i < RENDER_DIST; i++) {
    generateChunk(i * CHUNK_SIZE);
  }

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

  document.getElementById('start-btn').addEventListener('click', startGame);
  document.getElementById('restart-btn').addEventListener('click', restartGame);
}

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

function createPlayer() {
  player = new THREE.Group();

  // Main body - sleek arrow shape
  const bodyGeo = new THREE.ConeGeometry(0.8, 4, 6);
  bodyGeo.rotateX(Math.PI / 2);
  const bodyMat = new THREE.MeshStandardMaterial({
    color: 0xcccccc, flatShading: true, metalness: 0.7, roughness: 0.3
  });
  playerBody = new THREE.Mesh(bodyGeo, bodyMat);
  playerBody.castShadow = true;
  player.add(playerBody);

  // Wings
  const wingShape = new THREE.Shape();
  wingShape.moveTo(0, 0);
  wingShape.lineTo(-3, -1.5);
  wingShape.lineTo(-2.5, -0.5);
  wingShape.lineTo(0, 0.5);

  const wingGeoL = new THREE.ExtrudeGeometry(wingShape, { depth: 0.15, bevelEnabled: false });
  const wingMat = new THREE.MeshStandardMaterial({
    color: 0xee4422, flatShading: true, metalness: 0.5, roughness: 0.4
  });
  const wingL = new THREE.Mesh(wingGeoL, wingMat);
  wingL.position.set(0, -0.3, 0.5);
  player.add(wingL);

  const wingR = new THREE.Mesh(wingGeoL.clone(), wingMat);
  wingR.scale.x = -1;
  wingR.position.set(0, -0.3, 0.5);
  player.add(wingR);

  // Cockpit
  const cockpitGeo = new THREE.SphereGeometry(0.4, 6, 4, 0, Math.PI * 2, 0, Math.PI / 2);
  const cockpitMat = new THREE.MeshStandardMaterial({
    color: 0x33ccff, emissive: 0x1188aa, emissiveIntensity: 0.5,
    flatShading: true, metalness: 0.9, roughness: 0.1, transparent: true, opacity: 0.7
  });
  const cockpit = new THREE.Mesh(cockpitGeo, cockpitMat);
  cockpit.position.set(0, 0.5, -0.3);
  player.add(cockpit);

  // Engine glow
  const engineGeo = new THREE.SphereGeometry(0.4, 8, 8);
  const engineMat = new THREE.MeshStandardMaterial({
    color: 0xff6a00, emissive: 0xff6a00, emissiveIntensity: 3, flatShading: true
  });
  const engine = new THREE.Mesh(engineGeo, engineMat);
  engine.position.z = 2.2;
  player.add(engine);

  // Engine flame cone
  const flameGeo = new THREE.ConeGeometry(0.3, 1.5, 6);
  flameGeo.rotateX(-Math.PI / 2);
  const flameMat = new THREE.MeshStandardMaterial({
    color: 0xffaa00, emissive: 0xff8800, emissiveIntensity: 4,
    flatShading: true, transparent: true, opacity: 0.8
  });
  const flame = new THREE.Mesh(flameGeo, flameMat);
  flame.position.z = 3;
  player.add(flame);

  // Engine light
  const engineLight = new THREE.PointLight(0xff6a00, 3, 15);
  engineLight.position.z = 2.5;
  player.add(engineLight);

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

// ─── PROCEDURAL GENERATION ─────────────────────────────
function generateChunk(zPos) {
  const chunkKey = Math.round(zPos / CHUNK_SIZE);
  if (chunks.has(chunkKey)) return;

  const chunkGroup = new THREE.Group();
  const difficulty = game.difficulty;
  const canyonWidth = Math.max(20, 60 - difficulty * 3);
  const canyonHeight = Math.max(15, 40 - difficulty * 1.5);

  // Canyon floor
  const floorGeo = new THREE.PlaneGeometry(canyonWidth * 2 + 40, CHUNK_SIZE, 8, 4);
  const floorMat = new THREE.MeshStandardMaterial({
    color: 0x3d1c02, flatShading: true, roughness: 0.9
  });
  // Add some vertex displacement for terrain feel
  const posAttr = floorGeo.attributes.position;
  for (let i = 0; i < posAttr.count; i++) {
    const x = posAttr.getX(i);
    const y = posAttr.getY(i);
    posAttr.setZ(i, (Math.sin(x * 0.1) + Math.cos(y * 0.05)) * 2);
  }
  floorGeo.computeVertexNormals();
  const floor = new THREE.Mesh(floorGeo, floorMat);
  floor.rotation.x = -Math.PI / 2;
  floor.position.set(0, -canyonHeight / 2 + 2, zPos + CHUNK_SIZE / 2);
  floor.receiveShadow = true;
  chunkGroup.add(floor);

  // Canyon ceiling
  const ceilGeo = new THREE.PlaneGeometry(canyonWidth * 2 + 40, CHUNK_SIZE, 8, 4);
  const ceilMat = new THREE.MeshStandardMaterial({
    color: 0x2a1530, flatShading: true, roughness: 0.9
  });
  const ceilPosAttr = ceilGeo.attributes.position;
  for (let i = 0; i < ceilPosAttr.count; i++) {
    const x = ceilPosAttr.getX(i);
    const y = ceilPosAttr.getY(i);
    ceilPosAttr.setZ(i, -(Math.sin(x * 0.15) + Math.cos(y * 0.08)) * 2);
  }
  ceilGeo.computeVertexNormals();
  const ceiling = new THREE.Mesh(ceilGeo, ceilMat);
  ceiling.rotation.x = Math.PI / 2;
  ceiling.position.set(0, canyonHeight / 2 - 2, zPos + CHUNK_SIZE / 2);
  ceiling.receiveShadow = true;
  chunkGroup.add(ceiling);

  // Canyon walls (left and right)
  const wallColors = [0x8b4513, 0xa0522d, 0xcd853f, 0xb87333];
  
  for (let side = -1; side <= 1; side += 2) {
    // Main wall segments with varying height and shape
    const numSegments = 4 + Math.floor(Math.random() * 3);
    for (let s = 0; s < numSegments; s++) {
      const segZ = zPos + (s / numSegments) * CHUNK_SIZE;
      const wallHeight = canyonHeight * (0.6 + Math.random() * 0.4);
      const wallWidth = 8 + Math.random() * 15;
      const wallDepth = CHUNK_SIZE / numSegments - 2;

      // Low-poly wall with random vertex displacement
      const wGeo = new THREE.BoxGeometry(wallWidth, wallHeight, wallDepth, 3, 4, 2);
      const wPosAttr = wGeo.attributes.position;
      for (let i = 0; i < wPosAttr.count; i++) {
        const px = wPosAttr.getX(i);
        const py = wPosAttr.getY(i);
        const pz = wPosAttr.getZ(i);
        wPosAttr.setX(i, px + (Math.random() - 0.5) * 3);
        wPosAttr.setY(i, py + (Math.random() - 0.5) * 2);
      }
      wGeo.computeVertexNormals();

      const colorIdx = Math.floor(Math.random() * wallColors.length);
      const wMat = new THREE.MeshStandardMaterial({
        color: wallColors[colorIdx], flatShading: true, roughness: 0.85
      });
      const wall = new THREE.Mesh(wGeo, wMat);
      wall.position.set(side * (canyonWidth / 2 + wallWidth / 2), 0, segZ + wallDepth / 2);
      wall.castShadow = true;
      wall.receiveShadow = true;
      chunkGroup.add(wall);

      // Store collision data
      wall.userData.isObstacle = true;
      wall.userData.bounds = {
        minX: side * (canyonWidth / 2 + wallWidth / 2) - wallWidth / 2,
        maxX: side * (canyonWidth / 2 + wallWidth / 2) + wallWidth / 2,
        minY: -wallHeight / 2,
        maxY: wallHeight / 2,
        minZ: segZ,
        maxZ: segZ + wallDepth,
      };
    }
  }

  // Floating monoliths
  const numMonoliths = Math.floor(1 + difficulty * 0.5);
  for (let m = 0; m < numMonoliths; m++) {
    const mHeight = 8 + Math.random() * 12;
    const mWidth = 3 + Math.random() * 4;
    const mGeo = new THREE.BoxGeometry(mWidth, mHeight, mWidth, 1, 2, 1);
    
    // Displace vertices for low-poly look
    const mPosAttr = mGeo.attributes.position;
    for (let i = 0; i < mPosAttr.count; i++) {
      mPosAttr.setX(i, mPosAttr.getX(i) + (Math.random() - 0.5) * 1);
      mPosAttr.setY(i, mPosAttr.getY(i) + (Math.random() - 0.5) * 1);
    }
    mGeo.computeVertexNormals();

    const mMat = new THREE.MeshStandardMaterial({
      color: 0x6a3d7d, flatShading: true, metalness: 0.3, roughness: 0.6
    });
    const monolith = new THREE.Mesh(mGeo, mMat);
    
    const mz = zPos + Math.random() * CHUNK_SIZE;
    const mx = (Math.random() - 0.5) * canyonWidth * 0.7;
    const my = (Math.random() - 0.5) * canyonHeight * 0.5;
    monolith.position.set(mx, my, mz);
    monolith.rotation.y = Math.random() * Math.PI;
    monolith.castShadow = true;
    
    monolith.userData.isObstacle = true;
    monolith.userData.bounds = {
      minX: mx - mWidth / 2, maxX: mx + mWidth / 2,
      minY: my - mHeight / 2, maxY: my + mHeight / 2,
      minZ: mz - mWidth / 2, maxZ: mz + mWidth / 2,
    };
    
    chunkGroup.add(monolith);
  }

  // Tight squeezes (narrow gaps with pillars on both sides)
  if (Math.random() < 0.3 * difficulty) {
    const squeezeZ = zPos + Math.random() * CHUNK_SIZE;
    const gapWidth = Math.max(6, 14 - difficulty);
    
    for (let side = -1; side <= 1; side += 2) {
      const sqGeo = new THREE.BoxGeometry(8, canyonHeight * 0.7, 3);
      const sqMat = new THREE.MeshStandardMaterial({
        color: 0x5c3a1e, flatShading: true, roughness: 0.9
      });
      const squeezeWall = new THREE.Mesh(sqGeo, sqMat);
      squeezeWall.position.set(
        side * (gapWidth / 2 + 4),
        0,
        squeezeZ
      );
      squeezeWall.castShadow = true;
      
      squeezeWall.userData.isObstacle = true;
      squeezeWall.userData.bounds = {
        minX: side * (gapWidth / 2 + 4) - 4,
        maxX: side * (gapWidth / 2 + 4) + 4,
        minY: -canyonHeight * 0.35,
        maxY: canyonHeight * 0.35,
        minZ: squeezeZ - 1.5,
        maxZ: squeezeZ + 1.5,
      };
      
      chunkGroup.add(squeezeWall);
    }
  }

  // Energy orbs
  const numOrbs = 2 + Math.floor(Math.random() * 3);
  for (let o = 0; o < numOrbs; o++) {
    const orbGeo = new THREE.OctahedronGeometry(1, 0);
    const orbMat = new THREE.MeshStandardMaterial({
      color: 0xffaa00, emissive: 0xff8800, emissiveIntensity: 2,
      flatShading: true, metalness: 0.5
    });
    const orb = new THREE.Mesh(orbGeo, orbMat);
    
    // Place orbs in interesting positions (sometimes in tight spots)
    const oz = zPos + Math.random() * CHUNK_SIZE;
    const ox = (Math.random() - 0.5) * canyonWidth * 0.6;
    const oy = (Math.random() - 0.5) * canyonHeight * 0.4;
    orb.position.set(ox, oy, oz);
    
    // Orb glow light
    const orbLight = new THREE.PointLight(0xff8800, 1, 10);
    orb.add(orbLight);
    
    chunkGroup.add(orb);
    orbs.push({ mesh: orb, collected: false });
  }

  scene.add(chunkGroup);
  chunks.set(chunkKey, { group: chunkGroup, zPos: zPos });
}

function removeChunk(zPos) {
  const chunkKey = Math.round(zPos / CHUNK_SIZE);
  const chunk = chunks.get(chunkKey);
  if (!chunk) return;
  
  scene.remove(chunk.group);
  chunk.group.traverse(child => {
    if (child.geometry) child.geometry.dispose();
    if (child.material) child.material.dispose();
  });
  chunks.delete(chunkKey);
}

// ─── PARTICLES ─────────────────────────────────────────
function createTrailParticle() {
  const geo = new THREE.SphereGeometry(0.15, 4, 4);
  const mat = new THREE.MeshBasicMaterial({ 
    color: 0xff6a00, transparent: true, opacity: 0.8 
  });
  const mesh = new THREE.Mesh(geo, mat);
  return { mesh, life: 1.0, decay: 0.02 + Math.random() * 0.03 };
}

function createDebrisParticle(zPos) {
  const geo = new THREE.TetrahedronGeometry(0.3 + Math.random() * 0.5, 0);
  const mat = new THREE.MeshBasicMaterial({ 
    color: new THREE.Color().setHSL(0.08 + Math.random() * 0.05, 0.6, 0.4),
    transparent: true, opacity: 0.7 
  });
  const mesh = new THREE.Mesh(geo, mat);
  return { mesh, life: 1.0, decay: 0.01 + Math.random() * 0.02 };
}

function spawnTrailParticle() {
  if (trailParticles.length > 80) return;
  const p = createTrailParticle();
  p.mesh.position.copy(player.position);
  p.mesh.position.z += 2.5;
  scene.add(p.mesh);
  trailParticles.push(p);
}

function spawnDebris(zPos) {
  if (debrisParticles.length > 60) return;
  const difficulty = game.difficulty;
  const canyonWidth = Math.max(20, 60 - difficulty * 3);
  const canyonHeight = Math.max(15, 40 - difficulty * 1.5);
  
  const p = createDebrisParticle(zPos);
  p.mesh.position.set(
    (Math.random() - 0.5) * canyonWidth * 1.2,
    (Math.random() - 0.5) * canyonHeight,
    zPos + Math.random() * CHUNK_SIZE
  );
  scene.add(p.mesh);
  debrisParticles.push(p);
}

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

  // Debris particles - move toward player to simulate speed
  for (let i = debrisParticles.length - 1; i >= 0; i--) {
    const p = debrisParticles[i];
    const dz = game.speed * dt * 2;
    p.mesh.position.z += dz;
    p.life -= p.decay;
    p.mesh.material.opacity = p.life * 0.7;
    p.mesh.rotation.x += dt * 3;
    p.mesh.rotation.y += dt * 2;
    
    if (p.life <= 0 || p.mesh.position.z > player.position.z + 50) {
      scene.remove(p.mesh);
      p.mesh.geometry.dispose();
      p.mesh.material.dispose();
      debrisParticles.splice(i, 1);
    }
  }
}

// ─── COLLISION DETECTION ───────────────────────────────
function checkCollisions() {
  const px = player.position.x;
  const py = player.position.y;
  const pz = player.position.z;
  
  let closestDist = Infinity;

  chunks.forEach((chunk, key) => {
    chunk.group.traverse(child => {
      if (!child.userData.isObstacle || !child.userData.bounds) return;
      
      const b = child.userData.bounds;
      
      // Check if player is within Z range of this obstacle
      if (pz + PLAYER_RADIUS < b.minZ || pz - PLAYER_RADIUS > b.maxZ) return;
      
      // AABB vs sphere check
      const closestX = Math.max(b.minX, Math.min(px, b.maxX));
      const closestY = Math.max(b.minY, Math.min(py, b.maxY));
      const closestZ = Math.max(b.minZ, Math.min(pz, b.maxZ));
      
      const dx = px - closestX;
      const dy = py - closestY;
      const dz = pz - closestZ;
      const distSq = dx * dx + dy * dy + dz * dz;
      const dist = Math.sqrt(distSq);
      
      if (dist < PLAYER_RADIUS) {
        // Collision!
        gameOver();
        return;
      }
      
      if (dist < closestDist) {
        closestDist = dist;
      }
    });
  });

  // Near-miss detection
  if (closestDist < PLAYER_RADIUS * 4 && closestDist > PLAYER_RADIUS) {
    game.nearMissTimer = 0.3;
    const proximityBonus = Math.max(1, Math.floor((PLAYER_RADIUS * 4 - closestDist) / PLAYER_RADIUS));
    game.score += proximityBonus * game.multiplier;
    
    if (closestDist < PLAYER_RADIUS * 2.5) {
      game.multiplier = Math.min(game.multiplier + 0.5, 10);
      audio.playNearMiss();
      game.shakeAmount = 0.3;
      
      document.getElementById('near-miss-flash').style.opacity = '1';
      setTimeout(() => {
        document.getElementById('near-miss-flash').style.opacity = '0';
      }, 150);
    }
    
    updateMultiplierDisplay();
  }

  // Orb collection
  for (let i = orbs.length - 1; i >= 0; i--) {
    const orb = orbs[i];
    if (orb.collected) continue;
    
    const dx = px - orb.mesh.position.x;
    const dy = py - orb.mesh.position.y;
    const dz = pz - orb.mesh.position.z;
    const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
    
    if (dist < 3) {
      orb.collected = true;
      game.energy = Math.min(100, game.energy + 20);
      game.turbo = Math.min(100, game.turbo + 15);
      game.score += 50 * game.multiplier;
      audio.playCollect();
      
      // Remove orb mesh visually
      scene.remove(orb.mesh);
      orbs.splice(i, 1);
    }
    
    // Clean up far behind orbs
    if (orb.mesh.position.z < pz - CHUNK_SIZE) {
      scene.remove(orb.mesh);
      orbs.splice(i, 1);
    }
  }
}

// ─── GAME LOGIC ────────────────────────────────────────
const audio = new AudioEngine();

function startGame() {
  document.getElementById('start-screen').style.display = 'none';
  audio.init();
  game.running = true;
  resetGameState();
}

function restartGame() {
  document.getElementById('game-over').style.display = 'none';
  
  // Clean up old chunks and particles
  chunks.forEach((chunk) => {
    scene.remove(chunk.group);
    chunk.group.traverse(c => {
      if (c.geometry) c.geometry.dispose();
      if (c.material) c.material.dispose();
    });
  });
  chunks.clear();
  
  trailParticles.forEach(p => {
    scene.remove(p.mesh);
    p.mesh.geometry.dispose();
    p.mesh.material.dispose();
  });
  trailParticles.length = 0;
  
  debrisParticles.forEach(p => {
    scene.remove(p.mesh);
    p.mesh.geometry.dispose();
    p.mesh.material.dispose();
  });
  debrisParticles.length = 0;
  
  orbs.forEach(o => scene.remove(o.mesh));
  orbs.length = 0;

  resetGameState();
  game.running = true;
}

function resetGameState() {
  game.speed = BASE_SPEED;
  game.targetSpeed = BASE_SPEED;
  game.energy = 100;
  game.score = 0;
  game.multiplier = 1;
  game.turbo = 0;
  game.boosting = false;
  game.maxSpeed = 0;
  game.distance = 0;
  game.difficulty = 1;
  game.shakeAmount = 0;
  game.nearMissTimer = 0;
  game.chunkZ = 0;

  playerState.x = 0;
  playerState.y = 5;
  playerState.z = 0;
  playerState.vx = 0;
  playerState.vy = 0;
  playerState.roll = 0;
  playerState.pitch = 0;

  player.position.set(0, 5, 0);
  player.rotation.set(0, 0, 0);

  for (let i = -2; i < RENDER_DIST; i++) {
    generateChunk(i * CHUNK_SIZE);
  }
}

function gameOver() {
  game.running = false;
  audio.playCrash();
  
  document.getElementById('final-score').textContent = Math.floor(game.score).toLocaleString();
  document.getElementById('final-speed').textContent = `Max Speed: ${Math.floor(game.maxSpeed * 3.6)} km/h`;
  document.getElementById('final-distance').textContent = `Distance: ${Math.floor(game.distance)} m`;
  document.getElementById('game-over').style.display = 'flex';
}

function updateMultiplierDisplay() {
  const el = document.getElementById('multiplier');
  el.textContent = `x${game.multiplier.toFixed(1)}`;
  if (game.multiplier > 1) {
    el.classList.add('active');
  } else {
    el.classList.remove('active');
  }
}

// ─── MAIN UPDATE LOOP ──────────────────────────────────
let lastTime = performance.now();
let debrisTimer = 0;
let trailTimer = 0;

function update(timestamp) {
  requestAnimationFrame(update);
  
  const dt = Math.min((timestamp - lastTime) / 1000, 0.05);
  lastTime = timestamp;

  if (!game.running) {
    renderer.render(scene, camera);
    return;
  }

  // ── Input handling ──
  const moveSpeed = 25 * dt;
  const accelForce = 15 * dt;
  
  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;

  // Boost
  const boostInput = keys['ShiftLeft'] || keys['ShiftRight'];
  const brakeInput = keys['Space'];

  if (boostInput && game.turbo > 0) {
    game.boosting = true;
    game.turbo -= 30 * dt;
    game.targetSpeed = BOOST_SPEED;
    bloomPass.strength = 2.5;
    document.getElementById('boost-flash').style.opacity = '0.5';
  } else if (brakeInput) {
    game.boosting = false;
    game.targetSpeed = BASE_SPEED * 0.6;
    bloomPass.strength = Math.max(1.2, bloomPass.strength - dt * 3);
    document.getElementById('boost-flash').style.opacity = '0';
  } else {
    game.boosting = false;
    game.targetSpeed = BASE_SPEED + (game.difficulty - 1) * 5;
    bloomPass.strength += (1.2 - bloomPass.strength) * dt * 3;
    document.getElementById('boost-flash').style.opacity = '0';
  }

  if (game.turbo <= 0) game.turbo = 0;

  // ── Player physics ──
  playerState.vx += inputX * accelForce;
  playerState.vy += inputY * accelForce;
  
  // Damping
  playerState.vx *= Math.pow(0.92, dt * 60);
  playerState.vy *= Math.pow(0.92, dt * 60);

  playerState.x += playerState.vx * dt;
  playerState.y += playerState.vy * dt;
  
  // Clamp to canyon bounds (soft)
  const difficulty = game.difficulty;
  const canyonWidth = Math.max(20, 60 - difficulty * 3);
  const canyonHeight = Math.max(15, 40 - difficulty * 1.5);
  
  playerState.x = Math.max(-canyonWidth / 2 + 3, Math.min(canyonWidth / 2 - 3, playerState.x));
  playerState.y = Math.max(-canyonHeight / 2 + 5, Math.min(canyonHeight / 2 - 5, playerState.y));

  // Banking (roll) based on lateral velocity
  playerState.targetRoll = -playerState.vx * 0.04;
  playerState.roll += (playerState.targetRoll - playerState.roll) * dt * 8;
  
  // Pitch based on vertical movement
  playerState.pitch = -inputY * 0.15;

  // Speed interpolation
  game.speed += (game.targetSpeed - game.speed) * dt * 3;
  game.distance += game.speed * dt;
  game.maxSpeed = Math.max(game.maxSpeed, game.speed);

  // Move player forward
  playerState.z += game.speed * dt;
  
  // Apply position and rotation to player mesh
  player.position.set(playerState.x, playerState.y, playerState.z);
  player.rotation.z = playerState.roll;
  player.rotation.x = playerState.pitch;

  // ── Energy decay ──
  game.energy -= (2 + difficulty * 0.5) * dt;
  if (game.energy <= 0) {
    game.energy = 0;
    gameOver();
  }

  // ── Difficulty scaling ──
  game.difficulty = 1 + game.distance / 2000;

  // ── Multiplier decay ──
  if (game.nearMissTimer <= 0) {
    game.multiplier += (1 - game.multiplier) * dt * 0.5;
    updateMultiplierDisplay();
  } else {
    game.nearMissTimer -= dt;
  }

  // ── Chunk management ──
  const currentChunkKey = Math.round(playerState.z / CHUNK_SIZE);
  
  // Generate new chunks ahead
  for (let i = -2; i < RENDER_DIST; i++) {
    generateChunk((currentChunkKey + i) * CHUNK_SIZE);
  }
  
  // Remove old chunks behind
  const keysToRemove = [];
  chunks.forEach((chunk, key) => {
    if (key < currentChunkKey - 3) {
      keysToRemove.push(key);
    }
  });
  keysToRemove.forEach(k => removeChunk(k * CHUNK_SIZE));

  // ── Collision detection ──
  checkCollisions();

  // ── Particles ──
  trailTimer += dt;
  if (trailTimer > 0.03) {
    spawnTrailParticle();
    trailTimer = 0;
  }

  debrisTimer += dt;
  const debrisInterval = Math.max(0.05, 0.2 - difficulty * 0.01);
  if (debrisTimer > debrisInterval) {
    spawnDebris(playerState.z);
    debrisTimer = 0;
  }

  updateParticles(dt);

  // ── Camera follow ──
  const camDist = game.boosting ? 12 : 8;
  const camHeight = game.boosting ? 5 : 3.5;
  const targetCamX = playerState.x * 0.4;
  const targetCamY = playerState.y + camHeight;
  const targetCamZ = playerState.z - camDist;

  camera.position.x += (targetCamX - camera.position.x) * dt * 5;
  camera.position.y += (targetCamY - camera.position.y) * dt * 5;
  camera.position.z += (targetCamZ - camera.position.z) * dt * 8;

  // Camera shake
  if (game.shakeAmount > 0.01) {
    camera.position.x += (Math.random() - 0.5) * game.shakeAmount;
    camera.position.y += (Math.random() - 0.5) * game.shakeAmount;
    game.shakeAmount *= Math.pow(0.9, dt * 60);
  }

  // FOV based on speed
  const targetFov = 75 + (game.speed / BOOST_SPEED) * 20;
  camera.fov += (targetFov - camera.fov) * dt * 3;
  camera.updateProjectionMatrix();

  camera.lookAt(playerState.x, playerState.y, playerState.z + 20);

  // ── Motion blur intensity based on speed ──
  motionBlurPass.uniforms.blendFactor.value = (game.speed / BOOST_SPEED) * 0.3;

  // ── Update orb rotation ──
  orbs.forEach(orb => {
    if (!orb.collected) {
      orb.mesh.rotation.y += dt * 2;
      orb.mesh.rotation.x += dt * 1.5;
      orb.mesh.position.y += Math.sin(timestamp * 0.003 + orb.mesh.position.z) * 0.01;
    }
  });

  // ── Update HUD ──
  document.getElementById('score-display').textContent = Math.floor(game.score).toLocaleString();
  document.getElementById('speed-bar').style.height = `${(game.speed / BOOST_SPEED) * 100}%`;
  document.getElementById('turbo-bar').style.height = `${game.turbo}%`;
  document.getElementById('energy-bar').style.width = `${game.energy}%`;

  // Energy bar color warning
  const energyBar = document.getElementById('energy-bar');
  if (game.energy < 20) {
    energyBar.style.background = 'linear-gradient(to right, #ff0000, #ff4444)';
  } else {
    energyBar.style.background = 'linear-gradient(to right, #ff6a00, #ee0979)';
  }

  // Audio engine pitch
  audio.setEnginePitch(game.speed / BOOST_SPEED);

  // ── Render ──
  composer.render();
}

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

// ─── START ─────────────────────────────────────────────
init();
requestAnimationFrame(update);

</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingcap-qwen3.6-27b","provider":"lmstudio","persona":"full","sessionId":"a6d21427-76be-4500-abb3-0705b6e9cbb7","tokensIn":522963,"tokensOut":17430,"tokensTotal":540393,"turns":22,"toolCalls":21,"failedToolCalls":0,"timestamp":"2026-07-18T20:09:56.236Z"} -->