← Canyon Run results

Canyon Run

thinkingcap qwen3.6 27b · basic

Initial view of Canyon Run

Reviews & generation details

Reviews

No human review yet. Unreviewed does not mean unsuccessful.

Generation context

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

Generation prompt · exact text unavailable

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

Inspect original source 34,097 bytes · SHA-256 247d0ccc4bde
<!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', Arial, sans-serif; cursor: none; }
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: 8px; height: 200px; background: rgba(255,255,255,0.1);
border-radius: 4px; overflow: hidden;
}
#speed-bar {
position: absolute; bottom: 0; width: 100%; height: 50%;
background: linear-gradient(to top, #ff6a00, #ffcc00);
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.05);
border-radius: 4px; overflow: hidden;
}
#turbo-bar {
position: absolute; bottom: 0; width: 100%; height: 0%;
background: linear-gradient(to top, #ff006a, #ff6a00);
border-radius: 4px; transition: height 0.15s;
}

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

#score-display {
position: absolute; top: 20px; right: 30px;
font-size: 24px; font-weight: 700; color: #fff;
text-shadow: 0 0 10px rgba(255,255,255,0.3);
}

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

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

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

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

<div id="hud">
  <div id="score-display">SCORE: 0</div>
  <div id="multiplier"></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="game-over">
  <h1>CRASHED</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 RACER</h1>
  <div class="subtitle">Procedural Speed Run</div>
  <button id="start-btn" onclick="startGame()">LAUNCH</button>
  <div class="controls-info">
    W/S or ↑/↓ — Pitch &nbsp;|&nbsp; A/D or ←/→ — Roll &amp; Turn<br>
    SPACE — Boost &nbsp;|&nbsp; Collect orbs to refuel energy
  </div>
</div>

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

// ─── GLOBALS ──────────────────────────────────────────────
let scene, camera, renderer, composer, bloomPass, afterimagePass;
let player, playerBody;
let chunks = [];
let particles = [];
let orbs = [];
let gameActive = false;
let gameTime = 0;

// Player state
const playerState = {
  x: 0, y: 0, z: 0,
  vx: 0, vy: 0, vz: 0,
  roll: 0, pitch: 0, yaw: 0,
  speed: 0.8,
  maxSpeed: 2.5,
  energy: 100,
  score: 0,
  multiplier: 1,
  turbo: 0,
  boosting: false,
  nearMissTimer: 0,
  distance: 0,
  maxSpeedReached: 0
};

// Input
const keys = {};
window.addEventListener('keydown', e => { keys[e.code] = true; e.preventDefault(); });
window.addEventListener('keyup', e => { keys[e.code] = false; });

// Audio context
let audioCtx;
function initAudio() {
  audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}

// ─── SOUND SYNTHESIS ──────────────────────────────────────
const engineOsc = { osc: null, gain: null };
function startEngineSound() {
  if (!audioCtx) return;
  const osc = audioCtx.createOscillator();
  const gain = audioCtx.createGain();
  osc.type = 'sawtooth';
  osc.frequency.value = 60;
  gain.gain.value = 0.04;
  osc.connect(gain);
  gain.connect(audioCtx.destination);
  osc.start();
  engineOsc.osc = osc;
  engineOsc.gain = gain;
}

function updateEngineSound() {
  if (!engineOsc.osc) return;
  const speedRatio = playerState.speed / playerState.maxSpeed;
  engineOsc.osc.frequency.value = 50 + speedRatio * 120;
  engineOsc.gain.gain.value = 0.03 + speedRatio * 0.06;
}

function playCollectSound() {
  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.2);
  osc.connect(gain);
  gain.connect(audioCtx.destination);
  osc.start();
  osc.stop(audioCtx.currentTime + 0.2);
}

function playNearMissSound() {
  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 filter = audioCtx.createBiquadFilter();
  filter.type = 'bandpass';
  filter.frequency.value = 2000;
  filter.Q.value = 2;
  const gain = audioCtx.createGain();
  gain.gain.value = 0.12;
  source.connect(filter);
  filter.connect(gain);
  gain.connect(audioCtx.destination);
  source.start();
}

