← Canyon Run results

Canyon Run

Minimax m3 · basic

◈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

Human review · Jul 17, 2026 · rubric 1

Historical path-based review. Association with these exact bytes is unverified.

The reviewer marked this result as broken. It remains in the archive.

Visual design
4 / 5
UX / feel
1 / 5
Works
1 / 5
Prompt spirit
4 / 5
Delight
4 / 5
Gorgeous handling of lighting, decent to good models, but broken

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 38,068 bytes · SHA-256 a2208d942b22
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Canyon Racer</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #050010; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; color: #fff; }
canvas { display: block; }
#hud { position: fixed; inset: 0; pointer-events: none; z-index: 10; }
#speed-container { position: absolute; top: 28px; left: 50%; transform: translateX(-50%); width: 260px; text-align: center; }
#speed-label { font-size: 9px; letter-spacing: 6px; opacity: 0.5; margin-bottom: 8px; }
#speed-bar { width: 100%; height: 2px; background: rgba(255,255,255,0.12); position: relative; overflow: hidden; }
#speed-fill { position: absolute; top: 0; left: 0; height: 100%; background: linear-gradient(90deg, #ffaa44, #ff3366); width: 30%; box-shadow: 0 0 8px #ff8866; transition: width 0.15s; }
#score { position: absolute; top: 70px; left: 50%; transform: translateX(-50%); font-size: 30px; font-weight: 100; letter-spacing: 8px; color: #fff; text-shadow: 0 0 12px rgba(255,200,120,0.4); }
#multiplier { position: absolute; top: 50%; right: 60px; transform: translateY(-50%); font-size: 64px; font-weight: 100; color: #ffaa44; text-shadow: 0 0 24px rgba(255,170,68,0.5); letter-spacing: -2px; }
#multiplier-label { position: absolute; top: 50%; right: 60px; transform: translate(0, 70px); font-size: 10px; letter-spacing: 4px; opacity: 0.45; }
#multiplier.pulse { animation: pulse 0.4s; }
@keyframes pulse { 0% { transform: translateY(-50%) scale(1); } 30% { transform: translateY(-50%) scale(1.6); color: #fff; text-shadow: 0 0 40px #fff; } 100% { transform: translateY(-50%) scale(1); color: #ffaa44; } }
#energy-container { position: absolute; bottom: 56px; left: 50%; transform: translateX(-50%); width: 320px; text-align: center; }
#energy-label { font-size: 9px; letter-spacing: 6px; opacity: 0.5; margin-bottom: 8px; }
#energy-bar { width: 100%; height: 4px; background: rgba(255,255,255,0.08); }
#energy-fill { height: 100%; background: linear-gradient(90deg, #44ddff, #ffee66); width: 100%; box-shadow: 0 0 12px #ffee66; transition: width 0.2s, box-shadow 0.2s, background 0.2s; }
#turbo-container { position: absolute; bottom: 28px; left: 50%; transform: translateX(-50%); width: 320px; text-align: center; }
#turbo-label { font-size: 9px; letter-spacing: 6px; opacity: 0.5; margin-bottom: 8px; }
#turbo-bar { width: 100%; height: 2px; background: rgba(255,255,255,0.08); }
#turbo-fill { height: 100%; background: linear-gradient(90deg, #ff5522, #ffaa44); width: 0%; box-shadow: 0 0 8px #ff5522; transition: width 0.1s, box-shadow 0.2s; }
#turbo-fill.ready { box-shadow: 0 0 20px #ffaa44; }
.overlay { position: absolute; inset: 0; background: linear-gradient(135deg, rgba(15,5,40,0.92), rgba(50,15,30,0.88)); display: flex; flex-direction: column; justify-content: center; align-items: center; pointer-events: auto; text-align: center; z-index: 20; }
.overlay h1 { font-size: 64px; font-weight: 100; color: #ffaa44; letter-spacing: 14px; margin-bottom: 14px; text-shadow: 0 0 30px rgba(255,170,68,0.5); }
.overlay h2 { font-size: 12px; font-weight: 300; letter-spacing: 8px; color: rgba(255,255,255,0.6); margin-bottom: 40px; }
.overlay .stats { display: flex; gap: 70px; margin: 30px 0 10px; }
.overlay .stat { text-align: center; }
.overlay .stat .value { font-size: 38px; font-weight: 100; color: #ffeeaa; }
.overlay .stat .label { font-size: 9px; letter-spacing: 5px; opacity: 0.5; margin-top: 8px; }
.overlay .keys { margin-top: 24px; font-size: 11px; letter-spacing: 3px; opacity: 0.55; line-height: 2; }
.btn { margin-top: 40px; padding: 14px 40px; background: transparent; border: 1px solid #ffaa44; color: #ffaa44; font-size: 13px; letter-spacing: 8px; cursor: pointer; transition: all 0.2s; font-family: inherit; }
.btn:hover { background: #ffaa44; color: #1a0a2a; box-shadow: 0 0 30px rgba(255,170,68,0.4); }
#warning { position: absolute; top: 40%; left: 50%; transform: translate(-50%, -50%); font-size: 22px; font-weight: 200; letter-spacing: 10px; color: #ff4466; opacity: 0; transition: opacity 0.3s; text-shadow: 0 0 30px #ff4466; }
#speed-strip { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 1px; height: 100%; background: linear-gradient(180deg, transparent, rgba(255,255,255,0.05), transparent); pointer-events: none; opacity: 0.5; }
</style>
</head>
<body>
<canvas id="game"></canvas>
<div id="hud">
  <div id="speed-container">
    <div id="speed-label">VELOCITY</div>
    <div id="speed-bar"><div id="speed-fill"></div></div>
  </div>
  <div id="score">00000</div>
  <div id="multiplier">1.0×</div>
  <div id="multiplier-label">MULTIPLIER</div>
  <div id="energy-container">
    <div id="energy-label">ENERGY</div>
    <div id="energy-bar"><div id="energy-fill"></div></div>
  </div>
  <div id="turbo-container">
    <div id="turbo-label">TURBO</div>
    <div id="turbo-bar"><div id="turbo-fill"></div></div>
  </div>
  <div id="warning">ENERGY CRITICAL</div>
</div>
<div id="start" class="overlay">
  <h1>CANYON RACER</h1>
  <h2>PROCEDURAL HIGH-SPEED RUN</h2>
  <div class="keys">
    A &nbsp;/&nbsp; D &nbsp; BANK<br>
    W &nbsp;/&nbsp; S &nbsp; PITCH<br>
    SPACE &nbsp; TURBO BOOST<br>
    R &nbsp; RESTART
  </div>
  <button class="btn" id="start-btn">LAUNCH</button>
</div>
<div id="gameover" class="overlay" style="display: none;">
  <h1>RUN OVER</h1>
  <div class="stats">
    <div class="stat"><div class="value" id="final-score">0</div><div class="label">SCORE</div></div>
    <div class="stat"><div class="value" id="final-speed">0</div><div class="label">MAX SPEED</div></div>
    <div class="stat"><div class="value" id="final-mult">0</div><div class="label">MAX MULT</div></div>
  </div>
  <button class="btn" id="restart-btn">RESTART</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';
import { OutputPass } from 'three/addons/postprocessing/OutputPass.js';

// === Constants ===
const CHUNK_LENGTH = 80;
const CHUNKS_AHEAD = 5;
const CHUNKS_BEHIND = 2;
const PLAYER_RADIUS = 0.85;

// === Seeded RNG ===
function mulberry32(seed) {
  let s = seed >>> 0;
  return function() {
    s = (s + 0x6D2B79F5) >>> 0;
    let t = s;
    t = Math.imul(t ^ (t >>> 15), t | 1);
    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

// === Game State ===
const state = {
  playing: false,
  score: 0,
  speed: 55,
  maxSpeed: 0,
  multiplier: 1,
  maxMultiplier: 1,
  energy: 1,
  turbo: 0,
  difficulty: 0,
  shake: 0,
  nearMissCooldown: 0,
  spawnTrailDist: 0,
  spawnDebrisDist: 0,
  frameTime: 0,
};

// === Input ===
const keys = {};
window.addEventListener('keydown', e => {
  const k = e.key.toLowerCase();
  keys[k] = true;
  if (k === 'r') { if (state.playing) restart(); else if (document.getElementById('gameover').style.display === 'flex') restart(); }
  if (k === ' ') e.preventDefault();
});
window.addEventListener('keyup', e => { keys[e.key.toLowerCase()] = false; });

// === Three.js Setup ===
const canvas = document.getElementById('game');
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, powerPreference: 'high-performance' });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.15;
renderer.outputColorSpace = THREE.SRGBColorSpace;

const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0xff5522, 0.0055);

const camera = new THREE.PerspectiveCamera(72, window.innerWidth / window.innerHeight, 0.1, 800);
camera.position.set(0, 6, -12);

// === Sky ===
const skyGeom = new THREE.SphereGeometry(450, 32, 16);
const skyMat = new THREE.ShaderMaterial({
  uniforms: {
    topColor: { value: new THREE.Color(0x0a0228) },
    midColor: { value: new THREE.Color(0x552266) },
    horizonColor: { value: new THREE.Color(0xff7733) },
    bottomColor: { value: new THREE.Color(0xffaa66) },
    sunPos: { value: new THREE.Vector3(0.25, 0.08, 0.96) },
    sunColor: { value: new THREE.Color(0xffeecc) },
  },
  vertexShader: `varying vec3 vDir; void main() { vDir = normalize(position); gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`,
  fragmentShader: `
    uniform vec3 topColor; uniform vec3 midColor; uniform vec3 horizonColor; uniform vec3 bottomColor;
    uniform vec3 sunPos; uniform vec3 sunColor; varying vec3 vDir;
    void main() {
      float h = vDir.y;
      vec3 col = mix(bottomColor, horizonColor, smoothstep(-0.3, 0.0, h));
      col = mix(col, midColor, smoothstep(0.0, 0.18, h));
      col = mix(col, topColor, smoothstep(0.2, 0.7, h));
      float sunDot = max(dot(vDir, normalize(sunPos)), 0.0);
      col += sunColor * pow(sunDot, 64.0) * 2.0;
      col += sunColor * 0.4 * pow(sunDot, 6.0);
      col += vec3(0.3, 0.1, 0.2) * pow(sunDot, 2.0) * 0.2;
      gl_FragColor = vec4(col, 1.0);
    }
  `,
  side: THREE.BackSide,
  depthWrite: false,
});
const sky = new THREE.Mesh(skyGeom, skyMat);
scene.add(sky);

// === Lighting ===
const sunLight = new THREE.DirectionalLight(0xffbb77, 1.9);
sunLight.position.set(80, 22, 60);
sunLight.target.position.set(0, 0, 0);
scene.add(sunLight.target);
sunLight.castShadow = true;
sunLight.shadow.mapSize.width = 1024;
sunLight.shadow.mapSize.height = 1024;
sunLight.shadow.camera.left = -70;
sunLight.shadow.camera.right = 70;
sunLight.shadow.camera.top = 70;
sunLight.shadow.camera.bottom = -70;
sunLight.shadow.camera.near = 1;
sunLight.shadow.camera.far = 220;
sunLight.shadow.bias = -0.0008;
scene.add(sunLight);

const fillLight = new THREE.HemisphereLight(0xff8855, 0x110033, 0.45);
scene.add(fillLight);

const rimLight = new THREE.DirectionalLight(0x8855ff, 0.6);
rimLight.position.set(-60, 30, -40);
scene.add(rimLight);

// === Ground (follows player) ===
const groundMat = new THREE.MeshStandardMaterial({ color: 0x0a0414, roughness: 1, metalness: 0 });
const ground = new THREE.Mesh(new THREE.PlaneGeometry(3000, 3000), groundMat);
ground.rotation.x = -Math.PI / 2;
ground.position.y = -28;
ground.receiveShadow = true;
scene.add(ground);

// === Player ===
const playerGroup = new THREE.Group();
scene.add(playerGroup);

const bodyMat = new THREE.MeshStandardMaterial({ color: 0xeaeaff, metalness: 0.85, roughness: 0.22, flatShading: true });
const accentMat = new THREE.MeshStandardMaterial({ color: 0x884422, metalness: 0.4, roughness: 0.6, flatShading: true });

// Body
const bodyGeom = new THREE.ConeGeometry(0.55, 2.6, 4);
bodyGeom.rotateX(Math.PI / 2);
const body = new THREE.Mesh(bodyGeom, bodyMat);
body.castShadow = true;
playerGroup.add(body);

// Wings
const wingGeom = new THREE.BoxGeometry(3.4, 0.08, 1.1);
const wing = new THREE.Mesh(wingGeom, bodyMat);
wing.position.z = 0.3;
wing.castShadow = true;
playerGroup.add(wing);

// Wing tips
const tipGeom = new THREE.BoxGeometry(0.6, 0.18, 0.8);
const tipL = new THREE.Mesh(tipGeom, accentMat);
tipL.position.set(-1.7, -0.05, 0.3);
tipL.castShadow = true;
playerGroup.add(tipL);
const tipR = new THREE.Mesh(tipGeom, accentMat);
tipR.position.set(1.7, -0.05, 0.3);
tipR.castShadow = true;
playerGroup.add(tipR);

// Tail fin
const finGeom = new THREE.BoxGeometry(0.08, 0.6, 0.7);
const fin = new THREE.Mesh(finGeom, bodyMat);
fin.position.set(0, 0.4, -0.7);
playerGroup.add(fin);

// Cockpit
const cockpitGeom = new THREE.SphereGeometry(0.35, 8, 6, 0, Math.PI * 2, 0, Math.PI / 2);
const cockpitMat = new THREE.MeshStandardMaterial({ color: 0x222244, metalness: 0.9, roughness: 0.1 });
const cockpit = new THREE.Mesh(cockpitGeom, cockpitMat);
cockpit.position.set(0, 0.3, 0.4);
playerGroup.add(cockpit);

// Engine glow
const engineMat = new THREE.MeshBasicMaterial({ color: 0xffcc66 });
const engine = new THREE.Mesh(new THREE.SphereGeometry(0.32, 12, 8), engineMat);
engine.position.set(0, 0, -1.4);
playerGroup.add(engine);

const engineHalo = new THREE.Mesh(
  new THREE.SphereGeometry(0.7, 12, 8),
  new THREE.MeshBasicMaterial({ color: 0xff8833, transparent: true, opacity: 0.5, blending: THREE.AdditiveBlending, depthWrite: false })
);
engineHalo.position.copy(engine.position);
playerGroup.add(engineHalo);

const engineLight = new THREE.PointLight(0xff7733, 2.5, 10);
engineLight.position.set(0, 0, -1.6);
playerGroup.add(engineLight);

// Trail cone
const trailConeGeom = new THREE.ConeGeometry(0.32, 2.2, 8, 1, true);
trailConeGeom.rotateX(-Math.PI / 2);
const trailConeMat = new THREE.MeshBasicMaterial({
  color: 0xffaa44, transparent: true, opacity: 0.55,
  blending: THREE.AdditiveBlending, side: THREE.DoubleSide, depthWrite: false,
});
const trailCone = new THREE.Mesh(trailConeGeom, trailConeMat);
trailCone.position.set(0, 0, -2.6);
playerGroup.add(trailCone);

const player = {
  group: playerGroup,
  position: new THREE.Vector3(0, 6, 0),
  roll: 0, rollVel: 0,
  pitch: 0, pitchVel: 0,
  yaw: 0, yawVel: 0,
  forwardSpeed: 55,
  radius: PLAYER_RADIUS,
};

// === World: Chunk System ===
const chunks = new Map();
const chunkGroup = new THREE.Group();
scene.add(chunkGroup);

function createWallSegment(x, z, length, depth, height, baseY, side, rng) {
  const geom = new THREE.BoxGeometry(depth, height, length);
  const hueShift = rng();
  const mat = new THREE.MeshStandardMaterial({
    color: 0x6a2a18,
    metalness: 0.15,
    roughness: 0.85,
    flatShading: true,
    emissive: 0x220a05,
    emissiveIntensity: 0.5,
  });
  mat.color.setHSL(0.04 + hueShift * 0.025, 0.55, 0.14 + hueShift * 0.08);
  const mesh = new THREE.Mesh(geom, mat);
  mesh.position.set(x, baseY + height / 2, z);
  mesh.castShadow = true;
  mesh.receiveShadow = true;
  return mesh;
}

function createMonolith(x, z, width, depth, height, bottom) {
  const geom = new THREE.BoxGeometry(width, height, depth);
  const mat = new THREE.MeshStandardMaterial({
    color: 0xaaaabb,
    metalness: 0.75,
    roughness: 0.32,
    flatShading: true,
  });
  const mesh = new THREE.Mesh(geom, mat);
  mesh.position.set(x, bottom + height / 2, z);
  mesh.castShadow = true;
  mesh.receiveShadow = true;
  return mesh;
}

function createOrb() {
  const group = new THREE.Group();
  const core = new THREE.Mesh(
    new THREE.OctahedronGeometry(0.5, 0),
    new THREE.MeshBasicMaterial({ color: 0xffeeaa })
  );
  group.add(core);
  const halo = new THREE.Mesh(
    new THREE.SphereGeometry(1.0, 12, 8),
    new THREE.MeshBasicMaterial({ color: 0xffaa44, transparent: true, opacity: 0.35, blending: THREE.AdditiveBlending, depthWrite: false })
  );
  group.add(halo);
  const ring = new THREE.Mesh(
    new THREE.TorusGeometry(0.75, 0.05, 6, 16),
    new THREE.MeshBasicMaterial({ color: 0xffeecc, transparent: true, opacity: 0.6 })
  );
  ring.rotation.x = Math.PI / 2;
  group.add(ring);
  group.userData.core = core;
  group.userData.halo = halo;
  group.userData.ring = ring;
  return group;
}

function generateChunk(index) {
  if (chunks.has(index)) return;
  const rng = mulberry32(index * 9173 + 7);
  const group = new THREE.Group();
  group.position.z = index * CHUNK_LENGTH;
  const walls = [];
  const monoliths = [];
  const orbs = [];

  const difficulty = Math.min(1, state.score / 12000);
  const baseWidth = 48 - difficulty * 22;
  const wallThick = 4;
  const wallHeight = 32 + rng() * 22;
  const wallBaseY = -16;

  const numSegments = 6;
  const segLength = CHUNK_LENGTH / numSegments;

  // Global curve function - canyon snakes left/right smoothly
  const curveAmp = 10 + difficulty * 8;

  for (let i = 0; i < numSegments; i++) {
    const zLocal = i * segLength + segLength / 2;
    const globalZ = index * CHUNK_LENGTH + zLocal;
    const curveX = Math.sin(globalZ * 0.022) * curveAmp;
    const wobble = (rng() - 0.5) * 4;

    const leftX = -baseWidth / 2 + curveX + (rng() - 0.5) * 3;
    const rightX = baseWidth / 2 + curveX + (rng() - 0.5) * 3;
    const segHeight = wallHeight + (rng() - 0.5) * 6;
    const segBaseY = wallBaseY + (rng() - 0.5) * 2;

    const wL = createWallSegment(leftX, zLocal, segLength + 1, wallThick, segHeight, segBaseY, 'left', rng);
    const wR = createWallSegment(rightX, zLocal, segLength + 1, wallThick, segHeight, segBaseY, 'right', rng);
    group.add(wL, wR);
    walls.push({ x: leftX, z: zLocal, depth: wallThick, width: segLength, height: segHeight, bottom: segBaseY });
    walls.push({ x: rightX, z: zLocal, depth: wallThick, width: segLength, height: segHeight, bottom: segBaseY });
  }

  // Monoliths
  const monoChance = 0.25 + difficulty * 0.4;
  let monoCount = 0;
  if (rng() < monoChance) monoCount = 1;
  if (rng() < difficulty * 0.35) monoCount++;

  for (let m = 0; m < monoCount; m++) {
    const monoZ = rng() * CHUNK_LENGTH;
    const globalZ2 = index * CHUNK_LENGTH + monoZ;
    const curveX2 = Math.sin(globalZ2 * 0.022) * curveAmp;
    const monoX = curveX2 + (rng() - 0.5) * (baseWidth - 14);
    const monoWidth = 3.5 + rng() * 4.5;
    const monoDepth = 3.5 + rng() * 4.5;
    const fromCeiling = rng() > 0.55;
    const monoHeight = 9 + rng() * 18;
    const monoBottom = fromCeiling ? 30 - monoHeight : -16;
    group.add(createMonolith(monoX, monoZ, monoWidth, monoDepth, monoHeight, monoBottom));
    monoliths.push({ x: monoX, z: monoZ, width: monoWidth, depth: monoDepth, height: monoHeight, bottom: monoBottom });
  }

  // Orbs
  const orbCount = 1 + Math.floor(rng() * 3);
  for (let i = 0; i < orbCount; i++) {
    const orbZ = (i + 0.5) * (CHUNK_LENGTH / orbCount) + (rng() - 0.5) * 6;
    const globalZ3 = index * CHUNK_LENGTH + orbZ;
    const curveX3 = Math.sin(globalZ3 * 0.022) * curveAmp;
    const orbX = curveX3 + (rng() - 0.5) * (baseWidth - 8);
    const orbY = 4 + rng() * 14;
    const orbMesh = createOrb();
    orbMesh.position.set(orbX, orbY, orbZ);
    group.add(orbMesh);
    orbs.push({ x: orbX, y: orbY, z: orbZ, mesh: orbMesh, collected: false });
  }

  chunkGroup.add(group);
  chunks.set(index, { index, group, walls, monoliths, orbs, z: index * CHUNK_LENGTH });
}

function despawnChunk(index) {
  const chunk = chunks.get(index);
  if (!chunk) return;
  chunk.group.traverse(obj => {
    if (obj.geometry) obj.geometry.dispose();
    if (obj.material) {
      if (Array.isArray(obj.material)) obj.material.forEach(m => m.dispose());
      else obj.material.dispose();
    }
  });
  chunkGroup.remove(chunk.group);
  chunks.delete(index);
}

function updateChunks() {
  const playerChunk = Math.floor(player.position.z / CHUNK_LENGTH);
  for (let i = playerChunk - CHUNKS_BEHIND; i <= playerChunk + CHUNKS_AHEAD; i++) {
    if (i >= 0) generateChunk(i);
  }
  for (const index of [...chunks.keys()]) {
    if (index < playerChunk - CHUNKS_BEHIND || index > playerChunk + CHUNKS_AHEAD) despawnChunk(index);
  }
}

// === Particles ===
const TRAIL_COUNT = 300;
const trailPositions = new Float32Array(TRAIL_COUNT * 3);
const trailColors = new Float32Array(TRAIL_COUNT * 3);
const trailSizes = new Float32Array(TRAIL_COUNT);
for (let i = 0; i < TRAIL_COUNT; i++) {
  trailPositions[i * 3 + 1] = -1000;
  trailSizes[i] = 0;
  trailColors[i * 3] = 1; trailColors[i * 3 + 1] = 0.6; trailColors[i * 3 + 2] = 0.3;
}
const trailGeom = new THREE.BufferGeometry();
trailGeom.setAttribute('position', new THREE.BufferAttribute(trailPositions, 3));
trailGeom.setAttribute('color', new THREE.BufferAttribute(trailColors, 3));
trailGeom.setAttribute('size', new THREE.BufferAttribute(trailSizes, 1));
const trailMat = new THREE.ShaderMaterial({
  vertexShader: `
    attribute float size; attribute vec3 color; varying vec3 vColor;
    void main() { vColor = color;
      vec4 mv = modelViewMatrix * vec4(position, 1.0);
      gl_PointSize = size * (320.0 / -mv.z);
      gl_Position = projectionMatrix * mv;
    }`,
  fragmentShader: `
    varying vec3 vColor;
    void main() {
      vec2 c = gl_PointCoord - vec2(0.5);
      float d = length(c);
      if (d > 0.5) discard;
      float a = pow(1.0 - d * 2.0, 2.0);
      gl_FragColor = vec4(vColor, a * 0.7);
    }`,
  transparent: true, depthWrite: false, blending: THREE.AdditiveBlending,
});
const trailPoints = new THREE.Points(trailGeom, trailMat);
scene.add(trailPoints);
let trailIndex = 0;

function spawnTrailParticle() {
  const i = trailIndex;
  trailPositions[i * 3] = player.position.x + (Math.random() - 0.5) * 0.4;
  trailPositions[i * 3 + 1] = player.position.y + (Math.random() - 0.5) * 0.2;
  trailPositions[i * 3 + 2] = player.position.z - 1.6 + (Math.random() - 0.5) * 0.6;
  const boost = state.turbo > 0.05 && (keys[' '] || keys['space']) ? 1 : 0;
  trailSizes[i] = 18 + Math.random() * 22 + boost * 15;
  trailColors[i * 3] = 1;
  trailColors[i * 3 + 1] = 0.5 + Math.random() * 0.25;
  trailColors[i * 3 + 2] = 0.2 + Math.random() * 0.15;
  trailIndex = (trailIndex + 1) % TRAIL_COUNT;
}

function updateTrail(dt) {
  for (let i = 0; i < TRAIL_COUNT; i++) {
    if (trailSizes[i] > 0.5) trailSizes[i] *= 0.94;
    else trailSizes[i] = 0;
  }
  trailGeom.attributes.size.needsUpdate = true;
  trailGeom.attributes.color.needsUpdate = true;
  trailGeom.attributes.position.needsUpdate = true;
}

// Speed debris
const DEBRIS_COUNT = 200;
const debrisPositions = new Float32Array(DEBRIS_COUNT * 3);
const debrisColors = new Float32Array(DEBRIS_COUNT * 3);
const debrisSizes = new Float32Array(DEBRIS_COUNT);
for (let i = 0; i < DEBRIS_COUNT; i++) {
  debrisPositions[i * 3 + 1] = -1000;
  debrisSizes[i] = 0;
  debrisColors[i * 3] = 1; debrisColors[i * 3 + 1] = 0.7; debrisColors[i * 3 + 2] = 0.4;
}
const debrisGeom = new THREE.BufferGeometry();
debrisGeom.setAttribute('position', new THREE.BufferAttribute(debrisPositions, 3));
debrisGeom.setAttribute('color', new THREE.BufferAttribute(debrisColors, 3));
debrisGeom.setAttribute('size', new THREE.BufferAttribute(debrisSizes, 1));
const debrisMat = trailMat.clone();
const debrisPoints = new THREE.Points(debrisGeom, debrisMat);
scene.add(debrisPoints);
let debrisIndex = 0;

function spawnDebris() {
  const i = debrisIndex;
  const angle = Math.random() * Math.PI * 2;
  const radius = 5 + Math.random() * 28;
  debrisPositions[i * 3] = player.position.x + Math.cos(angle) * radius;
  debrisPositions[i * 3 + 1] = player.position.y + Math.sin(angle) * radius * 0.6 - 2;
  debrisPositions[i * 3 + 2] = player.position.z + 8 + Math.random() * 50;
  debrisSizes[i] = 4 + Math.random() * 8;
  debrisColors[i * 3] = 1;
  debrisColors[i * 3 + 1] = 0.55 + Math.random() * 0.3;
  debrisColors[i * 3 + 2] = 0.3 + Math.random() * 0.2;
  debrisIndex = (debrisIndex + 1) % DEBRIS_COUNT;
}

function updateDebris(dt) {
  for (let i = 0; i < DEBRIS_COUNT; i++) {
    if (debrisSizes[i] > 0) {
      if (debrisPositions[i * 3 + 2] < player.position.z - 50) debrisSizes[i] = 0;
    }
  }
  debrisGeom.attributes.position.needsUpdate = true;
  debrisGeom.attributes.size.needsUpdate = true;
  debrisGeom.attributes.color.needsUpdate = true;
}

// === 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.85, 0.5, 0.55);
composer.addPass(bloomPass);
composer.addPass(new OutputPass());

// === Audio ===
const audio = {
  ctx: null, engineOsc: null, engineGain: null, filter: null,
  init() {
    try {
      this.ctx = new (window.AudioContext || window.webkitAudioContext)();
      this.engineOsc = this.ctx.createOscillator();
      this.engineOsc.type = 'sawtooth';
      this.engineOsc.frequency.value = 70;
      this.filter = this.ctx.createBiquadFilter();
      this.filter.type = 'lowpass';
      this.filter.frequency.value = 400;
      this.filter.Q.value = 2;
      this.engineGain = this.ctx.createGain();
      this.engineGain.gain.value = 0;
      this.engineOsc.connect(this.filter).connect(this.engineGain).connect(this.ctx.destination);
      this.engineOsc.start();
    } catch (e) { console.warn('Audio init failed', e); }
  },
  resume() { if (this.ctx && this.ctx.state === 'suspended') this.ctx.resume(); },
  updateEngine(speed, boost) {
    if (!this.engineOsc) return;
    const targetFreq = 55 + speed * 0.55 + boost * 90;
    const targetGain = 0.03 + speed * 0.0004 + boost * 0.05;
    this.engineOsc.frequency.setTargetAtTime(targetFreq, this.ctx.currentTime, 0.08);
    this.filter.frequency.setTargetAtTime(350 + speed * 4 + boost * 600, this.ctx.currentTime, 0.08);
    this.engineGain.gain.setTargetAtTime(targetGain, this.ctx.currentTime, 0.08);
  },
  playOrb() {
    if (!this.ctx) return;
    const osc = this.ctx.createOscillator();
    osc.type = 'sine';
    osc.frequency.setValueAtTime(660, this.ctx.currentTime);
    osc.frequency.exponentialRampToValueAtTime(1500, this.ctx.currentTime + 0.18);
    const g = this.ctx.createGain();
    g.gain.setValueAtTime(0.18, this.ctx.currentTime);
    g.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.28);
    osc.connect(g).connect(this.ctx.destination);
    osc.start(); osc.stop(this.ctx.currentTime + 0.28);
  },
  playNearMiss() {
    if (!this.ctx) return;
    const osc = this.ctx.createOscillator();
    osc.type = 'sawtooth';
    osc.frequency.setValueAtTime(220, this.ctx.currentTime);
    osc.frequency.exponentialRampToValueAtTime(900, this.ctx.currentTime + 0.13);
    const f = this.ctx.createBiquadFilter();
    f.type = 'bandpass'; f.frequency.value = 1100; f.Q.value = 5;
    const g = this.ctx.createGain();
    g.gain.setValueAtTime(0.14, this.ctx.currentTime);
    g.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.22);
    osc.connect(f).connect(g).connect(this.ctx.destination);
    osc.start(); osc.stop(this.ctx.currentTime + 0.22);
  },
  playCrash() {
    if (!this.ctx) return;
    const sz = this.ctx.sampleRate * 0.7;
    const buf = this.ctx.createBuffer(1, sz, this.ctx.sampleRate);
    const d = buf.getChannelData(0);
    for (let i = 0; i < sz; i++) d[i] = (Math.random() * 2 - 1) * (1 - i / sz) * 0.9;
    const src = this.ctx.createBufferSource();
    src.buffer = buf;
    const g = this.ctx.createGain();
    g.gain.value = 0.5;
    const f = this.ctx.createBiquadFilter();
    f.type = 'lowpass'; f.frequency.value = 1800;
    src.connect(f).connect(g).connect(this.ctx.destination);
    src.start();
    // Engine stop
    if (this.engineGain) this.engineGain.gain.setTargetAtTime(0, this.ctx.currentTime, 0.05);
  },
};

// === Game Logic ===
function updatePlayer(dt) {
  let rollInput = 0;
  if (keys['a'] || keys['arrowleft']) rollInput += 1;
  if (keys['d'] || keys['arrowright']) rollInput -= 1;
  let pitchInput = 0;
  if (keys['w'] || keys['arrowup']) pitchInput += 1;
  if (keys['s'] || keys['arrowdown']) pitchInput -= 1;

  const targetRollVel = rollInput * 4.5;
  player.rollVel += (targetRollVel - player.rollVel) * Math.min(1, dt * 7);
  player.roll += player.rollVel * dt;
  player.roll = Math.max(-1.6, Math.min(1.6, player.roll));
  if (rollInput === 0) player.roll *= (1 - dt * 1.8);

  player.yawVel = -player.roll * 0.95;
  player.yaw += player.yawVel * dt;

  const targetPitchVel = pitchInput * 2.8;
  player.pitchVel += (targetPitchVel - player.pitchVel) * Math.min(1, dt * 7);
  player.pitch += player.pitchVel * dt;
  player.pitch = Math.max(-0.75, Math.min(0.75, player.pitch));
  if (pitchInput === 0) player.pitch *= (1 - dt * 1.8);

  const difficulty = Math.min(1, state.score / 12000);
  state.difficulty = difficulty;
  let targetSpeed = 60 + difficulty * 40;
  const boosting = (keys[' '] || keys['space']) && state.turbo > 0.04;
  if (boosting) {
    targetSpeed = 110 + difficulty * 30;
    state.turbo = Math.max(0, state.turbo - dt * 0.4);
    state.shake = Math.min(0.35, state.shake + dt * 0.7);
  }
  player.forwardSpeed += (targetSpeed - player.forwardSpeed) * Math.min(1, dt * 2.5);
  state.speed = player.forwardSpeed;
  state.maxSpeed = Math.max(state.maxSpeed, player.forwardSpeed);

  const fx = Math.sin(player.yaw);
  const fz = Math.cos(player.yaw);
  player.position.x += fx * player.forwardSpeed * dt;
  player.position.z += fz * player.forwardSpeed * dt;
  player.position.y += Math.sin(player.pitch) * player.forwardSpeed * dt * 0.85;
  player.position.y = Math.max(-2, Math.min(42, player.position.y));

  playerGroup.rotation.set(player.pitch, player.yaw, player.roll);
  playerGroup.position.copy(player.position);

  const boostVis = boosting ? 1 : 0;
  engine.material.color.setHex(boostVis ? 0xffeeaa : 0xffaa44);
  engineHalo.material.opacity = 0.4 + boostVis * 0.5;
  engineHalo.scale.setScalar(1 + boostVis * 0.6);
  engineLight.intensity = 2.5 + boostVis * 4;
  trailCone.scale.z = 1 + boostVis * 2;
  trailCone.scale.x = 1 + boostVis * 0.5;
  trailCone.material.opacity = 0.55 + boostVis * 0.45;
}

function updateCamera(dt) {
  const offset = new THREE.Vector3(0, 3.8, -12);
  const camYaw = player.yaw;
  const camPitch = player.pitch * 0.45;
  const offsetRotated = offset.clone().applyEuler(new THREE.Euler(camPitch, camYaw, 0));
  const targetPos = player.position.clone().add(offsetRotated);
  camera.position.lerp(targetPos, Math.min(1, dt * 6));

  const lookAhead = new THREE.Vector3(0, 1.5, 28).applyEuler(new THREE.Euler(0, player.yaw, 0));
  const lookTarget = player.position.clone().add(lookAhead);
  camera.lookAt(lookTarget);

  const boosting = (keys[' '] || keys['space']) && state.turbo > 0.04;
  const targetFOV = 72 + (player.forwardSpeed - 55) * 0.42 + boosting * 14;
  camera.fov += (targetFOV - camera.fov) * Math.min(1, dt * 3);
  camera.updateProjectionMatrix();

  if (state.shake > 0.01) {
    camera.position.x += (Math.random() - 0.5) * state.shake;
    camera.position.y += (Math.random() - 0.5) * state.shake;
    camera.position.z += (Math.random() - 0.5) * state.shake * 0.4;
    state.shake *= 0.91;
  } else state.shake = 0;
}

function checkCollisions() {
  const px = player.position.x;
  const py = player.position.y;
  const pz = player.position.z;
  const r = player.radius;
  const currentChunk = Math.floor(pz / CHUNK_LENGTH);
  let nearMissed = false;

  for (let i = currentChunk - 1; i <= currentChunk + 2; i++) {
    const chunk = chunks.get(i);
    if (!chunk) continue;
    for (const w of chunk.walls) {
      const dx = Math.abs(px - w.x);
      const dz = Math.abs(pz - w.z);
      const dy = Math.abs(py - (w.bottom + w.height / 2));
      if (dz > w.width / 2 + 4) continue;
      const xC = dx < w.depth / 2 + r;
      const zC = dz < w.width / 2 + r;
      const yC = dy < w.height / 2 + r;
      if (xC && zC && yC) return { type: 'crash', obj: w };
      const xN = dx < w.depth / 2 + r + 2.2;
      const zN = dz < w.width / 2 + r + 1.8;
      const yN = dy < w.height / 2 + r + 2;
      if (xN && zN && yN && (pz - w.z) > -1.5 && (pz - w.z) < 1.5) nearMissed = true;
    }
    for (const m of chunk.monoliths) {
      const dx = Math.abs(px - m.x);
      const dz = Math.abs(pz - m.z);
      const dy = Math.abs(py - (m.bottom + m.height / 2));
      if (dz > m.depth / 2 + 4) continue;
      const xC = dx < m.width / 2 + r;
      const zC = dz < m.depth / 2 + r;
      const yC = dy < m.height / 2 + r;
      if (xC && zC && yC) return { type: 'crash', obj: m };
      const xN = dx < m.width / 2 + r + 2;
      const zN = dz < m.depth / 2 + r + 2;
      const yN = dy < m.height / 2 + r + 2;
      if (xN && zN && yN && (pz - m.z) > -1.8 && (pz - m.z) < 1.8) nearMissed = true;
    }
    for (const orb of chunk.orbs) {
      if (orb.collected) continue;
      const dSq = (px - orb.x) ** 2 + (py - orb.y) ** 2 + (pz - orb.z) ** 2;
      if (dSq < 2.2 * 2.2) {
        orb.collected = true;
        orb.mesh.visible = false;
        collectOrb();
      }
    }
  }

  if (nearMissed && state.nearMissCooldown <= 0) triggerNearMiss();
  else if (!nearMissed) state.nearMissCooldown = Math.max(0, state.nearMissCooldown - dt);
  return null;
}

function triggerNearMiss() {
  state.multiplier = Math.min(99, state.multiplier + 0.5);
  state.maxMultiplier = Math.max(state.maxMultiplier, state.multiplier);
  state.score += Math.floor(state.multiplier * 40);
  state.shake = Math.min(0.55, state.shake + 0.4);
  state.nearMissCooldown = 0.45;
  audio.playNearMiss();
  const el = document.getElementById('multiplier');
  el.classList.remove('pulse');
  void el.offsetWidth;
  el.classList.add('pulse');
}

function collectOrb() {
  state.energy = Math.min(1, state.energy + 0.22);
  state.score += Math.floor(80 * state.multiplier);
  state.turbo = Math.min(1, state.turbo + 0.18);
  audio.playOrb();
}

function updateGameLogic(dt) {
  state.energy -= dt * 0.085;
  if (state.energy <= 0) { gameOver(); return; }
  state.score += player.forwardSpeed * dt * 0.12 * state.multiplier;
  if (state.nearMissCooldown <= 0) state.multiplier = Math.max(1, state.multiplier - dt * 0.55);
  state.turbo = Math.max(0, state.turbo - dt * 0.018);

  state.spawnTrailDist += player.forwardSpeed * dt;
  if (state.spawnTrailDist > 0.35) { spawnTrailParticle(); state.spawnTrailDist = 0; }
  state.spawnDebrisDist += player.forwardSpeed * dt;
  const debrisInterval = 1.4 - (player.forwardSpeed - 55) * 0.008;
  if (state.spawnDebrisDist > Math.max(0.4, debrisInterval)) { spawnDebris(); state.spawnDebrisDist = 0; }

  const t = state.frameTime;
  for (const chunk of chunks.values()) {
    for (const orb of chunk.orbs) {
      if (!orb.collected) {
        orb.mesh.position.y = orb.y + Math.sin(t * 2.5 + orb.z * 0.1) * 0.4;
        orb.mesh.rotation.y = t * 1.8;
        orb.mesh.userData.ring.rotation.z = t * 1.2;
      }
    }
  }

  ground.position.x = player.position.x;
  ground.position.z = player.position.z;

  const boosting = (keys[' '] || keys['space']) && state.turbo > 0.04;
  audio.updateEngine(player.forwardSpeed, boosting ? 1 : 0);
}

function updateHUD() {
  const speedPct = Math.min(100, Math.max(0, ((state.speed - 40) / 100) * 100));
  document.getElementById('speed-fill').style.width = speedPct + '%';
  document.getElementById('score').textContent = Math.floor(state.score).toString().padStart(5, '0');
  document.getElementById('multiplier').textContent = state.multiplier.toFixed(1) + '×';
  const energyPct = Math.max(0, state.energy * 100);
  const ef = document.getElementById('energy-fill');
  ef.style.width = energyPct + '%';
  if (state.energy < 0.3) { ef.style.boxShadow = '0 0 24px #ff4466'; ef.style.background = 'linear-gradient(90deg, #ff4466, #ffaa44)'; }
  else { ef.style.boxShadow = '0 0 10px #ffee66'; ef.style.background = 'linear-gradient(90deg, #44ddff, #ffee66)'; }
  document.getElementById('warning').style.opacity = state.energy < 0.28 ? (Math.sin(state.frameTime * 8) > 0 ? 0.9 : 0.3) : '0';
  const turboPct = state.turbo * 100;
  const tf = document.getElementById('turbo-fill');
  tf.style.width = turboPct + '%';
  tf.classList.toggle('ready', state.turbo > 0.75);
}

function gameOver() {
  state.playing = false;
  audio.playCrash();
  document.getElementById('final-score').textContent = Math.floor(state.score);
  document.getElementById('final-speed').textContent = Math.floor(state.maxSpeed);
  document.getElementById('final-mult').textContent = state.maxMultiplier.toFixed(1) + '×';
  document.getElementById('gameover').style.display = 'flex';
  state.multiplier = 1;
}

function restart() {
  for (const idx of [...chunks.keys()]) despawnChunk(idx);
  state.playing = true;
  state.score = 0;
  state.speed = 55;
  state.maxSpeed = 0;
  state.multiplier = 1;
  state.maxMultiplier = 1;
  state.energy = 1;
  state.turbo = 0.3;
  state.shake = 0;
  state.nearMissCooldown = 0;
  state.spawnTrailDist = 0;
  state.spawnDebrisDist = 0;
  player.position.set(0, 6, 0);
  player.roll = 0; player.rollVel = 0;
  player.pitch = 0; player.pitchVel = 0;
  player.yaw = 0; player.yawVel = 0;
  player.forwardSpeed = 55;
  document.getElementById('gameover').style.display = 'none';
  document.getElementById('start').style.display = 'none';
  audio.resume();
  updateChunks();
}

document.getElementById('start-btn').addEventListener('click', () => {
  audio.init();
  restart();
});
document.getElementById('restart-btn').addEventListener('click', () => { restart(); });

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

// Initial chunks for background scene
updateChunks();

// === Main Loop ===
let lastTime = performance.now();
function animate() {
  requestAnimationFrame(animate);
  const now = performance.now();
  let dt = Math.min(0.05, (now - lastTime) / 1000);
  lastTime = now;
  state.frameTime += dt;

  if (state.playing) {
    updatePlayer(dt);
    updateChunks();
    const collision = checkCollisions();
    if (collision && collision.type === 'crash') gameOver();
    updateGameLogic(dt);
    updateTrail(dt);
    updateDebris(dt);
  } else {
    // Idle camera on start/gameover
    const t = state.frameTime;
    camera.position.x = Math.sin(t * 0.15) * 4;
    camera.position.y = 6 + Math.sin(t * 0.2) * 0.5;
    camera.position.z = -12;
    camera.lookAt(0, 5, 8);
    camera.fov = 72;
    camera.updateProjectionMatrix();
  }
  updateHUD();
  composer.render();
}
animate();
</script>
</body>
</html>
<!-- agent-meta {"model":"minimax/minimax-m3","provider":"openrouter","persona":"basic","sessionId":"6c1e5ea8-73ba-4d38-90e8-ba31de51d63a","tokensIn":1042,"tokensOut":48060,"tokensTotal":49102,"turns":1,"toolCalls":0,"failedToolCalls":0,"timestamp":"2026-07-13T22:15:06.359Z"} -->