← Canyon Run results

Canyon Run

Qwen 3.6 35b a3b · 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 37,760 bytes · SHA-256 779a4da1f08c
<!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: 'Courier New', monospace; }
canvas { display: block; }

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

#score {
  position: absolute; top: 30px; right: 40px;
  color: #fff; font-size: 28px; letter-spacing: 4px;
  text-shadow: 0 0 20px rgba(255,150,50,0.8);
  transition: transform 0.1s;
}
#score.pulse { transform: scale(1.3); }

#multiplier {
  position: absolute; top: 65px; right: 40px;
  color: #ff9933; font-size: 42px; font-weight: bold;
  text-shadow: 0 0 30px rgba(255,150,50,1);
  opacity: 0; transition: opacity 0.3s;
}
#multiplier.visible { opacity: 1; }

#energy-container {
  position: absolute; bottom: 40px; 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%; border-radius: 3px;
  background: linear-gradient(90deg, #ff3366, #ff9933, #ffdd33);
  box-shadow: 0 0 15px rgba(255,150,50,0.6);
  transition: width 0.15s;
}
#energy-label {
  position: absolute; bottom: 52px; left: 50%; transform: translateX(-50%);
  color: rgba(255,255,255,0.5); font-size: 10px; letter-spacing: 6px;
}

#speed-container {
  position: absolute; bottom: 40px; right: 40px;
  width: 120px; height: 80px;
}
#speed-line {
  position: absolute; bottom: 10px; left: 0; right: 0; height: 2px;
  background: rgba(255,255,255,0.15); border-radius: 1px;
}
#speed-fill {
  position: absolute; bottom: 10px; left: 0; height: 2px;
  background: linear-gradient(90deg, #ff6633, #ffdd33);
  border-radius: 1px; box-shadow: 0 0 8px rgba(255,100,50,0.5);
  transition: width 0.1s;
}
#speed-value {
  position: absolute; bottom: 22px; left: 50%; transform: translateX(-50%);
  color: #fff; font-size: 16px; letter-spacing: 2px;
  text-shadow: 0 0 10px rgba(255,150,50,0.5);
}
#speed-unit {
  position: absolute; bottom: 10px; left: 50%; transform: translateX(-50%);
  color: rgba(255,255,255,0.3); font-size: 9px; letter-spacing: 3px;
}

#boost-container {
  position: absolute; bottom: 55px; left: 40px;
  width: 8px; height: 120px; background: rgba(255,255,255,0.05);
  border-radius: 4px; overflow: hidden;
}
#boost-fill {
  position: absolute; bottom: 0; width: 100%;
  background: linear-gradient(0deg, #3366ff, #66ccff);
  border-radius: 4px; box-shadow: 0 0 10px rgba(100,150,255,0.5);
  transition: height 0.15s;
}
#boost-label {
  position: absolute; bottom: 30px; left: 40px;
  color: rgba(100,150,255,0.4); font-size: 9px; letter-spacing: 4px;
  transform: rotate(-90deg); transform-origin: left center;
}

#game-over {
  position: fixed; top: 0; left: 0; width: 100%; height: 100%;
  background: rgba(0,0,0,0.85); display: none; z-index: 100;
  flex-direction: column; align-items: center; justify-content: center;
}
#game-over.active { display: flex; }
#game-over h1 {
  color: #ff6633; font-size: 64px; letter-spacing: 12px;
  text-shadow: 0 0 40px rgba(255,100,50,0.8); margin-bottom: 40px;
}
#game-over .stat {
  color: rgba(255,255,255,0.6); font-size: 16px; letter-spacing: 4px;
  margin: 8px 0;
}
#game-over .stat span { color: #ffdd33; font-size: 24px; }
#restart-btn {
  margin-top: 50px; padding: 15px 50px;
  background: transparent; border: 2px solid #ff6633;
  color: #ff6633; font-family: 'Courier New', monospace;
  font-size: 18px; letter-spacing: 6px; cursor: pointer;
  pointer-events: all; transition: all 0.3s;
}
#restart-btn:hover {
  background: #ff6633; color: #000;
  box-shadow: 0 0 30px rgba(255,100,50,0.5);
}

#start-screen {
  position: fixed; top: 0; left: 0; width: 100%; height: 100%;
  background: rgba(0,0,0,0.9); display: flex;
  flex-direction: column; align-items: center; justify-content: center;
  z-index: 100;
}
#start-screen h1 {
  color: #ff6633; font-size: 72px; letter-spacing: 16px;
  text-shadow: 0 0 60px rgba(255,100,50,0.8); margin-bottom: 20px;
}
#start-screen .subtitle {
  color: rgba(255,255,255,0.4); font-size: 14px; letter-spacing: 8px;
  margin-bottom: 60px;
}
#start-screen .controls {
  color: rgba(255,255,255,0.3); font-size: 12px; letter-spacing: 3px;
  margin: 5px 0;
}
#start-screen .start-prompt {
  color: rgba(255,200,100,0.6); font-size: 16px; letter-spacing: 6px;
  margin-top: 60px; animation: pulse 2s infinite;
}
@keyframes pulse {
  0%, 100% { opacity: 0.4; }
  50% { opacity: 1; }
}
</style>
</head>
<body>

