← Race the Sun results

Race the Sun

Qwen 3.6 35b a3b · full

Initial view of Race the Sun

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 32,763 bytes · SHA-256 958a53964d45
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Solar Drift</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
html,body{width:100%;height:100%;overflow:hidden;background:#000}
canvas{display:block;width:100%;height:100%}
#ui{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none}
#hud{position:absolute;top:16px;left:16px;right:16px;display:none;justify-content:space-between;align-items:flex-start;font-family:'Courier New',monospace}
.hp{background:rgba(0,0,0,.55);padding:6px 12px;border:1px solid rgba(255,200,50,.25);border-radius:4px;color:#ffd060;text-shadow:0 0 8px rgba(255,180,30,.5)}
.hl{font-size:9px;text-transform:uppercase;letter-spacing:2px;opacity:.55;margin-bottom:1px}
.hv{font-size:17px;font-weight:bold}
#ebar{position:absolute;bottom:28px;left:50%;transform:translateX(-50%);width:220px;height:6px;background:rgba(0,0,0,.5);border:1px solid rgba(255,200,50,.25);border-radius:3px;overflow:hidden;display:none}
#efill{height:100%;width:100%;background:linear-gradient(90deg,#ff4422,#ffcc20,#66ff80);transition:width .1s}
#boostbar{position:absolute;bottom:16px;left:50%;transform:translateX(-50%);width:120px;height:4px;background:rgba(0,0,0,.5);border:1px solid rgba(100,200,255,.2);border-radius:2px;overflow:hidden;display:none}
#bfill{height:100%;width:100%;background:linear-gradient(90deg,#4488ff,#88ccff)}
#tscr,#gscr{position:absolute;top:0;left:0;width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;pointer-events:auto}
#tscr{background:radial-gradient(ellipse at 50% 65%,rgba(255,100,20,.12) 0%,rgba(0,0,0,.88) 70%)}
#gscr{background:rgba(0,0,0,.82);display:none}
.ttl{font-family:'Courier New',monospace;font-size:56px;font-weight:bold;color:#ffcc33;text-shadow:0 0 30px rgba(255,180,30,.8),0 0 60px rgba(255,100,20,.4);letter-spacing:6px;margin-bottom:8px}
.sub{font-family:'Courier New',monospace;font-size:13px;color:rgba(255,200,100,.5);letter-spacing:3px;margin-bottom:36px}
.btn{font-family:'Courier New',monospace;font-size:16px;padding:10px 36px;background:transparent;border:2px solid rgba(255,200,50,.55);color:#ffd060;cursor:pointer;letter-spacing:3px;text-transform:uppercase;transition:all .2s;pointer-events:auto}
.btn:hover{background:rgba(255,200,50,.12);border-color:#ffd060;box-shadow:0 0 18px rgba(255,180,30,.3)}
.gs{font-family:'Courier New',monospace;text-align:center;margin:24px 0}
.gi{margin:6px 0;font-size:15px;color:rgba(255,200,100,.75)}
.gi span{color:#ffd060;font-weight:bold}
.ghs{font-size:13px;color:rgba(255,200,100,.45);margin-top:8px}
.ch{font-family:'Courier New',monospace;font-size:10px;color:rgba(255,200,100,.3);letter-spacing:1px;margin-top:28px}
#sline{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;opacity:0;mix-blend-mode:screen}
#flop{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;opacity:0;background:radial-gradient(ellipse,rgba(255,200,100,.3),transparent 70%)}
</style>
</head>
<body>
<canvas id="gc"></canvas>
<canvas id="sline"></canvas>
<div id="ui">
  <div id="hud">
    <div class="hp"><div class="hl">Distance</div><div class="hv" id="hd">0m</div></div>
    <div class="hp"><div class="hl">Score</div><div class="hv" id="hs">0</div></div>
    <div class="hp"><div class="hl">Multiplier</div><div class="hv" id="hm">x1.0</div></div>
    <div class="hp"><div class="hl">Speed</div><div class="hv" id="hsp">0</div></div>
  </div>
  <div id="ebar"><div id="efill"></div></div>
  <div id="boostbar"><div id="bfill"></div></div>
  <div id="tscr">
    <div class="ttl">SOLAR DRIFT</div>
    <div class="sub">PILOT THROUGH THE ENDLESS SUNSET</div>
    <button class="btn" id="bs">LAUNCH</button>
    <div class="ch">WASD / ARROWS to steer · SPACE to boost · R to restart</div>
  </div>
  <div id="gscr">
    <div class="ttl" style="font-size:42px">SOLAR SET</div>
    <div class="gs">
      <div class="gi">Distance: <span id="gd">0m</span></div>
      <div class="gi">Score: <span id="gsc">0</span></div>
      <div class="gi">Multiplier: <span id="gm">x1.0</span></div>
    </div>
    <div class="ghs" id="ghs">High Score: 0</div>
    <button class="btn" id="br" style="margin-top:18px">RELAUNCH (R)</button>
  </div>
</div>
<div id="flop"></div>
<script>
// ============================================================
// === TUNABLE CONSTANTS ===
// ============================================================
const C={
  WORLD_CHUNK:200,RENDER_DIST:700,RENDER_BEHIND:60,
  GROUND:'#0a0510',GRID:'rgba(255,120,30,.07)',
  ROAD_W:220,ROAD:'#0e0814',ROAD_EDGE:'rgba(255,140,30,.18)',
  SUN_R:90,SUN_MNY:-250,SUN_LY:350,SUN_SINK:.00012,
  SUN_COLS:['#ffcc33','#ff8822','#ff4411','#cc2200'],
  FOV:500,
  BASE_SPD:1.5,MAX_SPD:4.5,ACCEL:.0004,
  LAT_SPD:3.0,PCH_SPD:2.0,
  BANK_AMT:.4,PCH_AMT:.22,
  BOOST_COST:.004,BOOST_MULT:1.6,BOOST_MAX:100,BOOST_RECH:.08,
  E_DRAIN:.006,E_SHADOW:.025,E_PICKUP:20,E_START:100,
  CAM_H:55,CAM_D:100,
  SHAKE_NEAR:.6,SHAKE_NEAR_D:25,SHAKE_CRASH:10,
  B_MIN_H:25,B_MAX_H:140,B_MIN_W:18,B_MAX_W:55,B_GAP:50,
  B_COLORS:['#1a0a25','#200e2e','#150820','#1e0c28','#120618'],
  B_EDGE:'rgba(255,100,30,.35)',
  OBS_GAP:70,OBS_GAP_MAX:150,
  OBS_TYPES:['wall','pillar','beam'],
  OBS_COLOR:'#ff3322',OBS_EDGE:'#ff7755',
  PICKUP_TYPES:['energy','token','multiplier'],
  PICKUP_COLORS:{energy:'#66ff88',token:'#ffcc33',multiplier:'#ff66ff'},
  P_LIFE:.7,THRUST_N:3,
  SKY_T:'#0a0515',SKY_M:'#1a0a25',SKY_B:'#ff4411',
  SL_THRESH:2.5,SL_MAX_OP:.25,
  FLASH_DUR:.2,
};

// ============================================================
// === SEEDED PRNG ===
// ============================================================
function mkRNG(s){let x=s|0;return()=>{x=(x*1664525+1013904223)|0;return(x>>>0)/4294967296}}
let SEED=Date.now()|0,rng=mkRNG(SEED);

// ============================================================
// === AUDIO ===
// ============================================================
const A={ctx:null,mg:null,wn:null,wg:null,
  init(){if(this.ctx)return;this.ctx=new(window.AudioContext||window.webkitAudioContext)();this.mg=this.ctx.createGain();this.mg.gain.value=.25;this.mg.connect(this.ctx.destination)},
  startWind(){if(!this.ctx||this.wn)return;const n=this.ctx.sampleRate*2,b=this.ctx.createBuffer(1,n,this.ctx.sampleRate),d=b.getChannelData(0);for(let i=0;i<n;i++)d[i]=Math.random()*2-1;const s=this.ctx.createBufferSource();s.buffer=b;s.loop=true;const f=this.ctx.createBiquadFilter();f.type='lowpass';f.frequency.value=350;f.Q.value=.5;this.wg=this.ctx.createGain();this.wg.gain.value=.06;s.connect(f);f.connect(this.wg);this.wg.connect(this.mg);s.start();this.wn={s,f,g:this.wg}},
  setWind(t){if(!this.wn)return;this.wn.f.frequency.value=250+t*700;this.wg.gain.value=.03+t*.14},
  blip(f,d,tp){if(!this.ctx)return;const o=this.ctx.createOscillator(),g=this.ctx.createGain();o.type=tp||'sine';o.frequency.value=f;g.gain.setValueAtTime(.12,this.ctx.currentTime);g.gain.exponentialRampToValueAtTime(.001,this.ctx.currentTime+d);o.connect(g);g.connect(this.mg);o.start();o.stop(this.ctx.currentTime+d)},
  pickup(tp){if(!this.ctx)return;const t=this.ctx.currentTime;
    if(tp==='energy'){const o=this.ctx.createOscillator(),g=this.ctx.createGain();o.type='sine';o.frequency.setValueAtTime(350,t);o.frequency.exponentialRampToValueAtTime(900,t+.12);g.gain.setValueAtTime(.1,t);g.gain.exponentialRampToValueAtTime(.001,t+.15);o.connect(g);g.connect(this.mg);o.start();o.stop(t+.15)}
    else if(tp==='token'){this.blip(1100,.07,'sine');setTimeout(()=>this.blip(1500,.06,'sine'),55)}
    else{this.blip(500,.1,'square');setTimeout(()=>this.blip(800,.08,'square'),70);setTimeout(()=>this.blip(1100,.1,'square'),140)}},
  nearMiss(){if(!this.ctx)return;const o=this.ctx.createOscillator(),g=this.ctx.createGain();o.type='triangle';o.frequency.value=180;g.gain.setValueAtTime(.07,this.ctx.currentTime);g.gain.exponentialRampToValueAtTime(.001,this.ctx.currentTime+.25);o.connect(g);g.connect(this.mg);o.start();o.stop(this.ctx.currentTime+.25)},
  crash(){if(!this.ctx)return;const n=this.ctx.sampleRate,b=this.ctx.createBuffer(1,n,this.ctx.sampleRate),d=b.getChannelData(0);for(let i=0;i<n;i++)d[i]=(Math.random()*2-1)*Math.exp(-i/(n*.12));const s=this.ctx.createBufferSource();s.buffer=b;const g=this.ctx.createGain();g.gain.value=.35;const f=this.ctx.createBiquadFilter();f.type='lowpass';f.frequency.value=700;s.connect(f);f.connect(g);g.connect(this.mg);s.start()},
  boost(){if(!this.ctx)return;const o=this.ctx.createOscillator(),g=this.ctx.createGain();o.type='sawtooth';o.frequency.setValueAtTime(120,this.ctx.currentTime);o.frequency.exponentialRampToValueAtTime(40,this.ctx.currentTime+.1);g.gain.value=.05;g.gain.exponentialRampToValueAtTime(.001,this.ctx.currentTime+.12);o.connect(g);g.connect(this.mg);o.start();o.stop(this.ctx.currentTime+.12)},
  stopWind(){if(this.wn){this.wn.s.stop();this.wn=null}},
};

// ============================================================
// === MATH ===
// ============================================================
const V3={proj:(v,cx,cy,cz,fov,cw,ch)=>{const dz=v.z-cz;if(dz<.1)return null;const s=fov/dz;return{sx:(v.x-cx)*s+cw/2,sy:(v.y-cy)*s+ch/2,s,dz}}};
const lerp=(a,b,t)=>a+(b-a)*t,clamp=(v,a,b)=>Math.max(a,Math.min(b,v)),rand=(a,b)=>a+Math.random()*(b-a),randI=(a,b)=>Math.floor(rand(a,b+1)),randC=a=>a[Math.floor(Math.random()*a.length)];

// ============================================================
// === GAME STATE ===
// ============================================================
let S={
  ph:'title',
  px:0,py:0,pz:0,pvx:0,pvy:0,
  bank:0,pitch:0,
  spd:C.BASE_SPD,
  energy:C.E_START,boost:C.BOOST_MAX,boosting:false,
  dist:0,score:0,mult:1,chainT:0,
  shX:0,shY:0,flash:0,
  sunY:C.SUN_MNY,sunSet:false,
  blds:[],obs:[],pcks:[],parts:[],sls:[],
  keys:{},
  lt:0,dt:0,
  hs:parseInt(localStorage.getItem('sdHS'))||0,
  nearMissCD:0,
};

// ============================================================
// === CANVAS ===
// ============================================================
const cv=document.getElementById('gc'),cx=cv.getContext('2d');
const sl=document.getElementById('sline'),sc=sl.getContext('2d');
function resize(){cv.width=innerWidth;cv.height=innerHeight;sl.width=innerWidth;sl.height=innerHeight}
addEventListener('resize',resize);resize();

// ============================================================
// === WORLD GEN ===
// ============================================================
function genWorld(){
  S.blds=[];S.obs=[];S.pcks=[];S.parts=[];S.sls=[];
  const sa=S.pz+C.RENDER_DIST,b=S.pz-C.RENDER_BEHIND;
  for(let z=b;z<sa;z+=rand(C.B_GAP*.8,C.B_GAP*1.4)){
    const side=randC(['left','right']);
    S.blds.push({x:side==='left'?-rand(C.ROAD_W*.2,C.ROAD_W*1.6):rand(C.ROAD_W*.2,C.ROAD_W*1.6),y:0,z,w:rand(C.B_MIN_W,C.B_MAX_W),h:rand(C.B_MIN_H,C.B_MAX_H),d:rand(C.B_MIN_W*.5,C.B_MAX_W*.5),c:randC(C.B_COLORS),side});
  }
  for(let z=50;z<sa;z+=rand(C.OBS_GAP,C.OBS_GAP_MAX))spawnObs(z);
  for(let z=35;z<sa;z+=rand(50,90))spawnPck(z);
}
function spawnObs(z){
  const tp=randC(C.OBS_TYPES),o={type:tp,z};
  if(tp==='wall'){o.gapC=rand(-C.ROAD_W*.3,C.ROAD_W*.3);o.gap=rand(35,65);o.w=C.ROAD_W*1.3;o.h=rand(18,40);o.d=rand(6,16)}
  else if(tp==='pillar'){o.x=rand(-C.ROAD_W*.5,C.ROAD_W*.5);o.w=rand(10,22);o.h=rand(25,60);o.d=rand(10,22)}
  else{o.x=rand(-C.ROAD_W*.25,C.ROAD_W*.25);o.w=rand(50,90);o.h=rand(6,12);o.d=rand(6,12)}
  S.obs.push(o);
}
function spawnPck(z){
  const tp=randC(C.PICKUP_TYPES);
  S.pcks.push({type:tp,z,x:rand(-C.ROAD_W*.45,C.ROAD_W*.45),y:rand(-45,-12),r:12,bob:Math.random()*Math.PI*2,c:C.PICKUP_COLORS[tp]});
}

// ============================================================
// === PARTICLES ===
// ============================================================
function sPart(x,y,z,vx,vy,vz,col,life,sz){S.parts.push({x,y,z,vx,vy,vz,col,life:life||C.P_LIFE,ml:life||C.P_LIFE,sz:sz||3})}
function sThrust(){
  const n=S.boosting?6:C.THRUST_N;
  for(let i=0;i<n;i++)sPart(S.px+rand(-3,3),S.py-6+rand(-2,2),S.pz+10,rand(-.3,.3),rand(-.3,.3),rand(.6,1.8),randC(['#ffcc33','#ff8822','#ff4411','#ff6600']),rand(.3,.6),rand(2,4));
}
function sPckFX(x,y,z,col){for(let i=0;i<18;i++){const a=Math.random()*Math.PI*2,sp=rand(.5,3.5);sPart(x,y,z,Math.cos(a)*sp,Math.sin(a)*sp,rand(-1,1),col,rand(.3,.7),rand(2,5))}}
function sCrashFX(x,y,z){for(let i=0;i<50;i++){const a=Math.random()*Math.PI*2,sp=rand(1,7);sPart(x,y,z,Math.cos(a)*sp,Math.sin(a)*sp,rand(-2,2),randC(['#ff4422','#ff8844','#ffcc33','#ffaa11','#fff']),rand(.5,1.5),rand(3,10))}}

// ============================================================
// === INPUT ===
// ============================================================
addEventListener('keydown',e=>{S.keys[e.code]=true;if(e.code==='KeyR'&&S.ph==='gameover')restart();if(e.code==='Space')e.preventDefault()});
addEventListener('keyup',e=>{S.keys[e.code]=false});
document.getElementById('bs').onclick=()=>{A.init();A.startWind();start()};
document.getElementById('br').onclick=()=>restart();

// ============================================================
// === GAME CONTROL ===
// ============================================================
function start(){
  A.init();A.startWind();
  SEED=Date.now()|0;rng=mkRNG(SEED);
  S={ph:'playing',px:0,py:0,pz:0,pvx:0,pvy:0,bank:0,pitch:0,
    spd:C.BASE_SPD,energy:C.E_START,boost:C.BOOST_MAX,boosting:false,
    dist:0,score:0,mult:1,chainT:0,shX:0,shY:0,flash:0,
    sunY:C.SUN_MNY,sunSet:false,blds:[],obs:[],pcks:[],parts:[],sls:[],
    keys:{},lt:performance.now(),dt:0,hs:S.hs,nearMissCD:0};
  genWorld();
  document.getElementById('tscr').style.display='none';
  document.getElementById('gscr').style.display='none';
  document.getElementById('hud').style.display='flex';
  document.getElementById('ebar').style.display='block';
  document.getElementById('boostbar').style.display='block';
  requestAnimationFrame(loop);
}
function restart(){A.stopWind();start()}
function gover(){
  S.ph='gameover';A.crash();A.stopWind();S.flash=C.FLASH_DUR;
  sCrashFX(S.px,S.py,S.pz+20);
  const hs=Math.max(S.hs,S.score);localStorage.setItem('sdHS',hs.toString());S.hs=hs;
  document.getElementById('gd').textContent=Math.floor(S.dist)+'m';
  document.getElementById('gsc').textContent=Math.floor(S.score);
  document.getElementById('gm').textContent='x'+S.mult.toFixed(1);
  document.getElementById('ghs').textContent='High Score: '+Math.floor(hs);
  document.getElementById('gscr').style.display='flex';
  document.getElementById('hud').style.display='none';
  document.getElementById('ebar').style.display='none';
  document.getElementById('boostbar').style.display='none';
}

// ============================================================
// === UPDATE ===
// ============================================================
function update(dt){
  if(S.ph!=='playing')return;
  S.spd=Math.min(C.MAX_SPD*1.3,S.spd+C.ACCEL*dt*60);
  
  // Input
  let ix=0,iy=0;
  if(S.keys['ArrowLeft']||S.keys['KeyA'])ix-=1;
  if(S.keys['ArrowRight']||S.keys['KeyD'])ix+=1;
  if(S.keys['ArrowUp']||S.keys['KeyW'])iy+=1;
  if(S.keys['ArrowDown']||S.keys['KeyS'])iy-=1;
  
  // Boost
  S.boosting=!!S.keys['Space']&&S.boost>0;
  if(S.boosting){
    S.boost=Math.max(0,S.boost-C.BOOST_COST*dt*60);
    S.spd=Math.min(C.MAX_SPD*1.3,S.spd+.03*dt*60);
    if(Math.random()<.25)A.boost();
  }else{
    S.boost=Math.min(C.BOOST_MAX,S.boost+C.BOOST_RECH*dt*60);
  }
  
  // Movement with inertia
  const lt2=ix*C.LAT_SPD,pt=iy*C.PCH_SPD;
  const ease=1-Math.exp(-10*dt);
  S.pvx=lerp(S.pvx,lt2,ease);
  S.pvy=lerp(S.pvy,pt,ease);
  S.px+=S.pvx*dt*60;
  S.py+=S.pvy*dt*60;
  S.px=clamp(S.px,-C.ROAD_W*1.3,C.ROAD_W*1.3);
  S.py=clamp(S.py,-55,35);
  
  // Visual tilt
  S.bank=lerp(S.bank,S.pvx*C.BANK_AMT,ease);
  S.pitch=lerp(S.pitch,S.pvy*C.PCH_AMT,ease);
  
  // Forward
  const ma=S.spd*dt*60;
  S.dist+=ma;S.pz+=ma;
  
  // Sun
  S.sunY=lerp(S.sunY,C.SUN_LY,C.SUN_SINK*dt*60);
  if(S.sunY>=C.SUN_LY-10){S.sunSet=true;gover();return}
  
  // Energy
  let inSh=false;
  for(const b of S.blds){const dz=b.z-S.pz;if(dz>-20&&dz<120){const d=Math.abs(S.px-b.x);if(d<b.w*.7+C.ROAD_W*.25){inSh=true;break}}}
  S.energy-=(inSh?C.E_SHADOW:C.E_DRAIN)*dt*60;
  if(S.energy<=0){S.energy=0;gover();return}
  
  // Score
  S.chainT=Math.max(0,S.chainT-dt);
  S.score+=ma*S.mult*.12;
  
  // Move world
  for(const b of S.blds)b.z-=ma;
  for(const o of S.obs)o.z-=ma;
  for(const p of S.pcks)p.z-=ma;
  S.blds=S.blds.filter(b=>b.z>-C.WORLD_CHUNK);
  S.obs=S.obs.filter(o=>o.z>-C.WORLD_CHUNK);
  S.pcks=S.pcks.filter(p=>p.z>-C.WORLD_CHUNK);
  
  // Regenerate world
  const sa=S.pz+C.RENDER_DIST,b=S.pz-C.RENDER_BEHIND;
  let lastBZ=b;
  for(const bl of S.blds)if(bl.z>lastBZ)lastBZ=bl.z;
  while(lastBZ<sa){lastBZ+=rand(C.B_GAP*.7,C.B_GAP*1.5);
    S.blds.push({x:randC(['left','right'])==='left'?-rand(C.ROAD_W*.2,C.ROAD_W*1.6):rand(C.ROAD_W*.2,C.ROAD_W*1.6),y:0,z:lastBZ,w:rand(C.B_MIN_W,C.B_MAX_W),h:rand(C.B_MIN_H,C.B_MAX_H),d:rand(C.B_MIN_W*.5,C.B_MAX_W*.5),c:randC(C.B_COLORS),side:randC(['left','right'])})}
  
  let lastOZ=b;for(const o of S.obs)if(o.z>lastOZ)lastOZ=o.z;
  while(lastOZ<sa){lastOZ+=rand(C.OBS_GAP,C.OBS_GAP_MAX);spawnObs(lastOZ)}
  
  let lastPZ=b;for(const p of S.pcks)if(p.z>lastPZ)lastPZ=p.z;
  while(lastPZ<sa){lastPZ+=rand(50,90);spawnPck(lastPZ)}
  
  // Collisions
  checkCol();
  
  // Near misses
  if(S.nearMissCD>0)S.nearMissCD-=dt;
  for(const o of S.obs){
    const dz=o.z-S.pz;if(dz>-5&&dz<15&&S.nearMissCD<=0){
      if(o.type==='wall'){const ig=S.px>(o.gapC-o.gap/2+8)&&S.px<(o.gapC+o.gap/2-8);if(!ig)continue}
      else{const d=Math.abs(S.px-(o.x||0));if(d<(o.w/2+10))continue}
      const d2=Math.abs(dz);
      if(d2<10){
        S.score+=30*S.mult;S.shX=rand(-4,4);S.shY=rand(-3,3);
        A.nearMiss();S.nearMissCD=.3;
      }
    }
  }
  
  // Particles
  for(const p of S.parts){p.x+=p.vx*dt*60;p.y+=p.vy*dt*60;p.z+=p.vz*dt*60;p.life-=dt}
  S.parts=S.parts.filter(p=>p.life>0);
  
  // Thrust
  if(Math.random()<.6)sThrust();
  
  // Speed lines
  if(S.spd>C.SL_THRESH){if(Math.random()<.35)S.sls.push({x:rand(0,cv.width),y:rand(0,cv.height),len:rand(30,100),spd:rand(10,18),op:rand(.1,.4)})}
  for(const s of S.sls)s.y+=s.spd*dt*60;
  S.sls=S.sls.filter(s=>s.y<cv.height+s.len);
  
  // Shake decay
  S.shX*=Math.exp(-8*dt);S.shY*=Math.exp(-8*dt);
  S.flash=Math.max(0,S.flash-dt);
  
  // Audio
  A.setWind((S.spd-C.BASE_SPD)/(C.MAX_SPD-C.BASE_SPD));
  
  // HUD
  document.getElementById('hd').textContent=Math.floor(S.dist)+'m';
  document.getElementById('hs').textContent=Math.floor(S.score);
  document.getElementById('hm').textContent='x'+S.mult.toFixed(1);
  document.getElementById('hsp').textContent=Math.floor(S.spd*100)+' km/h';
  document.getElementById('efill').style.width=S.energy+'%';
  document.getElementById('bfill').style.width=S.boost+'%';
}

function checkCol(){
  const pr=8;
  for(const o of S.obs){
    const dz=o.z-S.pz;if(dz>-o.d&&dz<o.d){
      if(o.type==='wall'){const ig=S.px>(o.gapC-o.gap/2+pr)&&S.px<(o.gapC+o.gap/2-pr);if(!ig){gover();return}}
      else{const dx=Math.abs(S.px-(o.x||0)),dy=Math.abs(S.py);if(dx<o.w/2+pr&&dy<o.h/2+pr){gover();return}}}
  }
  for(let i=S.pcks.length-1;i>=0;i--){
    const p=S.pcks[i],dz=p.z-S.pz;
    if(dz>-10&&dz<10){const dx=S.px-p.x,dy=S.py-p.y;if(Math.sqrt(dx*dx+dy*dy)<p.r+pr){
      A.pickup(p.type);sPckFX(p.x,p.y,p.z,p.c);
      if(p.type==='energy')S.energy=Math.min(100,S.energy+C.E_PICKUP);
      else if(p.type==='token')S.score+=100*S.mult;
      else if(p.type==='multiplier'){S.mult=Math.min(10,S.mult+.5);S.chainT=3}
      S.pcks.splice(i,1);
    }}
  }
}

// ============================================================
// === RENDER ===
// ============================================================
function render(){
  const W=cv.width,H=cv.height,cw=W/2,ch=H/2;
  const camX=S.px*.3,camY=S.py+C.CAM_H,camZ=S.pz-C.CAM_D;
  const shX=S.shX,shY=S.shY;
  
  cx.fillStyle='#000';cx.fillRect(0,0,W,H);
  
  // Sky
  const sp=(S.sunY-C.SUN_MNY)/(C.SUN_LY-C.SUN_MNY);
  const sg=cx.createLinearGradient(0,0,0,H);
  sg.addColorStop(0,`rgb(${Math.floor(lerp(10,50,sp))},${Math.floor(lerp(5,20,sp))},${Math.floor(lerp(21,35,sp))})`);
  sg.addColorStop(.4,`rgb(${Math.floor(lerp(26,160,sp))},${Math.floor(lerp(10,70,sp))},${Math.floor(lerp(37,25,sp))})`);
  sg.addColorStop(1,`rgb(${Math.floor(lerp(40,255,sp))},${Math.floor(lerp(15,80,sp))},${Math.floor(lerp(10,25,sp))})`);
  cx.fillStyle=sg;cx.fillRect(0,0,W,H);
  
  // Sun
  const sy=ch-(S.sunY-camY)*C.FOV/1000,sx=cw+shX,sr=C.SUN_R*(C.FOV/1000);
  const gl=cx.createRadialGradient(sx,sy,sr*.3,sx,sy,sr*5);
  gl.addColorStop(0,'rgba(255,200,50,.25)');gl.addColorStop(.3,'rgba(255,100,20,.08)');gl.addColorStop(1,'rgba(255,40,10,0)');
  cx.fillStyle=gl;cx.fillRect(0,0,W,H);
  const sg2=cx.createRadialGradient(sx,sy,0,sx,sy,sr);
  sg2.addColorStop(0,'#fff8e0');sg2.addColorStop(.3,'#ffcc33');sg2.addColorStop(.6,'#ff8822');sg2.addColorStop(1,'#cc2200');
  cx.beginPath();cx.arc(sx,sy,sr,0,Math.PI*2);cx.fillStyle=sg2;cx.fill();
  cx.shadowColor='#ff8822';cx.shadowBlur=50;
  cx.beginPath();cx.arc(sx,sy,sr*.85,0,Math.PI*2);cx.fillStyle='rgba(255,140,30,.25)';cx.fill();
  cx.shadowBlur=0;
  
  // Ground
  const hy=ch-(0-camY)*C.FOV/1000;
  const gg=cx.createLinearGradient(0,hy,0,H);
  gg.addColorStop(0,'#140a18');gg.addColorStop(1,'#0a0510');
  cx.fillStyle=gg;cx.fillRect(0,hy,W,H-hy);
  
  // Grid
  const gs=20,gs2=Math.floor(S.pz/gs)*gs;
  cx.lineWidth=1;
  for(let z=gs2;z<S.pz+C.RENDER_DIST;z+=gs){
    const rz=z-S.pz;if(rz<0||rz>C.RENDER_DIST)continue;
    const p1=V3.proj({x:-C.ROAD_W*2,y:0,z},camX,camY,camZ,C.FOV,cw,ch);
    const p2=V3.proj({x:C.ROAD_W*2,y:0,z},camX,camY,camZ,C.FOV,cw,ch);
    if(!p1||!p2)continue;
    const a=Math.max(0,1-rz/C.RENDER_DIST)*.12;
    cx.strokeStyle=`rgba(255,120,30,${a})`;
    cx.beginPath();cx.moveTo(p1.sx+shX,p1.sy+shY);cx.lineTo(p2.sx+shX,p2.sy+shY);cx.stroke();
  }
  
  // Road
  const rl=V3.proj({x:-C.ROAD_W,y:0,z:S.pz+1},camX,camY,camZ,C.FOV,cw,ch);
  const rr=V3.proj({x:C.ROAD_W,y:0,z:S.pz+1},camX,camY,camZ,C.FOV,cw,ch);
  const rlf=V3.proj({x:-C.ROAD_W,y:0,z:S.pz+C.RENDER_DIST},camX,camY,camZ,C.FOV,cw,ch);
  const rrf=V3.proj({x:C.ROAD_W,y:0,z:S.pz+C.RENDER_DIST},camX,camY,camZ,C.FOV,cw,ch);
  if(rl&&rr&&rlf&&rrf){
    cx.fillStyle=C.ROAD;
    cx.beginPath();cx.moveTo(rl.sx,rl.sy);cx.lineTo(rr.sx,rr.sy);cx.lineTo(rrf.sx,rrf.sy);cx.lineTo(rlf.sx,rlf.sy);cx.closePath();cx.fill();
    cx.strokeStyle=C.ROAD_EDGE;cx.lineWidth=2;
    cx.beginPath();cx.moveTo(rl.sx,rl.sy);cx.lineTo(rlf.sx,rlf.sy);cx.stroke();
    cx.beginPath();cx.moveTo(rr.sx,rr.sy);cx.lineTo(rrf.sx,rrf.sy);cx.stroke();
    // Center line
    const rcn=V3.proj({x:0,y:0,z:S.pz+1},camX,camY,camZ,C.FOV,cw,ch);
    const rcf=V3.proj({x:0,y:0,z:S.pz+C.RENDER_DIST},camX,camY,camZ,C.FOV,cw,ch);
    if(rcn&&rcf){cx.strokeStyle='rgba(255,150,30,.08)';cx.lineWidth=1;cx.setLineDash([8,18]);cx.beginPath();cx.moveTo(rcn.sx,rcn.sy);cx.lineTo(rcf.sx,rcf.sy);cx.stroke();cx.setLineDash([])}
  }
  
  // Objects sorted far->near
  const objs=[];
  for(const b of S.blds){const dz=b.z-S.pz;if(dz>-5&&dz<C.RENDER_DIST)objs.push({t:'b',o:b,z:dz})}
  for(const o of S.obs){const dz=o.z-S.pz;if(dz>-20&&dz<C.RENDER_DIST)objs.push({t:'o',o:o,z:dz})}
  for(const p of S.pcks){const dz=p.z-S.pz;if(dz>-20&&dz<C.RENDER_DIST)objs.push({t:'p',o:p,z:dz})}
  objs.sort((a,b)=>b.z-a.z);
  
  for(const it of objs){
    if(it.t==='b')rBld(it.o,camX,camY,camZ,cw,ch,shX,shY);
    else if(it.t==='o')rObs(it.o,camX,camY,camZ,cw,ch,shX,shY);
    else rPck(it.o,camX,camY,camZ,cw,ch,shX,shY);
  }
  
  // Craft
  rCraft(cw+shX,ch+shY+38);
  
  // Particles
  for(const p of S.parts){
    const pr=V3.proj({x:p.x,y:p.y,z:p.z},camX,camY,camZ,C.FOV,cw,ch);
    if(!pr)continue;
    const a=p.life/p.ml,sz=Math.max(1,p.sz*pr.s*2);
    cx.globalAlpha=a;cx.fillStyle=p.color;
    cx.beginPath();cx.arc(pr.sx+shX,pr.sy+shY,sz,0,Math.PI*2);cx.fill();
  }
  cx.globalAlpha=1;
  
  // Speed lines
  sc.clearRect(0,0,sl.width,sl.height);
  if(S.sls.length>0){
    const op=Math.min(1,(S.spd-C.SL_THRESH)/(C.MAX_SPD-C.SL_THRESH))*C.SL_MAX_OP;
    sc.globalAlpha=op;sc.strokeStyle='#ffcc33';sc.lineWidth=1;
    for(const s of S.sls){sc.beginPath();sc.moveTo(s.x,s.y);sc.lineTo(s.x,s.y+s.len);sc.stroke()}
    sc.globalAlpha=1;
  }
  
  // Flash
  if(S.flash>0){cx.fillStyle=`rgba(255,200,100,${S.flash/C.FLASH_DUR*.3})`;cx.fillRect(0,0,W,H)}
  
  // Vignette
  const vg=cx.createRadialGradient(cw,ch,H*.25,cw,ch,H*.85);
  vg.addColorStop(0,'rgba(0,0,0,0)');vg.addColorStop(1,'rgba(0,0,0,.45)');
  cx.fillStyle=vg;cx.fillRect(0,0,W,H);
  
  // Energy warning
  if(S.energy<25){const pulse=Math.sin(performance.now()*.005)*.5+.5;cx.fillStyle=`rgba(255,0,0,${pulse*.08*(1-S.energy/25)})`;cx.fillRect(0,0,W,H)}
  
  // Near miss flash
  if(S.nearMissCD>.2){cx.fillStyle=`rgba(255,200,50,${(S.nearMissCD-.2)*.15})`;cx.fillRect(0,0,W,H)}
}

function rBld(b,cx2,cy2,cz2,cw,ch,shX,shY){
  const p=(x,y,z)=>V3.proj({x,y,z},cx2,cy2,cz2,C.FOV,cw,ch);
  const corners=[
    p(b.x-b.w/2,0,b.z-b.d/2),p(b.x+b.w/2,0,b.z-b.d/2),
    p(b.x+b.w/2,0,b.z+b.d/2),p(b.x-b.w/2,0,b.z+b.d/2),
    p(b.x-b.w/2,b.h,b.z-b.d/2),p(b.x+b.w/2,b.h,b.z-b.d/2),
    p(b.x+b.w/2,b.h,b.z+b.d/2),p(b.x-b.w/2,b.h,b.z+b.d/2),
  ];
  const faces=[[0,1,5,4],[2,3,7,6],[0,3,7,4],[1,2,6,5],[4,5,6,7]];
  for(const face of faces){
    const pts=face.map(i=>corners[i]);
    if(pts.some(pt=>!pt||pt.z<.1))continue;
    const ax=pts[1].sx-pts[0].sx,ay=pts[1].sy-pts[0].sy;
    const bx2=pts[2].sx-pts[0].sx,by2=pts[2].sy-pts[0].sy;
    if(ax*by2-ay*bx2<0)continue;
    const avgZ=face.reduce((s,i)=>s+corners[i].z,0)/face.length;
    const fade=Math.max(0,1-avgZ/C.RENDER_DIST);
    let br=.25;if(face[0]===4)br=.45;if(face[0]===1)br=.35;if(face[0]===3)br=.2;if(face[0]===0)br=.18;
    cx.globalAlpha=fade;cx.fillStyle=b.c;
    cx.beginPath();cx.moveTo(pts[0].sx+shX,pts[0].sy+shY);
    for(let i=1;i<pts.length;i++)cx.lineTo(pts[i].sx+shX,pts[i].sy+shY);
    cx.closePath();cx.fill();
    cx.strokeStyle=C.B_EDGE;cx.lineWidth=.8;cx.stroke();
  }
  cx.globalAlpha=1;
}

function rObs(o,cx2,cy2,cz2,cw,ch,shX,shY){
  const p=(x,y,z)=>V3.proj({x,y,z},cx2,cy2,cz2,C.FOV,cw,ch);
  if(o.type==='wall'){
    const le=o.gapC-o.gap/2,rs=o.gapC+o.gap/2;
    const c1=[p(le-o.w/2,0,o.z-o.d/2),p(le,0,o.z-o.d/2),p(le,0,o.z+o.d/2),p(le-o.w/2,0,o.z+o.d/2),p(le-o.w/2,o.h,o.z-o.d/2),p(le,o.h,o.z-o.d/2),p(le,o.h,o.z+o.d/2),p(le-o.w/2,o.h,o.z+o.d/2)];
    drawBox(c1,o.c,o.edge||C.OBS_EDGE,shX,shY);
    const c2=[p(rs,0,o.z-o.d/2),p(rs+o.w/2,0,o.z-o.d/2),p(rs+o.w/2,0,o.z+o.d/2),p(rs,0,o.z+o.d/2),p(rs,o.h,o.z-o.d/2),p(rs+o.w/2,o.h,o.z-o.d/2),p(rs+o.w/2,o.h,o.z+o.d/2),p(rs,o.h,o.z+o.d/2)];
    drawBox(c2,o.c,o.edge||C.OBS_EDGE,shX,shY);
    // Gap warning
    const ge1=p(le,0,o.z),ge2=p(rs,0,o.z);
    if(ge1&&ge2){cx.strokeStyle='rgba(255,100,30,.4)';cx.lineWidth=2;cx.setLineDash([3,3]);
      cx.beginPath();cx.moveTo(ge1.sx+shX,ge1.sy+shY);cx.lineTo(ge1.sx+shX,ge1.sy+shY-o.h*ge1.s*2);cx.stroke();
      cx.beginPath();cx.moveTo(ge2.sx+shX,ge2.sy+shY);cx.lineTo(ge2.sx+shX,ge2.sy+shY-o.h*ge2.s*2);cx.stroke();
      cx.setLineDash([])}
  }else{
    const c=[p(o.x-o.w/2,0,o.z-o.d/2),p(o.x+o.w/2,0,o.z-o.d/2),p(o.x+o.w/2,0,o.z+o.d/2),p(o.x-o.w/2,0,o.z+o.d/2),
      p(o.x-o.w/2,o.h,o.z-o.d/2),p(o.x+o.w/2,o.h,o.z-o.d/2),p(o.x+o.w/2,o.h,o.z+o.d/2),p(o.x-o.w/2,o.h,o.z+o.d/2)];
    drawBox(c,o.c,C.OBS_EDGE,shX,shY);
  }
}

function drawBox(corners,col,edge,shX,shY){
  const faces=[[0,1,5,4],[2,3,7,6],[0,3,7,4],[1,2,6,5],[4,5,6,7]];
  for(const face of faces){
    const pts=face.map(i=>corners[i]);
    if(pts.some(pt=>!pt||pt.z<.1))continue;
    const ax=pts[1].sx-pts[0].sx,ay=pts[1].sy-pts[0].sy;
    const bx2=pts[2].sx-pts[0].sx,by2=pts[2].sy-pts[0].sy;
    if(ax*by2-ay*bx2<0)continue;
    const avgZ=face.reduce((s,i)=>s+corners[i].z,0)/face.length;
    const fade=Math.max(0,1-avgZ/C.RENDER_DIST);
    cx.globalAlpha=fade;cx.fillStyle=col;
    cx.beginPath();cx.moveTo(pts[0].sx+shX,pts[0].sy+shY);
    for(let i=1;i<pts.length;i++)cx.lineTo(pts[i].sx+shX,pts[i].sy+shY);
    cx.closePath();cx.fill();cx.strokeStyle=edge;cx.lineWidth=.8;cx.stroke();
  }
  cx.globalAlpha=1;
}

function rPck(p,cx2,cy2,cz2,cw,ch,shX,shY){
  const bob=Math.sin(p.bob+performance.now()*.003)*6;
  const pr=V3.proj({x:p.x,y:p.y+bob,z:p.z},cx2,cy2,cz2,C.FOV,cw,ch);
  if(!pr)return;
  const r=p.r*pr.s*2;
  // Glow
  const gl=cx.createRadialGradient(pr.sx+shX,pr.sy+shY,0,pr.sx+shX,pr.sy+shY,r*3);
  gl.addColorStop(0,p.c+'50');gl.addColorStop(1,p.c+'00');
  cx.fillStyle=gl;cx.fillRect(pr.sx+shX-r*3,pr.sy+shY-r*3,r*6,r*6);
  cx.fillStyle=p.c;cx.strokeStyle='#fff';cx.lineWidth=1.5;
  if(p.type==='energy'){
    cx.beginPath();cx.moveTo(pr.sx+shX-r*.2,pr.sy+shY-r);cx.lineTo(pr.sx+shX+r*.3,pr.sy+shY-r*.2);
    cx.lineTo(pr.sx+shX-r*.1,pr.sy+shY-r*.1);cx.lineTo(pr.sx+shX+r*.2,pr.sy+shY+r);
    cx.lineTo(pr.sx+shX-r*.3,pr.sy+shY+r*.2);cx.lineTo(pr.sx+shX+r*.1,pr.sy+shY+r*.1);
    cx.lineTo(pr.sx+shX-r*.2,pr.sy+shY-r);cx.closePath();cx.fill();cx.stroke();
  }else if(p.type==='token'){
    cx.beginPath();cx.moveTo(pr.sx+shX,pr.sy+shY-r);cx.lineTo(pr.sx+shX+r,pr.sy+shY);
    cx.lineTo(pr.sx+shX,pr.sy+shY+r);cx.lineTo(pr.sx+shX-r,pr.sy+shY);cx.closePath();cx.fill();cx.stroke();
  }else{
    cx.beginPath();cx.arc(pr.sx+shX,pr.sy+shY,r,0,Math.PI*2);cx.fill();cx.stroke();
    cx.beginPath();cx.arc(pr.sx+shX,pr.sy+shY,r*.5,0,Math.PI*2);cx.stroke();
  }
}

function rCraft(cx2,cy2){
  const s=1.4;
  cx.save();cx.translate(cx2,cy2);cx.rotate(-S.bank);
  // Shadow
  cx.fillStyle='rgba(0,0,0,.25)';cx.beginPath();cx.ellipse(0,18,28*s,7*s,0,0,Math.PI*2);cx.fill();
  // Body
  cx.fillStyle='#2a1a30';cx.strokeStyle='#ff8844';cx.lineWidth=2;
  cx.beginPath();cx.moveTo(0,-32*s);cx.lineTo(-18*s,8*s);cx.lineTo(-12*s,22*s);cx.lineTo(12*s,22*s);cx.lineTo(18*s,8*s);cx.closePath();cx.fill();cx.stroke();
  // Cockpit
  cx.fillStyle='#ffcc44';cx.beginPath();cx.moveTo(0,-18*s);cx.lineTo(-7*s,4*s);cx.lineTo(7*s,4*s);cx.closePath();cx.fill();
  // Wing accents
  cx.strokeStyle='#ff6622';cx.lineWidth=1.5;
  cx.beginPath();cx.moveTo(-16*s,6*s);cx.lineTo(-22*s,16*s);cx.stroke();
  cx.beginPath();cx.moveTo(16*s,6*s);cx.lineTo(22*s,16*s);cx.stroke();
  // Thrust
  const tl=S.boosting?22:12;
  const tg=cx.createLinearGradient(0,22*s,0,22*s+tl);
  tg.addColorStop(0,'#ffcc33');tg.addColorStop(.5,'#ff6622');tg.addColorStop(1,'rgba(255,50,10,0)');
  cx.fillStyle=tg;cx.beginPath();cx.moveTo(-5*s,22*s);cx.lineTo(5*s,22*s);cx.lineTo(2*s,22*s+tl);cx.lineTo(-2*s,22*s+tl);cx.closePath();cx.fill();
  // Engine glow
  cx.shadowColor='#ff8822';cx.shadowBlur=18;cx.fillStyle='#ffcc33';
  cx.beginPath();cx.ellipse(0,24*s,3.5*s,2.5*s,0,0,Math.PI*2);cx.fill();cx.shadowBlur=0;
  cx.restore();
}

// ============================================================
// === LOOP ===
// ============================================================
function loop(ts){
  if(S.ph!=='playing')return;
  const dt=Math.min((ts-S.lt)/1000,.05);S.lt=ts;S.dt=dt;
  update(dt);render();
  requestAnimationFrame(loop);
}

// ============================================================
// === TITLE ===
// ============================================================
function rTitle(){
  const W=cv.width,H=cv.height,cw=W/2,ch=H/2,t=performance.now()*.001;
  const sg=cx.createLinearGradient(0,0,0,H);
  sg.addColorStop(0,'#0a0515');sg.addColorStop(.5,'#1a0a25');sg.addColorStop(1,'#ff4411');
  cx.fillStyle=sg;cx.fillRect(0,0,W,H);
  const sg2=cx.createRadialGradient(cw,ch*.75,0,cw,ch*.75,100);
  sg2.addColorStop(0,'#fff8e0');sg2.addColorStop(.3,'#ffcc33');sg2.addColorStop(.6,'#ff8822');sg2.addColorStop(1,'#cc2200');
  cx.beginPath();cx.arc(cw,ch*.75,80,0,Math.PI*2);cx.fillStyle=sg2;cx.fill();
  cx.fillStyle='#0a0510';cx.fillRect(0,ch*.82,W,H-ch*.82);
  cx.strokeStyle='rgba(255,120,30,.06)';cx.lineWidth=1;
  for(let i=0;i<18;i++){const y=ch*.82+i*(H-ch*.82)/18;cx.beginPath();cx.moveTo(0,y);cx.lineTo(W,y);cx.stroke()}
  cx.fillStyle='rgba(255,255,255,.4)';
  for(let i=0;i<60;i++){
    const x=(Math.sin(i*127.1+t*.02)*.5+.5)*W,y=(Math.sin(i*311.7+t*.01)*.5+.5)*ch*.65;
    cx.beginPath();cx.arc(x,y,Math.sin(t+i)*.5+.8,0,Math.PI*2);cx.fill();
  }
  for(let i=0;i<25;i++){
    const x=(Math.sin(i*73.1+t*.3)*.5+.5)*W,y=(Math.cos(i*47.3+t*.2)*.5+.5)*H;
    cx.fillStyle=`rgba(255,180,50,${Math.sin(t*2+i)*.25+.25})`;
    cx.beginPath();cx.arc(x,y,2,0,Math.PI*2);cx.fill();
  }
}
function tLoop(){if(S.ph==='playing')return;rTitle();requestAnimationFrame(tLoop)}
S.hs=parseInt(localStorage.getItem('sdHS'))||0;
requestAnimationFrame(tLoop);
</script>
</body>
</html>