← Rally results

Rally

GPT 6 Luna · full

Initial view of Rally

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 30,423 bytes · SHA-256 d4f8250ea25b
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
<title>RALLY//RUSH — Alpine Sprint</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@500;600;700;800;900&family=Space+Mono:wght@400;700&display=swap');
:root{--lime:#d7ff3f;--ink:#101713;--cream:#f3f1e8;--orange:#ff6947;--glass:rgba(12,20,17,.76)}
*{box-sizing:border-box}html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#9dc9c1;color:var(--cream);font-family:'Barlow Condensed',Arial,sans-serif;user-select:none}#game{position:fixed;inset:0}#view{display:block;width:100%;height:100%;outline:none}.scanlines{position:fixed;inset:0;pointer-events:none;z-index:2;background:linear-gradient(180deg,transparent 50%,rgba(0,0,0,.055) 51%);background-size:100% 4px;mix-blend-mode:multiply;opacity:.38}.vignette{position:fixed;inset:0;pointer-events:none;z-index:2;background:radial-gradient(ellipse at 50% 48%,transparent 48%,rgba(8,16,12,.38) 100%)}
#hud{position:fixed;inset:0;z-index:4;pointer-events:none}.brand{position:absolute;top:22px;left:30px;font-weight:900;font-size:20px;letter-spacing:2px;color:white;text-shadow:0 2px 14px #18251c}.brand span{color:var(--lime)}.stage{font:10px 'Space Mono',monospace;letter-spacing:2px;color:#e6f1df;opacity:.86;margin-top:1px}.topright{position:absolute;right:30px;top:24px;text-align:right;font:11px 'Space Mono',monospace;letter-spacing:1px}.topright b{font-size:13px;color:var(--lime)}
.panel{background:var(--glass);border:1px solid rgba(255,255,255,.18);backdrop-filter:blur(8px);box-shadow:0 9px 32px #14231b30}.mapbox{position:absolute;left:28px;top:103px;width:174px;height:169px;padding:9px 11px 10px}.label{font:9px 'Space Mono',monospace;color:#aebcae;letter-spacing:1.5px}.mapbox canvas{width:100%;height:130px;display:block;margin-top:4px}.lapbox{position:absolute;right:30px;top:91px;text-align:right}.lapnum{font-size:32px;line-height:1;font-weight:800;letter-spacing:-1px}.lapnum small{font-size:13px;color:#b4c5b6;font-weight:600;letter-spacing:1px}.timer{font:22px 'Space Mono',monospace;font-weight:700;margin-top:8px}.best{font:10px 'Space Mono',monospace;color:#becbbb;margin-top:5px}.best strong{color:var(--lime);font-weight:400}.center-msg{position:absolute;left:50%;top:33%;transform:translate(-50%,-50%);text-align:center;text-shadow:0 4px 25px #14211c;min-width:270px}.count{font-size:88px;font-weight:900;line-height:.9;color:var(--lime);letter-spacing:-5px}.submsg{font:11px 'Space Mono',monospace;letter-spacing:3px;margin-top:12px}.bottom{position:absolute;bottom:22px;left:50%;transform:translateX(-50%);display:flex;align-items:end;gap:16px}.speedpanel{width:178px;height:96px;padding:10px 14px;position:relative}.speedrow{display:flex;align-items:baseline;gap:6px}.speed{font-size:55px;font-weight:800;line-height:1;letter-spacing:-2px}.unit{font:10px 'Space Mono',monospace;color:#bac6b9}.meter{height:5px;background:#566259;margin-top:6px}.meter i{display:block;background:var(--lime);height:100%;width:0;transition:width .08s}.gearbox{font-size:11px;font-weight:700;position:absolute;right:14px;top:14px;text-align:right;color:#cbd6ca}.gearbox b{font-size:23px;color:var(--lime);display:block;line-height:1.1}.status{width:100px;padding-bottom:7px;font:9px 'Space Mono',monospace;line-height:1.9;color:#d4ddcf}.status span{color:var(--lime)}.help{position:absolute;bottom:28px;right:29px;font:9px 'Space Mono',monospace;color:#e3ebe1;text-align:right;line-height:1.9;text-shadow:0 2px 6px #17231c}.key{display:inline-block;border:1px solid #d5e2d4;border-radius:2px;padding:0 4px;margin-left:3px;color:white}.toast{position:absolute;top:47%;left:50%;transform:translate(-50%,-50%);font:12px 'Space Mono',monospace;letter-spacing:2px;color:var(--lime);opacity:0;transition:opacity .25s}.overlay{position:fixed;inset:0;z-index:8;display:flex;align-items:center;justify-content:center;background:rgba(7,15,11,.52);backdrop-filter:blur(3px)}.overlay.hidden{display:none}.dialog{width:min(430px,calc(100% - 36px));padding:34px 38px;background:#18221c;border:1px solid #52614e;box-shadow:0 25px 90px #07100acc;text-align:center}.eyebrow{font:10px 'Space Mono',monospace;letter-spacing:3px;color:var(--lime)}.dialog h1{font-size:56px;line-height:.95;margin:12px 0 9px;font-weight:900;letter-spacing:-2px}.dialog p{font:11px 'Space Mono',monospace;color:#c4d0c2;line-height:1.8;margin:10px auto 22px;max-width:310px}.dialog button{border:0;background:var(--lime);color:#172019;font:800 15px 'Barlow Condensed',sans-serif;letter-spacing:1.5px;padding:13px 27px;cursor:pointer;min-width:170px;transition:transform .15s,background .15s}.dialog button:hover{transform:translateY(-2px);background:#ecff9a}.secondary{display:block!important;margin:10px auto 0;background:transparent!important;color:#dbe5d8!important;border:1px solid #63705f!important}.touch{display:none;position:fixed;z-index:5;bottom:15px;left:15px;right:15px;justify-content:space-between;pointer-events:none}.touchgroup{display:flex;gap:8px;align-items:end}.touch button{pointer-events:auto;background:#1320189c;border:1px solid #dae5d488;color:white;border-radius:50%;width:58px;height:58px;font:700 13px 'Space Mono',monospace;touch-action:none}.touch .gas{width:73px;height:73px;background:#d7ff3fbd;color:#182019}.touch .brake{background:#ff6947b9}.touch .hand{width:48px;height:48px;font-size:10px}@media(max-width:700px){.brand{left:15px;top:14px;font-size:16px}.stage{font-size:8px}.topright{right:14px;top:15px;font-size:9px}.mapbox{left:12px;top:70px;width:132px;height:134px;padding:7px}.mapbox canvas{height:101px}.lapbox{top:72px;right:14px}.lapnum{font-size:25px}.timer{font-size:17px}.best{font-size:8px}.bottom{bottom:91px;gap:8px}.speedpanel{width:153px;height:83px;padding:8px 11px}.speed{font-size:46px}.status{width:72px;font-size:8px}.help{display:none}.center-msg{top:36%}.count{font-size:74px}.touch{display:flex}.dialog{padding:28px 24px}.dialog h1{font-size:48px}}
</style>
</head><body><div id="game"><canvas id="view"></canvas></div><div class="scanlines"></div><div class="vignette"></div>
<div id="hud"><div class="brand">RALLY<span>//</span>RUSH<div class="stage">ALPINE SPRINT · STAGE 04</div></div><div class="topright">RACE <b>01</b> &nbsp;·&nbsp; <span id="stateTxt">READY</span></div>
<div class="mapbox panel"><div class="label">ROUTE / 3.8 KM</div><canvas id="minimap" width="260" height="190"></canvas></div>
<div class="lapbox"><div class="label">LAP</div><div class="lapnum"><span id="lap">1</span><small> / 3</small></div><div class="timer" id="timer">00:00.000</div><div class="best">BEST &nbsp;<strong id="best">--:--.---</strong></div></div>
<div class="center-msg" id="center"><div class="count" id="count">3</div><div class="submsg" id="submsg">GET READY</div></div><div class="toast" id="toast">OFF TRACK — FIND THE LINE</div>
<div class="bottom"><div class="speedpanel panel"><div class="label">SPEED</div><div class="speedrow"><span class="speed" id="speed">0</span><span class="unit">KM/H</span></div><div class="meter"><i id="meter"></i></div><div class="gearbox">GEAR<b id="gear">N</b></div></div><div class="status"><div>GRIP &nbsp;<span id="grip">◎◎◎</span></div><div>RPM &nbsp; <span id="rpm">1200</span></div></div></div>
<div class="help"><span class="key">W</span> ACCEL &nbsp;<span class="key">A D</span> STEER<br><span class="key">SPACE</span> HANDBRAKE &nbsp;<span class="key">C</span> CAM<br><span class="key">P</span> PAUSE &nbsp;<span class="key">R</span> RESTART</div></div>
<div class="touch"><div class="touchgroup"><button data-key="ArrowLeft">◀</button><button data-key="ArrowRight">▶</button></div><div class="touchgroup"><button class="hand" data-key="Space">DRIFT</button><button class="brake" data-key="ArrowDown">BRAKE</button><button class="gas" data-key="ArrowUp">GO</button></div></div>
<div class="overlay" id="menu"><div class="dialog"><div class="eyebrow">RALLY CHAMPIONSHIP · 01/08</div><h1>CHASE<br>THE SUMMIT.</h1><p>Three laps. One alpine course. Keep it on the line, or let the mountain take your time.<br><br><b>WASD / ARROWS</b> drive &nbsp;·&nbsp; <b>SPACE</b> drift</p><button id="startBtn">START RALLY &nbsp; ↗</button></div></div>
<div class="overlay hidden" id="pause"><div class="dialog"><div class="eyebrow">STAGE PAUSED</div><h1>TAKE A<br>BREATHER.</h1><p>Your run is waiting right here.</p><button id="resumeBtn">RESUME</button><button class="secondary" id="restartBtn">RESTART STAGE</button></div></div>
<div class="overlay hidden" id="finish"><div class="dialog"><div class="eyebrow">FINISH LINE · ALPINE SPRINT</div><h1 id="finishTitle">SUMMIT<br>SECURED.</h1><p id="finishText">FINAL TIME &nbsp; 00:00.000</p><button id="againBtn">RACE AGAIN &nbsp; ↗</button></div></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
(()=>{'use strict';
const $=id=>document.getElementById(id), canvas=$('view'), mapCanvas=$('minimap'), ctx=mapCanvas.getContext('2d');
const keys=new Set(); let audio=null, engineOsc=null, engineGain=null, skidGain=null;
const state={mode:'menu',time:0,lap:1,lapStart:0,best:localStorage.getItem('rallyBest')||'', speed:0, steer:0, heading:0, lateral:0, dist:0, trackT:0, camHigh:false, countdown:0, lastCheckpoint:0, checkpointGate:0, offTimer:0, totalTime:0,seed:12345};
const clamp=(v,a,b)=>Math.max(a,Math.min(b,v)), lerp=(a,b,t)=>a+(b-a)*t;
const rand=()=>{state.seed=(state.seed*1664525+1013904223)>>>0;return state.seed/4294967296};
const THREE=window.THREE; let renderer,scene,camera,car,world,threeOK=false, carRoot, scenery=[], particles=[], skidMarks=[];
// Spline control points describe one complete alpine loop in X/Z.
const points=[[-5,0,0],[35,2,-72],[105,7,-112],[185,3,-90],[222,0,-35],[206,5,38],[150,11,93],[74,6,115],[5,2,86],[-43,8,36],[-37,3,-28]];
function curveAt(t){let n=points.length, f=((t%n)+n)%n, i=Math.floor(f), u=f-i,p0=points[(i-1+n)%n],p1=points[i],p2=points[(i+1)%n],p3=points[(i+2)%n];let cat=(a,b,c,d)=>.5*((2*b)+(-a+c)*u+(2*a-5*b+4*c-d)*u*u+(-a+3*b-3*c+d)*u*u*u);return {x:cat(p0[0],p1[0],p2[0],p3[0]),y:cat(p0[1],p1[1],p2[1],p3[1]),z:cat(p0[2],p1[2],p2[2],p3[2])}}
const trackN=1200, samples=[]; for(let i=0;i<=trackN;i++)samples.push(curveAt(i/trackN*points.length));
state.pos={x:samples[0].x,y:samples[0].y,z:samples[0].z};state.heading=Math.atan2(samples[2].x-samples[0].x,samples[2].z-samples[0].z);
function nearest(pos){let best=1e9, bi=0; // coarse search with local continuity preference
 let expected=Math.round(state.trackT/points.length*trackN);let start=Math.max(0,expected-100),end=Math.min(trackN,expected+100);
 for(let i=start;i<=end;i+=2){let p=samples[i],d=(pos.x-p.x)**2+(pos.z-p.z)**2;if(d<best){best=d;bi=i}}
 let a=Math.max(0,bi-3),b=Math.min(trackN,bi+3);for(let i=a;i<=b;i++){let p=samples[i],d=(pos.x-p.x)**2+(pos.z-p.z)**2;if(d<best){best=d;bi=i}}
 let p=samples[bi],q=samples[Math.min(trackN,bi+1)],dx=q.x-p.x,dz=q.z-p.z,l=Math.hypot(dx,dz)||1;return {i:bi,t:bi/trackN*points.length,p,tx:dx/l,tz:dz/l,side:((pos.x-p.x)*(-dz)+(pos.z-p.z)*dx)/l,dist:Math.sqrt(best)} }
function initThree(){if(!THREE)return false;try{renderer=new THREE.WebGLRenderer({canvas,antialias:true,alpha:false});renderer.setPixelRatio(Math.min(devicePixelRatio,1.7));renderer.setSize(innerWidth,innerHeight);renderer.shadowMap.enabled=true;renderer.shadowMap.type=THREE.PCFSoftShadowMap;renderer.outputEncoding=THREE.sRGBEncoding;renderer.toneMapping=THREE.ACESFilmicToneMapping;renderer.toneMappingExposure=1.12;
scene=new THREE.Scene();scene.background=new THREE.Color(0x9abeb5);scene.fog=new THREE.Fog(0x9abeb5,95,380);camera=new THREE.PerspectiveCamera(64,innerWidth/innerHeight,.1,600);
scene.add(new THREE.HemisphereLight(0xe0f4e9,0x50634a,1.15));let sun=new THREE.DirectionalLight(0xffefcf,2.2);sun.position.set(-90,160,-60);sun.castShadow=true;sun.shadow.mapSize.set(1024,1024);sun.shadow.camera.left=-150;sun.shadow.camera.right=150;sun.shadow.camera.top=150;sun.shadow.camera.bottom=-150;scene.add(sun);
const mat=(c,r=.88)=>new THREE.MeshStandardMaterial({color:c,roughness:r,side:THREE.DoubleSide});const grass=mat(0x55784c),road=mat(0x393e3b),edge=mat(0xbab79b),white=mat(0xe7e2cb),yellow=mat(0xd9db46),red=mat(0xeb6045),dark=mat(0x17201d),rubber=mat(0x101313),glass=mat(0x294346,.3),green=mat(0x356544);
let ground=new THREE.Mesh(new THREE.PlaneGeometry(1200,1200),grass);ground.rotation.x=-Math.PI/2;ground.position.y=-2.5;ground.receiveShadow=true;scene.add(ground);
function ribbon(width,yoff,material,side=0){let verts=[],idx=[];for(let i=0;i<=trackN;i++){let p=samples[i],q=samples[Math.min(trackN,i+1)],dx=q.x-p.x,dz=q.z-p.z,l=Math.hypot(dx,dz)||1,nx=-dz/l,nz=dx/l;for(let s of [-1,1])verts.push(p.x+nx*(side+s*width/2),p.y+yoff,p.z+nz*(side+s*width/2));if(i<trackN){let a=i*2;idx.push(a,a+2,a+1,a+1,a+2,a+3)}}let g=new THREE.BufferGeometry();g.setAttribute('position',new THREE.Float32BufferAttribute(verts,3));g.setIndex(idx);g.computeVertexNormals();let mesh=new THREE.Mesh(g,material);mesh.receiveShadow=true;scene.add(mesh);return mesh}
ribbon(15,.04,edge);ribbon(11,.09,road);ribbon(.23,.14,white,5.15);ribbon(.23,.14,white,-5.15);
// Dashed center stripe as short geometry strips.
for(let i=0;i<trackN;i+=24){let verts=[],idx=[];for(let j=0;j<=10;j++){let p=samples[i+j],q=samples[Math.min(trackN,i+j+1)],dx=q.x-p.x,dz=q.z-p.z,l=Math.hypot(dx,dz)||1,nx=-dz/l,nz=dx/l;verts.push(p.x+nx*.11,p.y+.16,p.z+nz*.11,p.x-nx*.11,p.y+.16,p.z-nz*.11);if(j<10){let a=j*2;idx.push(a,a+2,a+1,a+1,a+2,a+3)}}let g=new THREE.BufferGeometry();g.setAttribute('position',new THREE.Float32BufferAttribute(verts,3));g.setIndex(idx);g.computeVertexNormals();scene.add(new THREE.Mesh(g,white))}
// Car geometry built from low-poly primitive shapes.
carRoot=new THREE.Group();carRoot.rotation.y=Math.PI;scene.add(carRoot);
function box(parent,w,h,d,m,x,y,z){let o=new THREE.Mesh(new THREE.BoxGeometry(w,h,d),m);o.position.set(x,y,z);o.castShadow=true;o.receiveShadow=true;parent.add(o);return o}
box(carRoot,2.05,.58,3.65,red,0,.8,0);box(carRoot,1.73,.76,1.72,red,0,1.38,-.12);box(carRoot,1.52,.57,1.42,glass,0,1.48,-.18);box(carRoot,2.18,.14,.28,dark,0,1.38,1.77);box(carRoot,2.35,.13,.48,dark,0,1.27,2.03);box(carRoot,.18,.25,.16,red,-.74,1.48,-.91);box(carRoot,.18,.25,.16,red,.74,1.48,-.91);
for(let x of [-.99,.99])for(let z of [-1.16,1.15]){let w=new THREE.Mesh(new THREE.CylinderGeometry(.43,.43,.26,10),rubber);w.rotation.z=Math.PI/2;w.position.set(x,.51,z);w.castShadow=true;carRoot.add(w);let hub=new THREE.Mesh(new THREE.CylinderGeometry(.19,.19,.275,8),mat(0x87918a));hub.rotation.z=Math.PI/2;hub.position.set(x*1.01,.51,z);carRoot.add(hub)}
for(let x of [-.68,.68]){let lamp=box(carRoot,.4,.18,.09,mat(0xfff2bd),x,.88,-1.86);lamp.material.emissive=new THREE.Color(0x332b12);let tail=box(carRoot,.32,.17,.08,red,x,.85,1.86);tail.material.emissive=new THREE.Color(0x7f170a)}
// Tree, rock and roadside props are procedural, deterministic and instanced as simple meshes.
const trunkMat=mat(0x584532), leafMats=[mat(0x315d42),mat(0x3c7048),mat(0x527b48)], rockMat=mat(0x777c6c);
function makeTree(x,y,z,s=1){let g=new THREE.Group();let tr=new THREE.Mesh(new THREE.CylinderGeometry(.15*s,.23*s,2.0*s,5),trunkMat);tr.position.y=1*s;tr.castShadow=true;g.add(tr);for(let k=0;k<3;k++){let c=new THREE.Mesh(new THREE.ConeGeometry((1.05-k*.19)*s,1.8*s,5),leafMats[(k+Math.floor(rand()*3))%3]);c.position.y=(2.0+k*.67)*s;c.castShadow=true;g.add(c)}g.position.set(x,y,z);scene.add(g);return g}
function addSign(p,tx,tz,text){let g=new THREE.Group();let post=new THREE.Mesh(new THREE.BoxGeometry(.14,1.5,.14),dark);post.position.y=.75;g.add(post);let board=new THREE.Mesh(new THREE.BoxGeometry(1.9,.76,.12),yellow);board.position.y=1.65;g.add(board);let stripe=new THREE.Mesh(new THREE.BoxGeometry(1.3,.08,.14),dark);stripe.position.set(0,1.69,0);g.add(stripe);g.position.set(p.x, p.y,p.z);g.rotation.y=Math.atan2(tx,tz);scene.add(g)}
for(let i=0;i<170;i++){let k=Math.floor(rand()*trackN),p=samples[k],q=samples[Math.min(trackN,k+2)],dx=q.x-p.x,dz=q.z-p.z,l=Math.hypot(dx,dz)||1,nx=-dz/l,nz=dx/l,side=(rand()<.5?-1:1)*(11+rand()*39),x=p.x+nx*side,z=p.z+nz*side;if(rand()<.76)makeTree(x,-.5,z,.7+rand()*.8);else{let rock=new THREE.Mesh(new THREE.DodecahedronGeometry(.6+rand()*.85,0),rockMat);rock.position.set(x,-.25,z);rock.scale.y=.6;rock.castShadow=true;scene.add(rock);scenery.push({x,z,r:1.2,type:'rock'})}}
for(let i=0;i<points.length;i++){let p=curveAt(i+.15),q=curveAt(i+.18),dx=q.x-p.x,dz=q.z-p.z,l=Math.hypot(dx,dz)||1;addSign({x:p.x-dz/l*9,y:p.y,z:p.z+dx/l*9},dx/l,dz/l)}
// Crowd-like colorful flags and a finish arch.
for(let i=0;i<9;i++){let p=samples[36+i*2];let post=box(scene,.12,2,.12,white,p.x+9,p.y+1,p.z);let flag=box(scene,1.3,.55,.09,[red,yellow,green][i%3],p.x+9,p.y+2.12,p.z);}
function arch(){let i=0,p=samples[i],q=samples[2],dx=q.x-p.x,dz=q.z-p.z,l=Math.hypot(dx,dz),nx=-dz/l,nz=dx/l;for(let s of [-1,1])box(scene,.34,4,.34,red,p.x+nx*s*7,p.y+2,p.z+nz*s*7);let top=box(scene,14,.65,.5,yellow,p.x,p.y+4,p.z);top.rotation.y=Math.atan2(dx,dz)}arch();
car=carRoot;
// Soft procedural particle puffs (individual low-poly translucent spheres).
threeOK=true;return true}catch(e){console.warn('3D unavailable; using canvas fallback',e);return false}}
let fallback=!initThree();
function resize(){if(threeOK){renderer.setSize(innerWidth,innerHeight);camera.aspect=innerWidth/innerHeight;camera.updateProjectionMatrix()}else{canvas.width=innerWidth*devicePixelRatio;canvas.height=innerHeight*devicePixelRatio}}addEventListener('resize',resize);resize();
function audioStart(){if(audio)return;try{audio=new (window.AudioContext||window.webkitAudioContext)();engineOsc=audio.createOscillator();engineOsc.type='sawtooth';engineGain=audio.createGain();engineGain.gain.value=.0001;let filter=audio.createBiquadFilter();filter.type='lowpass';filter.frequency.value=480;engineOsc.connect(filter).connect(engineGain).connect(audio.destination);engineOsc.start();let noise=audio.createBufferSource(),buf=audio.createBuffer(1,audio.sampleRate*2,audio.sampleRate),d=buf.getChannelData(0);for(let i=0;i<d.length;i++)d[i]=Math.random()*2-1;noise.buffer=buf;noise.loop=true;let nf=audio.createBiquadFilter();nf.type='lowpass';nf.frequency.value=700;skidGain=audio.createGain();skidGain.gain.value=0;noise.connect(nf).connect(skidGain).connect(audio.destination);noise.start()}catch(e){}}
function beep(freq=660,dur=.13){if(!audio)return;let o=audio.createOscillator(),g=audio.createGain();o.frequency.value=freq;o.type='sine';g.gain.setValueAtTime(.13,audio.currentTime);g.gain.exponentialRampToValueAtTime(.001,audio.currentTime+dur);o.connect(g).connect(audio.destination);o.start();o.stop(audio.currentTime+dur)}
function format(t){let m=Math.floor(t/60),s=Math.floor(t%60),ms=Math.floor(t*1000%1000);return String(m).padStart(2,'0')+':'+String(s).padStart(2,'0')+'.'+String(ms).padStart(3,'0')}
function showToast(){ $('toast').style.opacity=1;clearTimeout(showToast.t);showToast.t=setTimeout(()=>$('toast').style.opacity=0,850)}
function restart(){state.mode='countdown';state.time=0;state.lap=1;state.lapStart=0;state.totalTime=0;state.speed=0;state.steer=0;state.lateral=0;state.heading=0;state.dist=0;state.trackT=0;state.lastCheckpoint=0;state.checkpointGate=0;state.countdown=3;state.offTimer=0;state.pos={x:samples[0].x,y:samples[0].y,z:samples[0].z};$('menu').classList.add('hidden');$('pause').classList.add('hidden');$('finish').classList.add('hidden');$('center').style.display='block';$('count').textContent='3';$('submsg').textContent='GET READY';$('stateTxt').textContent='COUNTDOWN';audioStart();if(audio.state==='suspended')audio.resume();beep(520)}
function setPause(){if(state.mode==='race'){state.mode='paused';$('pause').classList.remove('hidden');$('stateTxt').textContent='PAUSED'}else if(state.mode==='paused'){state.mode='race';$('pause').classList.add('hidden');$('stateTxt').textContent='RACE ON'}}
$('startBtn').onclick=restart;$('resumeBtn').onclick=setPause;$('restartBtn').onclick=restart;$('againBtn').onclick=restart;
addEventListener('keydown',e=>{let k=e.key.toLowerCase();if(['arrowup','arrowdown','arrowleft','arrowright',' '].includes(k))e.preventDefault();keys.add(k===' ' ? 'space':k);if(k==='p'&&!e.repeat)setPause();if(k==='r'&&!e.repeat)restart();if(k==='c'&&!e.repeat){state.camHigh=!state.camHigh;toastText(state.camHigh?'CINEMATIC CAM':'CHASE CAM')}if(k==='enter'&&state.mode==='menu')restart()});addEventListener('keyup',e=>keys.delete(e.key.toLowerCase()===' '? 'space':e.key.toLowerCase()));
document.querySelectorAll('.touch button').forEach(b=>{let k=b.dataset.key.toLowerCase();b.addEventListener('pointerdown',e=>{e.preventDefault();keys.add(k===' '? 'space':k)});['pointerup','pointercancel','pointerleave'].forEach(ev=>b.addEventListener(ev,()=>keys.delete(k===' '? 'space':k)))});
function toastText(t){$('toast').textContent=t;$('toast').style.opacity=1;clearTimeout(showToast.t);showToast.t=setTimeout(()=>$('toast').style.opacity=0,1000)}
function update(dt){if(state.mode==='countdown'){state.countdown-=dt;if(state.countdown<=0){state.mode='race';state.lapStart=0;$('stateTxt').textContent='RACE ON';$('center').style.display='none';beep(880,.28)}else{let v=Math.ceil(state.countdown);$('count').textContent=v===1?'1':v;$('submsg').textContent='GET READY';if(Math.floor(state.countdown)!==Math.floor(state.countdown+dt))beep(550+((3-v)*70))}return}if(state.mode!=='race')return;
state.time+=dt;state.totalTime+=dt;let forward=keys.has('w')||keys.has('arrowup'),back=keys.has('s')||keys.has('arrowdown'),left=keys.has('a')||keys.has('arrowleft'),right=keys.has('d')||keys.has('arrowright'),hand=keys.has('space');let near=nearest(state.pos),off=Math.abs(near.side)>6.3,surface=off? (Math.abs(near.side)>11?'grass':'gravel'): 'road';let traction=off?(surface==='grass'?.48:.72):1;
if(forward)state.speed+=31*dt*traction;else state.speed-=7.5*dt;if(back)state.speed-=state.speed>2?43*dt:18*dt;state.speed=clamp(state.speed,-12,58*traction);if(hand)state.speed*=Math.max(0,1-1.4*dt);
let turn=(right?1:0)-(left?1:0);let steerTarget=turn*(.43+Math.min(Math.abs(state.speed)/48,1)*.54);state.steer=lerp(state.steer,steerTarget,Math.min(1,dt*(hand?9:5)));let direction=state.speed>=0?1:-1;
let desiredHeading=Math.atan2(near.tx,near.tz);let angleDiff=Math.atan2(Math.sin(desiredHeading-state.heading),Math.cos(desiredHeading-state.heading));let roadFollow=clamp(angleDiff,-.11,.11);state.heading+= (roadFollow + state.steer*(.025+Math.min(Math.abs(state.speed)/58,.8)*.044)*direction)*dt*60;
let slide=hand||off?.25:0;let movementHeading=state.heading+state.steer*slide*.55;state.pos.x+=Math.sin(movementHeading)*state.speed*dt;state.pos.z+=Math.cos(movementHeading)*state.speed*dt;
near=nearest(state.pos);state.trackT=near.t;let width=5.5;if(Math.abs(near.side)>width){state.speed*=Math.max(.2,1-2.1*dt);state.pos.x-=(-near.tz)*Math.sign(near.side)*Math.min(1.3,dt*7);state.pos.z-=(near.tx)*Math.sign(near.side)*Math.min(1.3,dt*7);if(state.offTimer<.2)showToast();state.offTimer+=dt}else state.offTimer=0;
// Gate order uses three separated course sectors; only forward sequential crossings count.
let gate=Math.floor((near.i/trackN)*12);if(gate!==state.checkpointGate){let expected=(state.checkpointGate+1)%12;if(gate===expected){state.checkpointGate=gate;if(gate===0){if(state.lastCheckpoint>7){let lapTime=state.time-state.lapStart;if(lapTime>8){if(!state.best||lapTime<parseTime(state.best)){state.best=format(lapTime);localStorage.setItem('rallyBest',state.best)}state.lap++;state.lapStart=state.time;if(state.lap>3){finish();return}}}}state.lastCheckpoint=gate}}
// obstacle contact against props
for(let o of scenery){let dx=state.pos.x-o.x,dz=state.pos.z-o.z,d=Math.hypot(dx,dz);if(d<o.r+1.15){let a=Math.atan2(dx,dz);state.pos.x+=Math.sin(a)*.34;state.pos.z+=Math.cos(a)*.34;state.speed*=.78;crash();break}}
if(engineOsc){let rpm=1100+Math.abs(state.speed)*105;engineOsc.frequency.setTargetAtTime(42+rpm*.075,audio.currentTime,.07);engineGain.gain.setTargetAtTime(.018+Math.min(Math.abs(state.speed)/58,.7)*.025,audio.currentTime,.12);skidGain.gain.setTargetAtTime((hand||off?.12:0)*.07,audio.currentTime,.08)}
let kmh=Math.round(Math.abs(state.speed)*3.6);$('speed').textContent=kmh;$('meter').style.width=clamp(kmh/210*100,0,100)+'%';$('gear').textContent=state.speed<1?'N':String(clamp(Math.floor(Math.abs(state.speed)/10)+1,1,6));$('rpm').textContent=String(Math.round(1100+Math.abs(state.speed)*105));$('lap').textContent=state.lap;$('timer').textContent=format(state.time-state.lapStart);$('best').textContent=state.best||'--:--.---';$('grip').textContent=off?'◎◉○':hand?'◎◎◉':'◎◎◎';
}
function parseTime(s){let a=s.split(':');return +a[0]*60+parseFloat(a[1])}
let lastCrash=0;function crash(){if(performance.now()-lastCrash>450){lastCrash=performance.now();beep(105,.16)}}
function finish(){state.mode='finish';$('stateTxt').textContent='FINISHED';$('center').style.display='none';$('finishText').innerHTML='FINAL TIME &nbsp; <b style="color:#d7ff3f">'+format(state.time)+'</b><br>BEST LAP &nbsp; <b style="color:#d7ff3f">'+(state.best||'--:--.---')+'</b>';if(audio){beep(660,.16);setTimeout(()=>beep(880,.3),180)}$('finish').classList.remove('hidden')}
function drawMap(){let w=mapCanvas.width,h=mapCanvas.height;ctx.clearRect(0,0,w,h);let minX=-75,maxX=255,minZ=-145,maxZ=145;let sx=(x)=>14+(x-minX)/(maxX-minX)*(w-28),sy=(z)=>8+(z-minZ)/(maxZ-minZ)*(h-16);ctx.lineCap='round';ctx.lineJoin='round';ctx.beginPath();samples.forEach((p,i)=>i?ctx.lineTo(sx(p.x),sy(p.z)):ctx.moveTo(sx(p.x),sy(p.z)));ctx.strokeStyle='#718173';ctx.lineWidth=12;ctx.stroke();ctx.strokeStyle='#e1e0c8';ctx.lineWidth=8;ctx.stroke();ctx.strokeStyle='#39423c';ctx.lineWidth=6;ctx.stroke();let p=state.pos||samples[0];ctx.beginPath();ctx.arc(sx(p.x),sy(p.z),6,0,Math.PI*2);ctx.fillStyle='#d7ff3f';ctx.fill();ctx.strokeStyle='#fff';ctx.lineWidth=2;ctx.stroke()}
// Three dimensional chase-camera update with subtle acceleration lag.
let camPos=null,lookPos=null;
function renderThree(dt){let p=state.pos||samples[0],near=nearest(p),roadYaw=Math.atan2(near.tx,near.tz);car.position.set(p.x,p.y+.12,p.z);car.rotation.y=(state.heading||roadYaw)+Math.PI;car.rotation.z=clamp(-state.steer*.07,-.09,.09);let behind=state.camHigh?13:9.2,height=state.camHigh?9.5:5.2;let target=new THREE.Vector3(p.x-Math.sin(state.heading||roadYaw)*behind,p.y+height,p.z-Math.cos(state.heading||roadYaw)*behind);if(!camPos)camPos=target.clone();camPos.lerp(target,1-Math.exp(-dt*3.2));let lp=new THREE.Vector3(p.x,p.y+1.1,p.z+Math.cos(state.heading||roadYaw)*5);if(!lookPos)lookPos=lp.clone();lookPos.lerp(lp,1-Math.exp(-dt*5));camera.position.copy(camPos);camera.lookAt(lookPos);renderer.render(scene,camera)}
// Canvas-only fallback maintains a playable pseudo-3D view when a CDN is unreachable.
function renderFallback(){let dpr=devicePixelRatio||1,w=innerWidth,h=innerHeight;canvas.width=w*dpr;canvas.height=h*dpr;let c=canvas.getContext('2d');c.setTransform(dpr,0,0,dpr,0,0);let horizon=h*.43;c.fillStyle='#9fc9c1';c.fillRect(0,0,w,horizon);let gr=c.createLinearGradient(0,horizon,0,h);gr.addColorStop(0,'#7a965e');gr.addColorStop(1,'#425d3d');c.fillStyle=gr;c.fillRect(0,horizon,w,h-horizon);
// layered mountain silhouettes
for(let layer=0;layer<3;layer++){c.beginPath();let base=horizon+layer*22;c.moveTo(0,base);for(let x=0;x<=w+30;x+=30){let y=base-30-Math.sin(x*.009+layer*2+state.time*.025)*24-Math.cos(x*.021+layer)*11;c.lineTo(x,y)}c.lineTo(w,horizon+80);c.lineTo(0,horizon+80);c.closePath();c.fillStyle=['#829d91','#718b73','#637d5d'][layer];c.fill()}
// perspective track curves made from projected centerline strips
let p=state.pos||samples[0],near=nearest(p),centerX=w/2;let path=[];for(let j=0;j<44;j++){let ahead= j*3+3,idx=(near.i+ahead*2)%trackN,q=samples[idx],q2=samples[(idx+2)%trackN],dx=q2.x-q.x,dz=q2.z-q.z,ang=Math.atan2(dx,dz),relx=q.x-p.x,relz=q.z-p.z;let depth=1/(1+ahead*.038),x=centerX+(relx*Math.cos(state.heading)-relz*Math.sin(state.heading))*depth*7, y=horizon+(1-depth)*(h*.72)+ (q.y-p.y)*depth*5, half=depth*w*.62;path.push({x,y,half})}
for(let j=path.length-1;j>0;j--){let a=path[j],b=path[j-1];c.fillStyle=j%2?'#d0c7a1':'#9b9a7a';c.beginPath();c.moveTo(a.x-a.half*1.2,a.y);c.lineTo(a.x+a.half*1.2,a.y);c.lineTo(b.x+b.half*1.2,b.y);c.lineTo(b.x-b.half*1.2,b.y);c.fill();c.fillStyle='#393f3a';c.beginPath();c.moveTo(a.x-a.half,a.y);c.lineTo(a.x+a.half,a.y);c.lineTo(b.x+b.half,b.y);c.lineTo(b.x-b.half,b.y);c.fill();if(j%4===0){c.strokeStyle='#ebe5c4';c.lineWidth=Math.max(1,path[j].half*.014);c.beginPath();c.moveTo(a.x,a.y);c.lineTo(b.x,b.y);c.stroke()}}
// procedural pine silhouettes close to roadside
for(let i=0;i<22;i++){let side=i%2?1:-1,depth=.15+(i*37%100)/115,x=(i*137%w),y=horizon+depth*h*.55,size=depth*38;c.fillStyle=i%3?'#31523c':'#426446';c.beginPath();c.moveTo(x,y-size*2);c.lineTo(x-size*.65,y+size*.4);c.lineTo(x-size*.22,y+size*.35);c.lineTo(x-size*.78,y+size);c.lineTo(x+size*.78,y+size);c.lineTo(x+size*.22,y+size*.35);c.lineTo(x+size*.65,y+size*.4);c.closePath();c.fill()}
// Car silhouette
let cx=w/2+state.steer*28,cy=h*.78,scale=Math.min(w,h)/700;c.save();c.translate(cx,cy);c.scale(scale,scale);c.fillStyle='#131916';c.fillRect(-84,5,20,53);c.fillRect(64,5,20,53);c.fillStyle='#e85d45';c.beginPath();c.moveTo(-96,34);c.lineTo(-84,-22);c.lineTo(-58,-39);c.lineTo(58,-39);c.lineTo(84,-22);c.lineTo(96,34);c.lineTo(69,51);c.lineTo(-69,51);c.closePath();c.fill();c.fillStyle='#243d3e';c.beginPath();c.moveTo(-57,-27);c.lineTo(-42,-46);c.lineTo(42,-46);c.lineTo(57,-27);c.closePath();c.fill();c.fillStyle='#252c27';c.fillRect(-102,43,204,9);c.fillStyle='#ffdf9a';c.fillRect(-68,18,23,9);c.fillRect(45,18,23,9);c.fillStyle='#d7ff3f';c.fillRect(-11,8,22,5);c.restore();}
function frame(now){let dt=Math.min(.05,(now-(frame.last||now))/1000);frame.last=now;update(dt);drawMap();if(threeOK)renderThree(dt);else renderFallback();requestAnimationFrame(frame)}requestAnimationFrame(frame);
})();
</script></body></html>
<!-- agent-meta {"model":"openai/gpt-6-luna","provider":"openrouter","persona":"full","sessionId":"2a84b2c3-f8b0-4851-ae6d-41571a281d55","tokensIn":187343,"tokensOut":16068,"tokensTotal":203411,"cost":0.012296765,"turns":11,"toolCalls":10,"failedToolCalls":0,"timestamp":"2026-09-23T02:52:37.265Z"} -->