<div id="hud">
  <div id="score">0</div>
  <div id="multiplier">×1</div>

  <div id="energy-label">ENERGY</div>
  <div id="energy-container"><div id="energy-bar"></div></div>

  <div id="speed-container">
    <div id="speed-value">0</div>
    <div id="speed-line"></div>
    <div id="speed-fill"></div>
    <div id="speed-unit">KM/H</div>
  </div>

  <div id="boost-label">TURBO</div>
  <div id="boost-container"><div id="boost-fill"></div></div>
</div>

<div id="game-over">
  <h1>CRASHED</h1>
  <div class="stat">SCORE <span id="final-score">0</span></div>
  <div class="stat">MAX SPEED <span id="max-speed">0</span> KM/H</div>
  <div class="stat">NEAR MISSES <span id="near-misses">0</span></div>
  <button id="restart-btn">RESTART</button>
</div>

<div id="start-screen">
  <h1>CANYON</h1>
  <div class="subtitle">PROCEDURAL RACER</div>
  <div class="controls">← → or A/D — STEER</div>
  <div class="controls">↑ ↓ or W/S — PITCH</div>
  <div class="controls">SHIFT — BOOST</div>
  <div class="start-prompt">PRESS ENTER TO START</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 { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';

// ─── CONFIG ───
const CFG = {
  chunkSize: 120,
  chunkCount: 8,
  baseSpeed: 80,
  maxSpeed: 250,
  boostSpeed: 400,
  playerZ: 0,
  wallWidth: 180,
  wallMinHeight: 30,
  wallMaxHeight: 120,
  energyMax: 100,
  energyDecay: 3.5,
  energyRecharge: 18,
  multiplierDecay: 0.3,
  boostMax: 100,
  boostDrain: 30,
  boostRecharge: 5,
  cameraShakeIntensity: 0.08,
  nearMissDist: 12,
  orbSpawnChance: 0.08,
  obstacleSpawnChance: 0.04,
  fogNear: 60,
  fogFar: 400,
};

// ─── STATE ───
let state = {
  running: false,
  score: 0,
  multiplier: 1,
  energy: CFG.energyMax,
  boost: 0,
  speed: CFG.baseSpeed,
  maxSpeedReached: 0,
  nearMisses: 0,
  playerPos: new THREE.Vector3(0, 10, 0),
  playerVel: new THREE.Vector3(0, 0, 0),
  playerRot: new THREE.Euler(0, 0, 0),
  targetRot: new THREE.Euler(0, 0, 0),
  playerBank: 0,
  targetBank: 0,
  lastTime: 0,
  chunks: new Map(),
  orbs: [],
  debrisParticles: [],
  trailParticles: [],
  audioCtx: null,
  engineOsc: null,
  engineGain: null,
  gameStartTime: 0,
};

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

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

function playNearMiss() {
  if (!state.audioCtx) return;
  const ctx = state.audioCtx;
  const osc = ctx.createOscillator();
  const gain = ctx.createGain();
  osc.type = 'sawtooth';
  osc.frequency.setValueAtTime(200, ctx.currentTime);
  osc.frequency.exponentialRampToValueAtTime(80, ctx.currentTime + 0.2);
  gain.gain.setValueAtTime(0.08, ctx.currentTime);
  gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.25);
  osc.connect(gain);
  gain.connect(ctx.destination);
  osc.start();
  osc.stop(ctx.currentTime + 0.25);
}

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

function updateEngineSound() {
  if (!state.engineOsc || !state.audioCtx) return;
  const speedRatio = state.speed / CFG.boostSpeed;
  state.engineOsc.frequency.setTargetAtTime(
    55 + speedRatio * 120, state.audioCtx.currentTime, 0.1
  );
  state.engineGain.gain.setTargetAtTime(
    0.03 + speedRatio * 0.06, state.audioCtx.currentTime, 0.1
  );
}

// ─── THREE.JS SETUP ───
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.5, 500);
const 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.1;
document.body.appendChild(renderer.domElement);

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

const bloomPass = new UnrealBloomPass(
  new THREE.Vector2(window.innerWidth, window.innerHeight),
  1.2, 0.4, 0.85
);
composer.addPass(bloomPass);

// Custom motion blur shader
const MotionBlurShader = {
  uniforms: {
    tDiffuse: { value: null },
    tPrevious: { value: null },
    intensity: { value: 0.3 },
  },
  vertexShader: `varying vec2 vUv; void main(){ vUv=uv; gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.0); }`,
  fragmentShader: `
    uniform sampler2D tDiffuse;
    uniform sampler2D tPrevious;
    uniform float intensity;
    varying vec2 vUv;
    void main(){
      vec4 current = texture2D(tDiffuse, vUv);
      vec4 previous = texture2D(tPrevious, vUv);
      gl_FragColor = mix(current, previous, intensity);
    }
  `,
};

