← Canyon Run results

Canyon Run

ling 3.0 tiny:free · full

Initial view of Canyon Run

Browser check: Artifact runtime error. 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 44,255 bytes · SHA-256 35a138a1d1bb
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>High-Speed Procedural Canyon Racer</title>
<style>
  * { margin: 0; padding: 0; box-sizing: border-box; }
  body { overflow: hidden; background: #000; font-family: 'Segoe UI', sans-serif; }
  canvas { display: block; }

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

  .hud-top {
    position: absolute; top: 16px; left: 50%; transform: translateX(-50%);
    display: flex; gap: 30px; align-items: flex-end;
  }

  .hud-item {
    background: rgba(0,0,0,0.5); border: 1px solid rgba(255,180,60,0.3);
    border-radius: 8px; padding: 8px 18px; text-align: center;
    min-width: 90px;
  }

  .hud-label { font-size: 10px; color: rgba(255,200,100,0.8); text-transform: uppercase; letter-spacing: 1px; }
  .hud-value { font-size: 22px; font-weight: 700; color: #FFD700; font-variant-numeric: tabular-nums; }
  .hud-value.boost { color: #00E5FF; text-shadow: 0 0 10px rgba(0,229,255,0.6); }
  .hud-value.nearmiss { color: #FF6B35; text-shadow: 0 0 10px rgba(255,107,53,0.6); }

  .speedometer-wrap {
    position: absolute; top: 12px; right: 24px;
    width: 130px; height: 130px;
  }

  .speedometer-wrap canvas { width: 100%; height: 100%; }

  .energy-bar-wrap {
    position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%);
    width: 200px; height: 12px;
    background: rgba(0,0,0,0.6); border-radius: 6px;
    border: 1px solid rgba(255,100,50,0.3);
  }
  .energy-bar-fill {
    height: 100%; border-radius: 6px;
    background: linear-gradient(90deg, #FF6B35, #FFD700, #00E5FF);
    transition: width 0.1s;
  }

  .turbo-indicator {
    position: absolute; top: 50%; left: 50%;
    transform: translate(-50%, -50%);
    font-size: 14px; color: rgba(0,229,255,0.9);
    font-weight: 700; letter-spacing: 2px;
    text-shadow: 0 0 10px rgba(0,229,255,0.8);
    opacity: 0; transition: opacity 0.2s;
    pointer-events: none;
  }
  .turbo-indicator.active { opacity: 1; }

  .multiplier-display {
    position: absolute; top: 60px; left: 50%; transform: translateX(-50%);
    font-size: 48px; font-weight: 900; color: #FFD700;
    text-shadow: 0 0 20px rgba(255,215,0,0.8);
    opacity: 0; transition: all 0.15s;
    pointer-events: none;
  }
  .multiplier-display.active { opacity: 1; }

  .hud-bottom {
    position: absolute; bottom: 10px; left: 16px; right: 16px;
    display: flex; justify-content: space-between; align-items: flex-end;
  }

  .btn {
    pointer-events: all; cursor: pointer;
    background: rgba(255,180,60,0.2); border: 1px solid rgba(255,180,60,0.5);
    color: #FFD700; padding: 8px 18px; border-radius: 6px;
    font-size: 13px; font-weight: 600; letter-spacing: 1px;
    transition: all 0.15s;
  }
  .btn:hover { background: rgba(255,180,60,0.4); border-color: #FFD700; }
  .btn:active { transform: scale(0.95); }
  .btn:focus-visible { outline: 2px solid #FFD700; outline-offset: 2px; }

  #gameOverScreen {
    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;
  }
  #gameOverScreen.visible { display: flex; }

  .go-title {
    font-size: 56px; font-weight: 900; color: #FFD700;
    text-shadow: 0 0 30px rgba(255,215,0,0.6);
    margin-bottom: 8px; letter-spacing: 3px;
  }
  .go-subtitle {
    font-size: 18px; color: rgba(255,200,100,0.7); margin-bottom: 30px;
  }
  .go-stats {
    display: flex; gap: 40px; margin-bottom: 30px;
  }
  .go-stat {
    text-align: center;
  }
  .go-stat-label { font-size: 11px; color: rgba(255,200,100,0.6); text-transform: uppercase; letter-spacing: 2px; }
  .go-stat-value { font-size: 28px; font-weight: 700; color: #FFD700; margin-top: 4px; }
  .go-stat-value.high { color: #FF6B35; }

  .restart-btn {
    pointer-events: all; cursor: pointer;
    background: rgba(0,229,255,0.15); border: 1px solid rgba(0,229,255,0.4);
    color: #00E5FF; padding: 12px 36px; border-radius: 6px;
    font-size: 15px; font-weight: 700; letter-spacing: 2px;
    transition: all 0.2s;
  }
  .restart-btn:hover { background: rgba(0,229,255,0.3); border-color: #00E5FF; }
  .restart-btn:focus-visible { outline: 2px solid #00E5FF; outline-offset: 2px; }

  #pauseOverlay {
    position: fixed; top: 0; left: 0; width: 100%; height: 100%;
    background: rgba(0,0,0,0.6); display: none;
    align-items: center; justify-content: center;
    z-index: 90;
  }
  #pauseOverlay.visible { display: flex; }
  .pause-text {
    font-size: 48px; font-weight: 900; color: #FFD700;
    text-shadow: 0 0 20px rgba(255,215,0,0.6); letter-spacing: 6px;
  }

  #challengeIndicator {
    position: absolute; top: 100px; left: 50%; transform: translateX(-50%);
    font-size: 11px; color: rgba(255,107,53,0.7); letter-spacing: 1px;
    pointer-events: none;
  }

  #mobileControls {
    position: fixed; bottom: 15px; left: 0; right: 0;
    display: none; justify-content: space-between; padding: 0 20px;
    z-index: 20; pointer-events: none;
  }
  .mobile-btn {
    width: 60px; height: 60px; border-radius: 50%;
    background: rgba(255,180,60,0.2); border: 2px solid rgba(255,180,60,0.4);
    color: #FFD700; font-size: 22px;
    display: flex; align-items: center; justify-content: center;
    pointer-events: all; user-select: none;
  }
  .mobile-btn:active { background: rgba(255,180,60,0.5); }

  @media (max-width: 768px) {
    .hud-value { font-size: 16px; }
    .hud-item { padding: 4px 10px; min-width: 60px; }
    .speedometer-wrap { width: 80px; height: 80px; }
    .go-title { font-size: 32px; }
    .go-stat-value { font-size: 20px; }
  }
</style>
</head>
<body>

<canvas id="gameCanvas"></canvas>

<div id="hud">
  <div class="hud-top">
    <div class="hud-item">
      <div class="hud-label">Speed</div>
      <div class="hud-value" id="speedVal">0</div>
    </div>
    <div class="hud-item">
      <div class="hud-label">Score</div>
      <div class="hud-value" id="scoreVal">0</div>
    </div>
    <div class="hud-item">
      <div class="hud-label">Near-Miss</div>
      <div class="hud-value nearmiss" id="multiplierVal">1.0x</div>
    </div>
    <div class="hud-item">
      <div class="hud-label">Turbo</div>
      <div class="hud-value boost" id="turboVal">0%</div>
    </div>
  </div>

  <div class="speedometer-wrap">
    <canvas id="speedometerCanvas" width="130" height="130"></canvas>
  </div>

  <div class="energy-bar-wrap">
    <div class="energy-bar-fill" id="energyBarFill" style="width:100%"></div>
  </div>

  <div class="turbo-indicator" id="turboIndicator">BOOST!</div>
  <div class="multiplier-display" id="multiplierDisplay"></div>

  <div class="hud-bottom">
    <button class="btn" id="pauseBtn" aria-label="Pause">PAUSE</button>
    <button class="btn" id="restartBtn" aria-label="Restart">RESTART</button>
  </div>
</div>

<div id="challengeIndicator">SCORE: 0</div>

<div id="gameOverScreen">
  <div class="go-title">GAME OVER</div>
  <div class="go-subtitle">Energy depleted. The canyon claims another pilot.</div>
  <div class="go-stats">
    <div class="go-stat">
      <div class="go-stat-label">Final Score</div>
      <div class="go-stat-value" id="finalScore">0</div>
    </div>
    <div class="go-stat">
      <div class="go-stat-label">Max Speed</div>
      <div class="go-stat-value high" id="maxSpeed">0</div>
    </div>
    <div class="go-stat">
      <div class="go-stat-label">Near-Miss</div>
      <div class="go-stat-value" id="finalMultiplier">1.0x</div>
    </div>
    <div class="go-stat">
      <div class="go-stat-label">Distance</div>
      <div class="go-stat-value" id="finalDistance">0m</div>
    </div>
  </div>
  <button class="restart-btn" id="restartBtn2">RACE AGAIN</button>
</div>

<div id="pauseOverlay">
  <div class="pause-text">PAUSED</div>
</div>

<div id="mobileControls">
  <div class="mobile-btn" id="btnUp" aria-label="Pitch up">&#9650;</div>
  <div class="mobile-btn" id="btnDown" aria-label="Pitch down">&#9660;</div>
  <div class="mobile-btn" id="btnLeft" aria-label="Steer left">&#9664;</div>
  <div class="mobile-btn" id="btnRight" aria-label="Steer right">&#9654;</div>
  <div class="mobile-btn" id="btnBoost" aria-label="Boost">BOOST</div>
</div>

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

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

const CFG = {
  canyonWidth: 320,
  wallHeight: 280,
  playerRadius: 4.5,
  playerSpeed: 2,
  playerMaxSpeed: 18,
  playerAccel: 0.45,
  playerDecel: 0.3,
  playerSteer: 0.35,
  energyDecay: 0.35,
  energyOrbValue: 22,
  turboFillRate: 0.5,
  turboMax: 100,
  nearMissThreshold: 25,
  nearMissDebounce: 0.5,
  chunkWidth: 600,
  chunkDepth: 400,
};

const canvas = document.getElementById('gameCanvas');
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: false });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.2;

const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a0a12);
scene.fog = new THREE.FogExp2(0x0a0a12, 0.0015);

const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.5, 2000);
camera.position.set(0, 60, 200);

const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
const bloomPass = new UnrealBloomPass(
  new THREE.Vector2(window.innerWidth, window.innerHeight),
  0.8, 0.4, 0.85
);
composer.addPass(bloomPass);

let audioCtx;

function initAudio() {
  if (audioCtx) return;
  try {
    audioCtx = new (window.AudioContext || window.webkitAudioContext)();
    const engineOsc = audioCtx.createOscillator();
    engineOsc.type = 'sine'; engineOsc.frequency.value = 80;
    const engineGain = audioCtx.createGain(); engineGain.gain.value = 0.12;
    engineOsc.connect(engineGain); engineGain.connect(audioCtx.destination); engineOsc.start();

    const collectOsc = audioCtx.createOscillator();
    collectOsc.type = 'sine'; collectOsc.frequency.value = 1200;
    const collectGain = audioCtx.createGain(); collectGain.gain.value = 0.1;
    collectOsc.connect(collectGain); collectGain.connect(audioCtx.destination); collectOsc.start();

    const nearMissOsc = audioCtx.createOscillator();
    nearMissOsc.type = 'sine'; nearMissOsc.frequency.value = 1800;
    const nearMissGain = audioCtx.createGain(); nearMissGain.gain.value = 0.08;
    nearMissOsc.connect(nearMissGain); nearMissGain.connect(audioCtx.destination); nearMissOsc.start();

    const boostOsc = audioCtx.createOscillator();
    boostOsc.type = 'triangle'; boostOsc.frequency.value = 200;
    const boostGain = audioCtx.createGain(); boostGain.gain.value = 0.1;
    boostOsc.connect(boostGain); boostGain.connect(audioCtx.destination); boostOsc.start();
  } catch (e) { console.warn('Audio init failed:', e); }
}

function playCollect() {
  if (!audioCtx) return;
  try {
    const osc = audioCtx.createOscillator();
    osc.type = 'sine'; osc.frequency.value = 1200;
    const gain = audioCtx.createGain(); gain.gain.value = 0.15;
    osc.connect(gain); gain.connect(audioCtx.destination);
    osc.start(); gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.3);
    osc.stop(audioCtx.currentTime + 0.3);
  } catch (e) { /* ignore */ }
}

function playNearMiss() {
  if (!audioCtx) return;
  try {
    const osc = audioCtx.createOscillator();
    osc.type = 'sine'; osc.frequency.value = 1800;
    const gain = audioCtx.createGain(); gain.gain.value = 0.1;
    osc.connect(gain); gain.connect(audioCtx.destination);
    osc.start(); osc.frequency.exponentialRampToValueAtTime(600, audioCtx.currentTime + 0.25);
    gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.3);
    osc.stop(audioCtx.currentTime + 0.3);
  } catch (e) { /* ignore */ }
}