function playCrashSound() {
  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 initThree() {
  scene = new THREE.Scene();
  scene.fog = new THREE.FogExp2(0x1a0a2e, 0.008);

  camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.5, 500);
  camera.position.set(0, 3, -8);

  renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
  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.8;
  bloomPass.radius = 0.6;
  composer.addPass(bloomPass);

  afterimagePass = new AfterimagePass(0.3);
  composer.addPass(afterimagePass);

  // Lighting
  const ambientLight = new THREE.AmbientLight(0x4422aa, 0.4);
  scene.add(ambientLight);

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

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

  // Sky dome
  createSky();

  window.addEventListener('resize', onResize);
}

function createSky() {
  const skyGeo = new THREE.SphereGeometry(400, 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;
        vec3 bottom = vec3(0.6, 0.2, 0.05);
        vec3 mid = vec3(0.4, 0.1, 0.3);
        vec3 top = vec3(0.05, 0.02, 0.15);
        vec3 col = mix(bottom, mid, smoothstep(-0.1, 0.3, h));
        col = mix(col, top, smoothstep(0.3, 0.9, h));
        gl_FragColor = vec4(col, 1.0);
      }
    `
  });
  const sky = new THREE.Mesh(skyGeo, skyMat);
  scene.add(sky);
}

// ─── PLAYER CRAFT ─────────────────────────────────────────
function createPlayer() {
  player = new THREE.Group();

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

  // Wings
  const wingGeo = new THREE.BufferGeometry();
  const wingVerts = new Float32Array([
    -0.15, 0, -0.5,   -2.0, -0.3, -0.8,   -0.15, 0, 0.3,
    0.15, 0, -0.5,    0.15, 0, 0.3,       2.0, -0.3, -0.8
  ]);
  wingGeo.setAttribute('position', new THREE.BufferAttribute(wingVerts, 3));
  wingGeo.computeVertexNormals();
  const wingMat = new THREE.MeshStandardMaterial({
    color: 0xff4466, flatShading: true, metalness: 0.5, roughness: 0.3
  });
  const wings = new THREE.Mesh(wingGeo, wingMat);
  wings.castShadow = true;
  player.add(wings);

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

  // Engine light
  const engineLight = new THREE.PointLight(0xff4400, 2, 8);
  engineLight.position.z = 1.5;
  player.add(engineLight);

  scene.add(player);
}

// ─── PROCEDURAL CHUNK GENERATION ──────────────────────────
const CHUNK_LENGTH = 60;
const CHUNK_COUNT = 12;
let nextChunkZ = 0;
let difficulty = 1;

function createChunk(zPos) {
  const chunk = new THREE.Group();
  chunk.userData.z = zPos;
  chunk.userData.obstacles = [];
  chunk.userData.orbs = [];

  // Canyon walls on both sides
  generateCanyonWalls(chunk, zPos);

  // Obstacles
  if (Math.random() < 0.4 + difficulty * 0.1) {
    generateMonoliths(chunk, zPos);
  }

  // Tight squeezes
  if (difficulty > 1.5 && Math.random() < 0.2) {
    generateSqueeze(chunk, zPos);
  }

  // Energy orbs
  const orbCount = 2 + Math.floor(Math.random() * 3);
  for (let i = 0; i < orbCount; i++) {
    createOrbInChunk(chunk, zPos);
  }

  scene.add(chunk);
  return chunk;
}

function generateCanyonWalls(chunk, zPos) {
  const wallMat1 = new THREE.MeshStandardMaterial({
    color: 0x8B4513, flatShading: true, roughness: 0.9
  });
  const wallMat2 = new THREE.MeshStandardMaterial({
    color: 0x6B3410, flatShading: true, roughness: 0.9
  });
  const accentMat = new THREE.MeshStandardMaterial({
    color: 0xff8844, flatShading: true, emissive: 0x331100, emissiveIntensity: 0.3
  });

  for (let i = 0; i < CHUNK_LENGTH; i += 4) {
    const z = zPos + i;
    const canyonWidth = Math.max(8, 25 - difficulty * 2);
    const heightVar = 15 + Math.random() * 20;

    // Left wall segments
    const lw = 3 + Math.random() * 4;
    const lh = heightVar + Math.sin(i * 0.3) * 8;
    const leftWallGeo = new THREE.BoxGeometry(lw, lh, 5);
    const leftWall = new THREE.Mesh(leftWallGeo, i % 8 === 0 ? accentMat : wallMat1);
    leftWall.position.set(-canyonWidth - lw / 2 + Math.sin(i * 0.1) * 3, lh / 2 - 5, z);
    leftWall.castShadow = true;
    leftWall.receiveShadow = true;
    chunk.add(leftWall);

    // Right wall segments
    const rw = 3 + Math.random() * 4;
    const rh = heightVar + Math.cos(i * 0.25) * 8;
    const rightWallGeo = new THREE.BoxGeometry(rw, rh, 5);
    const rightWall = new THREE.Mesh(rightWallGeo, i % 8 === 0 ? accentMat : wallMat2);
    rightWall.position.set(canyonWidth + rw / 2 - Math.cos(i * 0.1) * 3, rh / 2 - 5, z);
    rightWall.castShadow = true;
    rightWall.receiveShadow = true;
    chunk.add(rightWall);

    // Ground
    const groundGeo = new THREE.BoxGeometry(canyonWidth * 2 + lw + rw, 1, 5);
    const groundMat = new THREE.MeshStandardMaterial({ color: 0x3a1a0a, flatShading: true });
    const ground = new THREE.Mesh(groundGeo, groundMat);
    ground.position.set(0, -8, z);
    ground.receiveShadow = true;
    chunk.add(ground);

    // Ceiling (occasional)
    if (Math.random() < 0.3) {
      const ceilGeo = new THREE.BoxGeometry(canyonWidth * 1.5, 2, 4);
      const ceilMat = new THREE.MeshStandardMaterial({ color: 0x5a2a1a, flatShading: true });
      const ceil = new THREE.Mesh(ceilGeo, ceilMat);
      ceil.position.set(Math.sin(i * 0.15) * 3, 18 + Math.random() * 5, z);
      ceil.castShadow = true;
      chunk.add(ceil);
    }
  }

  // Store wall bounds for collision
  const canyonWidth = Math.max(8, 25 - difficulty * 2);
  chunk.userData.canyonBounds = { minX: -canyonWidth, maxX: canyonWidth };
}

function generateMonoliths(chunk, zPos) {
  const monolithMat = new THREE.MeshStandardMaterial({
    color: 0x9944cc, flatShading: true, emissive: 0x220033, emissiveIntensity: 0.5
  });

  const count = Math.floor(1 + difficulty * 0.5);
  for (let i = 0; i < count; i++) {
    const z = zPos + Math.random() * CHUNK_LENGTH;
    const canyonWidth = chunk.userData.canyonBounds ? 
      (chunk.userData.canyonBounds.maxX - chunk.userData.canyonBounds.minX) / 2 : 15;
    
    const x = (Math.random() - 0.5) * canyonWidth * 1.4;
    const y = (Math.random() - 0.5) * 10;
    const w = 1 + Math.random() * 3;
    const h = 6 + Math.random() * 12;
    const d = 1 + Math.random() * 2;

    const geo = new THREE.BoxGeometry(w, h, d);
    const mesh = new THREE.Mesh(geo, monolithMat);
    mesh.position.set(x, y, z);
    mesh.rotation.y = Math.random() * Math.PI;
    mesh.castShadow = true;
    chunk.add(mesh);

    chunk.userData.obstacles.push({
      minX: x - w / 2, maxX: x + w / 2,
      minY: y - h / 2, maxY: y + h / 2,
      minZ: z - d / 2, maxZ: z + d / 2
    });
  }
}

function generateSqueeze(chunk, zPos) {
  const squeezeMat = new THREE.MeshStandardMaterial({
    color: 0xaa5533, flatShading: true, roughness: 0.8
  });

  const squeezeZ = zPos + CHUNK_LENGTH * 0.5;
  const gapWidth = Math.max(4, 12 - difficulty);
  const canyonHalfW = chunk.userData.canyonBounds ? 
    (chunk.userData.canyonBounds.maxX - chunk.userData.canyonBounds.minX) / 2 : 15;

  // Left squeeze wall
  const lwGeo = new THREE.BoxGeometry(canyonHalfW * 0.6, 20, 3);
  const lw = new THREE.Mesh(lwGeo, squeezeMat);
  lw.position.set(-canyonHalfW + gapWidth / 2 + canyonHalfW * 0.3, 5, squeezeZ);
  lw.castShadow = true;
  chunk.add(lw);

  // Right squeeze wall
  const rwGeo = new THREE.BoxGeometry(canyonHalfW * 0.6, 20, 3);
  const rw = new THREE.Mesh(rwGeo, squeezeMat);
  rw.position.set(canyonHalfW - gapWidth / 2 - canyonHalfW * 0.3, 5, squeezeZ);
  rw.castShadow = true;
  chunk.add(rw);

  // Top squeeze
  const twGeo = new THREE.BoxGeometry(gapWidth + 4, 3, 3);
  const tw = new THREE.Mesh(twGeo, squeezeMat);
  tw.position.set(0, 12, squeezeZ);
  tw.castShadow = true;
  chunk.add(tw);

  // Bottom squeeze
  const bwGeo = new THREE.BoxGeometry(gapWidth + 4, 3, 3);
  const bw = new THREE.Mesh(bwGeo, squeezeMat);
  bw.position.set(0, -6, squeezeZ);
  bw.castShadow = true;
  chunk.add(bw);

  chunk.userData.obstacles.push({
    minX: -gapWidth / 2, maxX: gapWidth / 2,
    minY: -4.5, maxY: 10.5,
    minZ: squeezeZ - 1.5, maxZ: squeezeZ + 1.5
  });
}

function createOrbInChunk(chunk, zPos) {
  const orbGeo = new THREE.IcosahedronGeometry(0.4, 1);
  const orbMat = new THREE.MeshBasicMaterial({ color: 0xffcc00 });
  const orb = new THREE.Mesh(orbGeo, orbMat);

  const canyonHalfW = chunk.userData.canyonBounds ? 
    (chunk.userData.canyonBounds.maxX - chunk.userData.canyonBounds.minX) / 2 : 15;
  
  orb.position.set(
    (Math.random() - 0.5) * canyonHalfW * 1.6,
    (Math.random() - 0.5) * 12,
    zPos + Math.random() * CHUNK_LENGTH
  );

  // Glow light
  const orbLight = new THREE.PointLight(0xffcc00, 1.5, 8);
  orb.add(orbLight);

  chunk.add(orb);
  chunk.userData.orbs.push({ mesh: orb, collected: false });
}

// ─── PARTICLE SYSTEMS ─────────────────────────────────────
function createTrailParticle() {
  const geo = new THREE.BufferGeometry();
  const positions = new Float32Array([0, 0, 0]);
  geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));

  const mat = new THREE.PointsMaterial({
    color: 0xff6600, size: 0.15, transparent: true, opacity: 0.8,
    blending: THREE.AdditiveBlending, depthWrite: false
  });

  const particle = new THREE.Points(geo, mat);
  particle.userData.life = 1.0;
  particle.userData.decay = 0.02 + Math.random() * 0.03;
  scene.add(particle);
  particles.push(particle);
}

function createDebrisParticle(x, y, z) {
  const geo = new THREE.TetrahedronGeometry(0.15, 0);
  const mat = new THREE.MeshBasicMaterial({ color: 0xff8844 });
  const mesh = new THREE.Mesh(geo, mat);
  mesh.position.set(x, y, z);
  mesh.userData.vx = (Math.random() - 0.5) * 2;
  mesh.userData.vy = Math.random() * 2;
  mesh.userData.vz = -Math.random() * 3;
  mesh.userData.life = 1.0;
  scene.add(mesh);
  particles.push(mesh);
}

// ─── CAMERA SHAKE ─────────────────────────────────────────
let cameraShakeIntensity = 0;
function addCameraShake(amount) {
  cameraShakeIntensity = Math.min(cameraShakeIntensity + amount, 1.5);
}

// ─── COLLISION DETECTION ──────────────────────────────────
const playerBounds = { radius: 0.8 };

function checkCollisions() {
  const px = playerState.x;
  const py = playerState.y;
  const pz = playerState.z;
  const r = playerBounds.radius;

  for (let i = chunks.length - 1; i >= 0; i--) {
    const chunk = chunks[i];
    if (!chunk.userData.obstacles) continue;

    // Check canyon bounds
    if (chunk.userData.canyonBounds) {
      const cb = chunk.userData.canyonBounds;
      const inChunkZ = pz > chunk.userData.z && pz < chunk.userData.z + CHUNK_LENGTH;
      if (inChunkZ) {
        if (px - r < cb.minX || px + r > cb.maxX) {
          return true; // Collision with canyon wall
        }
      }
    }

    // Check obstacles
    for (const obs of chunk.userData.obstacles) {
      const inZ = pz > obs.minZ - 2 && pz < obs.maxZ + 2;
      if (!inZ) continue;

      const closestX = Math.max(obs.minX, Math.min(px, obs.maxX));
      const closestY = Math.max(obs.minY, Math.min(py, obs.maxY));
      const dx = px - closestX;
      const dy = py - closestY;
      const dist = Math.sqrt(dx * dx + dy * dy);

      if (dist < r) {
        return true; // Collision
      }

      // Near-miss detection
      if (dist < r + 2.5 && dist > r) {
        triggerNearMiss();
      }
    }
  }

  return false;
}

function checkOrbCollection() {
  const px = playerState.x;
  const py = playerState.y;
  const pz = playerState.z;

  for (const chunk of chunks) {
    if (!chunk.userData.orbs) continue;
    for (const orbData of chunk.userData.orbs) {
      if (orbData.collected) continue;
      const orbPos = orbData.mesh.position;
      const dx = px - orbPos.x;
      const dy = py - orbPos.y;
      const dz = pz - orbPos.z;
      const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);

      if (dist < 2.0) {
        orbData.collected = true;
        orbData.mesh.visible = false;
        playerState.energy = Math.min(100, playerState.energy + 15);
        playerState.score += 50 * playerState.multiplier;
        playCollectSound();

        // Burst particles
        for (let i = 0; i < 8; i++) {
          createDebrisParticle(orbPos.x, orbPos.y, orbPos.z);
        }
      }
    }
  }
}

// ─── NEAR MISS SYSTEM ─────────────────────────────────────
function triggerNearMiss() {
  if (playerState.nearMissTimer > 0) return;
  playerState.nearMissTimer = 0.5;
  playerState.multiplier = Math.min(playerState.multiplier + 0.5, 10);
  playerState.score += 100 * playerState.multiplier;

  addCameraShake(0.3);
  playNearMissSound();

  const flash = document.getElementById('near-miss-flash');
  flash.style.opacity = '1';
  setTimeout(() => { flash.style.opacity = '0'; }, 150);

  const multEl = document.getElementById('multiplier');
  multEl.textContent = 'x' + playerState.multiplier.toFixed(1);
  multEl.classList.add('visible', 'pulse');
  setTimeout(() => multEl.classList.remove('pulse'), 200);
}

// ─── GAME LOOP ────────────────────────────────────────────
let lastTime = 0;

function gameLoop(timestamp) {
  if (!gameActive) return;

  const dt = Math.min((timestamp - lastTime) / 1000, 0.05);
  lastTime = timestamp;
  gameTime += dt;

  updatePlayer(dt);
  updateChunks();
  updateParticles(dt);
  updateCamera(dt);
  updateHUD();
  updateEngineSound();

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

  composer.render();
  requestAnimationFrame(gameLoop);
}

function updatePlayer(dt) {
  const accel = 8.0;
  const damping = 0.92;
  const turnSpeed = 3.5;
  const pitchSpeed = 2.5;

  // Input handling
  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 boosting = keys['Space'] && playerState.turbo > 0;
  if (boosting) {
    playerState.boosting = true;
    playerState.turbo -= dt * 30;
    if (playerState.turbo <= 0) {
      playerState.turbo = 0;
      playerState.boosting = false;
    }
  } else {
    playerState.boosting = false;
    // Build turbo on straightaways at high speed
    if (Math.abs(inputX) < 0.1 && playerState.speed > 1.5) {
      playerState.turbo = Math.min(100, playerState.turbo + dt * 8);
    }
  }

  const boostMultiplier = boosting ? 2.0 : 1.0;
  const targetSpeed = (0.8 + difficulty * 0.15) * boostMultiplier;
  playerState.speed += (targetSpeed - playerState.speed) * dt * 3;

  // Track max speed
  if (playerState.speed > playerState.maxSpeedReached) {
    playerState.maxSpeedReached = playerState.speed;
  }

  // Apply forces with momentum
  playerState.vx += inputX * accel * dt;
  playerState.vy += inputY * accel * dt;
  playerState.vx *= damping;
  playerState.vy *= damping;

  // Clamp velocity
  const maxV = 15;
  playerState.vx = Math.max(-maxV, Math.min(maxV, playerState.vx));
  playerState.vy = Math.max(-maxV, Math.min(maxV, playerState.vy));

  // Update position
  playerState.x += playerState.vx * dt;
  playerState.y += playerState.vy * dt;
  playerState.z += playerState.speed * 60 * dt;
  playerState.distance = playerState.z;

  // Energy decay
  playerState.energy -= dt * (3 + difficulty * 0.5);
  if (playerState.energy <= 0) {
    gameOver();
    return;
  }

  // Score from distance
  playerState.score += playerState.speed * dt * 10;

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

  // Banking animation
  const targetRoll = -inputX * 0.6;
  const targetPitch = inputY * 0.3;
  playerState.roll += (targetRoll - playerState.roll) * dt * 8;
  playerState.pitch += (targetPitch - playerState.pitch) * dt * 8;

  // Apply to mesh
  if (player) {
    player.position.set(playerState.x, playerState.y, playerState.z);
    player.rotation.z = playerState.roll;
    player.rotation.x = playerState.pitch;

    // Engine glow intensity based on speed
    const engineLight = player.children.find(c => c.isPointLight);
    if (engineLight) {
      engineLight.intensity = 1 + playerState.speed * 2;
    }
  }

  // Trail particles
  if (Math.random() < 0.5 + playerState.speed * 0.3) {
    createTrailParticle();
  }

  // Debris for speed feel
  if (playerState.speed > 1.5 && Math.random() < 0.2) {
    createDebrisParticle(
      playerState.x + (Math.random() - 0.5) * 4,
      playerState.y + (Math.random() - 0.5) * 4,
      playerState.z - 3
    );
  }

  // Collision check
  if (checkCollisions()) {
    gameOver();
    return;
  }

  // Orb collection
  checkOrbCollection();
}

function updateChunks() {
  const playerZ = playerState.z;

  // Generate new chunks ahead
  while (nextChunkZ < playerZ + CHUNK_LENGTH * CHUNK_COUNT) {
    const chunk = createChunk(nextChunkZ);
    chunks.push(chunk);
    nextChunkZ += CHUNK_LENGTH;
  }

  // Remove old chunks behind
  for (let i = chunks.length - 1; i >= 0; i--) {
    if (chunks[i].userData.z + CHUNK_LENGTH < playerZ - CHUNK_LENGTH * 2) {
      scene.remove(chunks[i]);
      chunks.splice(i, 1);
    }
  }
}

function updateParticles(dt) {
  for (let i = particles.length - 1; i >= 0; i--) {
    const p = particles[i];
    p.userData.life -= p.userData.decay || 0.02;

    if (p.isPoints) {
      // Trail particle
      p.position.copy(player.position);
      p.material.opacity = p.userData.life * 0.6;
    } else {
      // Debris
      p.position.x += (p.userData.vx || 0) * dt;
      p.position.y += (p.userData.vy || 0) * dt;
      p.position.z += (p.userData.vz || 0) * dt;
      p.material.opacity = p.userData.life;
    }

    if (p.userData.life <= 0) {
      scene.remove(p);
      particles.splice(i, 1);
    }
  }
}

function updateCamera(dt) {
  const targetX = playerState.x * 0.7;
  const targetY = playerState.y * 0.5 + 3;
  const camZ = playerState.z - (boosting ? 12 : 8);

  camera.position.x += (targetX - camera.position.x) * dt * 4;
  camera.position.y += (targetY - camera.position.y) * dt * 4;
  camera.position.z = camZ;

  // Camera shake
  if (cameraShakeIntensity > 0.01) {
    camera.position.x += (Math.random() - 0.5) * cameraShakeIntensity;
    camera.position.y += (Math.random() - 0.5) * cameraShakeIntensity;
    cameraShakeIntensity *= 0.9;
  }

  // FOV based on speed
  const targetFov = boosting ? 90 : 70 + playerState.speed * 8;
  camera.fov += (targetFov - camera.fov) * dt * 3;
  camera.updateProjectionMatrix();

  // Look ahead
  const lookTarget = new THREE.Vector3(
    playerState.x * 0.3,
    playerState.y * 0.3 + 1,
    playerState.z + 40
  );
  camera.lookAt(lookTarget);

  // Bloom intensity based on boost
  bloomPass.strength = boosting ? 2.5 : 1.8;
}

function updateHUD() {
  const speedRatio = (playerState.speed - 0.5) / (playerState.maxSpeed * 2);
  document.getElementById('speed-bar').style.height = Math.max(0, Math.min(100, speedRatio * 100)) + '%';

  document.getElementById('turbo-bar').style.height = playerState.turbo + '%';

  document.getElementById('energy-bar').style.width = playerState.energy + '%';
  if (playerState.energy < 25) {
    document.getElementById('energy-bar').style.background = '#ff0044';
  } else {
    document.getElementById('energy-bar').style.background = 'linear-gradient(to right, #ff006a, #ff6a00, #ffcc00)';
  }

  document.getElementById('score-display').textContent = 'SCORE: ' + Math.floor(playerState.score);

  const multEl = document.getElementById('multiplier');
  if (playerState.multiplier <= 1.0) {
    multEl.classList.remove('visible');
  } else {
    multEl.textContent = 'x' + playerState.multiplier.toFixed(1);
  }
}

// ─── GAME OVER / START ────────────────────────────────────
function gameOver() {
  gameActive = false;
  playCrashSound();

  if (engineOsc.osc) {
    engineOsc.osc.stop();
    engineOsc.osc = null;
  }

  document.getElementById('final-score').textContent = Math.floor(playerState.score);
  document.getElementById('max-speed').textContent = (playerState.maxSpeedReached * 60).toFixed(0) + ' km/h';
  document.getElementById('distance').textContent = Math.floor(playerState.distance);
  document.getElementById('game-over').style.display = 'flex';
}

window.startGame = function() {
  document.getElementById('start-screen').style.display = 'none';
  initAudio();
  startEngineSound();
  gameActive = true;
  lastTime = performance.now();
  requestAnimationFrame(gameLoop);
};

window.restartGame = function() {
  // Clean up
  for (const chunk of chunks) scene.remove(chunk);
  chunks = [];
  for (const p of particles) scene.remove(p);
  particles = [];
  nextChunkZ = 0;
  gameTime = 0;

  // Reset player state
  Object.assign(playerState, {
    x: 0, y: 0, z: 0,
    vx: 0, vy: 0, vz: 0,
    roll: 0, pitch: 0, yaw: 0,
    speed: 0.8,
    energy: 100,
    score: 0,
    multiplier: 1,
    turbo: 0,
    boosting: false,
    nearMissTimer: 0,
    distance: 0,
    maxSpeedReached: 0
  });

  difficulty = 1;
  cameraShakeIntensity = 0;

  if (player) {
    player.position.set(0, 0, 0);
    player.rotation.set(0, 0, 0);
  }

  document.getElementById('game-over').style.display = 'none';
  startEngineSound();
  gameActive = true;
  lastTime = performance.now();
  requestAnimationFrame(gameLoop);
};

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

// ─── INIT ─────────────────────────────────────────────────
initThree();
createPlayer();

</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingcap-qwen3.6-27b","provider":"lmstudio","persona":"basic","sessionId":"c827920f-0f54-4e72-b6a3-e74d8e4f420f","tokensIn":981,"tokensOut":11584,"tokensTotal":12565,"turns":1,"toolCalls":0,"failedToolCalls":0,"timestamp":"2026-07-19T01:39:22.913Z"} -->