const motionBlurPass = new ShaderPass(MotionBlurShader);
motionBlurPass.uniforms.intensity.value = 0.15;
composer.addPass(motionBlurPass);

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

const ambientLight = new THREE.AmbientLight(0x443355, 0.6);
scene.add(ambientLight);

const hemiLight = new THREE.HemisphereLight(0xff9944, 0x332244, 0.8);
scene.add(hemiLight);

// Rim light
const rimLight = new THREE.DirectionalLight(0xff4422, 1.0);
rimLight.position.set(-30, 20, -50);
scene.add(rimLight);

// ─── FOG ───
scene.fog = new THREE.Fog(0x1a0a2e, CFG.fogNear, CFG.fogFar);

// ─── MATERIALS ───
const wallMat = new THREE.MeshStandardMaterial({
  color: 0x2a1a3e,
  roughness: 0.85,
  metalness: 0.1,
  flatShading: true,
});

const wallMat2 = new THREE.MeshStandardMaterial({
  color: 0x3a2255,
  roughness: 0.8,
  metalness: 0.15,
  flatShading: true,
});

const floorMat = new THREE.MeshStandardMaterial({
  color: 0x1a0a2e,
  roughness: 0.9,
  metalness: 0.05,
  flatShading: true,
});

const ceilingMat = new THREE.MeshStandardMaterial({
  color: 0x2a1540,
  roughness: 0.85,
  metalness: 0.1,
  flatShading: true,
});

const orbMat = new THREE.MeshStandardMaterial({
  color: 0xffdd33,
  emissive: 0xffaa00,
  emissiveIntensity: 2.0,
  roughness: 0.2,
  metalness: 0.8,
});

const orbMat2 = new THREE.MeshStandardMaterial({
  color: 0x33ffaa,
  emissive: 0x00ff88,
  emissiveIntensity: 2.0,
  roughness: 0.2,
  metalness: 0.8,
});

const playerMat = new THREE.MeshStandardMaterial({
  color: 0xccccdd,
  emissive: 0x2244aa,
  emissiveIntensity: 0.3,
  roughness: 0.3,
  metalness: 0.7,
});

const playerGlowMat = new THREE.MeshStandardMaterial({
  color: 0x3366ff,
  emissive: 0x3366ff,
  emissiveIntensity: 3.0,
  roughness: 0.1,
  metalness: 0.9,
});

const obstacleMat = new THREE.MeshStandardMaterial({
  color: 0x553366,
  roughness: 0.7,
  metalness: 0.2,
  flatShading: true,
});

const obstacleMat2 = new THREE.MeshStandardMaterial({
  color: 0x664477,
  roughness: 0.65,
  metalness: 0.25,
  flatShading: true,
});

// ─── PLAYER CRAFT ───
function createPlayerCraft() {
  const group = new THREE.Group();

  // Main body
  const bodyGeo = new THREE.ConeGeometry(0.8, 4, 4);
  bodyGeo.rotateX(Math.PI / 2);
  const body = new THREE.Mesh(bodyGeo, playerMat);
  body.castShadow = true;
  group.add(body);

  // Cockpit
  const cockpitGeo = new THREE.BoxGeometry(0.6, 0.5, 1.2);
  cockpitGeo.translate(0, 0.3, -0.3);
  const cockpit = new THREE.Mesh(cockpitGeo, playerMat);
  cockpit.castShadow = true;
  group.add(cockpit);

  // Wings
  const wingGeo = new THREE.BoxGeometry(5, 0.08, 1.5);
  const wing = new THREE.Mesh(wingGeo, playerMat);
  wing.position.set(0, -0.1, 0.5);
  wing.castShadow = true;
  group.add(wing);

  // Wing tips (glowing)
  const tipGeo = new THREE.ConeGeometry(0.15, 0.6, 4);
  tipGeo.rotateZ(Math.PI / 2);
  const tipL = new THREE.Mesh(tipGeo, playerGlowMat);
  tipL.position.set(-2.6, -0.1, 0.5);
  group.add(tipL);
  const tipR = new THREE.Mesh(tipGeo, playerGlowMat);
  tipR.position.set(2.6, -0.1, 0.5);
  group.add(tipR);

  // Tail fins
  const finGeo = new THREE.ConeGeometry(0.5, 1.2, 3);
  finGeo.rotateX(-Math.PI / 2);
  const fin1 = new THREE.Mesh(finGeo, playerMat);
  fin1.position.set(0.5, 0.3, 1.8);
  fin1.rotation.z = Math.PI / 6;
  group.add(fin1);
  const fin2 = new THREE.Mesh(finGeo, playerMat);
  fin2.position.set(-0.5, 0.3, 1.8);
  fin2.rotation.z = -Math.PI / 6;
  group.add(fin2);

  // Engine glow
  const engineGeo = new THREE.CylinderGeometry(0.3, 0.6, 0.8, 6);
  engineGeo.rotateX(Math.PI / 2);
  const engine = new THREE.Mesh(engineGeo, playerGlowMat);
  engine.position.set(0, 0, 2.2);
  group.add(engine);

  group.scale.set(1.2, 1.2, 1.2);
  return group;
}