function playBoost() {
  if (!audioCtx) return;
  try {
    const osc = audioCtx.createOscillator();
    osc.type = 'triangle'; osc.frequency.value = 200;
    osc.frequency.exponentialRampToValueAtTime(400, audioCtx.currentTime + 0.3);
    const gain = audioCtx.createGain(); gain.gain.value = 0.12;
    osc.connect(gain); gain.connect(audioCtx.destination);
    osc.start(); gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.4);
    osc.stop(audioCtx.currentTime + 0.4);
  } catch (e) { /* ignore */ }
}

function playGameOver() {
  if (!audioCtx) return;
  try {
    const osc = audioCtx.createOscillator();
    osc.type = 'sawtooth'; osc.frequency.value = 440;
    osc.frequency.exponentialRampToValueAtTime(100, audioCtx.currentTime + 0.8);
    const gain = audioCtx.createGain(); gain.gain.value = 0.15;
    osc.connect(gain); gain.connect(audioCtx.destination);
    osc.start(); gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 1.0);
    osc.stop(audioCtx.currentTime + 1.0);
  } catch (e) { /* ignore */ }
}

const sunLight = new THREE.DirectionalLight(0xFFD700, 1.5);
sunLight.position.set(500, 300, 200);
sunLight.target.position.set(0, 0, 0);
scene.add(sunLight); scene.add(sunLight.target);

