← Canyon Run results

Canyon Run

thinkingcap qwen3.6 27b · composer

◈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 39,624 bytes · SHA-256 9720d9187755
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Canyon Racer</title>
<script src="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js"></script>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #0a0510; font-family: 'Segoe UI', system-ui, sans-serif; }
#game-container { position: relative; width: 100vw; height: 100vh; }
#game-canvas { display: block; width: 100%; height: 100%; }

/* HUD */
#hud { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; }
#speed-bar { position: absolute; right: 30px; bottom: 40px; width: 6px; height: 200px; background: rgba(255,255,255,0.1); border-radius: 3px; overflow: hidden; }
#speed-fill { position: absolute; bottom: 0; width: 100%; height: 50%; background: linear-gradient(to top, #ff6b35, #ffd700); border-radius: 3px; transition: height 0.1s; box-shadow: 0 0 10px rgba(255,107,53,0.5); }
#multiplier { position: absolute; right: 50px; bottom: 40px; font-size: 48px; font-weight: 900; color: #ffd700; text-shadow: 0 0 20px rgba(255,215,0,0.5); letter-spacing: -2px; }
#energy-bar { position: absolute; left: 30px; bottom: 40px; width: 200px; height: 6px; background: rgba(255,255,255,0.1); border-radius: 3px; overflow: hidden; }
#energy-fill { position: absolute; left: 0; top: 0; width: 100%; height: 100%; background: linear-gradient(to right, #7209b7, #ff6b35); border-radius: 3px; transition: width 0.1s; box-shadow: 0 0 10px rgba(114,9,183,0.5); }
#turbo-bar { position: absolute; left: 30px; bottom: 60px; width: 200px; height: 4px; background: rgba(255,255,255,0.1); border-radius: 2px; overflow: hidden; }
#turbo-fill { position: absolute; left: 0; top: 0; width: 0%; height: 100%; background: linear-gradient(to right, #00d4ff, #76ff03); border-radius: 2px; transition: width 0.1s; box-shadow: 0 0 8px rgba(0,212,255,0.5); }