const player = createPlayerCraft();
scene.add(player);

// ─── PLAYER LIGHT ───
const playerLight = new THREE.PointLight(0x3366ff, 3, 30);
playerLight.position.set(0, 0, 2);
player.add(playerLight);

// ─── PROCEDURAL CANYON GENERATION ───
function seededRandom(seed) {
  let s = seed;
  return function() {
    s = (s * 9301 + 49297) % 233280;
    return s / 233280;
  };
}

function generateCanyonSegment(zStart, zEnd, seed) {
  const rng = seededRandom(seed);
  const group = new THREE.Group();
  const segments = 12;
  const dz = (zEnd - zStart) / segments;

  // Canyon width varies
  let width = CFG.wallWidth * (0.5 + rng() * 0.8);
  let leftX = -width / 2;
  let rightX = width / 2;
  let height = CFG.wallMinHeight + rng() * (CFG.wallMaxHeight - CFG.wallMinHeight);

  for (let i = 0; i < segments; i++) {
    const z0 = zStart + i * dz;
    const z1 = z0 + dz;

    // Vary canyon shape
    width = CFG.wallWidth * (0.4 + rng() * 0.9);
    leftX = -width / 2 + (rng() - 0.5) * 20;
    rightX = width / 2 + (rng() - 0.5) * 20;
    height = CFG.wallMinHeight + rng() * (CFG.wallMaxHeight - CFG.wallMinHeight);

    const h0 = height + (rng() - 0.5) * 30;
    const h1 = height + (rng() - 0.5) * 30;

    // Left wall
    const lwGeo = new THREE.BufferGeometry();
    const lwVerts = new Float32Array([
      leftX - 10, -15, z0,  leftX, h0, z0,  leftX, -h1, z1,
      leftX - 10, -15, z0,  leftX, -h1, z1,  leftX - 10, -15, z1,
    ]);
    lwGeo.setAttribute('position', new THREE.BufferAttribute(lwVerts, 3));
    lwGeo.computeVertexNormals();
    const lw = new THREE.Mesh(lwGeo, i % 2 === 0 ? wallMat : wallMat2);
    lw.castShadow = true;
    lw.receiveShadow = true;
    group.add(lw);

    // Right wall
    const rwGeo = new THREE.BufferGeometry();
    const rwVerts = new Float32Array([
      rightX + 10, -15, z0,  rightX, -h1, z1,  rightX, h0, z1,
      rightX + 10, -15, z0,  rightX, h0, z0,  rightX + 10, -15, z1,
    ]);
    rwGeo.setAttribute('position', new THREE.BufferAttribute(rwVerts, 3));
    rwGeo.computeVertexNormals();
    const rw = new THREE.Mesh(rwGeo, i % 2 === 0 ? wallMat : wallMat2);
    rw.castShadow = true;
    rw.receiveShadow = true;
    group.add(rw);

    // Floor
    const fGeo = new THREE.BufferGeometry();
    const fVerts = new Float32Array([
      leftX, -15, z0,  rightX, -15, z0,  rightX, -15, z1,
      leftX, -15, z0,  rightX, -15, z1,  leftX, -15, z1,
    ]);
    fGeo.setAttribute('position', new THREE.BufferAttribute(fVerts, 3));
    fGeo.computeVertexNormals();
    const floor = new THREE.Mesh(fGeo, floorMat);
    floor.receiveShadow = true;
    group.add(floor);

    // Ceiling (sometimes)
    if (rng() > 0.3) {
      const ceilH = h0 * 0.3 + rng() * 20;
      const ceilGeo = new THREE.BufferGeometry();
      const cVerts = new Float32Array([
        leftX, ceilH, z0,  rightX, ceilH, z0,  rightX, ceilH, z1,
        leftX, ceilH, z0,  rightX, ceilH, z1,  leftX, ceilH, z1,
      ]);
      ceilGeo.setAttribute('position', new THREE.BufferAttribute(cVerts, 3));
      ceilGeo.computeVertexNormals();
      const ceil = new THREE.Mesh(ceilGeo, ceilingMat);
      ceil.receiveShadow = true;
      group.add(ceil);
    }

    // Store wall positions for collision
    group.userData.walls = group.userData.walls || [];
    group.userData.walls.push({
      z0, z1,
      leftX: leftX - 5,
      rightX: rightX + 5,
      top: Math.max(h0, h1),
      bottom: -15,
    });
  }

  // Add obstacles
  if (rng() > 0.3) {
    const oz = zStart + dz * rng();
    const oType = rng();
    if (oType < 0.5) {
      // Monolith
      const monGeo = new THREE.BoxGeometry(
        3 + rng() * 5,
        15 + rng() * 30,
        3 + rng() * 5
      );
      const mon = new THREE.Mesh(monGeo, rng() > 0.5 ? obstacleMat : obstacleMat2);
      mon.position.set(
        (rng() - 0.5) * width * 0.6,
        -15 + (15 + rng() * 30) / 2,
        oz
      );
      mon.castShadow = true;
      mon.receiveShadow = true;
      mon.userData.boundingSphere = {
        center: mon.position.clone(),
        radius: Math.max(mon.geometry.parameters.width, mon.geometry.parameters.height, mon.geometry.parameters.depth) / 2 * 1.2,
      };
      group.add(mon);
    } else {
      // Pillar cluster
      for (let p = 0; p < 2 + Math.floor(rng() * 3); p++) {
        const pGeo = new THREE.CylinderGeometry(1.5 + rng() * 2, 2 + rng() * 3, 20 + rng() * 25, 5);
        const pillar = new THREE.Mesh(pGeo, obstacleMat2);
        pillar.position.set(
          (rng() - 0.5) * width * 0.5,
          -15 + (20 + rng() * 25) / 2,
          oz + (rng() - 0.5) * 10
        );
        pillar.castShadow = true;
        pillar.userData.boundingSphere = {
          center: pillar.position.clone(),
          radius: 3 + rng() * 2,
        };
        group.add(pillar);
      }
    }
  }

  // Add energy orbs
  if (rng() > 0.5) {
    const orbZ = zStart + dz * rng();
    const orbX = (rng() - 0.5) * width * 0.5;
    const orbY = 5 + rng() * 25;
    const orbGeo = new THREE.IcosahedronGeometry(1.2, 0);
    const orb = new THREE.Mesh(orbGeo, rng() > 0.5 ? orbMat : orbMat2);
    orb.position.set(orbX, orbY, orbZ);
    orb.userData.boundingSphere = { center: orb.position.clone(), radius: 1.5 };
    orb.userData.value = rng() > 0.7 ? 25 : 15;
    group.add(orb);
    state.orbs.push(orb);
  }

  return group;
}