const hemiLight = new THREE.HemisphereLight(0xFFD700, 0x1a0a2e, 0.3);
scene.add(hemiLight);
const ambientLight = new THREE.AmbientLight(0x1a0a2e, 0.2);
scene.add(ambientLight);

const skyGeo = new THREE.SphereGeometry(1200, 32, 32);
const skyMat = new THREE.ShaderMaterial({
  uniforms: {
    topColor: { value: new THREE.Color(0x0a0a1a) },
    bottomColor: { value: new THREE.Color(0xFF8C00) },
    offset: { value: 200 },
    exponent: { value: 0.5 },
  },
  vertexShader: `
    varying vec3 vWorldPos;
    void main() {
      vec4 worldPos = modelMatrix * vec4(position, 1.0);
      vWorldPos = worldPos.xyz;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `,
  fragmentShader: `
    uniform vec3 topColor;
    uniform vec3 bottomColor;
    uniform float offset;
    uniform float exponent;
    varying vec3 vWorldPos;
    void main() {
      float h = normalize(vWorldPos + vec3(0.0, offset, 0.0)).y;
      vec3 col = mix(bottomColor, topColor, pow(max(h, 0.0), exponent));
      gl_FragColor = vec4(col, 1.0);
    }
  `,
  side: THREE.BackSide,
});
const sky = new THREE.Mesh(skyGeo, skyMat);
scene.add(sky);

const groundGeo = new THREE.PlaneGeometry(3000, 3000);
const groundMat = new THREE.MeshStandardMaterial({ color: 0x1a1a2e, roughness: 0.95, metalness: 0.05 });
const ground = new THREE.Mesh(groundGeo, groundMat);
ground.rotation.x = -Math.PI / 2;
ground.position.y = -0.5;
scene.add(ground);

const playerGroup = new THREE.Group();
const bodyGeo = new THREE.ConeGeometry(2.5, 3.5, 6);
bodyGeo.rotateZ(Math.PI / 2);
const bodyMat = new THREE.MeshStandardMaterial({
  color: 0x00E5FF, emissive: 0x004466, emissiveIntensity: 0.3,
  metalness: 0.8, roughness: 0.2,
});
const body = new THREE.Mesh(bodyGeo, bodyMat);
body.castShadow = true;
playerGroup.add(body);

const cockpitGeo = new THREE.BoxGeometry(1.2, 1.5, 0.8);
const cockpitMat = new THREE.MeshStandardMaterial({
  color: 0x00CCFF, emissive: 0x002244, emissiveIntensity: 0.5,
  metalness: 0.6, roughness: 0.3,
});
const cockpit = new THREE.Mesh(cockpitGeo, cockpitMat);
cockpit.position.set(0, 0.4, 0.5);
playerGroup.add(cockpit);

const engineGeo = new THREE.SphereGeometry(0.8, 6, 6);
const engineMat = new THREE.MeshStandardMaterial({
  color: 0xFF6B35, emissive: 0xFF6B35, emissiveIntensity: 1.5,
  transparent: true, opacity: 0.9,
});
const engine = new THREE.Mesh(engineGeo, engineMat);
engine.position.set(0, -1.2, -1.8);
playerGroup.add(engine);

const exhaustGeo = new THREE.BoxGeometry(0.3, 1.5, 0.3);
const exhaustMat = new THREE.MeshStandardMaterial({
  color: 0xFF6B35, emissive: 0xFF6B35, emissiveIntensity: 0.8,
  transparent: true, opacity: 0.6,
});
const exhaust = new THREE.Mesh(exhaustGeo, exhaustMat);
exhaust.position.set(0, -0.8, -1.8);
playerGroup.add(exhaust);

const trailGeo = new THREE.BufferGeometry();
const trailPositions = new Float32Array(120 * 3);
trailGeo.setAttribute('position', new THREE.BufferAttribute(trailPositions, 3));
const trailMat = new THREE.PointsMaterial({
  color: 0x00E5FF, size: 0.15, transparent: true, opacity: 0.7,
  blending: THREE.AdditiveBlending, depthWrite: false,
});
const trail = new THREE.Points(trailGeo, trailMat);
playerGroup.add(trail);

const shadowGeo = new THREE.CircleGeometry(3, 8);
const shadowMat = new THREE.MeshBasicMaterial({ color: 0x000000, transparent: true, opacity: 0.3 });
const shadow = new THREE.Mesh(shadowGeo, shadowMat);
shadow.rotation.x = -Math.PI / 2;
shadow.position.set(0, 0.01, 0);
playerGroup.add(shadow);

playerGroup.position.set(0, 0, 0);
scene.add(playerGroup);

const wallGeo = new THREE.BoxGeometry(1, CFG.wallHeight, 15);
const wallMat = new THREE.MeshStandardMaterial({
  color: 0x3D2B1F, flatShading: true, roughness: 0.95, metalness: 0.1,
});

