Race the Sun
Qwen 3.6 35b a3b · full

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>