// ─── PARTICLE SYSTEMS ───
function createDebrisParticle() {
  const geo = new THREE.BufferGeometry();
  const size = 0.1 + Math.random() * 0.3;
  const positions = new Float32Array([0, 0, 0]);
  geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
  const mat = new THREE.PointsMaterial({
    color: new THREE.Color().setHSL(0.08 + Math.random() * 0.12, 0.8, 0.6),
    size,
    transparent: true,
    opacity: 0.6,
    depthWrite: false,
  });
  const points = new THREE.Points(geo, mat);
  return points;
}

function initDebris() {
  for (let i = 0; i < 80; i++) {
    const p = createDebrisParticle();
    resetDebrisParticle(p);
    p.userData.alive = true;
    scene.add(p);
    state.debrisParticles.push(p);
  }
}

function resetDebrisParticle(p) {
  const spread = 40;
  p.position.set(
    (Math.random() - 0.5) * spread,
    Math.random() * 40 - 10,
    state.playerPos.z + 30 + Math.random() * 50
  );
  p.userData.speed = 0.5 + Math.random() * 1.5;
}

function updateDebris(dt) {
  for (const p of state.debrisParticles) {
    p.position.z -= state.speed * dt;
    p.position.x += (Math.random() - 0.5) * 0.3;
    if (p.position.z < state.playerPos.z - 20) {
      resetDebrisParticle(p);
    }
  }
}

// Trail particles
function spawnTrailParticle() {
  const geo = new THREE.BufferGeometry();
  const size = 0.05 + Math.random() * 0.15;
  const positions = new Float32Array([0, 0, 0]);
  geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
  const mat = new THREE.PointsMaterial({
    color: new THREE.Color().setHSL(0.6 + Math.random() * 0.1, 1, 0.7),
    size,
    transparent: true,
    opacity: 0.8,
    depthWrite: false,
    blending: THREE.AdditiveBlending,
  });
  const points = new THREE.Points(geo, mat);
  points.position.set(
    player.position.x + (Math.random() - 0.5) * 0.5,
    player.position.y + (Math.random() - 0.5) * 0.3,
    player.position.z + 2.5
  );
  points.userData.life = 0.5 + Math.random() * 0.5;
  points.userData.maxLife = points.userData.life;
  scene.add(points);
  state.trailParticles.push(points);
}

function updateTrailParticles(dt) {
  for (let i = state.trailParticles.length - 1; i >= 0; i--) {
    const p = state.trailParticles[i];
    p.userData.life -= dt;
    p.position.z -= state.speed * dt * 0.3;
    p.material.opacity = (p.userData.life / p.userData.maxLife) * 0.8;
    if (p.userData.life <= 0) {
      scene.remove(p);
      state.trailParticles.splice(i, 1);
    }
  }
}

// ─── CHUNK MANAGEMENT ───
function generateChunk(zCenter) {
  const key = Math.round(zCenter / CFG.chunkSize);
  if (state.chunks.has(key)) return;

  const zStart = zCenter - CFG.chunkSize / 2;
  const zEnd = zCenter + CFG.chunkSize / 2;
  const chunk = generateCanyonSegment(zStart, zEnd, key * 7919 + 104729);
  chunk.userData.key = key;
  chunk.userData.zCenter = zCenter;
  scene.add(chunk);
  state.chunks.set(key, chunk);
}