const leftWallMeshes = [];
const rightWallMeshes = [];
const gapMeshes = [];
const monolithMeshes = [];
const orbMeshes = [];
const boostItemMeshes = [];

function createCanyonWalls(chunkX, chunkZ) {
  leftWallMeshes.forEach(m => scene.remove(m));
  rightWallMeshes.forEach(m => scene.remove(m));
  gapMeshes.forEach(m => scene.remove(m));
  leftWallMeshes = [];
  rightWallMeshes = [];
  gapMeshes = [];

  const gapCount = 2 + Math.floor(Math.random() * 3);
  const gapData = [];
  for (let i = 0; i < gapCount; i++) {
    gapData.push({
      x: -CFG.canyonWidth / 2 + (i + 1) * (CFG.canyonWidth / (gapCount + 1)),
      width: CFG.gapMinWidth + Math.random() * (CFG.gapMaxWidth - CFG.gapMinWidth),
    });
  }
  gapData.sort((a, b) => a.x - b.x);

  for (const g of gapData) {
    const gx = g.x;
    const gw = g.width;
    const gH = CFG.wallHeight;

    const lw = new THREE.Mesh(wallGeo, wallMat);
    lw.position.set(gx - gw / 2 - 1, gH / 2, chunkZ);
    lw.renderOrder = 1;
    scene.add(lw);
    leftWallMeshes.push(lw);

    const rw = new THREE.Mesh(wallGeo, wallMat);
    rw.position.set(gx + gw / 2 + 1, gH / 2, chunkZ);
    rw.renderOrder = 1;
    scene.add(rw);
    rightWallMeshes.push(rw);

    const gapFloorGeo = new THREE.BoxGeometry(gw, 1, 30);
    const gapFloorMat = new THREE.MeshStandardMaterial({
      color: 0x0a0a1a, transparent: true, opacity: 0.15,
      side: THREE.DoubleSide,
    });
    const gapFloor = new THREE.Mesh(gapFloorGeo, gapFloorMat);
    gapFloor.position.set(gx, 0.01, chunkZ);
    gapFloor.renderOrder = 0;
    scene.add(gapFloor);
    gapMeshes.push(gapFloor);
  }
}

function createMonolith(chunkX, chunkZ, diff) {
  const height = 50 + Math.random() * 80;
  const mono = new THREE.Mesh(
    new THREE.BoxGeometry(2, height, 3),
    new THREE.MeshStandardMaterial({
      color: 0x8B4513, flatShading: true, roughness: 0.9, metalness: 0.1,
    })
  );
  mono.position.set(chunkX + 150, height / 2, chunkZ);
  mono.rotation.y = Math.random() * Math.PI;
  scene.add(mono);
  monolithMeshes.push(mono);
  return mono;
}

function createOrb(chunkX, chunkZ) {
  const orb = new THREE.Mesh(
    new THREE.SphereGeometry(1.2, 8, 8),
    new THREE.MeshStandardMaterial({
      color: 0xFFD700, emissive: 0xFFD700, emissiveIntensity: 2.0,
      metalness: 0.3, roughness: 0.1,
    })
  );
  orb.position.set(chunkX + 100, 3 + Math.random() * 20, chunkZ);
  orb.scale.setScalar(0.5 + Math.random() * 0.8);
  orb.userData = { active: true, pulse: Math.random() * Math.PI * 2 };
  scene.add(orb);
  orbMeshes.push(orb);
}

function createBoostItem(chunkX, chunkZ) {
  const boostItem = new THREE.Mesh(
    new THREE.OctahedronGeometry(1.5),
    new THREE.MeshStandardMaterial({
      color: 0x00E5FF, emissive: 0x00E5FF, emissiveIntensity: 2.5,
      metalness: 0.3, roughness: 0.1,
    })
  );
  boostItem.position.set(chunkX + 100, 5 + Math.random() * 15, chunkZ);
  boostItem.scale.setScalar(0.6 + Math.random() * 0.6);
  boostItem.userData = { active: true, pulse: Math.random() * Math.PI * 2 };
  scene.add(boostItem);
  boostItemMeshes.push(boostItem);
}

const particleSystem = new THREE.Points(new THREE.BufferGeometry(), new THREE.PointsMaterial({
  color: 0xFFD700, size: 0.3, transparent: true, opacity: 0.6,
  blending: THREE.AdditiveBlending, depthWrite: false,
}));
scene.add(particleSystem);

const particlePool = [];
const MAX_PARTICLES = 200;

function initParticlePool() {
  for (let i = 0; i < MAX_PARTICLES; i++) {
    const geo = new THREE.BoxGeometry(0.15, 0.15, 0.15);
    const mat = new THREE.MeshStandardMaterial({
      color: 0xFFD700, emissive: 0xFFD700, emissiveIntensity: 0.5,
      transparent: true, opacity: 0.8,
      blending: THREE.AdditiveBlending, depthWrite: false,
    });
    const m = new THREE.Mesh(geo, mat);
    m.visible = false;
    scene.add(m);
    particlePool.push({ mesh: m, life: 0, maxLife: 0, vx: 0, vy: 0, vz: 0, active: false });
  }
}

function emitParticle(x, y, z, vx, vy, vz, life) {
  for (let p of particlePool) {
    if (!p.active) {
      p.mesh.visible = true;
      p.mesh.position.set(x, y, z);
      p.mesh.scale.setScalar(0.8 + Math.random() * 0.4);
      p.mesh.material.color.setHex(0xFFD700);
      p.mesh.material.emissive.setHex(0xFFD700);
      p.mesh.material.opacity = 0.9;
      p.vx = vx; p.vy = vy; p.vz = vz;
      p.life = life; p.maxLife = life;
      p.active = true;
      return;
    }
  }
}

function updateParticles(dt) {
  for (let p of particlePool) {
    if (!p.active) continue;
    p.mesh.position.x += p.vx * dt;
    p.mesh.position.y += p.vy * dt;
    p.mesh.position.z += p.vz * dt;
    p.vy -= 2 * dt;
    p.life -= dt;
    if (p.life <= 0) {
      p.active = false;
      p.mesh.visible = false;
    }
  }
}

