← Canyon Run results

Canyon Run

ling 3.0 Flash · full

Initial view of Canyon Run

Reviews & generation details

Reviews

No human review yet. Unreviewed does not mean unsuccessful.

Generation context

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

Generation prompt · exact text unavailable

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

Inspect original source 33,640 bytes · SHA-256 f1e28b6d7d52
<!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:#0a0a0f;font-family:'Segoe UI',system-ui,sans-serif;user-select:none}
canvas{display:block}
#hud{position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:10}
#score{position:absolute;top:20px;left:30px;color:#fff;font-size:22px;font-weight:300;letter-spacing:2px}
#multiplier{position:absolute;top:50px;right:40px;font-size:52px;font-weight:800;color:#ffd700;text-shadow:0 0 30px rgba(255,215,0,.6);opacity:0;transition:opacity .2s,transform .15s;transform:scale(1)}
#multiplier.active{opacity:1;transform:scale(1.2)}
#speed-indicator{position:absolute;bottom:40px;left:50%;transform:translateX(-50%);width:280px;height:3px;background:rgba(255,255,255,.08);border-radius:2px;overflow:visible}
#speed-fill{height:100%;width:0%;background:linear-gradient(90deg,#ff6b35,#ffd700);border-radius:2px;transition:width .08s}
#energy-container{position:absolute;bottom:55px;left:50%;transform:translateX(-50%);width:200px;height:6px;background:rgba(0,0,0,.5);border-radius:3px;overflow:hidden}
#energy-fill{height:100%;width:100%;background:linear-gradient(90deg,#00ffcc,#00ccff);border-radius:3px;transition:width .15s;box-shadow:0 0 8px #00ffcc}
#boost-container{position:absolute;bottom:70px;left:50%;transform:translateX(-50%);width:200px;height:4px;background:rgba(0,0,0,.4);border-radius:2px;overflow:hidden}
#boost-fill{height:100%;width:0%;background:linear-gradient(90deg,#ff3366,#ff6b35);border-radius:2px;transition:width .1s;box-shadow:0 0 6px #ff3366}
#game-over{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(5,5,10,.92);display:none;flex-direction:column;align-items:center;justify-content:center;z-index:100;pointer-events:all;backdrop-filter:blur(8px)}
#game-over h1{color:#ff3366;font-size:56px;font-weight:800;margin-bottom:8px;letter-spacing:4px;text-shadow:0 0 40px rgba(255,51,102,.5)}
#game-over .stat{color:rgba(255,255,255,.7);font-size:16px;margin:6px 0;font-weight:300}
#game-over .stat span{color:#ffd700;font-weight:600}
#restart-btn{margin-top:30px;padding:14px 44px;background:linear-gradient(135deg,#ff6b35,#ff3366);border:none;color:#fff;font-size:16px;font-weight:700;border-radius:30px;cursor:pointer;pointer-events:all;letter-spacing:2px;transition:transform .2s,box-shadow .2s}
#restart-btn:hover{transform:scale(1.06);box-shadow:0 0 30px rgba(255,51,102,.4)}
#start-screen{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(5,5,10,.85);display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:200;pointer-events:all;backdrop-filter:blur(4px)}
#start-screen h1{color:#ffd700;font-size:44px;font-weight:800;margin-bottom:6px;letter-spacing:6px;text-shadow:0 0 40px rgba(255,215,0,.4)}
#start-screen .sub{color:rgba(255,255,255,.5);font-size:14px;margin:4px 0;letter-spacing:1px}
#start-screen .controls{margin-top:24px;color:rgba(255,255,255,.5);font-size:13px;text-align:center;line-height:1.8}
#start-screen .controls kbd{background:rgba(255,255,255,.1);padding:2px 8px;border-radius:4px;color:rgba(255,255,255,.7);font-size:12px}
.start-btn{margin-top:36px;padding:14px 48px;background:linear-gradient(135deg,#ff6b35,#ff3366);border:none;color:#fff;font-size:16px;font-weight:700;border-radius:30px;cursor:pointer;pointer-events:all;letter-spacing:2px;transition:transform .2s}
.start-btn:hover{transform:scale(1.05)}
.hidden{display:none!important}
#near-miss-flash{position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:5;opacity:0;transition:opacity .1s;background:radial-gradient(ellipse at center,transparent 40%,rgba(255,215,0,.08) 100%)}
#near-miss-flash.active{opacity:1}
</style>
</head>
<body>
<div id="hud" class="hidden">
  <div id="score">0</div>
  <div id="multiplier">x1</div>
  <div id="speed-indicator"><div id="speed-fill"></div></div>
  <div id="energy-container"><div id="energy-fill"></div></div>
  <div id="boost-container"><div id="boost-fill"></div></div>
</div>
<div id="near-miss-flash"></div>
<div id="game-over">
  <h1>GAME OVER</h1>
  <div class="stat">Final Score: <span id="final-score">0</span></div>
  <div class="stat">Max Speed: <span id="max-speed">0</span></div>
  <div class="stat">Max Multiplier: <span id="max-multiplier">x1</span></div>
  <button id="restart-btn">RESTART</button>
</div>
<div id="start-screen">
  <h1>CANYON RACER</h1>
  <div class="sub">Procedural High-Speed Flight</div>
  <div class="controls">
    <kbd>W</kbd>/<kbd>↑</kbd> Accelerate &nbsp; <kbd>S</kbd>/<kbd>↓</kbd> Brake<br>
    <kbd>A</kbd>/<kbd>←</kbd> Bank Left &nbsp; <kbd>D</kbd>/<kbd>→</kbd> Bank Right<br>
    <kbd>SPACE</kbd> Boost &nbsp; &nbsp; Collect orbs to survive. Fly close to walls for bonus points.
  </div>
  <button class="start-btn" id="start-btn">LAUNCH</button>
</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';

/* ===== CONSTANTS ===== */
const CHUNK_LEN = 180;
const CANYON_BASE_WIDTH = 70;
const CANYON_MIN_WIDTH = 18;
const WALL_THICK = 4;
const WALL_HEIGHT = 45;
const PLAYER_RADIUS = 1.8;
const PLAYER_H = 3;
const NEAR_MISS_DIST = 9;
const COLLISION_DIST = 3.5;
const ENERGY_DECAY = 0.35;
const ORB_RESTORE = 22;
const BOOST_MAX = 100;
const BOOST_DRAIN = 25;
const BOOST_FORCE = 40;
const BASE_SPEED = 55;
const MAX_SPEED = 220;
const ACCEL = 18;
const BRAKE = 22;
const YAW_SPEED = 2.2;
const PITCH_SPEED = 1.8;
const BANK_LERP = 6;
const NUM_SEGS = 30;

/* ===== STATE ===== */
let gameState = 'menu';
let score = 0;
let maxSpeed = 0;
let maxMultiplier = 1;
let multiplier = 1;
let multiplierTimer = 0;
let energy = 100;
let boostMeter = 0;
let isBoosting = false;
let speed = BASE_SPEED;
let shakeIntensity = 0;
let nearMissFlashTimer = 0;
let lastChunkZ = 0;

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

/* ===== AUDIO ===== */
class AudioSys {
  constructor(){ this.ctx=null; this.ready=false; this.osc1=null; this.osc2=null; this.gain1=null; this.gain2=null; }
  init(){
    if(this.ready) return;
    try{
      this.ctx = new (window.AudioContext||window.webkitAudioContext)();
      this.osc1 = this.ctx.createOscillator();
      this.gain1 = this.ctx.createGain();
      this.osc1.type='sawtooth'; this.osc1.frequency.value=50; this.gain1.gain.value=0.025;
      this.osc1.connect(this.gain1); this.gain1.connect(this.ctx.destination); this.osc1.start();
      this.osc2 = this.ctx.createOscillator();
      this.gain2 = this.ctx.createGain();
      this.osc2.type='triangle'; this.osc2.frequency.value=100; this.gain2.gain.value=0.012;
      this.osc2.connect(this.gain2); this.gain2.connect(this.ctx.destination); this.osc2.start();
      this.ready=true;
    }catch(e){}
  }
  updateEngine(spd){
    if(!this.ready) return;
    const t=this.ctx.currentTime;
    const f=45+spd*0.25;
    this.osc1.frequency.setTargetAtTime(f,t,0.1);
    this.osc2.frequency.setTargetAtTime(f*2,t,0.1);
    this.gain1.gain.setTargetAtTime(0.02+spd*0.0002,t,0.1);
    this.gain2.gain.setTargetAtTime(0.008+spd*0.00008,t,0.1);
  }
  _tone(freq,dur,type='sine',vol=0.12,fEnd=null){
    if(!this.ready) return;
    const o=this.ctx.createOscillator(),g=this.ctx.createGain();
    o.type=type; o.frequency.value=freq;
    if(fEnd) o.frequency.exponentialRampToValueAtTime(Math.max(20,fEnd),this.ctx.currentTime+dur);
    g.gain.setValueAtTime(vol,this.ctx.currentTime);
    g.gain.exponentialRampToValueAtTime(0.001,this.ctx.currentTime+dur);
    o.connect(g); g.connect(this.ctx.destination); o.start(); o.stop(this.ctx.currentTime+dur);
  }
  _noise(dur,vol=0.06,freq=2000,Q=1){
    if(!this.ready) return;
    const sr=this.ctx.sampleRate, len=Math.floor(sr*dur);
    const buf=this.ctx.createBuffer(1,len,sr);
    const d=buf.getChannelData(0);
    for(let i=0;i<len;i++) d[i]=(Math.random()*2-1)*Math.pow(1-i/len,1.5);
    const src=this.ctx.createBufferSource(); src.buffer=buf;
    const filt=this.ctx.createBiquadFilter(); filt.type='bandpass'; filt.frequency.value=freq; filt.Q.value=Q;
    const g=this.ctx.createGain();
    g.gain.setValueAtTime(vol,this.ctx.currentTime);
    g.gain.exponentialRampToValueAtTime(0.001,this.ctx.currentTime+dur);
    src.connect(filt); filt.connect(g); g.connect(this.ctx.destination); src.start();
  }
  playCollect(){ this._tone(880,0.25,'sine',0.1,1760); setTimeout(()=>this._tone(1320,0.15,'sine',0.08,2640),80); }
  playNearMiss(){ this._noise(0.12,0.07,2500,0.8); }
  playBoost(){ this._tone(120,0.35,'sawtooth',0.08,900); }
  playCollision(){ this._noise(0.35,0.18,800,0.5); }
  playGameOver(){ this._tone(440,1.2,'sawtooth',0.12,55); }
}
const audio = new AudioSys();

/* ===== THREE.JS SETUP ===== */
const renderer = new THREE.WebGLRenderer({ antialias:true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.1;
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);

const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x0a0a1a, 0.0012);

const camera = new THREE.PerspectiveCamera(70, window.innerWidth/window.innerHeight, 1, 2000);
camera.position.set(0, 8, -20);

const hemiLight = new THREE.HemisphereLight(0x3a1a5a, 0x2a1a0a, 0.4);
scene.add(hemiLight);
const dirLight = new THREE.DirectionalLight(0xff8c40, 1.2);
dirLight.position.set(50, 80, -30);
dirLight.castShadow = true;
dirLight.shadow.mapSize.set(1024, 1024);
dirLight.shadow.camera.near = 10; dirLight.shadow.camera.far = 500;
dirLight.shadow.camera.left = -100; dirLight.shadow.camera.right = 100;
dirLight.shadow.camera.top = 100; dirLight.shadow.camera.bottom = -100;
scene.add(dirLight);
scene.add(new THREE.AmbientLight(0x1a0a2a, 0.3));

/* Post-processing */
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);

/* ===== PLAYER ===== */
const player = {
  mesh: null, engineGlow: null,
  position: new THREE.Vector3(0, 10, 0),
  yaw: 0, pitch: 0, bank: 0, bankTarget: 0,
  speed: BASE_SPEED,
  quaternion: new THREE.Quaternion()
};

function createPlayerCraft(){
  const g = new THREE.Group();
  const bodyGeo = new THREE.ConeGeometry(1.2, 5, 6);
  bodyGeo.rotateX(Math.PI/2);
  const bodyMat = new THREE.MeshStandardMaterial({ color:0xd0d0d8, metalness:0.9, roughness:0.15, flatShading:true });
  const body = new THREE.Mesh(bodyGeo, bodyMat); body.castShadow = true; g.add(body);
  const wingGeo = new THREE.BoxGeometry(5, 0.15, 1.8);
  const wingMat = new THREE.MeshStandardMaterial({ color:0x9090a0, metalness:0.8, roughness:0.25, flatShading:true });
  const wing = new THREE.Mesh(wingGeo, wingMat); wing.position.z = 0.3; wing.castShadow = true; g.add(wing);
  const finGeo = new THREE.BoxGeometry(0.15, 2.5, 2);
  const fin = new THREE.Mesh(finGeo, wingMat); fin.position.set(0, 0.5, -2.2); g.add(fin);
  const engGeo = new THREE.SphereGeometry(0.6, 8, 8);
  const engMat = new THREE.MeshStandardMaterial({ color:0xff6600, emissive:0xff4400, emissiveIntensity:3, flatShading:true });
  const eng = new THREE.Mesh(engGeo, engMat); eng.position.z = -2.5; g.add(eng);
  player.mesh = g; player.engineGlow = eng;
  scene.add(g);
}

/* ===== CANYON GENERATION ===== */
const activeChunks = [];
const generatedZSet = new Set();
const obstacles = [];

function canyonCenterX(z){
  return Math.sin(z*0.0015)*60 + Math.sin(z*0.004)*25 + Math.sin(z*0.0007)*40;
}
function canyonWidth(z){
  const diff = Math.min(score*0.008, 35);
  const v = Math.sin(z*0.003)*15 + Math.sin(z*0.008)*8;
  return Math.max(CANYON_MIN_WIDTH, CANYON_BASE_WIDTH - v - diff);
}
function canyonFloorY(z){ return Math.sin(z*0.002)*3 + 2; }
function canyonCeilingY(z){ return WALL_HEIGHT + Math.sin(z*0.0025)*5; }

function generateChunk(startZ){
  const endZ = startZ + CHUNK_LEN;
  const meshes = [];
  const geos = [];
  const mats = [];

  const wallMat = new THREE.MeshStandardMaterial({ color:0x3a3a4e, flatShading:true, metalness:0.3, roughness:0.7 });
  const floorMat = new THREE.MeshStandardMaterial({ color:0x2a2a3a, flatShading:true, metalness:0.2, roughness:0.8 });
  const ceilMat = new THREE.MeshStandardMaterial({ color:0x35354a, flatShading:true, metalness:0.3, roughness:0.7 });
  mats.push(wallMat, floorMat, ceilMat);

  const segLen = CHUNK_LEN / NUM_SEGS;

  for(let i = 0; i < NUM_SEGS; i++){
    const z = startZ + (i+0.5)*segLen;
    const cx = canyonCenterX(z);
    const w = canyonWidth(z);
    const fy = canyonFloorY(z);
    const cy = canyonCeilingY(z);

    // Left wall
    const lwGeo = new THREE.BoxGeometry(WALL_THICK, cy-fy, segLen);
    const lw = new THREE.Mesh(lwGeo, wallMat);
    lw.position.set(cx - w/2 - WALL_THICK/2, fy + (cy-fy)/2, z);
    lw.castShadow = true; lw.receiveShadow = true;
    meshes.push(lw); geos.push(lwGeo);

    // Right wall
    const rwGeo = new THREE.BoxGeometry(WALL_THICK, cy-fy, segLen);
    const rw = new THREE.Mesh(rwGeo, wallMat);
    rw.position.set(cx + w/2 + WALL_THICK/2, fy + (cy-fy)/2, z);
    rw.castShadow = true; rw.receiveShadow = true;
    meshes.push(rw); geos.push(rwGeo);

    // Floor
    const flGeo = new THREE.BoxGeometry(w, 2, segLen);
    const fl = new THREE.Mesh(flGeo, floorMat);
    fl.position.set(cx, fy - 1, z); fl.receiveShadow = true;
    meshes.push(fl); geos.push(flGeo);

    // Ceiling
    const clGeo = new THREE.BoxGeometry(w, 2, segLen);
    const cl = new THREE.Mesh(clGeo, ceilMat);
    cl.position.set(cx, cy + 1, z); cl.receiveShadow = true;
    meshes.push(cl); geos.push(clGeo);
  }

  // Obstacles (only after some score)
  if(score > 200){
    const numObs = Math.floor(1 + score*0.003 + Math.random()*2);
    for(let o = 0; o < numObs; o++){
      const oz = startZ + Math.random()*CHUNK_LEN;
      const ocx = canyonCenterX(oz);
      const ow = canyonWidth(oz);
      const type = Math.random();
      if(type < 0.5){
        // Monolith
        const h = 15 + Math.random()*25;
        const mw = 3 + Math.random()*3;
        const mGeo = new THREE.BoxGeometry(mw, h, mw);
        const mMat = new THREE.MeshStandardMaterial({ color:0x5a4a6a, flatShading:true, metalness:0.5, roughness:0.4, emissive:0x2a1a3a, emissiveIntensity:0.3 });
        const monolith = new THREE.Mesh(mGeo, mMat);
        const side = Math.random()<0.5 ? -1 : 1;
        monolith.position.set(ocx + side*(ow/2 + mw + 2 + Math.random()*10), h/2 + canyonFloorY(oz), oz);
        monolith.castShadow = true;
        meshes.push(monolith); geos.push(mGeo); mats.push(mMat);
        obstacles.push({ mesh:monolith, type:'monolith', radius:Math.max(mw,h)/2, position:monolith.position.clone() });
      } else {
        // Floating octahedron
        const sGeo = new THREE.OctahedronGeometry(2 + Math.random()*2, 0);
        const sMat = new THREE.MeshStandardMaterial({ color:0x6a3a5a, flatShading:true, metalness:0.6, roughness:0.3, emissive:0x3a1a3a, emissiveIntensity:0.4 });
        const obs = new THREE.Mesh(sGeo, sMat);
        const side = Math.random()<0.5 ? -1 : 1;
        obs.position.set(ocx + side*ow*(0.3+Math.random()*0.3), 10+Math.random()*20, oz);
        obs.castShadow = true;
        meshes.push(obs); geos.push(sGeo); mats.push(sMat);
        obstacles.push({ mesh:obs, type:'float', radius:3, position:obs.position.clone() });
      }
    }
  }

  // Energy orbs
  const numOrbs = 2 + Math.floor(Math.random()*3);
  for(let e = 0; e < numOrbs; e++){
    const ez = startZ + Math.random()*CHUNK_LEN;
    const ecx = canyonCenterX(ez);
    const ewy = canyonWidth(ez);
    const orbGeo = new THREE.SphereGeometry(1.2, 12, 12);
    const orbMat = new THREE.MeshStandardMaterial({ color:0x00ffcc, emissive:0x00ffcc, emissiveIntensity:2, flatShading:true });
    const orb = new THREE.Mesh(orbGeo, orbMat);
    const side = Math.random()<0.5 ? -1 : 1;
    const risk = Math.random();
    orb.position.set(ecx + side*ewy*(0.1+risk*0.35), 8+Math.random()*20, ez);
    orb.userData = { type:'orb', collected:false };
    meshes.push(orb); geos.push(orbGeo); mats.push(orbMat);
    obstacles.push({ mesh:orb, type:'orb', radius:1.5, position:orb.position.clone() });
  }

  // Boost items
  if(Math.random() < 0.4){
    const bz = startZ + CHUNK_LEN*0.3 + Math.random()*CHUNK_LEN*0.4;
    const bcx = canyonCenterX(bz);
    const bwy = canyonWidth(bz);
    const boostGeo = new THREE.OctahedronGeometry(2.5, 0);
    const boostMat = new THREE.MeshStandardMaterial({ color:0xff6b35, emissive:0xff4400, emissiveIntensity:2.5, flatShading:true });
    const boostMesh = new THREE.Mesh(boostGeo, boostMat);
    boostMesh.position.set(bcx, 15, bz);
    boostMesh.userData = { type:'boost', collected:false };
    meshes.push(boostMesh); geos.push(boostGeo); mats.push(boostMat);
    obstacles.push({ mesh:boostMesh, type:'boost', radius:3, position:boostMesh.position.clone() });
  }

  meshes.forEach(m => scene.add(m));
  activeChunks.push({ startZ, endZ, meshes, geos, mats });
}

function cleanupChunks(){
  const pz = player.position.z;
  for(let i = activeChunks.length-1; i >= 0; i--){
    const c = activeChunks[i];
    if(c.endZ < pz - 400){
      c.meshes.forEach(m => scene.remove(m));
      c.geos.forEach(g => g.dispose());
      c.mats.forEach(m => m.dispose());
      activeChunks.splice(i, 1);
    }
  }
  // Clean up obstacles that are far behind
  for(let i = obstacles.length-1; i >= 0; i--){
    if(obstacles[i].mesh.position.z < pz - 400){
      scene.remove(obstacles[i].mesh);
      obstacles[i].mesh.geometry.dispose();
      obstacles[i].mesh.material.dispose();
      obstacles.splice(i, 1);
    }
  }
}

/* ===== PARTICLES ===== */
// Trail
const TRAIL_MAX = 300;
const trailPos = new Float32Array(TRAIL_MAX*3);
const trailSizes = new Float32Array(TRAIL_MAX);
const trailAlphas = new Float32Array(TRAIL_MAX);
const trailGeo = new THREE.BufferGeometry();
trailGeo.setAttribute('position', new THREE.BufferAttribute(trailPos, 3));
trailGeo.setAttribute('size', new THREE.BufferAttribute(trailSizes, 1));
trailGeo.setAttribute('aAlpha', new THREE.BufferAttribute(trailAlphas, 1));
const trailMat = new THREE.ShaderMaterial({
  uniforms:{},
  vertexShader:`
    attribute float size;
    attribute float aAlpha;
    varying float vAlpha;
    void main(){
      vAlpha = aAlpha;
      vec4 mv = modelViewMatrix * vec4(position, 1.0);
      gl_PointSize = size * (200.0 / -mv.z);
      gl_Position = projectionMatrix * mv;
    }
  `,
  fragmentShader:`
    varying float vAlpha;
    void main(){
      float d = length(gl_PointCoord - 0.5) * 2.0;
      if(d > 1.0) discard;
      float a = vAlpha * (1.0 - d*d);
      gl_FragColor = vec4(1.0, 0.55, 0.1, a * 0.7);
    }
  `,
  transparent:true, depthWrite:false, blending:THREE.AdditiveBlending
});
const trailPoints = new THREE.Points(trailGeo, trailMat);
trailPoints.frustumCulled = false;
scene.add(trailPoints);

let trailIdx = 0;
const trailLifetimes = new Float32Array(TRAIL_MAX);
const trailVels = new Float32Array(TRAIL_MAX*3);

function emitTrail(){
  const i = trailIdx % TRAIL_MAX;
  const idx = i*3;
  const back = new THREE.Vector3(0,0,2).applyQuaternion(player.quaternion);
  trailPos[idx] = player.position.x + back.x*2 + (Math.random()-0.5)*0.5;
  trailPos[idx+1] = player.position.y + back.y*2 + (Math.random()-0.5)*0.5;
  trailPos[idx+2] = player.position.z + back.z*2;
  trailSizes[i] = 1.5 + Math.random()*2;
  trailAlphas[i] = 0.8;
  trailLifetimes[i] = 0.6 + Math.random()*0.4;
  trailVels[idx] = (Math.random()-0.5)*5;
  trailVels[idx+1] = (Math.random()-0.5)*5;
  trailVels[idx+2] = (Math.random()-0.5)*5;
  trailIdx++;
}

function updateTrail(dt){
  for(let i = 0; i < TRAIL_MAX; i++){
    trailLifetimes[i] -= dt;
    trailAlphas[i] = Math.max(0, trailLifetimes[i]/0.8);
    trailPos[i*3] += trailVels[i*3]*dt;
    trailPos[i*3+1] += trailVels[i*3+1]*dt;
    trailPos[i*3+2] += trailVels[i*3+2]*dt;
    trailSizes[i] *= 0.98;
  }
  trailGeo.attributes.position.needsUpdate = true;
  trailGeo.attributes.size.needsUpdate = true;
  trailGeo.attributes.aAlpha.needsUpdate = true;
}

// Speed lines
const SL_MAX = 100;
const slPos = new Float32Array(SL_MAX*3);
const slSizes = new Float32Array(SL_MAX);
const slAlphas = new Float32Array(SL_MAX);
const slGeo = new THREE.BufferGeometry();
slGeo.setAttribute('position', new THREE.BufferAttribute(slPos, 3));
slGeo.setAttribute('size', new THREE.BufferAttribute(slSizes, 1));
slGeo.setAttribute('aAlpha', new THREE.BufferAttribute(slAlphas, 1));
const slMat = new THREE.ShaderMaterial({
  uniforms:{},
  vertexShader:`
    attribute float size;
    attribute float aAlpha;
    varying float vAlpha;
    void main(){
      vAlpha = aAlpha;
      vec4 mv = modelViewMatrix * vec4(position, 1.0);
      gl_PointSize = size * (150.0 / -mv.z);
      gl_Position = projectionMatrix * mv;
    }
  `,
  fragmentShader:`
    varying float vAlpha;
    void main(){
      float d = length(gl_PointCoord - 0.5) * 2.0;
      if(d > 1.0) discard;
      float a = vAlpha * (1.0 - d);
      gl_FragColor = vec4(0.8, 0.85, 1.0, a * 0.4);
    }
  `,
  transparent:true, depthWrite:false, blending:THREE.AdditiveBlending
});
const speedLines = new THREE.Points(slGeo, slMat);
speedLines.frustumCulled = false;
scene.add(speedLines);

let slIdx = 0;
const slLifetimes = new Float32Array(SL_MAX);

function emitSpeedLine(){
  const i = slIdx % SL_MAX;
  const angle = Math.random()*Math.PI*2;
  const r = 15 + Math.random()*40;
  slPos[i*3] = Math.cos(angle)*r;
  slPos[i*3+1] = Math.sin(angle)*r*0.5 + 10;
  slPos[i*3+2] = player.position.z + (Math.random()-0.5)*100;
  slSizes[i] = 0.5 + Math.random()*1.5;
  slAlphas[i] = 0.5;
  slLifetimes[i] = 0.3 + Math.random()*0.3;
  slIdx++;
}

function updateSpeedLines(dt){
  for(let i = 0; i < SL_MAX; i++){
    slLifetimes[i] -= dt;
    slAlphas[i] = Math.max(0, slLifetimes[i]/0.4);
    slPos[i*3+2] += speed*dt*0.5;
  }
  slGeo.attributes.position.needsUpdate = true;
  slGeo.attributes.size.needsUpdate = true;
  slGeo.attributes.aAlpha.needsUpdate = true;
}

/* ===== COLLECTIBLE UPDATES ===== */
function updateCollectibles(){
  for(let i = obstacles.length-1; i >= 0; i--){
    const c = obstacles[i];
    if(c.collected) continue;
    c.mesh.rotation.y += 2*dt;
    c.mesh.rotation.x += 1*dt;
    c.mesh.position.y += Math.sin(Date.now()*0.003 + (c.phase||0)) * 0.01;

    const dist = player.position.distanceTo(c.mesh.position);
    if(dist < 4){
      c.collected = true;
      scene.remove(c.mesh);
      c.mesh.geometry.dispose();
      c.mesh.material.dispose();
      if(c.type === 'orb'){
        energy = Math.min(100, energy + ORB_RESTORE);
        score += 50 * multiplier;
        audio.playCollect();
      } else if(c.type === 'boost'){
        boostMeter = BOOST_MAX;
        score += 100 * multiplier;
        audio.playBoost();
      }
      obstacles.splice(i, 1);
    }
  }
}

/* ===== COLLISION ===== */
function checkCollisions(){
  const px = player.position.x;
  const py = player.position.y;
  const pz = player.position.z;
  const cx = canyonCenterX(pz);
  const w = canyonWidth(pz);
  const fy = canyonFloorY(pz);
  const cy = canyonCeilingY(pz);

  if(px - PLAYER_RADIUS < cx - w/2 || px + PLAYER_RADIUS > cx + w/2) return true;
  if(py - PLAYER_H/2 < fy + 1 || py + PLAYER_H/2 > cy - 1) return true;

  for(const c of obstacles){
    if(c.collected) continue;
    if(c.type === 'orb' || c.type === 'boost') continue;
    const dist = player.position.distanceTo(c.mesh.position);
    if(dist < PLAYER_RADIUS + (c.radius||2)) return true;
  }
  return false;
}

function checkNearMiss(){
  const px = player.position.x;
  const pz = player.position.z;
  const cx = canyonCenterX(pz);
  const w = canyonWidth(pz);
  const leftWall = cx - w/2;
  const rightWall = cx + w/2;

  const distLeft = Math.abs(px - leftWall);
  const distRight = Math.abs(px - rightWall);
  const minWallDist = Math.min(distLeft, distRight);

  if(minWallDist < NEAR_MISS_DIST && minWallDist > COLLISION_DIST){
    return 1 - (minWallDist / NEAR_MISS_DIST);
  }
  return 0;
}

/* ===== HUD ===== */
const scoreEl = document.getElementById('score');
const multEl = document.getElementById('multiplier');
const speedFill = document.getElementById('speed-fill');
const energyFill = document.getElementById('energy-fill');
const boostFill = document.getElementById('boost-fill');
const nearMissFlash = document.getElementById('near-miss-flash');

function updateHUD(){
  scoreEl.textContent = Math.floor(score).toLocaleString();
  const spdPct = ((speed - BASE_SPEED) / (MAX_SPEED - BASE_SPEED)) * 100;
  speedFill.style.width = Math.min(100, spdPct) + '%';
  energyFill.style.width = energy + '%';
  boostFill.style.width = (boostMeter / BOOST_MAX * 100) + '%';

  if(multiplier > 1){
    multEl.textContent = 'x' + multiplier.toFixed(1);
    multEl.classList.add('active');
  } else {
    multEl.classList.remove('active');
  }

  if(energy < 25){
    energyFill.style.background = 'linear-gradient(90deg, #ff3366, #ff6b35)';
    energyFill.style.boxShadow = '0 0 10px #ff3366';
  } else {
    energyFill.style.background = 'linear-gradient(90deg, #00ffcc, #00ccff)';
    energyFill.style.boxShadow = '0 0 8px #00ffcc';
  }

  if(nearMissFlashTimer > 0) nearMissFlash.classList.add('active');
  else nearMissFlash.classList.remove('active');
}

/* ===== GAME STATE ===== */
function startGame(){
  gameState = 'playing';
  score = 0; maxSpeed = 0; maxMultiplier = 1;
  multiplier = 1; multiplierTimer = 0;
  energy = 100; boostMeter = 0; isBoosting = false;
  speed = BASE_SPEED; shakeIntensity = 0; nearMissFlashTimer = 0;
  player.position.set(0, 10, 0);
  player.yaw = 0; player.pitch = 0; player.bank = 0; player.bankTarget = 0;
  player.speed = BASE_SPEED;
  player.quaternion.identity();

  // Cleanup old
  activeChunks.forEach(c => {
    c.meshes.forEach(m => scene.remove(m));
    c.geos.forEach(g => g.dispose());
    c.mats.forEach(m => m.dispose());
  });
  activeChunks.length = 0;
  obstacles.forEach(o => { scene.remove(o.mesh); o.mesh.geometry.dispose(); o.mesh.material.dispose(); });
  obstacles.length = 0;
  generatedZSet.clear();
  trailIdx = 0; slIdx = 0;

  // Initial chunks
  for(let z = -CHUNK_LEN; z < 1000; z += CHUNK_LEN){
    generatedZSet.add(z);
    generateChunk(z);
  }
  lastChunkZ = 1000;

  document.getElementById('start-screen').classList.add('hidden');
  document.getElementById('hud').classList.remove('hidden');
  document.getElementById('game-over').classList.add('hidden');
  audio.init();
}

function gameOver(){
  gameState = 'gameover';
  audio.playGameOver();
  document.getElementById('final-score').textContent = Math.floor(score).toLocaleString();
  document.getElementById('max-speed').textContent = Math.floor(maxSpeed);
  document.getElementById('max-multiplier').textContent = 'x' + maxMultiplier.toFixed(1);
  document.getElementById('game-over').style.display = 'flex';
  document.getElementById('hud').classList.add('hidden');
}

function restartGame(){
  document.getElementById('game-over').style.display = 'none';
  startGame();
}

/* ===== INPUT ===== */
window.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;
});
window.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;
});
document.getElementById('start-btn').addEventListener('click', startGame);
document.getElementById('restart-btn').addEventListener('click', restartGame);