function removeChunk(key) {
  const chunk = state.chunks.get(key);
  if (!chunk) return;
  scene.remove(chunk);
  // Clean up orbs in this chunk
  state.orbs = state.orbs.filter(o => {
    if (o.parent === chunk) {
      scene.remove(o);
      o.geometry.dispose();
      return false;
    }
    return true;
  });
  state.chunks.delete(key);
}

function updateChunks() {
  const playerChunk = Math.round(state.playerPos.z / CFG.chunkSize);
  // Generate ahead
  for (let i = playerChunk + 1; i <= playerChunk + CFG.chunkCount; i++) {
    generateChunk(i * CFG.chunkSize);
  }
  // Remove behind
  for (const [key, chunk] of state.chunks) {
    if (key < playerChunk - 2) {
      removeChunk(key);
    }
  }
}

// ─── COLLISION DETECTION ───
function checkCollisions() {
  const playerRadius = 2.0;
  const playerSphere = {
    center: state.playerPos.clone(),
    radius: playerRadius,
  };

  // Check walls
  let closestWallDist = Infinity;
  for (const [key, chunk] of state.chunks) {
    const walls = chunk.userData.walls;
    if (!walls) continue;
    for (const wall of walls) {
      // Check if player is near this wall segment
      if (state.playerPos.z < wall.z0 - 10 || state.playerPos.z > wall.z1 + 10) continue;

      // Distance to left wall
      const distL = state.playerPos.x - wall.leftX;
      const distR = wall.rightX - state.playerPos.x;

      closestWallDist = Math.min(closestWallDist, Math.min(distL, distR));

      // Collision check
      if (distL < playerRadius || distR < playerRadius) return true;

      // Near-miss detection
      if (distL < CFG.nearMissDist && distL > playerRadius) {
        recordNearMiss(distL);
      }
      if (distR < CFG.nearMissDist && distR > playerRadius) {
        recordNearMiss(distR);
      }
    }
  }

  // Check obstacles
  for (const [key, chunk] of state.chunks) {
    chunk.traverse(child => {
      if (child.userData.boundingSphere) {
        const bs = child.userData.boundingSphere;
        const dist = playerSphere.center.distanceTo(bs.center);
        if (dist < bs.radius + playerRadius) {
          // Check if close enough to be a near miss
          const clearance = dist - bs.radius;
          if (clearance < CFG.nearMissDist && clearance > playerRadius) {
            recordNearMiss(clearance);
          }
          return true; // collision
        }
      }
      return false;
    });
  }

  return false;
}

function recordNearMiss(dist) {
  const intensity = 1 - (dist / CFG.nearMissDist);
  if (intensity > 0.5) {
    state.multiplier = Math.min(state.multiplier + 0.1 * intensity, 10);
    state.nearMisses++;
    state.boost = Math.min(state.boost + 3 * intensity, CFG.boostMax);
    document.getElementById('multiplier').textContent = `×${state.multiplier.toFixed(1)}`;
    document.getElementById('multiplier').classList.add('visible');
    playNearMiss();

    // Pulse score display
    const scoreEl = document.getElementById('score');
    scoreEl.classList.add('pulse');
    setTimeout(() => scoreEl.classList.remove('pulse'), 100);
  }
}

// ─── ORB COLLECTION ───
function checkOrbCollection() {
  for (let i = state.orbs.length - 1; i >= 0; i--) {
    const orb = state.orbs[i];
    const dist = state.playerPos.distanceTo(orb.position);
    if (dist < 4) {
      // Collected!
      state.energy = Math.min(state.energy + orb.userData.value, CFG.energyMax);
      state.score += Math.floor(50 * state.multiplier);
      state.boost = Math.min(state.boost + 10, CFG.boostMax);
      playPing();

      // Remove orb
      scene.remove(orb);
      orb.geometry.dispose();
      state.orbs.splice(i, 1);
    }
  }
}

// ─── INPUT ───
const keys = {};
window.addEventListener('keydown', e => {
  keys[e.code] = true;
  if (e.code === 'Enter' && !state.running) {
    startGame();
  }
  if (e.code === 'Enter') {
    e.preventDefault();
  }
});
window.addEventListener('keyup', e => { keys[e.code] = false; });

// ─── CAMERA ───
const cameraOffset = new THREE.Vector3(0, 4, -12);
const cameraLookOffset = new THREE.Vector3(0, 2, 20);
let cameraShake = 0;
let targetFOV = 70;

function updateCamera(dt) {
  // FOV based on speed
  const speedRatio = state.speed / CFG.boostSpeed;
  targetFOV = 70 + speedRatio * 25;
  camera.fov += (targetFOV - camera.fov) * dt * 3;
  camera.updateProjectionMatrix();

  // Camera position
  const targetPos = state.playerPos.clone().add(cameraOffset);
  targetPos.x += state.playerVel.x * 2;
  targetPos.y += state.playerVel.y * 1;

  // Smooth follow
  camera.position.lerp(targetPos, dt * 5);

  // Look ahead
  const lookTarget = state.playerPos.clone().add(cameraLookOffset);
  lookTarget.x += state.playerVel.x * 3;
  camera.lookAt(lookTarget);

  // Camera shake
  if (cameraShake > 0.001) {
    camera.position.x += (Math.random() - 0.5) * cameraShake;
    camera.position.y += (Math.random() - 0.5) * cameraShake * 0.5;
    cameraShake *= 0.92;
  }
}