const state = {
  phase: 'menu',
  score: 0,
  multiplier: 1.0,
  multiplierTimer: 0,
  energy: 100,
  turbo: 0,
  maxTurbo: CFG.turboMax,
  speed: 0,
  maxSpeed: 0,
  nearMiss: 0,
  nearMissTimer: 0,
  highSpeed: 0,
  distance: 0,
  difficulty: 1,
  player: {
    position: new THREE.Vector3(0, 0, 0),
    velocity: new THREE.Vector3(0, 0, 0),
    yaw: 0,
    pitch: 0,
    roll: 0,
    prevYaw: 0,
    prevPitch: 0,
    prevRoll: 0,
    thrust: 0,
  },
  cameraShake: { x: 0, y: 0, intensity: 0, decay: 0.9 },
  chunkHistory: [],
  nextChunkX: 100,
  lastChunkZ: 0,
  spawnTimer: 0,
  challengeScore: 0,
  nearMissActive: false,
  lastNearMissTime: 0,
  highSpeedCount: 0,
};

function checkWallCollision(playerPos, wall) {
  const w = wall.position;
  if (isNaN(w.x) || isNaN(w.y) || isNaN(w.z)) return false;
  const wCenter = new THREE.Vector3(w.x, w.y, w.z);
  const wallHalfW = 0.5;
  const wallHalfH = CFG.wallHeight / 2;
  const wallHalfD = 7.5;

  const px = playerPos.x, py = playerPos.y, pz = playerPos.z;
  const pr = CFG.playerRadius;

  if (px > wCenter.x - wallHalfW && px < wCenter.x + wallHalfW &&
      py > wCenter.y - wallHalfH && py < wCenter.y + wallHalfH &&
      pz > wCenter.z - wallHalfD && pz < wCenter.z + wallHalfD) {
    return true;
  }
  return false;
}

function checkObstacleCollision(playerPos, obj) {
  const p = playerPos;
  const pr = CFG.playerRadius;
  if (isNaN(p.x) || isNaN(p.y) || isNaN(p.z)) return false;
  if (isNaN(obj.position.x) || isNaN(obj.position.y) || isNaN(obj.position.z)) return false;
  if (p.distanceTo(obj.position) < pr + 0.5) return true;
  return false;
}

function checkOrbCollision(playerPos, orb) {
  const p = playerPos;
  const pr = CFG.playerRadius;
  if (isNaN(p.x) || isNaN(p.y) || isNaN(p.z)) return false;
  if (isNaN(orb.position.x) || isNaN(orb.position.y) || isNaN(orb.position.z)) return false;
  if (p.distanceTo(orb.position) < pr + orb.geometry.parameters.radius) return true;
  return false;
}

function checkBoostItemCollision(playerPos, boost) {
  const p = playerPos;
  const pr = CFG.playerRadius;
  if (isNaN(p.x) || isNaN(p.y) || isNaN(p.z)) return false;
  if (isNaN(boost.position.x) || isNaN(boost.position.y) || isNaN(boost.position.z)) return false;
  if (p.distanceTo(boost.position) < pr + boost.geometry.parameters.radius) return true;
  return false;
}

function isNaN(v) { return typeof v === 'number' && isNaN(v); }

function isValidPos(v) { return !isNaN(v) && isFinite(v); }

function checkWallCollision(player) {
  const p = player.position;
  const px = p.x, py = p.y, pz = p.z;
  const pr = CFG.playerRadius;

  for (const wall of state.chunkHistory[0].walls) {
    const w = wall.position;
    if (!isValidPos(w.x) || !isValidPos(w.y) || !isValidPos(w.z)) continue;

    if (px > w.x - 0.5 && px < w.x + 0.5 &&
        py > w.y - 140 && py < w.y + 140 &&
        pz > w.z - 7.5 && pz < w.z + 7.5) {
      return true;
    }
  }
  return false;
}

function checkObstacleCollision(player) {
  const p = player.position;
  const pr = CFG.playerRadius;

  for (const obj of state.chunkHistory[0].obstacles) {
    if (!isValidPos(obj.position.x) || !isValidPos(obj.position.y) || !isValidPos(obj.position.z)) continue;
    if (p.distanceTo(obj.position) < pr + 0.5) {
      return true;
    }
  }
  return false;
}

function checkOrbCollision(player) {
  const p = player.position;
  const pr = CFG.playerRadius;

  for (const orb of state.chunkHistory[0].orbs) {
    if (!orb.userData.active) continue;
    if (!isValidPos(orb.position.x) || !isValidPos(orb.position.y) || !isValidPos(orb.position.z)) continue;
    if (p.distanceTo(orb.position) < pr + orb.geometry.parameters.radius) {
      state.energy = Math.min(100, state.energy + CFG.energyOrbValue);
      state.score += 10 * state.multiplier;
      playCollect();
      orb.userData.active = false;
      for (let i = 0; i < 5; i++) {
        emitParticle(orb.position.x, orb.position.y, orb.position.z,
          (Math.random() - 0.5) * 4, (Math.random() - 0.5) * 4, (Math.random() - 0.5) * 4, 0.5 + Math.random() * 0.5);
      }
      return true;
    }
  }
  return false;
}

function checkBoostItemCollision(player) {
  const p = player.position;
  const pr = CFG.playerRadius;

  for (const boost of state.chunkHistory[0].boostItems) {
    if (!boost.userData.active) continue;
    if (!isValidPos(boost.position.x) || !isValidPos(boost.position.y) || !isValidPos(boost.position.z)) continue;
    if (p.distanceTo(boost.position) < pr + boost.geometry.parameters.radius) {
      state.turbo = state.maxTurbo;
      state.score += 25 * state.multiplier;
      playBoost();
      boost.userData.active = false;
      for (let i = 0; i < 8; i++) {
        emitParticle(boost.position.x, boost.position.y, boost.position.z,
          (Math.random() - 0.5) * 6, (Math.random() - 0.5) * 6, (Math.random() - 0.5) * 6, 0.5 + Math.random() * 0.5);
      }
      return true;
    }
  }
  return false;
}

const speedValEl = document.getElementById('speedVal');
const scoreValEl = document.getElementById('scoreVal');
const multiplierValEl = document.getElementById('multiplierVal');
const turboValEl = document.getElementById('turboVal');
const energyBarFill = document.getElementById('energyBarFill');
const multiplierDisplay = document.getElementById('multiplierDisplay');
const turboIndicator = document.getElementById('turboIndicator');
const speedometerCanvas = document.getElementById('speedometerCanvas');
const speedometerCtx = speedometerCanvas.getContext('2d');
const challengeEl = document.getElementById('challengeIndicator');

