← Canyon Run results

Canyon Run

inkling · 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 27,328 bytes · SHA-256 b1193c4b3c35
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Golden Canyon Racer</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Rajdhani:wght@300;500;700&display=swap');

* { margin: 0; padding: 0; box-sizing: border-box; user-select: none; -webkit-user-select: none; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #050510; font-family: 'Rajdhani', sans-serif; color: #fff; }

#canvas-wrap { position: fixed; inset: 0; z-index: 1; }
canvas { display: block; width: 100%; height: 100%; }

/* HUD */
#hud {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 10;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding: 2rem;
  text-shadow: 0 0 10px rgba(255,180,60,0.4);
}

.top-bar {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  font-family: 'Orbitron', sans-serif;
  letter-spacing: 0.08em;
}

.score-display { font-size: 2.2rem; color: #ffcc66; text-shadow: 0 0 15px rgba(255,180,60,0.6); }
.multiplier-display { font-size: 3rem; font-weight: 900; color: #ffaa44; transition: transform 0.15s ease; }
.multiplier-display.active { animation: pulse 0.4s ease; color: #ff5500; text-shadow: 0 0 25px rgba(255,85,0,0.8); }
@keyframes pulse { 0%,100%{transform:scale(1)} 50%{transform:scale(1.15)} }

.energy-ring-wrap {
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  width: 180px; height: 180px;
  pointer-events: none;
}

.energy-ring-svg { width: 100%; height: 100%; transform: rotate(-90deg); }
.energy-ring-track { stroke: rgba(255,255,255,0.1); stroke-width: 4; fill: none; }
.energy-ring-fill { stroke: #ffcc66; stroke-width: 6; fill: none; stroke-linecap: round; filter: drop-shadow(0 0 6px rgba(255,200,100,0.6)); transition: stroke-dashoffset 0.2s linear; }

.center-label { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center; pointer-events: none; }
.center-label .label { font-size: 0.7rem; letter-spacing: 0.2em; color: rgba(255,255,255,0.6); text-transform: uppercase; }
.center-label .value { font-family: 'Orbitron', sans-serif; font-size: 1.6rem; color: #fff; text-shadow: 0 0 10px rgba(255,255,255,0.3); }

.bottom-bar {
  display: flex;
  justify-content: space-between;
  align-items: flex-end;
  font-size: 0.9rem;
  color: rgba(255,255,255,0.7);
}

.speed-bar-wrap { width: 220px; height: 6px; background: rgba(255,255,255,0.08); border-radius: 3px; overflow: hidden; position: relative; }
.speed-bar-fill { height: 100%; width: 0%; background: linear-gradient(90deg, #ffaa44, #ff5500); border-radius: 3px; box-shadow: 0 0 8px rgba(255,150,0,0.5); transition: width 0.1s linear; }

.turbo-bar-wrap { width: 160px; height: 4px; background: rgba(255,255,255,0.08); border-radius: 2px; overflow: hidden; }
.turbo-bar-fill { height: 100%; width: 0%; background: linear-gradient(90deg, #44ffaa, #00ffaa); border-radius: 2px; box-shadow: 0 0 8px rgba(0,255,170,0.4); }

/* Overlays */
#start-screen, #game-over-screen {
  position: fixed; inset: 0; z-index: 20;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  background: rgba(5,5,16,0.85); backdrop-filter: blur(12px);
  text-align: center;
  padding: 2rem;
}

#start-screen { display: flex; }
#game-over-screen { display: none; }

h1 { font-family: 'Orbitron', sans-serif; font-size: clamp(2.5rem, 6vw, 5rem); letter-spacing: 0.15em; color: #ffcc66; text-shadow: 0 0 30px rgba(255,200,100,0.5), 0 0 60px rgba(255,150,60,0.2); margin-bottom: 0.5rem; }
h1 span { color: #aa66ff; }
.subtitle { font-size: 1.1rem; color: #aaaacc; letter-spacing: 0.12em; margin-bottom: 3rem; }

.btn {
  font-family: 'Orbitron', sans-serif; font-size: 1rem; letter-spacing: 0.12em;
  padding: 1rem 3rem; border: 2px solid rgba(255,200,100,0.6); color: #ffcc66;
  background: rgba(255,200,100,0.05); backdrop-filter: blur(4px);
  border-radius: 4px; cursor: pointer; transition: all 0.2s ease; text-transform: uppercase;
  box-shadow: 0 0 15px rgba(255,200,100,0.1);
}
.btn:hover { background: rgba(255,200,100,0.15); border-color: rgba(255,200,100,1); box-shadow: 0 0 25px rgba(255,200,100,0.3); color: #fff; }

.game-over-stats { margin-bottom: 3rem; }
.stat-row { display: flex; justify-content: center; gap: 4rem; margin-bottom: 0.5rem; }
.stat-label { font-size: 0.8rem; color: rgba(255,255,255,0.5); letter-spacing: 0.15em; text-transform: uppercase; }
.stat-value { font-family: 'Orbitron', sans-serif; font-size: 2rem; color: #fff; }
</style>

<script src="https://unpkg.com/three@0.160.0/build/three.js"></script>
</head>
<body>

<div id="canvas-wrap">
  <canvas id="gl"></canvas>
</div>

<div id="hud">
  <div class="top-bar">
    <div>
      <div style="font-size:0.7rem; color:rgba(255,255,255,0.5); letter-spacing:0.15em;">SCORE</div>
      <div class="score-display" id="scoreUI">0</div>
    </div>
    <div style="text-align:center;">
      <div style="font-size:0.7rem; color:rgba(255,255,255,0.5); letter-spacing:0.15em;">MULTIPLIER</div>
      <div class="multiplier-display" id="multUI">1.0x</div>
    </div>
  </div>

  <div class="energy-ring-wrap" aria-label="Energy">
    <svg class="energy-ring-svg" viewBox="0 0 160 160">
      <circle class="energy-ring-track" cx="80" cy="80" r="70"/>
      <circle class="energy-ring-fill" id="energyRing" cx="80" cy="80" r="70" stroke-dasharray="440" stroke-dashoffset="440"/>
    </svg>
    <div class="center-label">
      <div class="label">Energy</div>
      <div class="value" id="energyUI">100%</div>
    </div>
  </div>

  <div class="bottom-bar">
    <div>
      <div style="font-size:0.7rem; color:rgba(255,255,255,0.5); letter-spacing:0.1em;">SPEED</div>
      <div class="speed-bar-wrap"><div class="speed-bar-fill" id="speedUI"></div></div>
    </div>
    <div style="text-align:right;">
      <div style="font-size:0.7rem; color:rgba(255,255,255,0.5); letter-spacing:0.1em;">TURBO</div>
      <div class="turbo-bar-wrap"><div class="turbo-bar-fill" id="turboUI"></div></div>
    </div>
  </div>
</div>

<div id="start-screen">
  <h1>GOLDEN <span>CANY</span></h1>
  <div class="subtitle">PROCEDURAL HIGH-SPEED RACER</div>
  <p style="max-width:420px; color:#aaa; line-height:1.6; margin-bottom:2.5rem; font-size:0.95rem;">
    Fly through the canyon. Collect energy. Avoid walls. The closer you fly, the higher your score. Turbo builds with speed — use it to break the sound barrier.
  </p>
  <button class="btn" id="startBtn">INITIATE FLIGHT</button>
  <div style="margin-top:2rem; color:#666; font-size:0.75rem;">WASD / Arrows — Pitch & Roll &nbsp;|&nbsp; SHIFT — Turbo</div>
</div>

<div id="game-over-screen">
  <h1 style="font-size:3.5rem;">CRASHED</h1>
  <div class="subtitle">FLIGHT RECORD</div>
  <div class="game-over-stats">
    <div class="stat-row">
      <div><div class="stat-label">FINAL SCORE</div><div class="stat-value" id="finalScore">0</div></div>
      <div><div class="stat-label">MAX SPEED</div><div class="stat-value" id="finalSpeed">0</div></div>
    </div>
    <div class="stat-row">
      <div><div class="stat-label">DISTANCE</div><div class="stat-value" id="finalDist">0</div></div>
      <div><div class="stat-label">MULTIPLIER</div><div class="stat-value" id="finalMult">1.0x</div></div>
    </div>
  </div>
  <button class="btn" id="restartBtn">RE-ENGAGE</button>
</div>

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

/* ===== Constants ===== */
const CHUNK_LENGTH = 160;
const LANE_WIDTH = 24;
const WALL_HEIGHT = 50;
const WALL_THICK = 3;

/* ===== Setup ===== */
const canvas = document.getElementById('gl');
const renderer = new THREE.WebGLRenderer({ canvas, alpha: true, antialias: true, powerPreference: 'high-performance' });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.outputColorSpace = THREE.SRGBColorSpace;

const scene = new THREE.Scene();
scene.background = new THREE.Color(0x050510);
scene.fog = new THREE.FogExp2(0x1a0a2e, 0.003);

const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 300);

/* ===== Lighting ===== */
const hemi = new THREE.HemisphereLight(0xffaa66, 0x330022, 0.8);
scene.add(hemi);

const dir = new THREE.DirectionalLight(0xffcc88, 1.0);
dir.position.set(20, 60, -30);
dir.castShadow = true;
dir.shadow.mapSize.width = 1024;
dir.shadow.mapSize.height = 1024;
dir.shadow.bias = -0.001;
scene.add(dir);

const dir2 = new THREE.DirectionalLight(0xaa66ff, 0.5);
dir2.position.set(-20, 20, 40);
scene.add(dir2);

/* ===== Post-Processing Targets ===== */
const width = window.innerWidth;
const height = window.innerHeight;
const bloomTarget = new THREE.WebGLRenderTarget(width, height, { samples: 4 });
const prevTarget = new THREE.WebGLRenderTarget(width, height, { samples: 4 });

const bloomComposer = new EffectComposer(renderer, bloomTarget);
bloomComposer.addPass(new RenderPass(scene, camera));
const bloomPass = new UnrealBloomPass(new THREE.Vector2(width, height), 1.6, 0.3, 0.9);
bloomComposer.addPass(bloomPass);

/* ===== Motion Blur ===== */
const motionMat = new THREE.ShaderMaterial({
  uniforms: {
    tDiffuse: { value: null },
    prevTexture: { value: null },
    blendFactor: { value: 0.15 },
    time: { value: 0 }
  },
  vertexShader: `
    varying vec2 vUv;
    void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }
  `,
  fragmentShader: `
    uniform sampler2D tDiffuse;
    uniform sampler2D prevTexture;
    uniform float blendFactor;
    uniform float time;
    varying vec2 vUv;
    void main() {
      vec4 curr = texture2D(tDiffuse, vUv);
      vec4 prev = texture2D(prevTexture, vUv);
      // Add slight chromatic separation on high speed
      vec4 currR = texture2D(tDiffuse, vUv + vec2(0.001, 0));
      vec2 shift = (blendFactor > 0.3) ? vec2(0.002, -0.002) : vec2(0,0);
      curr = mix(curr, texture2D(tDiffuse, vUv + shift), 0.15);
      gl_FragColor = mix(curr, prev, blendFactor);
    }
  `
});
const motionPlane = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), motionMat);
const motionScene = new THREE.Scene();
motionScene.add(motionPlane);
const motionCam = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 10);

/* ===== Copy pass for previous frame ===== */
const copyMat = new THREE.MeshBasicMaterial({ map: bloomTarget.texture });
const copyPlane = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), copyMat);
const copyScene = new THREE.Scene();
copyScene.add(copyPlane);
const copyCam = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 10);

/* ===== Craft ===== */
const craft = new THREE.Group();
const craftBody = new THREE.Mesh(
  new THREE.BoxGeometry(1.8, 0.5, 5),
  new THREE.MeshStandardMaterial({ color: 0xeeddff, roughness: 0.15, metalness: 0.7 })
);
craft.add(craftBody);

const craftWingL = new THREE.Mesh(
  new THREE.BoxGeometry(0.2, 0.15, 2.5),
  new THREE.MeshStandardMaterial({ color: 0x9999aa, roughness: 0.3, metalness: 0.5 })
);
craftWingL.position.set(-1.0, 0, 0);
craft.add(craftWingL);

const craftWingR = craftWingL.clone();
craftWingR.position.set(1.0, 0, 0);
craft.add(craftWingR);

// Engine glow
const glowGeo = new THREE.SphereGeometry(0.35, 16, 16);
const glowMat = new THREE.MeshStandardMaterial({ color: 0xffffaa, emissive: 0xff6600, emissiveIntensity: 4, roughness: 0.1 });
const engineGlow = new THREE.Mesh(glowGeo, glowMat);
engineGlow.position.set(0, 0, 2.6);
craft.add(engineGlow);

// Tip glow
const tipGlow = new THREE.Mesh(new THREE.SphereGeometry(0.2, 8, 8), new THREE.MeshStandardMaterial({ color: 0xffffff, emissive: 0x88ccff, emissiveIntensity: 3 }));
tipGlow.position.set(0, 0, -2.5);
craft.add(tipGlow);

scene.add(craft);

/* ===== Particles / Trail ===== */
const trailPoints = [];
const trailGeo = new THREE.BufferGeometry();
const trailMat = new THREE.LineBasicMaterial({ color: 0xffaa66, transparent: true, opacity: 0.4, linewidth: 2 });
const trailLine = new THREE.Line(trailGeo, trailMat);
scene.add(trailLine);

const dustGeo = new THREE.BufferGeometry();
const dustMat = new THREE.PointsMaterial({ color: 0xffcc88, size: 0.15, transparent: true, opacity: 0.6, sizeAttenuation: true });
const dustPoints = new THREE.Points(dustGeo, dustMat);
scene.add(dustPoints);

/* ===== Chunk System ===== */
const chunks = new Map(); // index -> group

function makeWallMesh(xPos, yPos, zPos, w, h, d, colorHex) {
  const geo = new THREE.BoxGeometry(w, h, d);
  const mat = new THREE.MeshStandardMaterial({ color: colorHex, roughness: 0.9, metalness: 0.05, flatShading: true });
  const mesh = new THREE.Mesh(geo, mat);
  mesh.position.set(xPos, yPos, zPos);
  return mesh;
}

function generateChunk(index) {
  const group = new THREE.Group();
  group.position.z = index * CHUNK_LENGTH;

  // Curved walls using multiple segments
  const segments = 6;
  for (let i = 0; i < segments; i++) {
    const zLocal = (i / (segments - 1)) * CHUNK_LENGTH - CHUNK_LENGTH / 2;
    const noise = Math.sin(index * 0.8 + i * 0.6) * 8 - 4 + Math.sin(index * 2.3 + i) * 3;
    const squeeze = (index > 10 && Math.random() > 0.7) ? 8 : 0;
    const leftX = -LANE_WIDTH / 2 - 6 + noise - squeeze / 2;
    const rightX = LANE_WIDTH / 2 + 6 - noise + squeeze / 2;

    // Left wall
    group.add(makeWallMesh(leftX - WALL_THICK / 2, WALL_HEIGHT / 2, zLocal, WALL_THICK, WALL_HEIGHT, CHUNK_LENGTH / segments + 1, 0xff6a22));
    // Right wall
    group.add(makeWallMesh(rightX + WALL_THICK / 2, WALL_HEIGHT / 2, zLocal, WALL_THICK, WALL_HEIGHT, CHUNK_LENGTH / segments + 1, 0x9933cc));
  }

  // Obstacles: floating monoliths and tight pillars
  if (index > 0) {
    const obstacleCount = Math.min(4, 1 + Math.floor(index / 10));
    for (let i = 0; i < obstacleCount; i++) {
      const zLocal = (Math.random() - 0.5) * CHUNK_LENGTH * 0.8;
      const xLocal = (Math.random() - 0.5) * 14;
      const yLocal = 8 + Math.random() * 15;
      const size = 2 + Math.random() * 3;
      const isMonolith = Math.random() > 0.3;
      const h = isMonolith ? 12 + Math.random() * 14 : 20 + Math.random() * 10;
      const color = isMonolith ? 0xddccff : 0x222233;
      const obs = makeWallMesh(xLocal, yLocal, zLocal, size, h, size, color);
      obs.userData = { type: 'obstacle', radius: size * 0.8 };
      group.add(obs);
    }
  }

  // Collectibles
  for (let i = 0; i < 3; i++) {
    const xLocal = (Math.random() - 0.5) * 16;
    const zLocal = (Math.random() - 0.5) * CHUNK_LENGTH * 0.7;
    const yLocal = 6 + Math.random() * 12;
    const orbGeo = new THREE.IcosahedronGeometry(1.2, 0);
    const orbMat = new THREE.MeshStandardMaterial({ color: 0xffffaa, emissive: 0xffaa00, emissiveIntensity: 3, roughness: 0.2, metalness: 0.8 });
    const orb = new THREE.Mesh(orbGeo, orbMat);
    orb.position.set(xLocal, yLocal, zLocal);
    orb.rotation.y = Math.random() * Math.PI * 2;
    orb.userData = { type: 'collectible', value: 15 };
    group.add(orb);
  }

  return group;
}

function getChunkIndex(z) {
  return Math.floor(z / CHUNK_LENGTH);
}

function regenerateWorld() {
  const idx = getChunkIndex(craft.position.z);
  for (let i = idx - 2; i <= idx + 10; i++) {
    if (!chunks.has(i)) {
      const chunk = generateChunk(i);
      scene.add(chunk);
      chunks.set(i, chunk);
    }
  }
  for (const [k, v] of chunks) {
    if (k < idx - 3 || k > idx + 12) {
      scene.remove(v);
      chunks.delete(k);
      // Dispose geometry/materials to avoid memory leak
      v.traverse(child => {
        if (child.geometry) child.geometry.dispose();
        if (child.material && child.material.dispose) child.material.dispose();
      });
    }
  }
}

/* ===== Sound ===== */
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();

function playTone(freq, duration, vol = 0.1, type = 'sine', detune = 0) {
  const osc = audioCtx.createOscillator();
  const gain = audioCtx.createGain();
  osc.type = type;
  osc.frequency.setValueAtTime(freq, audioCtx.currentTime);
  osc.detune.value = detune;
  gain.gain.setValueAtTime(vol, audioCtx.currentTime);
  gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + duration);
  osc.connect(gain);
  gain.connect(audioCtx.destination);
  osc.start();
  osc.stop(audioCtx.currentTime + duration);
  return osc;
}

function playEngineSound(freq, vol) {
  // Create low hum using a continuous oscillator handled separately
  // We'll just return nothing and manage a continuous engine oscillator
  return null;
}

let engineOsc = null;
let engineGain = null;

function startEngineSound() {
  if (engineOsc) return;
  engineOsc = audioCtx.createOscillator();
  engineGain = audioCtx.createGain();
  engineOsc.type = 'triangle';
  engineOsc.frequency.value = 55;
  engineGain.gain.value = 0.04;
  engineOsc.connect(engineGain);
  engineGain.connect(audioCtx.destination);
  engineOsc.start();
}

function stopEngineSound() {
  if (engineOsc) {
    engineOsc.stop();
    engineOsc = null;
    engineGain = null;
  }
}

function updateEngineSound(speed) {
  if (!engineOsc) return;
  engineOsc.frequency.value = 55 + speed * 6;
  engineGain.gain.value = 0.04 + speed * 0.03;
}

/* ===== Game State ===== */
let running = false;
let gameOver = false;
let score = 0;
let multiplier = 1.0;
let energy = 100;
let speed = 0; // current velocity
let turbo = 0; // turbo charge 0-1
let distance = 0;
let maxSpeed = 0;
let nearMissTimer = 0;

// Physics state
const craftState = {
  roll: 0,
  pitch: 0,
  bankTarget: 0,
  pitchTarget: 0
};

/* ===== Input ===== */
const keys = { left: false, right: false, up: false, down: false, shift: false };

window.addEventListener('keydown', e => {
  if (e.key === 'ArrowLeft' || e.key === 'a') keys.left = true;
  if (e.key === 'ArrowRight' || e.key === 'd') keys.right = true;
  if (e.key === 'ArrowUp' || e.key === 'w') keys.up = true;
  if (e.key === 'ArrowDown' || e.key === 's') keys.down = true;
  if (e.key === 'Shift') keys.shift = true;
  if (e.key === ' ' || e.key === 'Enter') {
    if (!running && !gameOver) startGame();
    else if (gameOver) startGame();
  }
});

window.addEventListener('keyup', e => {
  if (e.key === 'ArrowLeft' || e.key === 'a') keys.left = false;
  if (e.key === 'ArrowRight' || e.key === 'd') keys.right = false;
  if (e.key === 'ArrowUp' || e.key === 'w') keys.up = false;
  if (e.key === 'ArrowDown' || e.key === 's') keys.down = false;
  if (e.key === 'Shift') keys.shift = false;
});

/* ===== HUD Elements ===== */
const scoreUI = document.getElementById('scoreUI');
const multUI = document.getElementById('multUI');
const energyUI = document.getElementById('energyUI');
const energyRing = document.getElementById('energyRing');
const speedUI = document.getElementById('speedUI');
const turboUI = document.getElementById('turboUI');

/* ===== Game Functions ===== */
function startGame() {
  if (audioCtx.state === 'suspended') audioCtx.resume();
  startEngineSound();
  running = true;
  gameOver = false;
  score = 0;
  multiplier = 1.0;
  energy = 100;
  speed = 0;
  turbo = 0;
  distance = 0;
  maxSpeed = 0;
  craft.position.set(0, 15, 20);
  craft.rotation.set(0, 0, 0);
  craftState.roll = 0;
  craftState.pitch = 0;
  craftState.bankTarget = 0;
  craftState.pitchTarget = 0;
  chunks.clear();
  for (const child of scene.children.slice()) {
    if (child !== craft && child !== trailLine && child !== dustPoints && child !== motionPlane) {
      scene.remove(child);
    }
  }
  scene.add(craft);
  scene.add(trailLine);
  scene.add(dustPoints);
  regenerateWorld();
  document.getElementById('start-screen').style.display = 'none';
  document.getElementById('game-over-screen').style.display = 'none';
}

function endGame() {
  running = false;
  gameOver = true;
  stopEngineSound();
  document.getElementById('finalScore').textContent = Math.floor(score);
  document.getElementById('finalSpeed').textContent = Math.floor(maxSpeed);
  document.getElementById('finalDist').textContent = Math.floor(distance);
  document.getElementById('finalMult').textContent = multiplier.toFixed(1) + 'x';
  document.getElementById('game-over-screen').style.display = 'flex';
}

/* ===== Update ===== */
function updatePhysics(dt) {
  if (!running || gameOver) return;

  // Energy decay
  energy -= 0.8 * dt;
  if (energy < 0) { endGame(); return; }

  // Base acceleration
  let baseAccel = 0.35;
  if (keys.up) baseAccel += 0.4;
  if (keys.down) baseAccel -= 0.2;

  // Pitch target
  craftState.pitchTarget = keys.up ? 0.15 : keys.down ? -0.15 : 0;
  craftState.pitch += (craftState.pitchTarget - craftState.pitch) * 0.08;

  // Roll / bank
  craftState.bankTarget = keys.left ? -0.5 : keys.right ? 0.5 : 0;
  craftState.roll += (craftState.bankTarget - craftState.roll) * 0.1;

  // Turbo
  if (keys.shift && turbo > 0.2) {
    baseAccel *= 2.0;
    turbo -= 0.025 * dt * 60;
    speed *= 1.05; // extra boost
  } else {
    if (speed > 20) turbo = Math.min(1, turbo + 0.005 * dt * 60);
    else turbo = Math.max(0, turbo - 0.002 * dt * 60);
  }

  speed += baseAccel * dt * 60;
  speed = Math.max(5, Math.min(120, speed));
  if (speed > maxSpeed) maxSpeed = speed;

  // Move craft
  craft.rotation.x = craftState.pitch;
  craft.rotation.z = craftState.roll;

  // Forward motion based on pitch slightly
  const forwardMult = 1.0 + Math.abs(craftState.pitch) * 0.3;
  craft.position.z += speed * dt * 1.2 * forwardMult;
  distance += speed * dt * 1.2 * forwardMult;

  // Vertical movement
  const verticalTarget = (keys.up ? 1 : keys.down ? -1 : 0) * 2;
  craft.position.y += (verticalTarget - craft.position.y + 15) * 0.02; // spring back to middle
  craft.position.y = Math.max(5, Math.min(35, craft.position.y));
  craft.position.x += craftState.roll * 0.3 * dt * 60;
  craft.position.x = Math.max(-15, Math.min(15, craft.position.x));

  // Score and multiplier
  const baseScoreRate = 20;
  score += baseScoreRate * multiplier * dt * 60;
  multiplier = 1.0 + Math.min(4, speed / 30) * 0.5;

  // Near miss
  const nearThreshold = 4.0;
  let closestDist = Infinity;
  for (const [k, chunk] of chunks) {
    chunk.children.forEach(child => {
      if (!child.userData || !child.userData.type) return;
      const dist = craft.position.distanceTo(child.position);
      if (dist < closestDist) closestDist = dist;
    });
  }
  if (closestDist < nearThreshold && closestDist > 1.5) {
    multiplier += 0.05;
    nearMissTimer = 1.0;
    playTone(1200, 0.08, 0.05, 'sine', 200);
  } else {
    nearMissTimer *= 0.9;
  }

  // Regenerate chunks
  regenerateWorld();
}

function updateCollisions() {
  if (!running || gameOver) return;
  const craftRadius = 2.5;
  for (const [k, chunk] of chunks) {
    chunk.children.forEach(child => {
      if (child === craft) return;
      if (!child.userData || child.userData.type !== 'obstacle') return;
      // Bounding sphere approximation
      const dist = craft.position.distanceTo(child.position);
      const childR = child.userData.radius || 2;
      if (dist < craftRadius + childR) {
        // Crash
        playTone(150, 0.4, 0.1, 'square', -300);
        playTone(80, 0.6, 0.08, 'sine');
        endGame();
      }
      if (child.userData.type === 'collectible') {
        if (dist < 3) {
          energy = Math.min(100, energy + child.userData.value);
          playTone(800 + Math.random() * 400, 0.15, 0.05, 'sine');
          chunk.remove(child);
          child.geometry.dispose();
          child.material.dispose();
        }
      }
    });
  }
}

function updateVisuals() {
  // Trail
  trailPoints.push(new THREE.Vector3().copy(craft.position));
  if (trailPoints.length > 40) trailPoints.shift();
  const positions = new Float32Array(trailPoints.length * 3);
  for (let i = 0; i < trailPoints.length; i++) {
    positions[i * 3 + 0] = trailPoints[i].x;
    positions[i * 3 + 1] = trailPoints[i].y;
    positions[i * 3 + 2] = trailPoints[i].z - 1; // slightly behind
  }
  trailGeo.setAttribute('position', new THREE.BufferAttribute(positions, 3));

  // Dust particles
  if (Math.random() > 0.7) {
    const pts = [];
    for (let i = 0; i < 3; i++) {
      pts.push(
        craft.position.x + (Math.random() - 0.5) * 1,
        craft.position.y + (Math.random() - 0.5) * 0.5,
        craft.position.z - (Math.random() * 5 + 2)
      );
    }
    const arr = new Float32Array(pts);
    // Rebuild dust geo each frame is okay with small count
    dustGeo.setAttribute('position', new THREE.BufferAttribute(arr, 3));
  }

  // Engine glow intensity based on speed
  if (running) {
    engineGlow.scale.set(1 + speed * 0.01, 1 + speed * 0.01, 1);
  }

  // HUD
  scoreUI.textContent = Math.floor(score);
  multUI.textContent = multiplier.toFixed(1) + 'x';
  multUI.classList.toggle('active', nearMissTimer > 0.3);

  energyUI.textContent = Math.floor(energy) + '%';
  energyRing.setAttribute('stroke-dashoffset', 440 * (1 - energy / 100));

  speedUI.style.width = Math.min(100, (speed / 100) * 100) + '%';
  turboUI.style.width = Math.min(100, turbo * 100) + '%';

  // Motion blur blend factor based on speed
  motionMat.uniforms.blendFactor.value = 0.15 + Math.min(0.45, speed / 120);
  motionMat.uniforms.time.value = performance.now() / 1000;
}

/* ===== Render Loop ===== */
const clock = new THREE.Clock();

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

  if (running) {
    updatePhysics(dt);
    updateCollisions();
    updateEngineSound(speed);
  } else {
    updateEngineSound(0);
  }

  updateVisuals();

  // Camera follow
  const targetX = craft.position.x;
  const targetY = craft.position.y + 4;
  const targetZ = craft.position.z + 15;
  camera.position.lerp(new THREE.Vector3(targetX, targetY, targetZ), 0.05);
  camera.rotation.set(-0.2 + craftState.pitch * 3, 0, -craftState.roll * 0.1);
  camera.lookAt(craft.position.x, craft.position.y + 2, craft.position.z + 10);

  // Render bloom
  bloomComposer.render();

  // Render motion blur to screen
  motionMat.uniforms.tDiffuse.value = bloomTarget.texture;
  motionMat.uniforms.prevTexture.value = prevTarget.texture;
  renderer.setRenderTarget(null);
  renderer.render(motionScene, motionCam);

  // Save current bloom output to previous target for next frame
  renderer.setRenderTarget(prevTarget);
  renderer.render(copyScene, copyCam);
  renderer.setRenderTarget(null);
}

/* ===== Resize ===== */
window.addEventListener('resize', () => {
  const w = window.innerWidth;
  const h = window.innerHeight;
  camera.aspect = w / h;
  camera.updateProjectionMatrix();
  renderer.setSize(w, h);
  bloomTarget.setSize(w, h);
  prevTarget.setSize(w, h);
  bloomComposer.setSize(w, h);
});

/* ===== Init ===== */
document.getElementById('startBtn').addEventListener('click', () => {
  startGame();
});
document.getElementById('restartBtn').addEventListener('click', () => {
  startGame();
});

// Pre-warm a little
regenerateWorld();

// Start loop
renderLoop();

</script>
</body>
</html>