// ─── HUD UPDATE ───
function updateHUD() {
  const speedKmh = Math.floor(state.speed * 2.5);
  document.getElementById('score').textContent = Math.floor(state.score).toLocaleString();
  document.getElementById('energy-bar').style.width = `${(state.energy / CFG.energyMax) * 100}%`;
  document.getElementById('speed-value').textContent = speedKmh;
  document.getElementById('speed-fill').style.width = `${(state.speed / CFG.boostSpeed) * 100}%`;
  document.getElementById('boost-fill').style.height = `${(state.boost / CFG.boostMax) * 100}%`;

  // Energy bar color
  const energyPct = state.energy / CFG.energyMax;
  const energyBar = document.getElementById('energy-bar');
  if (energyPct < 0.2) {
    energyBar.style.background = 'linear-gradient(90deg, #ff0033, #ff3333)';
    energyBar.style.boxShadow = '0 0 20px rgba(255,0,50,0.8)';
  } else if (energyPct < 0.4) {
    energyBar.style.background = 'linear-gradient(90deg, #ff3333, #ff6633)';
    energyBar.style.boxShadow = '0 0 15px rgba(255,100,50,0.6)';
  } else {
    energyBar.style.background = 'linear-gradient(90deg, #ff3366, #ff9933, #ffdd33)';
    energyBar.style.boxShadow = '0 0 15px rgba(255,150,50,0.6)';
  }

  // Boost glow
  const boostFill = document.getElementById('boost-fill');
  if (state.boost > 50) {
    boostFill.style.boxShadow = '0 0 20px rgba(100,150,255,0.8)';
  } else {
    boostFill.style.boxShadow = '0 0 10px rgba(100,150,255,0.5)';
  }
}

// ─── GAME LOOP ───
function update(dt) {
  if (!state.running) return;

  const now = performance.now();
  const elapsed = (now - state.gameStartTime) / 1000;

  // Difficulty scaling
  const difficulty = 1 + Math.min(elapsed / 120, 2); // max 3x over 2 minutes
  const maxSpeed = CFG.maxSpeed * Math.min(1 + elapsed / 180, 2.5);

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

  // Speed management
  let targetSpeed = CFG.baseSpeed * difficulty;
  if (boosting && state.boost > 0) {
    targetSpeed = CFG.boostSpeed * difficulty;
    state.boost -= CFG.boostDrain * dt;
    state.boost = Math.max(state.boost, 0);
    cameraShake = Math.max(cameraShake, 0.03);
  }

  // Speed up gradually
  state.speed += (targetSpeed - state.speed) * dt * 0.5;
  state.speed = Math.max(CFG.baseSpeed, Math.min(state.speed, maxSpeed));

  // Track max speed
  const currentKmh = state.speed * 2.5;
  if (currentKmh > state.maxSpeedReached) state.maxSpeedReached = currentKmh;

  // Speed ratio for visual effects
  const speedRatio = state.speed / CFG.boostSpeed;

  // Player physics
  const steerForce = 40;
  const pitchForce = 25;
  const damping = 3.5;

  state.playerVel.x += (steerInput * steerForce - state.playerVel.x * damping) * dt;
  state.playerVel.y += (pitchInput * pitchForce - state.playerVel.y * damping) * dt;

  // Bank (roll) proportional to lateral velocity
  state.targetBank = -state.playerVel.x * 0.025;
  state.playerBank += (state.targetBank - state.playerBank) * dt * 8;

  // Pitch visual
  state.targetRot.x = pitchInput * 0.15;
  state.playerRot.x += (state.targetRot.x - state.playerRot.x) * dt * 6;

  // Update position
  state.playerPos.x += state.playerVel.x * dt;
  state.playerPos.y += state.playerVel.y * dt;
  state.playerPos.z += state.speed * dt;

  // Constrain player
  const currentChunk = state.chunks.get(Math.round(state.playerPos.z / CFG.chunkSize));
  let minX = -80, maxX = 80;
  if (currentChunk && currentChunk.userData.walls) {
    for (const wall of currentChunk.userData.walls) {
      if (Math.abs(state.playerPos.z - wall.z0) < 20) {
        minX = Math.max(minX, wall.leftX + 3);
        maxX = Math.min(maxX, wall.rightX - 3);
      }
    }
  }
  state.playerPos.x = Math.max(minX, Math.min(maxX, state.playerPos.x));
  state.playerPos.y = Math.max(-10, Math.min(60, state.playerPos.y));

  // Energy decay
  state.energy -= CFG.energyDecay * dt;
  if (state.energy <= 0) {
    gameOver();
    return;
  }

  // Multiplier decay
  state.multiplier = Math.max(1, state.multiplier - CFG.multiplierDecay * dt);
  if (state.multiplier <= 1.1) {
    document.getElementById('multiplier').classList.remove('visible');
  }

  // Boost recharge (slow)
  if (!boosting) {
    state.boost = Math.min(state.boost + CFG.boostRecharge * dt, CFG.boostMax);
  }

  // Score
  state.score += state.speed * dt * state.multiplier * 0.5;

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

  // Orb collection
  checkOrbCollection();

  // Update chunks
  updateChunks();

  // Camera
  updateCamera(dt);

  // Update player mesh
  player.position.copy(state.playerPos);
  player.rotation.z = state.playerBank;
  player.rotation.x = state.playerRot.x;

  // Engine glow pulse
  const glowPulse = 2.0 + Math.sin(now * 0.008) * 0.5 + (boosting ? 2.0 : 0);
  playerGlowMat.emissiveIntensity = glowPulse;

  // Animate orbs
  for (const orb of state.orbs) {
    orb.rotation.y += dt * 2;
    orb.rotation.x += dt * 1.5;
    orb.position.y += Math.sin(now * 0.003 + orb.position.x) * 0.02;
  }

  // Debris
  updateDebris(dt);

  // Trail particles
  if (Math.random() < 0.6 + speedRatio * 0.4) {
    spawnTrailParticle();
  }
  updateTrailParticles(dt);

  // Camera shake from near misses
  if (closestWallDist < 8) {
    cameraShake = Math.max(cameraShake, (8 - closestWallDist) * 0.015);
  }

  // Engine sound
  updateEngineSound();

  // HUD
  updateHUD();

  // Update bloom based on speed
  bloomPass.strength = 1.0 + (state.speed / CFG.boostSpeed) * 0.8;
}