/* Screens */
#start-screen, #game-over { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; background: rgba(10,5,16,0.85); z-index: 10; }
#start-screen h1 { font-size: 72px; font-weight: 900; color: #ff6b35; text-shadow: 0 0 40px rgba(255,107,53,0.6); letter-spacing: -3px; margin-bottom: 10px; }
#game-over h1 { font-size: 64px; font-weight: 900; color: #ff3d3d; text-shadow: 0 0 40px rgba(255,61,61,0.6); letter-spacing: -2px; margin-bottom: 30px; }
.subtitle { color: #c0a080; font-size: 18px; margin-bottom: 20px; }
.controls { color: #908070; font-size: 14px; margin-bottom: 40px; line-height: 1.8; }
#final-score, #max-speed { color: #ffd700; font-size: 28px; margin: 8px 0; font-weight: 600; }
button { pointer-events: all; padding: 15px 50px; font-size: 20px; font-weight: 700; border: none; border-radius: 4px; cursor: pointer; background: linear-gradient(135deg, #ff6b35, #e84118); color: white; letter-spacing: 2px; text-transform: uppercase; box-shadow: 0 4px 20px rgba(255,107,53,0.4); transition: transform 0.1s, box-shadow 0.1s; }
button:hover { transform: scale(1.05); box-shadow: 0 6px 30px rgba(255,107,53,0.6); }
/* ── css: score-display ── */
#score-display { position: absolute; top: 20px; right: 30px; font-size: 36px; font-weight: 900; color: #ffd700; text-shadow: 0 0 15px rgba(255,215,0,0.4); letter-spacing: -1px; }
</style>
</head>
<body>
<div id="game-container">
  <canvas id="game-canvas"></canvas>
  <div id="hud">
    <div id="speed-bar"><div id="speed-fill"></div></div>
    <div id="multiplier">x1.0</div>
    <div id="energy-bar"><div id="energy-fill"></div></div>
    <div id="turbo-bar"><div id="turbo-fill"></div></div>
  <div id="score-display">0</div></div>
  <div id="game-over" style="display:none;">
    <h1>CANYON BREACH</h1>
    <p id="final-score">Score: 0</p>
    <p id="max-speed">Max Speed: 0</p>
    <button id="restart-btn">RESTART</button>
  </div>
  <div id="start-screen">
    <h1>CANYON RACER</h1>
    <p class="subtitle">Navigate the procedural canyon at extreme speed</p>
    <p class="controls">WASD / Arrow Keys — Steer<br>SPACE — Boost</p>
    <button id="start-btn">LAUNCH</button>
  </div>
</div>
<script>
'use strict';

// ── const: CHUNK_SIZE ──
const CHUNK_SIZE = 200;

// ── const: CHUNKS_AHEAD ──
const CHUNKS_AHEAD = 6;

// ── const: CHUNKS_BEHIND ──
const CHUNKS_BEHIND = 2;

// ── const: PLAYER_SPEED_BASE ──
const PLAYER_SPEED_BASE = 80;

// ── const: PLAYER_MAX_SPEED ──
const PLAYER_MAX_SPEED = 180;

// ── const: BOOST_MULTIPLIER ──
const BOOST_MULTIPLIER = 1.8;

// ── const: TURN_RATE ──
const TURN_RATE = 3.5;

// ── const: BANK_ANGLE ──
const BANK_ANGLE = 0.4;

// ── const: ENERGY_DECAY ──
const ENERGY_DECAY = 3.0;

// ── const: ENERGY_MAX ──
const ENERGY_MAX = 100;

// ── const: ORB_ENERGY ──
const ORB_ENERGY = 25;

// ── const: NEAR_MISS_DIST ──
const NEAR_MISS_DIST = 6;

// ── const: COLLISION_RADIUS ──
const COLLISION_RADIUS = 2.5;

// ── const: COLORS ──
const COLORS = {
  skyTop: new THREE.Color(0x1a0533),
  skyBottom: new THREE.Color(0xff6b35),
  canyonWall: [new THREE.Color(0x4a2040), new THREE.Color(0x3d1f3c), new THREE.Color(0x5c2e50)],
  monolith: new THREE.Color(0x7209b7),
  squeeze: new THREE.Color(0x6b3fa0),
  orb: new THREE.Color(0xffd700),
  playerBody: new THREE.Color(0xc0c0c0),
  playerAccent: new THREE.Color(0xff6b35),
  engineGlow: new THREE.Color(0x00d4ff),
};

// ── class: AudioSystem ──
class AudioSystem {
  constructor() {
    this.ctx = null;
    this.engineOsc = null;
    this.engineGain = null;
    this.initialized = false;
  }
  init() {
    if (this.initialized) return;
    this.ctx = new (window.AudioContext || window.webkitAudioContext)();
    // Engine hum - low frequency oscillator with filter
    this.engineOsc = this.ctx.createOscillator();
    this.engineGain = this.ctx.createGain();
    const engineFilter = this.ctx.createBiquadFilter();
    engineFilter.type = 'lowpass';
    engineFilter.frequency.value = 200;
    this.engineOsc.type = 'sawtooth';
    this.engineOsc.frequency.value = 55;
    this.engineGain.gain.value = 0.08;
    this.engineOsc.connect(engineFilter);
    engineFilter.connect(this.engineGain);
    this.engineGain.connect(this.ctx.destination);
    this.engineOsc.start();
    this.initialized = true;
  }
  setEnginePitch(speedRatio) {
    if (!this.initialized) return;
    const freq = 55 + speedRatio * 80;
    this.engineOsc.frequency.setTargetAtTime(freq, this.ctx.currentTime, 0.1);
    this.engineGain.gain.setTargetAtTime(0.06 + speedRatio * 0.06, this.ctx.currentTime, 0.1);
  }
  playCollect() {
    if (!this.initialized) return;
    const osc = this.ctx.createOscillator();
    const gain = this.ctx.createGain();
    osc.type = 'sine';
    osc.frequency.setValueAtTime(880, this.ctx.currentTime);
    osc.frequency.exponentialRampToValueAtTime(1760, this.ctx.currentTime + 0.15);
    gain.gain.setValueAtTime(0.15, this.ctx.currentTime);
    gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.3);
    osc.connect(gain);
    gain.connect(this.ctx.destination);
    osc.start();
    osc.stop(this.ctx.currentTime + 0.3);
  }
  playNearMiss() {
    if (!this.initialized) return;
    const bufferSize = this.ctx.sampleRate * 0.2;
    const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
    const data = buffer.getChannelData(0);
    for (let i = 0; i < bufferSize; i++) {
      data[i] = (Math.random() * 2 - 1) * Math.exp(-i / (bufferSize * 0.3));
    }
    const source = this.ctx.createBufferSource();
    source.buffer = buffer;
    const filter = this.ctx.createBiquadFilter();
    filter.type = 'bandpass';
    filter.frequency.value = 2000;
    filter.Q.value = 2;
    const gain = this.ctx.createGain();
    gain.gain.setValueAtTime(0.12, this.ctx.currentTime);
    source.connect(filter);
    filter.connect(gain);
    gain.connect(this.ctx.destination);
    source.start();
  }
  playCrash() {
    if (!this.initialized) return;
    const bufferSize = this.ctx.sampleRate * 0.8;
    const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
    const data = buffer.getChannelData(0);
    for (let i = 0; i < bufferSize; i++) {
      data[i] = (Math.random() * 2 - 1) * Math.exp(-i / (bufferSize * 0.4));
    }
    const source = this.ctx.createBufferSource();
    source.buffer = buffer;
    const filter = this.ctx.createBiquadFilter();
    filter.type = 'lowpass';
    filter.frequency.setValueAtTime(3000, this.ctx.currentTime);
    filter.frequency.exponentialRampToValueAtTime(100, this.ctx.currentTime + 0.6);
    const gain = this.ctx.createGain();
    gain.gain.setValueAtTime(0.3, this.ctx.currentTime);
    source.connect(filter);
    filter.connect(gain);
    gain.connect(this.ctx.destination);
    source.start();
    // Sub bass hit
    const osc = this.ctx.createOscillator();
    const g2 = this.ctx.createGain();
    osc.type = 'sine';
    osc.frequency.setValueAtTime(80, this.ctx.currentTime);
    osc.frequency.exponentialRampToValueAtTime(30, this.ctx.currentTime + 0.5);
    g2.gain.setValueAtTime(0.25, this.ctx.currentTime);
    g2.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.5);
    osc.connect(g2);
    g2.connect(this.ctx.destination);
    osc.start();
    osc.stop(this.ctx.currentTime + 0.5);
  }
  playBoost() {
    if (!this.initialized) return;
    const osc = this.ctx.createOscillator();
    const gain = this.ctx.createGain();
    osc.type = 'sawtooth';
    osc.frequency.setValueAtTime(200, this.ctx.currentTime);
    osc.frequency.exponentialRampToValueAtTime(600, this.ctx.currentTime + 0.3);
    gain.gain.setValueAtTime(0.1, this.ctx.currentTime);
    gain.gain.exponentialRampToValueAtTime(0.02, this.ctx.currentTime + 0.4);
    osc.connect(gain);
    gain.connect(this.ctx.destination);
    osc.start();
    osc.stop(this.ctx.currentTime + 0.4);
  }
}

// ── class: PlayerCraft ──
class PlayerCraft {
  constructor(scene) {
    this.group = new THREE.Group();
    this.position = new THREE.Vector3(0, 0, 0);
    this.velocity = new THREE.Vector3(0, 0, 0);
    this.speed = PLAYER_SPEED_BASE;
    this.targetSpeed = PLAYER_SPEED_BASE;
    this.rollAngle = 0;
    this.pitchAngle = 0;
    this.buildModel(scene);
  }
  buildModel() {
    // Main body - sleek wedge shape
    const bodyGeo = new THREE.ConeGeometry(1.2, 4, 6);
    bodyGeo.rotateX(Math.PI / 2);
    const bodyMat = new THREE.MeshLambertMaterial({ color: COLORS.playerBody, flatShading: true });
    this.bodyMesh = new THREE.Mesh(bodyGeo, bodyMat);
    this.group.add(this.bodyMesh);

    // Cockpit accent
    const cockpitGeo = new THREE.BoxGeometry(0.6, 0.4, 1.5);
    const cockpitMat = new THREE.MeshLambertMaterial({ color: COLORS.playerAccent, flatShading: true });
    this.cockpitMesh = new THREE.Mesh(cockpitGeo, cockpitMat);
    this.cockpitMesh.position.set(0, 0.3, -0.2);
    this.group.add(this.cockpitMesh);

    // Wings
    const wingShape = new THREE.Shape();
    wingShape.moveTo(0, 0);
    wingShape.lineTo(3, -1);
    wingShape.lineTo(3, -1.5);
    wingShape.lineTo(0, -0.8);
    wingShape.closePath();

    const wingGeoL = new THREE.ExtrudeGeometry(wingShape, { depth: 0.15, bevelEnabled: false });
    const wingMat = new THREE.MeshLambertMaterial({ color: COLORS.playerBody, flatShading: true });
    
    this.wingL = new THREE.Mesh(wingGeoL, wingMat);
    this.wingL.rotation.x = -Math.PI / 2;
    this.wingL.position.set(0.5, -0.1, 0.3);
    this.group.add(this.wingL);

    const wingR = new THREE.Mesh(wingGeoL.clone(), wingMat);
    wingR.rotation.x = Math.PI / 2;
    wingR.rotation.z = Math.PI;
    wingR.position.set(-0.5, -0.1, 0.3);
    this.group.add(wingR);

    // Engine glow (emissive sphere)
    const engineGeo = new THREE.SphereGeometry(0.4, 6, 6);
    const engineMat = new THREE.MeshBasicMaterial({ color: COLORS.engineGlow });
    this.engineMesh = new THREE.Mesh(engineGeo, engineMat);
    this.engineMesh.position.set(0, 0, 2);
    this.group.add(this.engineMesh);

    // Engine point light
    this.engineLight = new THREE.PointLight(COLORS.engineGlow.getHex(), 3, 15);
    this.engineLight.position.copy(this.engineMesh.position);
    this.group.add(this.engineLight);
  }
  update(dt) {
    // Smooth speed transition
    const accelRate = 40;
    if (this.speed < this.targetSpeed) {
      this.speed += accelRate * dt;
    } else if (this.speed > this.targetSpeed) {
      this.speed -= accelRate * 2 * dt;
    }

    // Apply velocity from input
    const moveX = this.velocity.x * dt;
    const moveY = this.velocity.y * dt;
    
    this.position.x += moveX;
    this.position.y += moveY;

    // Banking (roll) based on horizontal movement
    const targetRoll = -this.velocity.x * 0.04;
    this.rollAngle += (targetRoll - this.rollAngle) * Math.min(1, dt * 8);
    
    // Pitch based on vertical movement
    const targetPitch = this.velocity.y * 0.03;
    this.pitchAngle += (targetPitch - this.pitchAngle) * Math.min(1, dt * 8);

    // Apply rotation to group
    this.group.position.copy(this.position);
    this.group.rotation.z = this.rollAngle;
    this.group.rotation.x = this.pitchAngle;

    // Engine glow pulse based on speed
    const speedRatio = (this.speed - PLAYER_SPEED_BASE) / (PLAYER_MAX_SPEED - PLAYER_SPEED_BASE);
    const engineScale = 1 + speedRatio * 0.5;
    this.engineMesh.scale.setScalar(engineScale);
    this.engineLight.intensity = 2 + speedRatio * 4;
  }
}

// ── class: ParticleSystem ──
class ParticleSystem {
  constructor(scene) {
    this.scene = scene;
    this.trailParticles = [];
    this.speedDust = [];
    this.maxTrail = 60;
    this.maxDust = 80;

    // Trail particle pool
    const trailGeo = new THREE.SphereGeometry(0.15, 4, 4);
    const trailMat = new THREE.MeshBasicMaterial({ color: COLORS.engineGlow });
    for (let i = 0; i < this.maxTrail; i++) {
      const p = new THREE.Mesh(trailGeo, trailMat.clone());
      p.visible = false;
      scene.add(p);
      this.trailParticles.push({ mesh: p, life: 0, maxLife: 1 });
    }

    // Speed dust pool
    const dustGeo = new THREE.BoxGeometry(0.2, 0.2, 0.8);
    const dustMat = new THREE.MeshBasicMaterial({ color: 0xffd700, transparent: true, opacity: 0.6 });
    for (let i = 0; i < this.maxDust; i++) {
      const p = new THREE.Mesh(dustGeo, dustMat.clone());
      p.visible = false;
      scene.add(p);
      this.speedDust.push({ mesh: p, life: 0, maxLife: 1, vx: 0, vy: 0 });
    }

    this.trailTimer = 0;
    this.dustTimer = 0;
  }
  emitTrail(pos) {
    const particle = this.trailParticles.find(p => !p.mesh.visible);
    if (!particle) return;
    particle.mesh.position.copy(pos);
    particle.mesh.position.x += (Math.random() - 0.5) * 0.3;
    particle.mesh.position.y += (Math.random() - 0.5) * 0.3;
    particle.life = 0;
    particle.maxLife = 0.6 + Math.random() * 0.4;
    particle.mesh.visible = true;
    particle.mesh.scale.setScalar(1);
  }
  emitDust(pos, speedRatio) {
    const particle = this.speedDust.find(p => !p.mesh.visible);
    if (!particle) return;
    particle.mesh.position.copy(pos);
    particle.mesh.position.x += (Math.random() - 0.5) * 15;
    particle.mesh.position.y += (Math.random() - 0.5) * 12;
    particle.life = 0;
    particle.maxLife = 0.3 + Math.random() * 0.5;
    particle.vx = (Math.random() - 0.5) * 2;
    particle.vy = (Math.random() - 0.5) * 2;
    particle.mesh.visible = true;
    const s = 1 + speedRatio * 3;
    particle.mesh.scale.set(1, 1, s);
    particle.mesh.material.opacity = 0.4 + Math.random() * 0.3;
  }
  update(dt) {
    // Update trail particles
    for (const p of this.trailParticles) {
      if (!p.mesh.visible) continue;
      p.life += dt;
      const t = p.life / p.maxLife;
      if (t >= 1) {
        p.mesh.visible = false;
        continue;
      }
      p.mesh.material.opacity = 1 - t;
      p.mesh.scale.setScalar(1 - t * 0.5);
    }

    // Update dust particles
    for (const p of this.speedDust) {
      if (!p.mesh.visible) continue;
      p.life += dt;
      const t = p.life / p.maxLife;
      if (t >= 1) {
        p.mesh.visible = false;
        continue;
      }
      p.mesh.position.x += p.vx * dt;
      p.mesh.position.y += p.vy * dt;
      p.mesh.material.opacity = (1 - t) * 0.6;
    }
  }
}

// ── class: CollisionDetector ──
class CollisionDetector {
  constructor() {
    this.nearMissCooldown = 0;
  }
  check(playerPos, playerRadius, chunkGroups) {
    let collision = false;
    let nearMiss = false;
    let minDist = Infinity;

    for (const [idx, group] of chunkGroups) {
      const worldZOffset = group.position.z;
      
      // Quick Z-range cull
      if (Math.abs(playerPos.z - worldZOffset) > CHUNK_SIZE * 0.6) continue;

      for (const col of (group.userData.colliders || [])) {
        // AABB vs sphere check
        const dx = Math.max(0, Math.abs(playerPos.x - col.x) - col.w);
        const dy = Math.max(0, Math.abs(playerPos.y - col.y) - col.h);
        const dz = Math.max(0, Math.abs(playerPos.z - (col.z + worldZOffset)) - col.d);
        
        const distSq = dx * dx + dy * dy + dz * dz;
        const dist = Math.sqrt(distSq);

        if (dist < playerRadius) {
          collision = true;
        }
        if (dist < NEAR_MISS_DIST && dist > playerRadius) {
          nearMiss = true;
          minDist = Math.min(minDist, dist);
        }
      }

      // Check orbs for collection
      group.traverse(child => {
        if (!child.userData.isOrb) return;
        const worldPos = new THREE.Vector3();
        child.getWorldPosition(worldPos);
        const orbDist = playerPos.distanceTo(worldPos);
        if (orbDist < 2.5) {
          child.userData.collected = true;
          // Hide the orb and its children
          child.visible = false;
          child.children.forEach(c => c.visible = false);
        }
      });
    }

    return { collision, nearMiss, minDist };
  }
}

// ── class: CanyonGenerator ──
class CanyonGenerator {
  constructor(scene) {
    this.scene = scene;
    this.chunks = new Map();
    this.nextChunkZ = 0;
    this.rngSeed = Math.random() * 999999;
  }
  seededRandom() {
    this.rngSeed = (this.rngSeed * 16807 + 0) % 2147483647;
    return (this.rngSeed - 1) / 2147483646;
  }
  getChunkIndex(z) {
    return Math.floor((z + CHUNK_SIZE * CHUNKS_AHEAD) / CHUNK_SIZE);
  }
  generateChunk(chunkZ, difficulty) {
    const group = new THREE.Group();
    group.position.z = chunkZ;
    const colliders = [];

    const baseWidth = Math.max(12, 30 - (difficulty || 0) * 2);
    const canyonType = this.seededRandom();

    if (canyonType < 0.5) {
      this.buildCanyonWalls(group, colliders, chunkZ, baseWidth, difficulty || 0);
    } else if (canyonType < 0.8) {
      this.buildMonoliths(group, colliders, chunkZ, baseWidth, difficulty || 0);
    } else {
      this.buildTightSqueeze(group, colliders, chunkZ, baseWidth, difficulty || 0);
    }

    this.addOrbs(group, chunkZ, baseWidth);
    group.userData.colliders = colliders;
    return group;
  }
  buildCanyonWalls(group, colliders, chunkZ, width, difficulty) {
    const wallMats = COLORS.canyonWall.map(c => new THREE.MeshLambertMaterial({ color: c, flatShading: true }));

    for (let z = 0; z < CHUNK_SIZE; z += 15) {
      const w = width + Math.sin(z * 0.05 + chunkZ * 0.01) * 8;
      const heightL = 20 + this.seededRandom() * 15;
      const heightR = 20 + this.seededRandom() * 15;

      const lwGeo = new THREE.BoxGeometry(4, heightL, 16);
      const lWall = new THREE.Mesh(lwGeo, wallMats[Math.floor(this.seededRandom() * 3)]);
      lWall.position.set(-w / 2 - 2, heightL / 2 - 5, z - CHUNK_SIZE / 2);
      group.add(lWall);

      const rwGeo = new THREE.BoxGeometry(4, heightR, 16);
      const rWall = new THREE.Mesh(rwGeo, wallMats[Math.floor(this.seededRandom() * 3)]);
      rWall.position.set(w / 2 + 2, heightR / 2 - 5, z - CHUNK_SIZE / 2);
      group.add(rWall);

      const groundGeo = new THREE.BoxGeometry(width, 1, 16);
      const groundMat = new THREE.MeshLambertMaterial({ color: 0x2a1530, flatShading: true });
      const ground = new THREE.Mesh(groundGeo, groundMat);
      ground.position.set(0, -8, z - CHUNK_SIZE / 2);
      group.add(ground);

      if (this.seededRandom() > 0.4) {
        const ceilGeo = new THREE.BoxGeometry(width + 10, 3, 16);
        const ceilMat = new THREE.MeshLambertMaterial({ color: 0x2a1530, flatShading: true });
        const ceiling = new THREE.Mesh(ceilGeo, ceilMat);
        ceiling.position.set(0, 18 + this.seededRandom() * 5, z - CHUNK_SIZE / 2);
        group.add(ceiling);
      }

      colliders.push({ x: -w/2 - 2, y: heightL/2 - 5, z: z - CHUNK_SIZE/2, w: 4, h: heightL, d: 16 });
      colliders.push({ x: w/2 + 2, y: heightR/2 - 5, z: z - CHUNK_SIZE/2, w: 4, h: heightR, d: 16 });
    }
  }
  buildMonoliths(group, colliders, chunkZ, width, difficulty) {
    const count = Math.floor(3 + (difficulty || 0) * 2);
    for (let i = 0; i < count; i++) {
      const zPos = this.seededRandom() * CHUNK_SIZE - CHUNK_SIZE / 2;
      const xPos = (this.seededRandom() - 0.5) * width * 0.7;
      const yPos = (this.seededRandom() - 0.3) * 15;
      
      const h = 8 + this.seededRandom() * 12;
      const w = 2 + this.seededRandom() * 3;
      const d = 2 + this.seededRandom() * 3;

      const geo = new THREE.BoxGeometry(w, h, d);
      const mat = new THREE.MeshLambertMaterial({ color: COLORS.monolith, flatShading: true });
      const mesh = new THREE.Mesh(geo, mat);
      mesh.position.set(xPos, yPos, zPos);
      group.add(mesh);

      const edgeGeo = new THREE.BoxGeometry(w + 0.2, h + 0.2, d + 0.2);
      const edges = new THREE.EdgesGeometry(edgeGeo);
      const lineMat = new THREE.LineBasicMaterial({ color: 0xff6b35 });
      const wireframe = new THREE.LineSegments(edges, lineMat);
      mesh.add(wireframe);

      colliders.push({ x: xPos, y: yPos, z: zPos, w: w/2 + COLLISION_RADIUS * 0.5, h: h/2, d: d/2 });
    }

    const groundGeo = new THREE.BoxGeometry(width, 1, CHUNK_SIZE);
    const groundMat = new THREE.MeshLambertMaterial({ color: 0x2a1530, flatShading: true });
    const ground = new THREE.Mesh(groundGeo, groundMat);
    ground.position.set(0, -8, 0);
    group.add(ground);

    for (let side of [-1, 1]) {
      const wallGeo = new THREE.BoxGeometry(2, 30, CHUNK_SIZE);
      const wallMat = new THREE.MeshLambertMaterial({ color: COLORS.canyonWall[0], flatShading: true });
      const wall = new THREE.Mesh(wallGeo, wallMat);
      wall.position.set(side * (width/2 + 1), 5, 0);
      group.add(wall);
    }
  }
  buildTightSqueeze(group, colliders, chunkZ, width, difficulty) {
    const squeezeWidth = Math.max(6, width * 0.4);

    for (let side of [-1, 1]) {
      const wallGeo = new THREE.BoxGeometry(3, 25, CHUNK_SIZE);
      const wallMat = new THREE.MeshLambertMaterial({ color: COLORS.squeeze, flatShading: true });
      const wall = new THREE.Mesh(wallGeo, wallMat);
      wall.position.set(side * squeezeWidth / 2, 3, 0);
      group.add(wall);

      for (let z = 10; z < CHUNK_SIZE - 10; z += 25) {
        const protrudeW = 1 + this.seededRandom() * 2;
        const pGeo = new THREE.BoxGeometry(protrudeW, 3 + this.seededRandom() * 4, 8);
        const pMat = new THREE.MeshLambertMaterial({ color: COLORS.squeeze, flatShading: true });
        const protrusion = new THREE.Mesh(pGeo, pMat);
        protrusion.position.set(side * (squeezeWidth / 2 - protrudeW / 2), this.seededRandom() * 10 - 3, z - CHUNK_SIZE / 2);
        group.add(protrusion);

        colliders.push({
          x: side * (squeezeWidth/2 - protrudeW/2),
          y: protrusion.position.y,
          z: z - CHUNK_SIZE/2,
          w: protrudeW/2 + COLLISION_RADIUS * 0.3,
          h: 2, d: 4
        });
      }

      colliders.push({ x: side * squeezeWidth/2, y: 3, z: 0, w: 1.5, h: 12.5, d: CHUNK_SIZE/2 });
    }

    const groundGeo = new THREE.BoxGeometry(width, 1, CHUNK_SIZE);
    const groundMat = new THREE.MeshLambertMaterial({ color: 0x2a1530, flatShading: true });
    group.add(new THREE.Mesh(groundGeo, groundMat)).position.set(0, -8, 0);

    const ceilGeo = new THREE.BoxGeometry(squeezeWidth + 4, 2, CHUNK_SIZE);
    const ceilMat = new THREE.MeshLambertMaterial({ color: 0x3d1f3c, flatShading: true });
    group.add(new THREE.Mesh(ceilGeo, ceilMat)).position.set(0, 16, 0);

    colliders.push({ x: 0, y: 15, z: 0, w: squeezeWidth/2 + 2, h: 1, d: CHUNK_SIZE/2 });
  }
  addOrbs(group, chunkZ, width) {
    const orbCount = Math.floor(4 + this.seededRandom() * 3);
    for (let i = 0; i < orbCount; i++) {
      const zPos = this.seededRandom() * CHUNK_SIZE - CHUNK_SIZE / 2;
      const xPos = (this.seededRandom() - 0.5) * width * 0.6;
      const yPos = (this.seededRandom() - 0.3) * 12;

      const orbGeo = new THREE.OctahedronGeometry(0.8, 0);
      const orbMat = new THREE.MeshBasicMaterial({ color: COLORS.orb });
      const orb = new THREE.Mesh(orbGeo, orbMat);
      orb.position.set(xPos, yPos, zPos);
      orb.userData.isOrb = true;
      group.add(orb);

      const glowGeo = new THREE.SphereGeometry(1.5, 6, 6);
      const glowMat = new THREE.MeshBasicMaterial({ color: COLORS.orb, transparent: true, opacity: 0.2 });
      const glow = new THREE.Mesh(glowGeo, glowMat);
      orb.add(glow);

      const orbLight = new THREE.PointLight(COLORS.orb.getHex(), 1, 8);
      orb.add(orbLight);
    }
  }
  update(playerZ) {
    while (this.nextChunkZ <= playerZ + CHUNK_SIZE * CHUNKS_AHEAD) {
      const idx = this.getChunkIndex(this.nextChunkZ);
      if (!this.chunks.has(idx)) {
        const group = this.generateChunk(this.nextChunkZ, 0);
        this.scene.add(group);
        this.chunks.set(idx, group);
      }
      this.nextChunkZ += CHUNK_SIZE;
    }

    for (const [idx, grp] of this.chunks) {
      if (grp.position.z < playerZ - CHUNK_SIZE * CHUNKS_BEHIND) {
        this.scene.remove(grp);
        this.disposeGroup(grp);
        this.chunks.delete(idx);
      }
    }
  }
  disposeGroup(group) {
    group.traverse(child => {
      if (child.geometry) child.geometry.dispose();
      if (child.material) {
        if (Array.isArray(child.material)) {
          child.material.forEach(m => m.dispose());
        } else {
          child.material.dispose();
        }
      }
    });
  }
}

// ── class: Game ──
class Game {
  constructor() {
    this.state = 'menu';
    this.score = 0;
    this.maxSpeed = 0;
    this.energy = ENERGY_MAX;
    this.turbo = 0;
    this.multiplier = 1;
    this.multiplierTimer = 0;
    this.difficulty = 0;
    this.cameraShake = { x: 0, y: 0, intensity: 0 };
    this.fovTarget = 75;
    this.boostActive = false;
    this.keys = {};

    // Three.js setup
    this.canvas = document.getElementById('game-canvas');
    this.renderer = new THREE.WebGLRenderer({ canvas: this.canvas, antialias: true });
    this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    
    this.scene = new THREE.Scene();
    this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
    this.scene.fog = new THREE.FogExp2(0x1a0533, 0.008);

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

    const dirLight = new THREE.DirectionalLight(0xffaa44, 1.2);
    dirLight.position.set(30, 50, -20);
    this.scene.add(dirLight);

    const hemiLight = new THREE.HemisphereLight(COLORS.skyBottom.getHex(), COLORS.skyTop.getHex(), 0.6);
    this.scene.add(hemiLight);

    // Background gradient sphere
    const bgGeo = new THREE.SphereGeometry(400, 16, 16);
    const bgMat = new THREE.ShaderMaterial({
      side: THREE.BackSide,
      uniforms: {
        topColor: { value: COLORS.skyTop },
        bottomColor: { value: COLORS.skyBottom }
      },
      vertexShader: `
        varying vec3 vWorldPosition;
        void main() {
          vec4 worldPos = modelMatrix * vec4(position, 1.0);
          vWorldPosition = worldPos.xyz;
          gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
        }
      `,
      fragmentShader: `
        uniform vec3 topColor;
        uniform vec3 bottomColor;
        varying vec3 vWorldPosition;
        void main() {
          float h = normalize(vWorldPosition).y * 0.5 + 0.5;
          gl_FragColor = vec4(mix(bottomColor, topColor, h), 1.0);
        }
      `
    });
    this.scene.add(new THREE.Mesh(bgGeo, bgMat));

    // Systems
    this.audio = new AudioSystem();
    this.player = new PlayerCraft(this.scene);
    this.generator = new CanyonGenerator(this.scene);
    this.particles = new ParticleSystem(this.scene);
    this.collisionDetector = new CollisionDetector();

    // HUD elements
    this.hudSpeedFill = document.getElementById('speed-fill');
    this.hudMultiplier = document.getElementById('multiplier');
    this.hudEnergyFill = document.getElementById('energy-fill');
    this.hudTurboFill = document.getElementById('turbo-fill');
    this.hudScoreDisplay = document.getElementById('score-display');
    this.screenGameOver = document.getElementById('game-over');
    this.screenStart = document.getElementById('start-screen');

    // Events
    window.addEventListener('resize', () => this.onResize());
    window.addEventListener('keydown', e => { this.keys[e.code] = true; });
    window.addEventListener('keyup', e => { this.keys[e.code] = false; });
    
    document.getElementById('start-btn').addEventListener('click', () => this.startGame());
    document.getElementById('restart-btn').addEventListener('click', () => this.restartGame());

    this.onResize();
    this.lastTime = performance.now();
    this.animate();
  }
  onResize() {
    const w = window.innerWidth;
    const h = window.innerHeight;
    this.camera.aspect = w / h;
    this.camera.updateProjectionMatrix();
    this.renderer.setSize(w, h);
  }
  startGame() {
    this.audio.init();
    this.screenStart.style.display = 'none';
    this.resetGameState();
    this.state = 'playing';
  }
  restartGame() {
    this.screenGameOver.style.display = 'none';
    for (const [idx, grp] of this.generator.chunks) {
      this.scene.remove(grp);
      this.generator.disposeGroup(grp);
    }
    this.generator.chunks.clear();
    this.generator.nextChunkZ = 0;
    this.resetGameState();
    this.state = 'playing';
  }
  resetGameState() {
    this.score = 0;
    this.maxSpeed = 0;
    this.energy = ENERGY_MAX;
    this.turbo = 0;
    this.multiplier = 1;
    this.difficulty = 0;
    this.boostActive = false;
    this.player.position.set(0, 0, 0);
    this.player.velocity.set(0, 0, 0);
    this.player.speed = PLAYER_SPEED_BASE;
    this.player.targetSpeed = PLAYER_SPEED_BASE;
    this.player.rollAngle = 0;
    this.player.pitchAngle = 0;
    this.camera.position.set(0, 3, 12);
    this.camera.lookAt(0, 0, -50);
    this.camera.fov = 75;
    this.camera.updateProjectionMatrix();
  }
  handleInput(dt) {
    const turnSpeed = TURN_RATE * (this.player.speed / PLAYER_SPEED_BASE);
    
    if (this.keys['ArrowLeft'] || this.keys['KeyA']) {
      this.player.velocity.x -= turnSpeed * dt;
    }
    if (this.keys['ArrowRight'] || this.keys['KeyD']) {
      this.player.velocity.x += turnSpeed * dt;
    }
    if (this.keys['ArrowUp'] || this.keys['KeyW']) {
      this.player.velocity.y += turnSpeed * dt;
    }
    if (this.keys['ArrowDown'] || this.keys['KeyS']) {
      this.player.velocity.y -= turnSpeed * dt;
    }

    const damping = Math.exp(-5 * dt);
    this.player.velocity.x *= damping;
    this.player.velocity.y *= damping;

    // Clamp position to canyon bounds (soft)
    if (this.player.position.x > 20) { this.player.position.x = 20; this.player.velocity.x *= -0.5; }
    if (this.player.position.x < -20) { this.player.position.x = -20; this.player.velocity.x *= -0.5; }
    if (this.player.position.y > 12) { this.player.position.y = 12; this.player.velocity.y *= -0.5; }
    if (this.player.position.y < -6) { this.player.position.y = -6; this.player.velocity.y *= -0.5; }

    // Boost
    const boosting = this.keys['Space'] && this.turbo > 10;
    if (boosting) {
      this.turbo -= 30 * dt;
      this.player.targetSpeed = PLAYER_MAX_SPEED * BOOST_MULTIPLIER;
      this.fovTarget = 95;
      if (!this.boostActive) {
        this.audio.playBoost();
        this.boostActive = true;
      }
    } else {
      this.player.targetSpeed = Math.min(PLAYER_MAX_SPEED, PLAYER_SPEED_BASE + this.difficulty * 8);
      this.fovTarget = 75;
      this.boostActive = false;
      
      // Build turbo on straightaways
      if (Math.abs(this.player.velocity.x) < 1 && Math.abs(this.player.velocity.y) < 1) {
        this.turbo = Math.min(100, this.turbo + 8 * dt);
      }
    }

    this.turbo = Math.max(0, Math.min(100, this.turbo));
  }
  updateGame(dt) {
    if (this.state !== 'playing') return;

    const speedRatio = (this.player.speed - PLAYER_SPEED_BASE) / (PLAYER_MAX_SPEED * BOOST_MULTIPLIER - PLAYER_SPEED_BASE);
    
    // Move player forward
    this.player.position.z -= this.player.speed * dt;
    
    if (this.player.speed > this.maxSpeed) {
      this.maxSpeed = this.player.speed;
    }

    this.handleInput(dt);
    this.player.update(dt);

    // Energy decay
    this.energy -= ENERGY_DECAY * dt;
    if (this.energy <= 0) {
      this.gameOver();
      return;
    }

    // Difficulty scaling
    this.difficulty = Math.min(15, this.score / 500);

    // Update canyon generator
    this.generator.update(this.player.position.z);

    // Collision detection & orb collection
    const result = this.collisionDetector.check(
      this.player.position, COLLISION_RADIUS, this.generator.chunks
    );

    if (result.collision) {
      this.audio.playCrash();
      this.gameOver();
      return;
    }

    // Check for collected orbs and award energy/score
    let orbsCollected = 0;
    for (const [idx, group] of this.generator.chunks) {
      group.traverse(child => {
        if (child.userData.isOrb && child.userData.collected) {
          orbsCollected++;
        }
      });
    }
    if (orbsCollected > 0) {
      this.energy = Math.min(ENERGY_MAX, this.energy + orbsCollected * ORB_ENERGY);
      this.score += orbsCollected * 25;
      this.audio.playCollect();
    }

    // Near miss bonus
    if (result.nearMiss && this.multiplierTimer <= 0) {
      this.multiplier = Math.min(10, this.multiplier + 0.5);
      this.multiplierTimer = 2;
      this.score += 50 * this.multiplier;
      this.cameraShake.intensity = 0.3;
      this.audio.playNearMiss();
    }

    // Multiplier decay
    if (this.multiplierTimer > 0) {
      this.multiplierTimer -= dt;
      if (this.multiplierTimer <= 0) {
        this.multiplier = Math.max(1, this.multiplier - 0.5);
      }
    }

    // Score from distance traveled
    this.score += this.player.speed * dt * 0.1 * this.multiplier;

    // Camera follow with shake
    const camOffsetX = this.player.position.x * 0.4 + this.cameraShake.x;
    const camOffsetY = 3 + this.player.position.y * 0.3 + this.cameraShake.y;
    this.camera.position.set(camOffsetX, camOffsetY, this.player.position.z + 12);
    this.camera.lookAt(this.player.position.x * 0.5, this.player.position.y * 0.5, this.player.position.z - 60);

    // FOV transition
    this.camera.fov += (this.fovTarget - this.camera.fov) * dt * 3;
    this.camera.updateProjectionMatrix();

    // Camera shake decay
    if (this.cameraShake.intensity > 0) {
      this.cameraShake.x = (Math.random() - 0.5) * this.cameraShake.intensity;
      this.cameraShake.y = (Math.random() - 0.5) * this.cameraShake.intensity;
      this.cameraShake.intensity *= Math.exp(-8 * dt);
    } else {
      this.cameraShake.x = 0;
      this.cameraShake.y = 0;
    }

    // Particles - trail
    this.particles.trailTimer += dt;
    if (this.particles.trailTimer > 0.02) {
      const trailPos = new THREE.Vector3(
        this.player.position.x, 
        this.player.position.y, 
        this.player.position.z + 2
      );
      this.particles.emitTrail(trailPos);
      this.particles.trailTimer = 0;
    }

    // Particles - speed dust (more frequent at higher speeds)
    this.particles.dustTimer += dt;
    const dustInterval = Math.max(0.01, 0.05 - speedRatio * 0.04);
    if (this.particles.dustTimer > dustInterval) {
      const dustPos = new THREE.Vector3(
        this.player.position.x, 
        this.player.position.y, 
        this.player.position.z + 8
      );
      this.particles.emitDust(dustPos, speedRatio);
      this.particles.dustTimer = 0;
    }

    this.particles.update(dt);
    this.audio.setEnginePitch(speedRatio);

    // Animate orbs (rotation)
    for (const [idx, group] of this.generator.chunks) {
      group.traverse(child => {
        if (child.userData.isOrb && child.visible) {
          child.rotation.y += dt * 3;
          child.rotation.x += dt * 1.5;
        }
      });
    }

    // Update HUD
    this.updateHUD(speedRatio);
  }
  updateHUD(speedRatio) {
    const speedPct = Math.min(1, Math.max(0, speedRatio));
    this.hudSpeedFill.style.height = (speedPct * 100) + '%';
    
    this.hudMultiplier.textContent = 'x' + this.multiplier.toFixed(1);
    if (this.multiplierTimer > 0) {
      this.hudMultiplier.style.transform = 'scale(' + (1 + Math.sin(Date.now() * 0.01) * 0.15) + ')';
      this.hudMultiplier.style.color = '#ff6b35';
    } else {
      this.hudMultiplier.style.transform = 'scale(1)';
      this.hudMultiplier.style.color = '#ffd700';
    }

    const energyPct = Math.max(0, this.energy / ENERGY_MAX);
    this.hudEnergyFill.style.width = (energyPct * 100) + '%';
    
    if (energyPct < 0.25) {
      this.hudEnergyFill.style.background = 'linear-gradient(to right, #ff3d3d, #ff6b35)';
    } else {
      this.hudEnergyFill.style.background = 'linear-gradient(to right, #7209b7, #ff6b35)';
    }

    this.hudTurboFill.style.width = (this.turbo) + '%';
    
    // Score display
    if (this.hudScoreDisplay) {
      this.hudScoreDisplay.textContent = Math.floor(this.score).toLocaleString();
    }
  }
  gameOver() {
    this.state = 'gameover';
    document.getElementById('final-score').textContent = 'Score: ' + Math.floor(this.score).toLocaleString();
    document.getElementById('max-speed').textContent = 'Max Speed: ' + Math.floor(this.maxSpeed);
    this.screenGameOver.style.display = 'flex';
  }
  animate() {
    requestAnimationFrame(() => this.animate());
    
    const now = performance.now();
    let dt = (now - this.lastTime) / 1000;
    this.lastTime = now;
    dt = Math.min(dt, 0.05);

    if (this.state === 'playing') {
      this.updateGame(dt);
    } else if (this.state === 'menu') {
      const t = now * 0.0001;
      this.camera.position.set(Math.sin(t) * 20, 8 + Math.cos(t * 0.7) * 3, -now * 0.005);
      this.camera.lookAt(0, 0, -this.camera.position.z - 60);
      if (this.generator.chunks.size < 4 || true) {
        this.generator.update(this.camera.position.z);
      }
    }

    this.renderer.render(this.scene, this.camera);
  }
}

// ── main ──
const game = new Game();
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingcap-qwen3.6-27b","provider":"lmstudio","persona":"composer","sessionId":"97bbf2ee-db39-4f89-9d3b-3b56020f2acc","tokensIn":1916411,"tokensOut":31548,"tokensTotal":1947959,"turns":39,"toolCalls":38,"failedToolCalls":0,"timestamp":"2026-07-18T02:47:16.323Z"} -->