function updateHUD() {
  speedValEl.textContent = state.speed.toFixed(1) + ' u/s';
  scoreValEl.textContent = state.score.toLocaleString();

  if (state.multiplier > 1) {
    multiplierValEl.textContent = state.multiplier.toFixed(1) + 'x';
    multiplierValEl.classList.add('nearmiss');
  } else {
    multiplierValEl.textContent = '1.0x';
    multiplierValEl.classList.remove('nearmiss');
  }

  const turboPct = Math.round((state.turbo / state.maxTurbo) * 100);
  turboValEl.textContent = turboPct + '%';
  turboValEl.classList.toggle('boost', state.turbo > 50);

  energyBarFill.style.width = state.energy + '%';
  energyBarFill.style.background = `linear-gradient(90deg, #FF6B35, #FFD700, #00E5FF)`;

  multiplierDisplay.textContent = state.multiplier.toFixed(1) + 'x';
  multiplierDisplay.classList.toggle('active', state.multiplier > 1);
  turboIndicator.classList.toggle('active', state.turbo > 50);
}

function showMultiplier(mult) {
  multiplierDisplay.textContent = mult.toFixed(1) + 'x';
  multiplierDisplay.classList.add('active');
}

function updateSpeedometer() {
  const ctx = speedometerCtx;
  const w = 130, h = 130;
  ctx.clearRect(0, 0, w, h);

  const cx = w / 2, cy = h / 2;
  const r = 50;

  ctx.beginPath();
  ctx.arc(cx, cy, r, 0, Math.PI * 2);
  ctx.strokeStyle = 'rgba(255,255,255,0.1)';
  ctx.lineWidth = 3;
  ctx.stroke();

  const speedNorm = Math.min(1, state.speed / CFG.playerMaxSpeed);
  const startAngle = Math.PI;
  const endAngle = Math.PI + Math.PI * 2 * speedNorm;

  ctx.beginPath();
  ctx.arc(cx, cy, r - 3, startAngle, endAngle);
  ctx.strokeStyle = state.speed > CFG.playerMaxSpeed * 0.8 ? '#FF6B35' : '#FFD700';
  ctx.lineWidth = 5;
  ctx.stroke();

  ctx.beginPath();
  ctx.arc(cx, cy, r - 3, startAngle, endAngle);
  ctx.strokeStyle = 'rgba(255,255,255,0.15)';
  ctx.lineWidth = 1;
  ctx.stroke();

  ctx.fillStyle = '#FFD700';
  ctx.font = 'bold 14px sans-serif';
  ctx.textAlign = 'center';
  ctx.fillText(Math.round(state.speed * 10) + 'km/h', cx, cy - 2);

  const needleAngle = startAngle + Math.PI * 2 * speedNorm;
  ctx.beginPath();
  ctx.moveTo(cx, cy);
  ctx.lineTo(cx + Math.cos(needleAngle) * 30, cy + Math.sin(needleAngle) * 30);
  ctx.strokeStyle = '#FFD700';
  ctx.lineWidth = 2;
  ctx.stroke();

  ctx.beginPath();
  ctx.arc(cx, cy, 3, 0, Math.PI * 2);
  ctx.fillStyle = '#FFD700';
  ctx.fill();
}

function startGame() {
  initAudio();
  state.phase = 'playing';
  state.score = 0;
  state.multiplier = 1.0;
  state.energy = 100;
  state.turbo = 0;
  state.speed = 0;
  state.maxSpeed = 0;
  state.nearMiss = 0;
  state.nearMissTimer = 0;
  state.highSpeed = 0;
  state.distance = 0;
  state.difficulty = 1;
  state.challengeScore = 0;
  state.player.position.set(0, 0, 0);
  state.player.velocity.set(0, 0, 0);
  state.player.yaw = 0;
  state.player.pitch = 0;
  state.player.roll = 0;
  state.player.prevYaw = 0;
  state.player.prevPitch = 0;
  state.player.prevRoll = 0;
  state.player.thrust = 0;
  state.cameraShake.intensity = 0;
  state.chunkHistory = [];
  state.nextChunkX = 100;
  state.lastChunkZ = 0;
  state.spawnTimer = 0;

  leftWallMeshes.forEach(m => scene.remove(m));
  rightWallMeshes.forEach(m => scene.remove(m));
  gapMeshes.forEach(m => scene.remove(m));
  monolithMeshes.forEach(m => scene.remove(m));
  orbMeshes.forEach(m => scene.remove(m));
  boostItemMeshes.forEach(m => scene.remove(m));

  leftWallMeshes = [];
  rightWallMeshes = [];
  gapMeshes = [];
  monolithMeshes = [];
  orbMeshes = [];
  boostItemMeshes = [];

  particlePool.forEach(p => { p.active = false; p.mesh.visible = false; });

  playerGroup.position.set(0, 0, 0);
  playerGroup.rotation.set(0, 0, 0);

  camera.position.set(0, 60, 200);
  camera.lookAt(0, 0, 0);

  document.getElementById('gameOverScreen').classList.remove('visible');
  document.getElementById('pauseOverlay').classList.remove('visible');

  multiplierDisplay.classList.remove('active');
  turboIndicator.classList.remove('active');
  energyBarFill.style.width = '100%';

  state.chunkHistory.push(generateChunk(100, 0, 1));
  updateHUD();
}

function pauseGame() {
  if (state.phase !== 'playing') return;
  state.phase = 'paused';
  document.getElementById('pauseOverlay').classList.add('visible');
}

function resumeGame() {
  if (state.phase !== 'paused') return;
  state.phase = 'playing';
  document.getElementById('pauseOverlay').classList.remove('visible');
}

function gameOver() {
  state.phase = 'gameover';
  playGameOver();

  document.getElementById('finalScore').textContent = state.score;
  document.getElementById('maxSpeed').textContent = state.maxSpeed.toFixed(1) + ' u/s';
  document.getElementById('finalMultiplier').textContent = state.multiplier.toFixed(1) + 'x';
  document.getElementById('finalDistance').textContent = Math.floor(state.distance) + 'm';

  setTimeout(() => {
    document.getElementById('gameOverScreen').classList.add('visible');
  }, 500);
}

function restartGame() {
  startGame();
}

const keys = { up: false, down: false, left: false, right: false, boost: false };