let closestWallDist = Infinity;

// ─── GAME STATE MANAGEMENT ───
function startGame() {
  // Reset state
  state.running = true;
  state.score = 0;
  state.multiplier = 1;
  state.energy = CFG.energyMax;
  state.boost = 0;
  state.speed = CFG.baseSpeed;
  state.maxSpeedReached = 0;
  state.nearMisses = 0;
  state.playerPos.set(0, 10, 0);
  state.playerVel.set(0, 0, 0);
  state.playerRot.set(0, 0, 0);
  state.playerBank = 0;
  state.trailParticles.forEach(p => scene.remove(p));
  state.trailParticles = [];
  state.gameStartTime = performance.now();

  // Clear old chunks
  for (const [key, chunk] of state.chunks) {
    scene.remove(chunk);
  }
  state.chunks.clear();
  for (const orb of state.orbs) {
    scene.remove(orb);
  }
  state.orbs = [];

  // Generate initial chunks
  for (let i = -2; i < CFG.chunkCount; i++) {
    generateChunk(i * CFG.chunkSize);
  }

  // Reset camera
  camera.position.set(0, 14, -12);
  camera.fov = 70;
  camera.updateProjectionMatrix();
  cameraShake = 0;

  // UI
  document.getElementById('game-over').classList.remove('active');
  document.getElementById('start-screen').style.display = 'none';
  document.getElementById('multiplier').classList.remove('visible');

  // Audio
  initAudio();
}

function gameOver() {
  state.running = false;
  playCrash();
  if (state.engineOsc) {
    state.engineOsc.stop();
    state.engineOsc = null;
  }

  document.getElementById('final-score').textContent = Math.floor(state.score).toLocaleString();
  document.getElementById('max-speed').textContent = Math.floor(state.maxSpeedReached);
  document.getElementById('near-misses').textContent = state.nearMisses;
  document.getElementById('game-over').classList.add('active');
}

// ─── RENDER LOOP ───
function animate(time) {
  requestAnimationFrame(animate);

  const dt = Math.min((time - state.lastTime) / 1000, 0.05);
  state.lastTime = time;

  if (state.running) {
    update(dt);
  }

  // Render
  composer.render();
}

// ─── INIT ───
// Generate initial terrain for start screen
for (let i = -2; i < 6; i++) {
  generateChunk(i * CFG.chunkSize);
}

// Animate orbs on start screen
function animateOrbs(time) {
  const now = time || performance.now();
  for (const orb of state.orbs) {
    orb.rotation.y += 0.01;
    orb.rotation.x += 0.005;
  }
  requestAnimationFrame(animateOrbs);
}
animateOrbs();

// Restart button
document.getElementById('restart-btn').addEventListener('click', startGame);

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

// Start render loop
animate(0);

</script>
</body>
</html>
<!-- agent-meta {"model":"qwen/qwen3.6-35b-a3b","provider":"lmstudio","persona":"full","sessionId":"7db1a5b9-3494-4234-9fd0-79a269aff94c","tokensIn":1218797,"tokensOut":19078,"tokensTotal":1237875,"turns":33,"toolCalls":32,"failedToolCalls":0,"timestamp":"2026-07-21T01:09:34.062Z"} -->