← Canyon Run results

Canyon Run

muse spark 1.2 · basic

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 40,226 bytes · SHA-256 113d6d4c0ed4
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CANYON RUSH — High-Speed Procedural Racer</title>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;600;700&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{--orange:#ff6b2c;--pink:#ff2e88;--purple:#7a2cff;--cyan:#2cfff8;--gold:#ffb020;--bg:#0a0a14}
body{background:var(--bg);color:#fff;font-family:'Space Grotesk',sans-serif;overflow:hidden;height:100vh;width:100vw}
#c{position:fixed;inset:0;width:100%;height:100%;display:block}
#hud{position:fixed;inset:0;pointer-events:none;z-index:10}
.top-bar{position:absolute;top:0;left:0;right:0;display:flex;justify-content:space-between;padding:18px 24px;align-items:flex-start}
.hud-group{display:flex;flex-direction:column;gap:6px}
.hud-label{font-size:9px;letter-spacing:0.28em;color:rgba(255,255,255,0.45);font-weight:600;text-transform:uppercase}
.hud-value{font-family:'JetBrains Mono',monospace;font-weight:700;letter-spacing:-0.02em}
#scoreVal{font-size:34px;color:#fff;line-height:1}
#scoreVal span{font-size:16px;color:var(--gold)}
#speedWrap{width:220px}
.speed-track{height:2px;background:rgba(255,255,255,0.12);position:relative;overflow:hidden;margin-top:6px}
#speedFill{height:100%;width:30%;background:linear-gradient(90deg,var(--orange),var(--gold),var(--cyan));box-shadow:0 0 10px var(--orange);transition:width 0.1s}
#speedNum{font-family:'JetBrains Mono',monospace;font-size:11px;margin-top:4px;color:rgba(255,255,255,0.7);display:flex;justify-content:space-between}
.mult-display{position:absolute;left:50%;top:42%;transform:translate(-50%,-50%);text-align:center;opacity:0;transition:all 0.2s}
.mult-display.active{opacity:1;transform:translate(-50%,-55%) scale(1.05)}
#multNum{font-size:72px;font-weight:800;font-style:italic;letter-spacing:-0.06em;line-height:0.9;color:#fff;text-shadow:0 0 30px var(--pink), 0 0 60px var(--pink)}
#multLabel{font-size:10px;letter-spacing:0.4em;color:var(--gold);margin-top:2px}
.near-flash{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-size:11px;letter-spacing:0.5em;color:var(--cyan);opacity:0;font-weight:700;pointer-events:none}
.near-flash.show{animation:flash 0.6s ease}
@keyframes flash{0%{opacity:0;transform:translate(-50%,-40%)}20%{opacity:1}100%{opacity:0;transform:translate(-50%,-60%)}}
.bottom-bar{position:absolute;bottom:0;left:0;right:0;padding:20px 24px;display:flex;align-items:flex-end;justify-content:space-between;gap:24px}
.energy-wrap{flex:1;max-width:520px}
.energy-track{height:8px;background:rgba(255,255,255,0.08);border:1px solid rgba(255,255,255,0.1);position:relative;overflow:hidden;backdrop-filter:blur(10px)}
#energyFill{height:100%;width:65%;background:linear-gradient(90deg,var(--orange) 0%, var(--pink) 50%, var(--cyan) 100%);box-shadow:0 0 18px var(--pink);transition:width 0.2s}
#energyFill.low{background:linear-gradient(90deg,#ff1a1a,#ff6b2c);animation:pulse 0.4s infinite alternate}
@keyframes pulse{from{opacity:1}to{opacity:0.6}}
.turbo-wrap{width:180px;text-align:right}
.turbo-track{height:4px;background:rgba(255,255,255,0.08);margin-top:6px;position:relative}
#turboFill{height:100%;width:0%;background:var(--cyan);box-shadow:0 0 12px var(--cyan);transition:width 0.15s}
#turboBtn{margin-top:8px;font-size:9px;letter-spacing:0.22em;color:rgba(255,255,255,0.6);border:1px solid rgba(44,255,248,0.3);padding:4px 10px;display:inline-block;background:rgba(44,255,248,0.08)}
#turboBtn.ready{color:var(--cyan);border-color:var(--cyan);background:rgba(44,255,248,0.18);box-shadow:0 0 12px rgba(44,255,248,0.5)}
.controls-hint{position:absolute;bottom:84px;left:50%;transform:translateX(-50%);display:flex;gap:8px;align-items:center;font-size:9px;letter-spacing:0.18em;color:rgba(255,255,255,0.35)}
.kbd{border:1px solid rgba(255,255,255,0.18);padding:3px 6px;font-family:'JetBrains Mono',monospace;font-size:9px;background:rgba(255,255,255,0.06)}
.overlay{position:fixed;inset:0;z-index:20;display:flex;align-items:center;justify-content:center;background:radial-gradient(1200px 600px at 50% 0%, rgba(255,107,44,0.22), transparent 70%), linear-gradient(180deg, rgba(10,6,20,0.92) 0%, rgba(8,8,18,0.96) 100%);backdrop-filter:blur(2px)}
.card{width:min(560px,92vw);border:1px solid rgba(255,255,255,0.08);background:linear-gradient(180deg, rgba(255,255,255,0.06), rgba(255,255,255,0.02));padding:32px 28px;position:relative;overflow:hidden}
.card::before{content:"";position:absolute;top:0;left:0;right:0;height:1px;background:linear-gradient(90deg, transparent, var(--orange), var(--pink), transparent)}
.eyebrow{font-size:10px;letter-spacing:0.42em;color:var(--gold);font-weight:700}
h1{font-size:44px;line-height:0.9;letter-spacing:-0.05em;margin:10px 0 10px;font-weight:700}
h1 i{font-style:italic;color:var(--orange);font-weight:300}
.desc{font-size:13px;line-height:1.6;color:rgba(255,255,255,0.62);max-width:46ch}
.stats-row{display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;margin:18px 0}
.stat{border:1px solid rgba(255,255,255,0.07);background:rgba(255,255,255,0.03);padding:12px;text-align:center}
.stat b{font-family:'JetBrains Mono',monospace;font-size:18px;display:block;color:#fff}
.stat span{font-size:9px;letter-spacing:0.18em;color:rgba(255,255,255,0.45)}
.btn{width:100%;padding:16px;background:#fff;color:#0a0a14;border:none;font-weight:800;letter-spacing:0.18em;font-size:13px;cursor:pointer;transition:all 0.15s;pointer-events:auto}
.btn:hover{background:var(--orange);color:#fff;box-shadow:0 0 30px rgba(255,107,44,0.5)}
.btn:active{transform:scale(0.99)}
.btn-alt{background:transparent;color:#fff;border:1px solid rgba(255,255,255,0.14);margin-top:10px}
.btn-alt:hover{background:rgba(255,255,255,0.08);box-shadow:none}
.legend{display:flex;gap:16px;margin-top:16px;flex-wrap:wrap}
.legend div{font-size:10px;color:rgba(255,255,255,0.5);display:flex;align-items:center;gap:6px;letter-spacing:0.08em}
.dot{width:8px;height:8px;border-radius:50%}
.hidden{display:none !important}
#shake{position:fixed;inset:0;pointer-events:none}
@media(max-width:700px){
  h1{font-size:34px}
  .top-bar{padding:12px}
  #scoreVal{font-size:26px}
  .bottom-bar{flex-direction:column;align-items:stretch}
}
</style>
<script async src="https://unpkg.com/es-module-shims@1.8.0/dist/es-module-shims.js"></script>
<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>
</head>
<body>
<canvas id="c"></canvas>
<div id="shake"></div>

<div id="hud">
  <div class="top-bar">
    <div class="hud-group">
      <div class="hud-label">Distance • Score</div>
      <div id="scoreVal" class="hud-value">00000 <span>+0</span></div>
      <div style="display:flex;gap:14px;margin-top:4px">
        <div style="font-family:'JetBrains Mono',monospace;font-size:10px;color:rgba(255,255,255,0.5)">MAX <b id="maxSpeed" style="color:#fff">0</b> KM/H</div>
        <div style="font-family:'JetBrains Mono',monospace;font-size:10px;color:rgba(255,255,255,0.5)">BEST <b id="bestScore" style="color:var(--gold)">0</b></div>
      </div>
    </div>
    <div class="hud-group" style="align-items:flex-end">
      <div class="hud-label">Velocity</div>
      <div id="speedWrap">
        <div class="speed-track"><div id="speedFill"></div></div>
        <div id="speedNum"><span id="speedKmh">0</span><span>KM/H</span></div>
      </div>
    </div>
  </div>

  <div class="mult-display" id="multDisplay">
    <div id="multNum">×1.0</div>
    <div id="multLabel">MULTIPLIER</div>
  </div>
  <div class="near-flash" id="nearFlash">NEAR MISS +250</div>

  <div class="controls-hint">
    <span class="kbd">A D</span> <span class="kbd">W S</span> BANK & PITCH &nbsp;•&nbsp; <span class="kbd">SPACE</span> TURBO
  </div>

  <div class="bottom-bar">
    <div class="energy-wrap">
      <div class="hud-label" style="display:flex;justify-content:space-between"><span>Energy Core</span><span id="energyPct" style="color:var(--gold)">65%</span></div>
      <div class="energy-track"><div id="energyFill"></div></div>
      <div style="font-size:9px;letter-spacing:0.12em;color:rgba(255,255,255,0.35);margin-top:4px">COLLECT ORBS TO REFUEL • GRAZE WALLS FOR MULTIPLIER</div>
    </div>
    <div class="turbo-wrap">
      <div class="hud-label">Turbo Charge</div>
      <div class="turbo-track"><div id="turboFill"></div></div>
      <div id="turboBtn">CHARGING</div>
    </div>
  </div>
</div>

<div id="startOverlay" class="overlay">
  <div class="card">
    <div class="eyebrow">PROCEDURAL CANYON RACER • VER 1.4</div>
    <h1>CANYON <i>RUSH</i></h1>
    <p class="desc">Skim the golden-hour canyon at Mach 1. Grazing walls builds your multiplier, but the core bleeds fast. Thread the needle, harvest orbs, and ride the turbo — the canyon never ends.</p>
    <div class="stats-row">
      <div class="stat"><b>∞</b><span>ENDLESS CHUNKS</span></div>
      <div class="stat"><b>×99</b><span>NEAR MISS</span></div>
      <div class="stat"><b>60</b><span>FPS TARGET</span></div>
    </div>
    <button class="btn" id="playBtn">IGNITE ENGINES — PLAY</button>
    <button class="btn btn-alt" id="howBtn">HOW TO FLY</button>
    <div class="legend">
      <div><span class="dot" style="background:var(--gold)"></span> ORB = +25 ENERGY</div>
      <div><span class="dot" style="background:var(--cyan)"></span> GRAZE = MULTIPLIER</div>
      <div><span class="dot" style="background:var(--pink)"></span> MONOLITH = DEATH</div>
    </div>
    <div id="howText" class="hidden" style="margin-top:14px;font-size:12px;line-height:1.6;color:rgba(255,255,255,0.6);border-top:1px solid rgba(255,255,255,0.06);padding-top:12px">
      <b style="color:#fff">Flight model:</b> Momentum-based. Roll (A/D) banks and yaws, Pitch (W/S) climbs/dives. Hold straight for passive turbo charge. Grazing within 1.5m of walls triggers <b style="color:var(--cyan)">NEAR MISS</b>. Collect amber orbs for energy. SPACE when turbo full.
    </div>
  </div>
</div>

<div id="gameOverOverlay" class="overlay hidden">
  <div class="card" style="text-align:center">
    <div class="eyebrow" style="color:var(--pink)">SIGNAL LOST • CORE DEPLETED</div>
    <h1 style="font-size:38px">CRASH <i style="color:var(--pink)">LANDING</i></h1>
    <div class="stats-row">
      <div class="stat"><b id="finalScore">0</b><span>FINAL SCORE</span></div>
      <div class="stat"><b id="finalDist">0m</b><span>DISTANCE</span></div>
      <div class="stat"><b id="finalSpeed">0</b><span>TOP SPEED</span></div>
    </div>
    <div style="display:flex;gap:10px">
      <button class="btn" id="restartBtn" style="flex:1">RETRY RUN</button>
      <button class="btn btn-alt" id="menuBtn" style="flex:0.6">MENU</button>
    </div>
    <div style="margin-top:12px;font-size:10px;letter-spacing:0.2em;color:rgba(255,255,255,0.3)">NEAR MISSES CANYON WALLS BUILD LEGEND</div>
  </div>
</div>

<script type="module">
import * as THREE from 'three';

const canvas = document.getElementById('c');
const scoreVal = document.getElementById('scoreVal');
const speedFill = document.getElementById('speedFill');
const speedKmh = document.getElementById('speedKmh');
const maxSpeedEl = document.getElementById('maxSpeed');
const bestScoreEl = document.getElementById('bestScore');
const energyFill = document.getElementById('energyFill');
const energyPct = document.getElementById('energyPct');
const turboFill = document.getElementById('turboFill');
const turboBtn = document.getElementById('turboBtn');
const multDisplay = document.getElementById('multDisplay');
const multNum = document.getElementById('multNum');
const nearFlash = document.getElementById('nearFlash');
const startOverlay = document.getElementById('startOverlay');
const gameOverOverlay = document.getElementById('gameOverOverlay');

let scene, camera, renderer, clock;
let player, playerGroup;
let dustSystem, trailGroup;
let chunks = [];
let orbs = [];
let obstacles = [];
let audioCtx;

const CHUNK_LEN = 50;
const NUM_CHUNKS = 7;
const CANYON_HALF = 16;

let state = 'menu'; // menu, playing, dead
let score = 0, distance = 0, multiplier = 1, nearMissCooldown = 0;
let energy = 72, turbo = 0, isBoosting = false, boostTime = 0;
let baseSpeed = 42, curSpeed = 42, maxSpeed = 0, difficulty = 0;
let keys = {};
let roll = 0, pitch = 0, targetRoll = 0, targetPitch = 0;
let shake = 0;
let bestScore = parseInt(localStorage.getItem('canyonBest')||'0');
bestScoreEl.textContent = bestScore.toLocaleString();

// audio
function initAudio(){
  if(audioCtx) return;
  audioCtx = new (window.AudioContext||window.webkitAudioContext)();
}
function beep(freq, dur, type='sine', gain=0.15){
  if(!audioCtx) return;
  const o = audioCtx.createOscillator();
  const g = audioCtx.createGain();
  o.type = type; o.frequency.value = freq;
  g.gain.value = gain;
  o.connect(g); g.connect(audioCtx.destination);
  o.start();
  g.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime+dur);
  o.stop(audioCtx.currentTime+dur);
}
function playCollect(){ beep(880,0.12,'sine',0.25); setTimeout(()=>beep(1320,0.18,'sine',0.18),70); }
function playNear(){ beep(220,0.15,'sawtooth',0.12); beep(440,0.25,'square',0.08); }
function playHit(){ beep(80,0.4,'square',0.3); beep(40,0.6,'sawtooth',0.25); }
function playBoost(){ beep(120,0.8,'sawtooth',0.18); }

// three setup
function init(){
  scene = new THREE.Scene();
  scene.fog = new THREE.Fog(0x1a0a2e, 60, 260);
  scene.background = new THREE.Color(0x0f0a1a);

  camera = new THREE.PerspectiveCamera(78, innerWidth/innerHeight, 0.1, 1000);
  camera.position.set(0,7,14);

  renderer = new THREE.WebGLRenderer({canvas, antialias:true, powerPreference:'high-performance'});
  renderer.setSize(innerWidth, innerHeight);
  renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
  renderer.shadowMap.enabled = true;
  renderer.shadowMap.type = THREE.PCFSoftShadowMap;
  renderer.toneMapping = THREE.ACESFilmicToneMapping;
  renderer.toneMappingExposure = 1.15;

  clock = new THREE.Clock();

  // lights - golden hour
  const hemi = new THREE.HemisphereLight(0xff8a4a, 0x1a0a3a, 0.9);
  scene.add(hemi);
  const sun = new THREE.DirectionalLight(0xffb87a, 2.2);
  sun.position.set(-80, 60, -40);
  sun.castShadow = true;
  sun.shadow.mapSize.set(2048,2048);
  sun.shadow.camera.near=1; sun.shadow.camera.far=300;
  sun.shadow.camera.left=-80; sun.shadow.camera.right=80; sun.shadow.camera.top=80; sun.shadow.camera.bottom=-80;
  sun.shadow.bias=-0.0005;
  scene.add(sun);
  const rim = new THREE.DirectionalLight(0x7a2cff, 0.9);
  rim.position.set(60,20,60);
  scene.add(rim);

  // player craft
  playerGroup = new THREE.Group();
  const bodyGeo = new THREE.ConeGeometry(0.9, 2.8, 3);
  bodyGeo.rotateX(Math.PI/2);
  const bodyMat = new THREE.MeshStandardMaterial({color:0xffffff, flatShading:true, metalness:0.6, roughness:0.25});
  const body = new THREE.Mesh(bodyGeo, bodyMat);
  body.castShadow = true;
  playerGroup.add(body);
  // wings
  const wingGeo = new THREE.BoxGeometry(2.6, 0.08, 0.9);
  const wingMat = new THREE.MeshStandardMaterial({color:0xe8e8ee, flatShading:true});
  const wing = new THREE.Mesh(wingGeo, wingMat);
  wing.position.set(0, -0.15, -0.35);
  wing.castShadow = true;
  playerGroup.add(wing);
  // cockpit
  const cock = new THREE.Mesh(new THREE.SphereGeometry(0.45,8,8), new THREE.MeshStandardMaterial({color:0x2cfff8, emissive:0x2cfff8, emissiveIntensity:1.2}));
  cock.position.set(0,0.25,0.2); cock.scale.set(1,0.7,1.2);
  playerGroup.add(cock);
  // engine glow
  const engine = new THREE.Mesh(new THREE.ConeGeometry(0.5,0.9,6), new THREE.MeshBasicMaterial({color:0xff6b2c}));
  engine.rotation.x = -Math.PI/2; engine.position.set(0,-0.1,-1.5);
  playerGroup.add(engine);
  engine.name='engine';
  // trail light
  const pLight = new THREE.PointLight(0xff6b2c, 8, 12);
  pLight.position.set(0,0,-1.6);
  playerGroup.add(pLight);

  player = new THREE.Group();
  player.add(playerGroup);
  player.position.set(0,5,0);
  scene.add(player);

  // ground plane subtle
  const groundGeo = new THREE.PlaneGeometry(200, 2000);
  const groundMat = new THREE.MeshStandardMaterial({color:0x1e102e, roughness:0.9});
  const ground = new THREE.Mesh(groundGeo, groundMat);
  ground.rotation.x = -Math.PI/2; ground.position.y = -1.2; ground.position.z = -500;
  ground.receiveShadow = true;
  scene.add(ground);

  // dust particles
  const dustCount = 600;
  const dustGeo = new THREE.BufferGeometry();
  const pos = new Float32Array(dustCount*3);
  const vel = new Float32Array(dustCount);
  for(let i=0;i<dustCount;i++){
    pos[i*3]=(Math.random()-0.5)*80;
    pos[i*3+1]=Math.random()*30;
    pos[i*3+2]= (Math.random()-0.5)*400 - 100;
    vel[i]= 0.5+Math.random()*1.5;
  }
  dustGeo.setAttribute('position', new THREE.BufferAttribute(pos,3));
  dustGeo.setAttribute('vel', new THREE.BufferAttribute(vel,1));
  const dustMat = new THREE.PointsMaterial({color:0xffc99a, size:0.18, transparent:true, opacity:0.55, sizeAttenuation:true});
  dustSystem = new THREE.Points(dustGeo, dustMat);
  scene.add(dustSystem);

  // trail particles behind ship
  trailGroup = new THREE.Group();
  scene.add(trailGroup);

  // chunks
  for(let i=0;i<NUM_CHUNKS;i++){
    const z = -i*CHUNK_LEN;
    createChunk(z, i===0?0:null);
  }

  window.addEventListener('resize', onResize);
  window.addEventListener('keydown', e=>{
    keys[e.code]=true;
    if(e.code==='Space'){
      if(turbo>=98 && state==='playing'){ startBoost(); }
      e.preventDefault();
    }
  });
  window.addEventListener('keyup', e=> keys[e.code]=false);

  // touch / pointer steering
  let dragging=false, startX=0;
  canvas.addEventListener('pointerdown', e=>{dragging=true; startX=e.clientX; initAudio();});
  canvas.addEventListener('pointerup', ()=>dragging=false);
  canvas.addEventListener('pointermove', e=>{
    if(!dragging||state!=='playing') return;
    const dx = (e.clientX - startX)/innerWidth;
    targetRoll = -dx*1.6;
  });
  canvas.addEventListener('pointerleave', ()=>dragging=false);

  animate();
}

function createChunk(z, forcedType){
  const group = new THREE.Group();
  group.position.z = z;
  scene.add(group);
  chunks.push(group);

  const typeRoll = forcedType!==null? forcedType : Math.random();
  // canyon walls - left/right cliffs
  const wallH = 14 + Math.random()*10;
  const wallMat = new THREE.MeshStandardMaterial({color:0x3a1a4a, flatShading:true, roughness:0.85});
  const wallMat2 = new THREE.MeshStandardMaterial({color:0x4a2a5a, flatShading:true, roughness:0.85});
  // left wall
  const leftW = 18 + Math.random()*8;
  const leftGeo = new THREE.BoxGeometry(leftW, wallH, CHUNK_LEN+2);
  const left = new THREE.Mesh(leftGeo, wallMat);
  left.position.set(-CANYON_HALF - leftW/2 + (Math.random()-0.5)*4, wallH/2-1.2, 0);
  left.castShadow=true; left.receiveShadow=true;
  group.add(left);
  // right wall
  const rightW = 18 + Math.random()*8;
  const rightGeo = new THREE.BoxGeometry(rightW, wallH, CHUNK_LEN+2);
  const right = new THREE.Mesh(rightGeo, wallMat2);
  right.position.set(CANYON_HALF + rightW/2 + (Math.random()-0.5)*4, wallH/2-1.2, 0);
  right.castShadow=true; right.receiveShadow=true;
  group.add(right);
  // top shadow cap illusion (dark boxes inside walls emulated)

  // decide obstacles
  const diff = difficulty;
  if(typeRoll < 0.28){
    // monolith field
    const count = 1 + Math.floor(Math.random()*2 + diff*0.6);
    for(let i=0;i<count;i++){
      const w = 2.2 + Math.random()*1.8;
      const h = 6 + Math.random()*9;
      const d = 2.2 + Math.random()*1.5;
      const mon = new THREE.Mesh(new THREE.BoxGeometry(w,h,d), new THREE.MeshStandardMaterial({color:0x2a2a36, flatShading:true, metalness:0.2, roughness:0.7}));
      mon.position.set((Math.random()-0.5)*(CANYON_HALF*1.3), h/2-1.2, (Math.random()-0.5)*(CHUNK_LEN*0.6));
      mon.castShadow=true; mon.receiveShadow=true;
      mon.userData.isObstacle=true;
      mon.userData.radius = Math.max(w,d)*0.7;
      group.add(mon);
      obstacles.push(mon);
    }
  } else if(typeRoll < 0.55){
    // tight squeeze gate
    const gap = Math.max(5.5 - diff*0.7, 3.2);
    const gateX = (Math.random()-0.5)*6;
    const h = 9 + Math.random()*4;
    const makePillar = (x) =>{
      const pil = new THREE.Mesh(new THREE.BoxGeometry(2.4, h, 2.4), new THREE.MeshStandardMaterial({color:0x5a3a6a, flatShading:true}));
      pil.position.set(x, h/2-1.2, (Math.random()-0.5)*6);
      pil.castShadow=true; pil.receiveShadow=true;
      pil.userData.isObstacle=true; pil.userData.radius=1.8;
      group.add(pil); obstacles.push(pil);
      // top beam if gap small
      if(gap<4.5){
        const beam = new THREE.Mesh(new THREE.BoxGeometry(gap+4.8,0.7,1.2), new THREE.MeshStandardMaterial({color:0x8a5a3a}));
        beam.position.set(gateX, h-0.6, pil.position.z);
        group.add(beam);
        beam.userData.isObstacle=true; beam.userData.radius=1.2;
        obstacles.push(beam);
      }
    };
    makePillar(gateX - gap/2 -1.2);
    makePillar(gateX + gap/2 +1.2);
  } else if(typeRoll < 0.72){
    // floating arch
    const arch = new THREE.Group();
    const aH = 7;
    const aW = 10 + Math.random()*4;
    const leg1 = new THREE.Mesh(new THREE.BoxGeometry(1.2,aH,1.2), new THREE.MeshStandardMaterial({color:0x4a2e5a}));
    leg1.position.set(-aW/2, aH/2-1.2,0); arch.add(leg1);
    const leg2 = leg1.clone(); leg2.position.x = aW/2; arch.add(leg2);
    const top = new THREE.Mesh(new THREE.BoxGeometry(aW+1.2,0.9,1.6), new THREE.MeshStandardMaterial({color:0xff6b2c, emissive:0xff6b2c, emissiveIntensity:0.15}));
    top.position.set(0,aH-0.8,0); arch.add(top);
    top.userData.isObstacle=true; top.userData.radius=1.0;
    leg1.userData.isObstacle=true; leg1.userData.radius=1.0; leg2.userData.isObstacle=true; leg2.userData.radius=1.0;
    arch.position.x = (Math.random()-0.5)*4;
    group.add(arch);
    obstacles.push(leg1,leg2,top);
  }

  // orbs - place in risky spots
  const orbCount = Math.random()<0.7 ? (Math.random()<0.5?2:1) : 0;
  for(let i=0;i<orbCount;i++){
    const orbGeo = new THREE.IcosahedronGeometry(0.7,1);
    const orbMat = new THREE.MeshStandardMaterial({color:0xffd27a, emissive:0xff9a1a, emissiveIntensity:1.6, flatShading:true});
    const orb = new THREE.Mesh(orbGeo, orbMat);
    // place near gap or wall for risk
    let rx, rz;
    if(obstacles.length>0 && Math.random()<0.75){
      const ref = obstacles[Math.floor(Math.random()*obstacles.length)];
      const wp = new THREE.Vector3(); ref.getWorldPosition(wp);
      rx = wp.x + (Math.random()-0.5)*3;
      rz = ref.position.z + (Math.random()-0.5)*4;
    } else {
      rx=(Math.random()-0.5)*(CANYON_HALF*1.1); rz=(Math.random()-0.5)*(CHUNK_LEN*0.7);
    }
    rx = THREE.MathUtils.clamp(rx, -CANYON_HALF+2, CANYON_HALF-2);
    orb.position.set(rx, 3+Math.random()*4, rz);
    orb.userData.isOrb=true;
    orb.userData.baseY = orb.position.y;
    orb.userData.phase = Math.random()*Math.PI*2;
    const light = new THREE.PointLight(0xffb020, 6, 10);
    light.position.copy(orb.position);
    group.add(light);
    orb.userData.light = light;
    // glow sprite
    const glow = new THREE.Mesh(new THREE.SphereGeometry(1.15,8,8), new THREE.MeshBasicMaterial({color:0xffb020, transparent:true, opacity:0.18}));
    orb.add(glow);
    group.add(orb);
    orbs.push(orb);
  }

  // ground deco rocks
  for(let i=0;i<3;i++){
    const r = new THREE.Mesh(new THREE.DodecahedronGeometry(0.6+Math.random()*0.6), new THREE.MeshStandardMaterial({color:0x2a1838, flatShading:true}));
    r.position.set((Math.random()-0.5)*22, 0, (Math.random()-0.5)*CHUNK_LEN);
    r.scale.y=0.6; r.rotation.set(Math.random()*Math.PI,Math.random()*Math.PI,0);
    group.add(r);
  }
}

function recycleChunks(){
  // player moves negative Z visually, but we keep player at 0 and move world positive
  // Instead move chunks forward (positive Z). When chunk z > 30 (behind player), move to front
  for(const g of chunks){
    if(g.position.z > 30){
      // find most negative
      let minZ = Math.min(...chunks.map(c=>c.position.z));
      g.position.z = minZ - CHUNK_LEN;
      // clear old orbs/obstacles associated
      // remove orbs/obstacles that belong to this group
      const toRemoveOrb = [];
      for(const o of orbs){ if(o.parent===g) toRemoveOrb.push(o); }
      // but we will just remove group children that are orbs/obstacles from arrays after recycling, better recreate
      // clear group
      // remove from arrays
      const children = [...g.children];
      for(const ch of children){
        if(ch.userData.isOrb || ch.userData.isObstacle){
          // remove from arrays
          const idxO = orbs.indexOf(ch); if(idxO>=0) orbs.splice(idxO,1);
          const idxB = obstacles.indexOf(ch); if(idxB>=0) obstacles.splice(idxB,1);
        }
        // keep walls? we will keep but re-randomize wall heights via scaling? simpler remove all and regenerate
      }
      // remove all children
      while(g.children.length) g.remove(g.children[0]);
      // obstacles for this group already removed, now regenerate content for new position
      // need to reconstruct similar to createChunk but reuse group
      // inline generation without pushing new chunk
      (function regen(group){
        const wallH = 14 + Math.random()*10;
        const leftW = 18 + Math.random()*8;
        const rightW = 18 + Math.random()*8;
        const wallMat = new THREE.MeshStandardMaterial({color:0x3a1a4a, flatShading:true});
        const wallMat2 = new THREE.MeshStandardMaterial({color:0x4a2a5a, flatShading:true});
        const left = new THREE.Mesh(new THREE.BoxGeometry(leftW, wallH, CHUNK_LEN+2), wallMat);
        left.position.set(-CANYON_HALF - leftW/2 + (Math.random()-0.5)*4, wallH/2-1.2, 0);
        left.castShadow=true; group.add(left);
        const right = new THREE.Mesh(new THREE.BoxGeometry(rightW, wallH, CHUNK_LEN+2), wallMat2);
        right.position.set(CANYON_HALF + rightW/2 + (Math.random()-0.5)*4, wallH/2-1.2, 0);
        right.castShadow=true; group.add(right);
        const typeRoll=Math.random();
        if(typeRoll < 0.30){
          const count = 1+ Math.floor(Math.random()*2 + difficulty*0.6);
          for(let i=0;i<count;i++){
            const w=2.2+Math.random()*1.8, h=6+Math.random()*9, d=2.2+Math.random()*1.5;
            const mon=new THREE.Mesh(new THREE.BoxGeometry(w,h,d), new THREE.MeshStandardMaterial({color:0x2a2a36, flatShading:true}));
            mon.position.set((Math.random()-0.5)*(CANYON_HALF*1.3), h/2-1.2, (Math.random()-0.5)*(CHUNK_LEN*0.6));
            mon.castShadow=true; mon.userData.isObstacle=true; mon.userData.radius=Math.max(w,d)*0.7;
            group.add(mon); obstacles.push(mon);
          }
        } else if(typeRoll<0.58){
          const gap=Math.max(5.5 - difficulty*0.7, 3.2);
          const gateX=(Math.random()-0.5)*6;
          const h=9+Math.random()*4;
          const make=(x)=>{
            const pil=new THREE.Mesh(new THREE.BoxGeometry(2.4,h,2.4), new THREE.MeshStandardMaterial({color:0x5a3a6a}));
            pil.position.set(x,h/2-1.2,(Math.random()-0.5)*6); pil.castShadow=true; pil.userData.isObstacle=true; pil.userData.radius=1.8;
            group.add(pil); obstacles.push(pil);
            if(gap<4.5){
              const beam=new THREE.Mesh(new THREE.BoxGeometry(gap+4.8,0.7,1.2), new THREE.MeshStandardMaterial({color:0x8a5a3a}));
              beam.position.set(gateX,h-0.6,pil.position.z); beam.userData.isObstacle=true; beam.userData.radius=1.2; group.add(beam); obstacles.push(beam);
            }
          };
          make(gateX -gap/2-1.2); make(gateX+gap/2+1.2);
        }
        const orbCount=Math.random()<0.65? (Math.random()<0.55?1:2):0;
        for(let i=0;i<orbCount;i++){
          const orb=new THREE.Mesh(new THREE.IcosahedronGeometry(0.7,1), new THREE.MeshStandardMaterial({color:0xffd27a, emissive:0xff9a1a, emissiveIntensity:1.6}));
          let rx=(Math.random()-0.5)*(CANYON_HALF*1.2), rz=(Math.random()-0.5)*(CHUNK_LEN*0.7);
          rx=THREE.MathUtils.clamp(rx,-CANYON_HALF+2,CANYON_HALF-2);
          orb.position.set(rx, 3+Math.random()*4, rz); orb.userData.isOrb=true; orb.userData.baseY=orb.position.y; orb.userData.phase=Math.random()*Math.PI*2;
          const glow=new THREE.Mesh(new THREE.SphereGeometry(1.15,8,8), new THREE.MeshBasicMaterial({color:0xffb020, transparent:true, opacity:0.18})); orb.add(glow);
          const light=new THREE.PointLight(0xffb020,6,10); light.position.copy(orb.position); group.add(light); orb.userData.light=light;
          group.add(orb); orbs.push(orb);
        }
        for(let i=0;i<3;i++){
          const r=new THREE.Mesh(new THREE.DodecahedronGeometry(0.6+Math.random()*0.6), new THREE.MeshStandardMaterial({color:0x2a1838, flatShading:true}));
          r.position.set((Math.random()-0.5)*22,0,(Math.random()-0.5)*CHUNK_LEN); r.scale.y=0.6; group.add(r);
        }
      })(g);
    }
  }
}

function startBoost(){
  if(isBoosting) return;
  isBoosting=true; boostTime=3.2; turbo=0; playBoost();
  shake=0.8;
}

function onResize(){
  camera.aspect=innerWidth/innerHeight; camera.updateProjectionMatrix();
  renderer.setSize(innerWidth, innerHeight);
}

function updateHUD(){
  const kmh = Math.round(curSpeed*3.2);
  speedKmh.textContent = kmh.toString().padStart(3,'0');
  maxSpeed = Math.max(maxSpeed, kmh);
  maxSpeedEl.textContent = maxSpeed;
  const pct = THREE.MathUtils.clamp(curSpeed/110,0,1);
  speedFill.style.width = (pct*100)+'%';
  energyFill.style.width = Math.max(0,energy)+'%';
  energyPct.textContent = Math.round(energy)+'%';
  if(energy<22) energyFill.classList.add('low'); else energyFill.classList.remove('low');
  turboFill.style.width = turbo+'%';
  if(turbo>=99){ turboBtn.textContent='SPACE — BOOST!'; turboBtn.classList.add('ready'); }
  else { turboBtn.textContent = turbo>60?'CHARGING '+Math.round(turbo)+'%':'CHARGING'; turboBtn.classList.remove('ready'); }
  scoreVal.innerHTML = Math.floor(score).toString().padStart(5,'0') + ` <span>×${multiplier.toFixed(1)}</span>`;
  multNum.textContent = '×'+multiplier.toFixed(1);
  multDisplay.classList.toggle('active', multiplier>1.05);
  if(multiplier>1.05) multNum.style.color = multiplier>3?'#2cfff8':'#fff';
}

let lastTrail=0;
function animate(){
  requestAnimationFrame(animate);
  const dt = Math.min(clock.getDelta(), 0.033);
  const t = clock.elapsedTime;

  if(state==='playing'){
    // input
    if(keys['KeyA']||keys['ArrowLeft']) targetRoll = 0.85;
    else if(keys['KeyD']||keys['ArrowRight']) targetRoll = -0.85;
    else {
      // decay to 0 but allow pointer influence if dragging handled
      if(!keys['KeyA']&&!keys['KeyD']&&!keys['ArrowLeft']&&!keys['ArrowRight']) targetRoll *= 0.88;
      if(Math.abs(targetRoll)<0.02) targetRoll=0;
    }
    if(keys['KeyW']||keys['ArrowUp']) targetPitch = -0.55;
    else if(keys['KeyS']||keys['ArrowDown']) targetPitch = 0.55;
    else targetPitch *= 0.88;

    roll = THREE.MathUtils.lerp(roll, targetRoll, 0.09);
    pitch = THREE.MathUtils.lerp(pitch, targetPitch, 0.09);
    playerGroup.rotation.z = roll;
    playerGroup.rotation.x = pitch*0.55;
    playerGroup.rotation.y = -roll*0.22;

    // speed logic
    baseSpeed = 42 + difficulty*14 + (multiplier-1)*4;
    if(isBoosting){
      curSpeed = baseSpeed + 58;
      boostTime -= dt;
      if(boostTime<=0) isBoosting=false;
    } else {
      curSpeed = THREE.MathUtils.lerp(curSpeed, baseSpeed, 0.04);
      // passive turbo charge when flying straight and fast
      if(Math.abs(roll)<0.15 && curSpeed>45) turbo = Math.min(100, turbo + dt*9);
    }

    // move world
    const move = curSpeed * dt;
    distance += move;
    score += move * 10 * (0.7 + multiplier*0.45);
    difficulty = Math.min(5, distance/320);
    // move chunks
    for(const g of chunks) g.position.z += move;
    // dust
    const posAttr = dustSystem.geometry.attributes.position;
    for(let i=0;i<posAttr.count;i++){
      let z = posAttr.getZ(i);
      z += move* (0.8 + posAttr.getX(i)*0);
      // actually move with speed
      posAttr.setZ(i, z);
      if(z>18){ posAttr.setZ(i, -380 + Math.random()*20); posAttr.setX(i,(Math.random()-0.5)*80); posAttr.setY(i, Math.random()*30); }
    }
    posAttr.needsUpdate=true;

    // player lateral/vertical movement constrained in canyon
    player.position.x += -roll * curSpeed * 0.045;
    player.position.y += -pitch * curSpeed * 0.028;
    player.position.x = THREE.MathUtils.clamp(player.position.x, -CANYON_HALF+1.6, CANYON_HALF-1.6);
    player.position.y = THREE.MathUtils.clamp(player.position.y, 1.2, 9.5);
    if(Math.abs(player.position.x) > CANYON_HALF-2) {
      // wall scrape
      energy -= dt*10;
      shake = 0.4;
      if(nearMissCooldown<=0){
        multiplier = Math.min(9.9, multiplier+0.15);
        nearMissCooldown=0.35;
        nearFlash.textContent='WALL GRAZE +'+Math.round(60*multiplier);
        nearFlash.classList.remove('show'); void nearFlash.offsetWidth; nearFlash.classList.add('show');
      }
    }

    // energy decay
    const decay = (6.2 + difficulty*1.6 + (isBoosting?5:0)) * dt;
    energy -= decay;
    if(energy<=0){ energy=0; triggerGameOver(); }

    // trail engine scale with speed/boost
    const eng = playerGroup.getObjectByName('engine');
    if(eng){ eng.scale.setScalar(1 + (curSpeed-40)*0.01 + (isBoosting?0.7:0)); eng.material.color.setHSL(isBoosting?0.52:0.07,1,0.55); }

    // orbs
    for(let i=orbs.length-1;i>=0;i--){
      const orb = orbs[i];
      orb.rotation.y += dt*1.6;
      orb.position.y = orb.userData.baseY + Math.sin(t*2 + orb.userData.phase)*0.35;
      if(orb.userData.light) orb.userData.light.position.copy(orb.position); // local? need world but approximate
      // world pos
      const wp = new THREE.Vector3(); orb.getWorldPosition(wp);
      const dist = wp.distanceTo(player.position);
      if(dist < 1.9){
        // collect
        energy = Math.min(100, energy+26);
        score += 420 * multiplier;
        turbo = Math.min(100, turbo+18);
        multiplier = Math.min(9.9, multiplier+0.22);
        playCollect();
        // pop effect
        orb.userData.light.intensity=0;
        orb.parent.remove(orb);
        orbs.splice(i,1);
        // small particle burst
        // flash
        shake=0.25;
        nearFlash.textContent='ORB +26 ENERGY';
        nearFlash.classList.remove('show'); void nearFlash.offsetWidth; nearFlash.classList.add('show');
        continue;
      }
      // cull behind
      if(wp.z > 18){ // behind player, already handled by recycle but remove if stray
        // no
      }
    }

    // obstacles collision / near miss
    let closestDist = Infinity;
    for(const obs of obstacles){
      const wp = new THREE.Vector3(); obs.getWorldPosition(wp);
      const d = wp.distanceTo(player.position);
      const rad = obs.userData.radius || 1.5;
      // near miss band 1.6 - 3.7
      if(d < rad + 1.1){
        // collision
        triggerGameOver(true);
        break;
      }
      if(d < rad + 3.4 && d > rad+0.9){
        closestDist = Math.min(closestDist, d - rad);
      }
    }
    if(closestDist < 2.2){
      shake = Math.max(shake, (2.2-closestDist)*0.35);
      camera.fov = 78 + (2.2-closestDist)*4 + (isBoosting?12:0);
      camera.updateProjectionMatrix();
      if(nearMissCooldown<=0){
        multiplier = Math.min(9.9, multiplier+0.035);
        turbo = Math.min(100, turbo+0.9);
        score += 38;
        nearMissCooldown = 0.28;
        if(Math.random()<0.3){
          nearFlash.textContent='NEAR MISS +'+Math.round(120*multiplier);
          nearFlash.classList.remove('show'); void nearFlash.offsetWidth; nearFlash.classList.add('show');
          playNear();
        }
      }
    } else {
      camera.fov = THREE.MathUtils.lerp(camera.fov, 78 + (isBoosting?14:0), 0.08);
      camera.updateProjectionMatrix();
      // slowly decay multiplier if not grazing
      if(!isBoosting) multiplier = Math.max(1, multiplier - dt*0.18);
    }
    if(nearMissCooldown>0) nearMissCooldown-=dt;

    recycleChunks();

    // subtle auto camera follow
    camera.position.x = THREE.MathUtils.lerp(camera.position.x, player.position.x*0.35, 0.07);
    camera.position.y = THREE.MathUtils.lerp(camera.position.y, player.position.y+2.2, 0.07);
    camera.lookAt(player.position.x*0.5, player.position.y-0.4, player.position.z - 14);

    // shake
    if(shake>0){
      camera.position.x += (Math.random()-0.5)*shake;
      camera.position.y += (Math.random()-0.5)*shake*0.6;
      shake = Math.max(0, shake - dt*2.2);
    }

    updateHUD();
  } else {
    // idle camera drift in menu/dead
    const idle = t*0.12;
    camera.position.x = Math.sin(idle)*1.2;
    camera.position.y = 6.5 + Math.sin(idle*0.7)*0.6;
    camera.lookAt(0,3,-14);
    // rotate orbs gently even when not playing
    for(const o of orbs){ o.rotation.y+=0.01; }
  }

  // engine trail particles occasional
  if(state==='playing' && t - lastTrail > 0.025){
    lastTrail=t;
    // simple expanding ring behind
    const p = new THREE.Mesh(new THREE.SphereGeometry(0.12,6,6), new THREE.MeshBasicMaterial({color: isBoosting?0x2cfff8:0xff8a3a, transparent:true, opacity:0.9}));
    p.position.copy(player.position); p.position.z -=1.2; p.position.y -=0.15;
    p.userData.life=0.45; p.userData.vel = new THREE.Vector3((Math.random()-0.5)*1.2, (Math.random()-0.5)*0.8, 1.5+Math.random()*2);
    trailGroup.add(p);
  }
  for(let i=trailGroup.children.length-1;i>=0;i--){
    const p=trailGroup.children[i];
    p.position.addScaledVector(p.userData.vel, dt*10);
    p.userData.life-=dt;
    p.material.opacity = p.userData.life*1.6;
    p.scale.multiplyScalar(1.015);
    if(p.userData.life<=0) trailGroup.remove(p);
  }

  renderer.render(scene, camera);
}

function triggerGameOver(crashed=false){
  if(state==='dead') return;
  state='dead';
  if(crashed) { playHit(); shake=1.2; } else { beep(60,1,'sawtooth',0.2); }
  document.getElementById('finalScore').textContent = Math.floor(score).toLocaleString();
  document.getElementById('finalDist').textContent = Math.floor(distance*3)+'m';
  document.getElementById('finalSpeed').textContent = maxSpeed+' KM/H';
  if(score>bestScore){ bestScore=Math.floor(score); localStorage.setItem('canyonBest',bestScore); bestScoreEl.textContent=bestScore.toLocaleString(); }
  gameOverOverlay.classList.remove('hidden');
  // dim engine
  const eng=playerGroup.getObjectByName('engine'); if(eng) eng.material.color.set(0x330000);
}

function resetGame(){
  score=0; distance=0; multiplier=1; energy=78; turbo=0; isBoosting=false; boostTime=0; difficulty=0; curSpeed=42; baseSpeed=42; maxSpeed=0; roll=0; pitch=0; targetRoll=0; targetPitch=0;
  player.position.set(0,5,0);
  playerGroup.rotation.set(0,0,0);
  nearMissCooldown=0;
  // clear chunks and rebuild
  for(const g of chunks) scene.remove(g);
  chunks=[]; orbs=[]; obstacles=[]; trailGroup.clear();
  for(let i=0;i<NUM_CHUNKS;i++) createChunk(-i*CHUNK_LEN, i===0?0:null);
  energyFill.classList.remove('low');
  gameOverOverlay.classList.add('hidden');
  startOverlay.classList.add('hidden');
  camera.fov=78; camera.updateProjectionMatrix();
  state='playing';
  initAudio();
  if(audioCtx.state==='suspended') audioCtx.resume();
  // hum
  beep(90,0.3,'sine',0.08);
}

document.getElementById('playBtn').addEventListener('click', ()=>{ resetGame(); });
document.getElementById('restartBtn').addEventListener('click', ()=>{ resetGame(); });
document.getElementById('menuBtn').addEventListener('click', ()=>{
  gameOverOverlay.classList.add('hidden');
  startOverlay.classList.remove('hidden');
  state='menu';
});
document.getElementById('howBtn').addEventListener('click', ()=>{
  document.getElementById('howText').classList.toggle('hidden');
});

init();
updateHUD();
</script>
</body>
</html>
<!-- agent-meta {"model":"meta/muse-spark-1.2","provider":"openrouter","persona":"basic","sessionId":"6fb19f24-e810-47c2-ba68-043fb7670626","tokensIn":924,"tokensOut":13627,"tokensTotal":14551,"cost":0.05906975,"turns":1,"toolCalls":0,"failedToolCalls":0,"timestamp":"2026-08-06T15:12:10.745Z"} -->