document.addEventListener('keydown', e => {
  if (e.code === 'KeyW' || e.code === 'ArrowUp') keys.up = true;
  if (e.code === 'KeyS' || e.code === 'ArrowDown') keys.down = true;
  if (e.code === 'KeyA' || e.code === 'ArrowLeft') keys.left = true;
  if (e.code === 'KeyD' || e.code === 'ArrowRight') keys.right = true;
  if (e.code === 'Space') { keys.boost = true; e.preventDefault(); }
  if (e.code === 'KeyP' || e.code === 'Escape') {
    if (state.phase === 'playing') pauseGame();
    else if (state.phase === 'paused') resumeGame();
  }
  if (e.code === 'KeyR') restartGame();
});
document.addEventListener('keyup', e => {
  if (e.code === 'KeyW' || e.code === 'ArrowUp') keys.up = false;
  if (e.code === 'KeyS' || e.code === 'ArrowDown') keys.down = false;
  if (e.code === 'KeyA' || e.code === 'ArrowLeft') keys.left = false;
  if (e.code === 'KeyD' || e.code === 'ArrowRight') keys.right = false;
  if (e.code === 'Space') keys.boost = false;
});

const btnUp = document.getElementById('btnUp');
const btnDown = document.getElementById('btnDown');
const btnLeft = document.getElementById('btnLeft');
const btnRight = document.getElementById('btnRight');
const btnBoost = document.getElementById('btnBoost');

function setupMobileControls() {
  btnUp.addEventListener('touchstart', e => { e.preventDefault(); keys.up = true; });
  btnUp.addEventListener('touchend', () => { keys.up = false; });
  btnDown.addEventListener('touchstart', e => { e.preventDefault(); keys.down = true; });
  btnDown.addEventListener('touchend', () => { keys.down = false; });
  btnLeft.addEventListener('touchstart', e => { e.preventDefault(); keys.left = true; });
  btnLeft.addEventListener('touchend', () => { keys.left = false; });
  btnRight.addEventListener('touchstart', e => { e.preventDefault(); keys.right = true; });
  btnRight.addEventListener('touchend', () => { keys.right = false; });
  btnBoost.addEventListener('touchstart', e => { e.preventDefault(); keys.boost = true; });
  btnBoost.addEventListener('touchend', () => { keys.boost = false; });
}
setupMobileControls();

let lastTime = 0;

function update(dt) {
  if (state.phase !== 'playing') return;

  state.difficulty = 1 + state.score / 500;

  const pitchInput = keys.up ? 1 : -1;
  const yawInput = keys.right ? 1 : (keys.left ? -1 : 0);
  const boostInput = keys.boost;

  state.player.thrust = 0;
  if (pitchInput > 0) state.player.thrust += CFG.playerAccel * pitchInput * dt;
  if (pitchInput < 0) state.player.thrust -= CFG.playerAccel * Math.abs(pitchInput) * dt;

  const steerInput = yawInput * CFG.playerSteer * dt;
  state.player.yaw += steerInput;
  state.player.roll += steerInput * 0.3 * dt;
  state.player.pitch = pitchInput * 0.15 * dt;

  if (boostInput && state.turbo < state.maxTurbo) {
    state.turbo = Math.min(state.maxTurbo, state.turbo + CFG.turboFillRate * dt);
  } else if (boostInput && state.turbo >= state.maxTurbo) {
    state.turbo = state.maxTurbo;
  } else if (!boostInput && state.turbo > 0) {
    state.turbo = Math.max(0, state.turbo - 0.5 * dt);
  }

  state.energy = Math.max(0, state.energy - CFG.energyDecay * dt);

  const speedMultiplier = 1 + state.turbo / state.maxTurbo * 0.5;
  const maxSpd = CFG.playerMaxSpeed * speedMultiplier;
  state.speed = Math.max(-5, Math.min(maxSpd, state.speed + state.player.thrust * (1 - CFG.playerDecel * dt)));

  state.player.position.x += state.player.velocity.x * dt;
  state.player.position.y += state.player.velocity.y * dt;
  state.player.position.z += state.player.speed * dt;

  playerGroup.position.copy(state.player.position);

  const camDist = 100;
  const camY = 50 + state.player.speed * 0.02;
  const camX = state.player.position.x;
  const camZ = state.player.position.z + camDist;

  if (state.cameraShake.intensity > 0) {
    const shakeX = (Math.random() - 0.5) * state.cameraShake.intensity * 2;
    const shakeY = (Math.random() - 0.5) * state.cameraShake.intensity * 2;
    camera.position.x = camX + shakeX;
    camera.position.y = camY + shakeY;
    camera.position.z = camZ;
    state.cameraShake.intensity *= 0.95;
    if (state.cameraShake.intensity < 0.01) state.cameraShake.intensity = 0;
  } else {
    camera.position.x = camX;
    camera.position.y = camY;
    camera.position.z = camZ;
  }

  const lookAt = new THREE.Vector3(state.player.position.x, state.player.position.y + 30, state.player.position.z);
  camera.lookAt(lookAt);

  const targetFov = 75 + state.turbo / state.maxTurbo * 15;
  camera.fov = THREE.MathUtils.lerp(camera.fov, targetFov, 0.1);
  camera.updateProjectionMatrix();

  if (state.energy <= 0) {
    gameOver();
    return;
  }

  const playerMinX = state.player.position.x - CFG.playerRadius;
  const playerMaxX = state.player.position.x + CFG.playerRadius;
  const playerMinY = state.player.position.y - CFG.playerRadius;
  const playerMaxY = state.player.position.y + CFG.playerRadius;
  const playerMinZ = state.player.position.z - CFG.playerRadius;
  const playerMaxZ = state.player.position.z + CFG.playerRadius;

  for (const wall of state.chunkHistory[0].walls) {
    const w = wall.position;
    if (playerMaxX > w.x - 0.5 && playerMinX < w.x + 0.5 &&
        playerMaxY > w.y - 140 && playerMinY < w.y + 140 &&
        playerMaxZ > w.z - 7.5 && playerMinZ < w.z + 7.5) {
      gameOver();
      return;
    }
  }

  for (const obj of state.chunkHistory[0].obstacles) {
    checkNearMiss(state.player, obj);
  }

  for (const orb of state.chunkHistory[0].orbs) {
    if (orb.userData.active && state.player.position.distanceTo(orb.position) < CFG.playerRadius + orb.geometry.parameters.radius) {
      state.energy = Math.min(100, state.energy + CFG.energyOrbValue);
      state.score += 10 * state.multiplier;
      playCollect();
      orb.userData.active = false;
      for (let i = 0; i < 5; i++) {
        emitParticle(orb.position.x, orb.position.y, orb.position.z,
          (Math.random() - 0.5) * 4, (Math.random() - 0.5) * 4, (Math.random() - 0.5) * 4, 0.5 + Math.random() * 0.5);
      }
    }
  }

  for (const boost of state.chunkHistory[0].boostItems) {
    if (boost.userData.active && state.player.position.distanceTo(boost.position) < CFG.playerRadius + boost.geometry.parameters.radius) {
      state.turbo = state.maxTurbo;
      state.score += 25 * state.multiplier;
      playBoost();
      boost.userData.active = false;
      for (let i = 0; i < 8; i++) {
        emitParticle(boost.position.x, boost.position.y, boost.position.z,
          (Math.random() - 0.5) * 6, (Math.random() - 0.5) * 6, (Math.random() - 0.5) * 6, 0.5 + Math.random() * 0.5);
      }
    }
  }

  state.spawnTimer -= dt;
  if (state.spawnTimer <= 0) {
    state.spawnTimer = 1.5 / (state.difficulty * 0.5);
    state.chunkHistory.push(generateChunk(state.nextChunkX, state.lastChunkZ, state.difficulty));
    state.nextChunkX += CFG.chunkWidth;
  }

  if (state.speed > 3) {
    const spread = state.speed * 0.02;
    const angle = state.player.yaw;
    emitParticle(
      state.player.position.x - Math.cos(angle) * 2,
      state.player.position.y - 1,
      state.player.position.z - Math.sin(angle) * 2,
      Math.cos(angle + Math.PI) * spread + (Math.random() - 0.5) * 2,
      -1 - Math.random() * 2,
      Math.sin(angle) * spread + (Math.random() - 0.5) * 2,
      0.4 + Math.random() * 0.3
    );
  }

  if (Math.random() < 0.1) {
    const px = state.player.position.x + (Math.random() - 0.5) * 100;
    const py = state.player.position.y + Math.random() * 200;
    const pz = state.player.position.z + 100 + Math.random() * 200;
    emitParticle(px, py, pz, (Math.random() - 0.5) * 0.5, -Math.random() * 0.5, (Math.random() - 0.5) * 0.5, 1.0 + Math.random() * 1.0);
  }

  state.score += state.speed * 0.01 * dt;
  state.distance += state.speed * dt;

  if (state.nearMissTimer > 0) {
    state.nearMissTimer -= dt;
    if (state.nearMissTimer <= 0) {
      state.multiplier = Math.max(1.0, state.multiplier - 0.3 * dt);
    }
  }

  if (state.speed > CFG.playerMaxSpeed * 0.8) {
    state.highSpeedCount++;
  } else {
    state.highSpeedCount = Math.max(0, state.highSpeedCount - 1);
  }
  state.maxSpeed = Math.max(state.maxSpeed, state.speed);

  updateParticles(dt);
  updateSpeedometer();
  updateHUD();
  challengeEl.textContent = `SCORE: ${state.score.toLocaleString()}`;
}

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

  if (state.phase === 'playing') {
    update(dt);
  }

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