/* ===== GAME LOOP ===== */
const clock = new THREE.Clock();

function animate(){
  requestAnimationFrame(animate);
  const dt = Math.min(clock.getDelta(), 0.05);

  if(gameState === 'playing'){
    // Player physics
    const yawInput = (keys.left?-1:0) + (keys.right?1:0);
    player.yaw += yawInput * YAW_SPEED * dt;
    const pitchInput = (keys.up?-1:0) + (keys.down?1:0);
    player.pitch += pitchInput * PITCH_SPEED * dt;
    player.pitch = THREE.MathUtils.clamp(player.pitch, -0.5, 0.5);
    player.bankTarget = -yawInput * 0.6;
    player.bank = THREE.MathUtils.lerp(player.bank, player.bankTarget, BANK_LERP * dt);
    player.quaternion.setFromEuler(new THREE.Euler(player.pitch, player.yaw, player.bank, 'YXZ'));

    const forward = new THREE.Vector3(0,0,-1).applyQuaternion(player.quaternion);

    if(keys.up) player.speed = Math.min(player.speed + ACCEL*dt, MAX_SPEED);
    if(keys.down) player.speed = Math.max(player.speed - BRAKE*dt, BASE_SPEED*0.3);
    player.speed *= (1 - 0.005*dt);

    // Boost
    if(keys.boost && boostMeter > 0 && !isBoosting){
      isBoosting = true;
      audio.playBoost();
    }
    if(isBoosting){
      boostMeter -= BOOST_DRAIN*dt;
      if(boostMeter <= 0){ boostMeter = 0; isBoosting = false; }
      player.speed = Math.min(player.speed + BOOST_FORCE*dt, MAX_SPEED*1.5);
    } else {
      boostMeter = Math.min(BOOST_MAX, boostMeter + 5*dt);
    }

    player.position.addScaledVector(forward, player.speed*dt);
    player.position.y = THREE.MathUtils.clamp(player.position.y, 3, WALL_HEIGHT-3);

    player.mesh.position.copy(player.position);
    player.mesh.quaternion.copy(player.quaternion);
    if(player.engineGlow) player.engineGlow.material.emissiveIntensity = 2 + (player.speed/MAX_SPEED)*3;

    // Trail
    if(Math.random() < 0.8 + (player.speed/MAX_SPEED)*0.5) emitTrail();
    if(player.speed > BASE_SPEED*1.3 && Math.random() < (player.speed/MAX_SPEED)*0.5) emitSpeedLine();

    audio.updateEngine(player.speed);
    if(player.speed > maxSpeed) maxSpeed = player.speed;
    score += player.speed*dt*0.1;

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

    // Collision
    if(checkCollisions()){
      audio.playCollision();
      shakeIntensity = 1.0;
      gameOver();
      return;
    }

    // Near miss
    const proximity = checkNearMiss();
    if(proximity > 0){
      multiplierTimer = 2;
      const newMult = 1 + proximity*5;
      if(newMult > multiplier){
        multiplier = newMult;
        if(multiplier > maxMultiplier) maxMultiplier = multiplier;
        score += proximity*100*dt;
        shakeIntensity = Math.max(shakeIntensity, proximity*0.3);
        nearMissFlashTimer = 0.3;
        if(proximity > 0.5) audio.playNearMiss();
      }
    }
    nearMissFlashTimer -= dt;
    multiplierTimer -= dt;
    if(multiplierTimer <= 0) multiplier = Math.max(1, multiplier - dt*0.5);

    // Collectibles
    updateCollectibles();
    updateTrail(dt);
    updateSpeedLines(dt);
    cleanupChunks();
    updateHUD();
  }

  // Camera
  const offset = new THREE.Vector3(0,6,-18).applyQuaternion(player.quaternion);
  const desiredPos = player.position.clone().add(offset);
  camera.position.lerp(desiredPos, 0.06);
  const lookTarget = player.position.clone().add(new THREE.Vector3(0,0,-60).applyQuaternion(player.quaternion));
  camera.lookAt(lookTarget);

  const targetFov = isBoosting ? 85 : 70;
  camera.fov = THREE.MathUtils.lerp(camera.fov, targetFov, 0.03);
  camera.updateProjectionMatrix();

  if(shakeIntensity > 0){
    camera.position.x += (Math.random()-0.5)*shakeIntensity*0.5;
    camera.position.y += (Math.random()-0.5)*shakeIntensity*0.3;
    shakeIntensity *= 0.9;
    if(shakeIntensity < 0.01) shakeIntensity = 0;
  }

  composer.render();
}

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

/* ===== SKY ===== */
const skyCanvas = document.createElement('canvas');
skyCanvas.width = 2; skyCanvas.height = 512;
const skyCtx = skyCanvas.getContext('2d');
const skyGrad = skyCtx.createLinearGradient(0,0,0,512);
skyGrad.addColorStop(0, '#0a0a1a');
skyGrad.addColorStop(0.3, '#1a0a2a');
skyGrad.addColorStop(0.6, '#3a1a2a');
skyGrad.addColorStop(0.8, '#6a2a1a');
skyGrad.addColorStop(1, '#cc4400');
skyCtx.fillStyle = skyGrad;
skyCtx.fillRect(0,0,2,512);
scene.background = new THREE.CanvasTexture(skyCanvas);

/* ===== START ===== */
createPlayerCraft();
animate();

</script>
</body>
</html>
<!-- agent-meta {"model":"inclusionai/ling-3.0-flash","provider":"openrouter","persona":"full","sessionId":"3df2f359-4784-4976-a5f1-390cbd719b85","tokensIn":1076828,"tokensOut":53054,"tokensTotal":1129882,"cost":0.012925142999999998,"turns":21,"toolCalls":20,"failedToolCalls":0,"timestamp":"2026-08-24T23:02:56.240Z"} -->