function generateChunk(x, z, diff) {
  const chunk = {
    x, z,
    walls: [],
    obstacles: [],
    orbs: [],
    boostItems: [],
    gaps: [],
    wallData: [],
    gapData: [],
  };

  const gapCount = 2 + Math.floor(Math.random() * 3);
  const gapData = [];
  for (let i = 0; i < gapCount; i++) {
    gapData.push({
      x: -CFG.canyonWidth / 2 + (i + 1) * (CFG.canyonWidth / (gapCount + 1)),
      width: CFG.gapMinWidth + Math.random() * (CFG.gapMaxWidth - CFG.gapMinWidth),
    });
  }
  gapData.sort((a, b) => a.x - b.x);

  for (const g of gapData) {
    const gx = g.x;
    const gw = g.width;
    const gH = CFG.wallHeight;

    const lw = new THREE.Mesh(wallGeo, wallMat);
    lw.position.set(gx - gw / 2 - 1, gH / 2, z);
    lw.renderOrder = 1;
    scene.add(lw);
    chunk.walls.push(lw);
    chunk.wallData.push({ pos: new THREE.Vector3(gx - gw / 2 - 1, gH / 2, z), halfW: 0.5, halfH: gH / 2, halfD: 7.5, type: 'wall' });

    const rw = new THREE.Mesh(wallGeo, wallMat);
    rw.position.set(gx + gw / 2 + 1, gH / 2, z);
    rw.renderOrder = 1;
    scene.add(rw);
    chunk.walls.push(rw);
    chunk.wallData.push({ pos: new THREE.Vector3(gx + gw / 2 + 1, gH / 2, z), halfW: 0.5, halfH: gH / 2, halfD: 7.5, type: 'wall' });

    const gapFloorGeo = new THREE.BoxGeometry(gw, 1, 30);
    const gapFloorMat = new THREE.MeshStandardMaterial({
      color: 0x0a0a1a, transparent: true, opacity: 0.15,
      side: THREE.DoubleSide,
    });
    const gapFloor = new THREE.Mesh(gapFloorGeo, gapFloorMat);
    gapFloor.position.set(gx, 0.01, z);
    gapFloor.renderOrder = 0;
    scene.add(gapFloor);
    chunk.gaps.push(gapFloor);
    chunk.gapData.push({ x: gx, width: gw });
  }

  const numMonoliths = 1 + Math.floor(Math.random() * Math.min(3, Math.floor(diff * 0.5)));
  for (let i = 0; i < numMonoliths; i++) {
    const mono = createMonolith(x, z, diff);
    chunk.obstacles.push(mono);
  }

  const numOrbs = 2 + Math.floor(Math.random() * 3);
  for (let i = 0; i < numOrbs; i++) {
    createOrb(x, z);
    chunk.orbs.push(chunk.orbs[chunk.orbs.length - 1]);
  }

  const numBoosts = 1 + Math.floor(Math.random() * 2);
  for (let i = 0; i < numBoosts; i++) {
    createBoostItem(x, z);
    chunk.boostItems.push(chunk.boostItems[chunk.boostItems.length - 1]);
  }

  return chunk;
}

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

  if (state.phase === 'playing') {
    update(dt);
  }

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

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

initParticlePool();
state.chunkHistory.push(generateChunk(100, 0, 1));
requestAnimationFrame(runLoop);
</script>
</body>
</html>