27:18Gemini 4 Hydrogen: Cheaper Tokens, More Expensive Answers?
Nov 13, 2026 · YouTube
18:40ERNIE 5.5 Flash Preview vs GLM 5.3 Flash: Baidu's new model is very impressive!
Nov 11, 2026 · YouTube
36:22Fable 5.2 robbing me blind, plus: Nex AGI's N2.6 vs GLM 5.3 Flash
Nov 11, 2026 · YouTube
14:03Gemini 4 Carbon: Can it handle my challenges? (First look with one-shot coding tests)
Nov 10, 2026 · YouTube
47:51Ranking major U.S. Open-Weights Models. (Where does Ripple 65B fit in?)
Nov 6, 2026 · YouTube
10:57"Crouton Alpha": the latest super-efficient coding model by Meta?
Nov 3, 2026 · YouTube
38:58Testing Particle-1: a post-trained DeepSeek V4.1 Flash specializing in coding and efficiency
Oct 28, 2026 · YouTube
22:14GPT-6.1 Terra Pro vs Terra: How Much Better Is Pro?
Oct 27, 2026 · YouTube
29:55Kimi K3.1 Pro Looked Incredible. Then I Kept Testing.
Oct 26, 2026 · YouTube
31:28Is this company reselling GPT Sol 6.1 at a loss (for non-agentic/text-only)?🤔 Is this sustainable?
Oct 20, 2026 · YouTube
17:58Nemotron 3.5 Super VL vs Opus 5.5: Testing NVIDIA’s new coding juggernaut
Oct 15, 2026 · YouTube
16:04Testing a *massive* new open-weights model 🤯: Muse Spark Open (vs. Nemotron, Opus 5.5, and more)
Oct 15, 2026 · YouTube
16:43Gemini 4 Argon: Great 🐈 Animation, but a broken escalator. (Coding challenge vs Opus 5.5, Sol 6.1)
Oct 13, 2026 · YouTube
16:37Meta's new "Muse Wonder" AI is impressive... but can it compete with Anthropic's models?
Oct 12, 2026 · YouTube
11:12WOW... Haiku 5.5 is very impressive. RIP OpenAI?
Oct 7, 2026 · YouTube
22:47Mistral Large 4 Is Cheap. But Is It Good Enough to Compete?
Oct 6, 2026 · YouTube
22:02Is Pareto 26.10 Preview Just Routing to GPT Sol? I Tested It Against Sol 6.1 Pro
Oct 1, 2026 · YouTube
11:22I Tested Sonnet 5.5 Against Opus 5.5. Is It Actually Cheaper?
Sep 28, 2026 · YouTube
30:00Did Elon Musk's team "train the gay away"... and make Grok 4.7 worse?
Sep 26, 2026 · YouTube
10:58So... how much better is Claude Opus 5.5 vs. GPT-6 Sol, really?
Sep 23, 2026 · YouTube
9:38Claude Opus 5.5... the BEST new model for coding!! 🤩
Sep 22, 2026 · YouTube
29:00Grok 4.7: improved, but GLM 5.3 FlashX beat it anyway (benchmark against Gemini, Sol, etc.)
Sep 22, 2026 · YouTube
16:54Stealth Union Alpha: Is this another cheap GPT model? (better than Luna, cheaper than Terra?)
Sep 16, 2026 · YouTube
16:45DeepSeek V4.1 Flash Is Shockingly Good for the Price
Sep 10, 2026 · YouTube
17:14How much better is GPT-6 Astra over Claude Opus 5? Real Website and Coding Tests
Sep 5, 2026 · YouTube
29:13Implementing a multiplayer game with GPT-6 Astra (+ Gemini 3.8, Opus 5, Sol Pro)
Sep 5, 2026 · YouTube
19:42Meta Muse Spark 1.3 Review: A Huge Leap Forward
Sep 2, 2026 · YouTube
17:31Testing Gemini 3.8 Flash on Real Coding Challenges (vs. other models)
Sep 2, 2026 · YouTube
19:45Fable 5.1 writes an animated novel using Ren'Py... did it do better than Sol Pro 5.6?
Sep 1, 2026 · YouTube
14:09Testing IBM Granite-4.2-8B! Can it name 100 women?
Aug 31, 2026 · YouTube
17:37Tencent Hy4 Preview Tested: Is It Really Better Than Kimi K3 and GLM 5.3?
Aug 28, 2026 · YouTube
23:58Qwen 3.8 Flash Tested: UI, Coding, Physics & more - compared to Opus 5, Sol Pro, Grok
Aug 27, 2026 · YouTube
You are a game developer who ships browser demos that have to work on the first click, in front of an audience, on someone else's laptop.
Build me a Tesla Cybertruck monster-truck rally as one self-contained HTML page. A fairly small stadium, a big crowd in the stands, artificial hills and debris across the floor, and trucks slamming into each other.
Then make it match the spec below exactly. The spec is the challenge; a great-looking rally that ignores it is a miss.
.html page. No build step. Saved to disk and double-clicked, it runs.localStorage, sessionStorage, cookies or indexedDB. It runs in a sandboxed frame where touching them throws.Both are checkable in ten seconds, which is why they are here.
#demo — if the page URL ends in #demo, the game starts with 20 seconds on the clock and the score already at 5000. This must run through the real game state, not a cosmetic overlay. Everything else behaves normally.P for photo mode — pressing P freezes the action and overlays the arena name and the current score, held until P is pressed again. The simulation stops; it does not keep playing behind the card.One sentence: the name of your rally and what it looks like.
Exactly one fenced html block containing the whole page and nothing else.
Exactly four bullets.
#demo sets, and why it is not an overlay.P is pressed — the render loop, the physics step, or both.THE RUSTDOME — Cybertruck Monster Rally: five angular stainless monster trucks, six metres tall on knobbies, slinging each other through concrete chicane blocks, knocked-out sedans, chain-reacting fuel drums and dirt ramps under the floodlights of a night oval packed with 1,600 bobbing fans.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"/>
<title>THE RUSTDOME — Cybertruck Monster Rally</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%;overflow:hidden;background:#04060a;color:#e9eff5;
font-family:"Segoe UI","Helvetica Neue",system-ui,Arial,sans-serif;-webkit-font-smoothing:antialiased;
user-select:none;-webkit-user-select:none;touch-action:none}
canvas#c{position:fixed;inset:0;display:block;width:100%;height:100%;transition:filter .35s ease}
canvas#c.photo{filter:contrast(1.1) saturate(1.18) brightness(.92)}
#vig{position:fixed;inset:0;pointer-events:none;z-index:2;
background:radial-gradient(120% 90% at 50% 42%,rgba(0,0,0,0) 42%,rgba(0,0,0,.55) 82%,rgba(0,0,0,.86) 100%)}
#scan{position:fixed;inset:0;pointer-events:none;z-index:3;opacity:.16;
background:repeating-linear-gradient(to bottom,rgba(255,255,255,.09) 0 1px,rgba(0,0,0,0) 1px 3px)}
.dsp{font-family:Impact,Haettenschweiler,"Franklin Gothic Bold","Arial Narrow Bold","Arial Black",sans-serif;
text-transform:uppercase;font-weight:400;letter-spacing:.012em}
.mono{font-family:ui-monospace,"SF Mono",Menlo,Consolas,"Roboto Mono",monospace;font-variant-numeric:tabular-nums}
.ch{clip-path:polygon(0 0,calc(100% - 13px) 0,100% 13px,100% 100%,13px 100%,0 calc(100% - 13px))}
.ch2{clip-path:polygon(11px 0,100% 0,100% calc(100% - 11px),calc(100% - 11px) 100%,0 100%,0 11px)}
.hz{height:5px;background:repeating-linear-gradient(115deg,#ffc21a 0 9px,#0b0e13 9px 18px)}
.lbl{font-size:9.5px;letter-spacing:.3em;text-transform:uppercase;color:#7f8d9c;font-weight:700}
#hud{position:fixed;inset:0;z-index:5;pointer-events:none;opacity:0;transition:opacity .4s}
#hud.on{opacity:1}
.plate{position:absolute;background:linear-gradient(150deg,rgba(17,21,28,.92),rgba(9,12,16,.86));
box-shadow:inset 0 0 0 1px rgba(150,170,190,.18),0 10px 30px rgba(0,0,0,.5)}
#pArena{top:16px;left:16px;padding:0 0 0 0;min-width:250px}
#pArena .in{padding:11px 18px 12px}
#arenaName{font-size:29px;line-height:.94;color:#f2f6fa}
#arenaName em{font-style:normal;color:#ffc21a}
#arenaSub{margin-top:5px;font-size:9.5px;letter-spacing:.22em;color:#8fa0b0;font-weight:700}
#live{display:inline-block;width:7px;height:7px;border-radius:50%;background:#ff3b30;margin-right:6px;
box-shadow:0 0 10px #ff3b30;animation:blink 1.4s infinite}
@keyframes blink{0%,100%{opacity:1}50%{opacity:.25}}
#pClock{top:16px;left:50%;transform:translateX(-50%);padding:8px 22px 10px;text-align:center;min-width:172px}
#clock{font-size:42px;line-height:.9;color:#eef4fa;letter-spacing:.02em}
#clock.warn{color:#ff5a3c;animation:pulse .6s infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.45}}
#pScore{top:16px;right:16px;padding:9px 20px 11px;text-align:right;min-width:230px}
#score{font-size:56px;line-height:.86;color:#ffc21a;text-shadow:0 0 26px rgba(255,194,26,.35)}
#score.bump{animation:bump .28s ease-out}
@keyframes bump{0%{transform:scale(1)}35%{transform:scale(1.14);color:#fff6d8}100%{transform:scale(1)}}
#combo{margin-top:4px;font-size:11px;letter-spacing:.16em;font-weight:800;color:#3fe0ff;opacity:0;transition:.2s}
#combo.on{opacity:1}
#demoTag{margin-top:5px;display:none;font-size:9.5px;letter-spacing:.24em;color:#ff8a1f;font-weight:800}
#demoTag.on{display:block}
#pKeys{bottom:16px;left:16px;padding:10px 16px}
#pKeys .row{font-size:11.5px;letter-spacing:.04em;color:#b6c3cf;line-height:1.75}
kbd{display:inline-block;min-width:20px;text-align:center;padding:1px 6px;margin:0 1px;font:700 10.5px/1.5 ui-monospace,Menlo,Consolas,monospace;
color:#0b0e13;background:linear-gradient(#eef3f8,#b9c6d2);border-radius:3px;box-shadow:0 2px 0 #56626d}
#pGauge{bottom:16px;right:16px;padding:10px 14px;width:212px}
.bar{height:7px;background:#1b222b;box-shadow:inset 0 0 0 1px rgba(150,170,190,.16);margin:4px 0 9px}
.bar i{display:block;height:100%;width:50%;transition:width .12s linear}
#hpFill{background:linear-gradient(90deg,#ff5a3c,#ffc21a)}
#nzFill{background:linear-gradient(90deg,#1f7dff,#3fe0ff)}
#gRow{display:flex;justify-content:space-between;align-items:baseline}
#spd{font-size:24px;color:#eef4fa;line-height:1}
#spd span{font-size:10px;letter-spacing:.16em;color:#7f8d9c}
#toasts{position:absolute;left:50%;top:33%;transform:translateX(-50%);width:460px;text-align:center}
.toast{font-size:27px;color:#fff;letter-spacing:.02em;line-height:1.15;margin-bottom:6px;
text-shadow:0 3px 16px rgba(0,0,0,.85);animation:tin .28s cubic-bezier(.2,1.6,.4,1) both}
.toast small{display:block;font:700 10px/1.6 "Segoe UI",sans-serif;letter-spacing:.28em;color:#8fa0b0}
.toast.gold{color:#ffc21a}.toast.red{color:#ff5a3c}.toast.cy{color:#3fe0ff}
.toast.out{animation:tout .45s ease-in forwards}
@keyframes tin{from{opacity:0;transform:translateY(16px) scale(.86)}to{opacity:1;transform:none}}
@keyframes tout{to{opacity:0;transform:translateY(-22px) scale(1.04)}}
.screen{position:fixed;inset:0;z-index:9;display:flex;align-items:center;justify-content:center;
background:radial-gradient(90% 70% at 50% 40%,rgba(8,11,16,.6),rgba(3,5,8,.95));backdrop-filter:blur(2px)}
.screen.hide{display:none}
.card{max-width:760px;padding:0 34px 34px;background:linear-gradient(155deg,rgba(20,25,33,.96),rgba(8,11,15,.96));
box-shadow:inset 0 0 0 1px rgba(150,170,190,.2),0 40px 100px rgba(0,0,0,.7)}
.card .hz{margin-bottom:26px}
.kicker{font-size:10px;letter-spacing:.42em;color:#ff8a1f;font-weight:800;margin-bottom:10px}
h1{font-size:min(13vw,104px);line-height:.82;color:#f4f8fc;margin-bottom:2px}
h1 em{font-style:normal;-webkit-text-stroke:2px #ffc21a;color:transparent}
.sub{font-size:13.5px;letter-spacing:.2em;color:#9fb0c0;font-weight:700;margin:14px 0 22px;line-height:1.7}
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:2px 26px;margin-bottom:24px}
.kv{display:flex;justify-content:space-between;gap:14px;font-size:12px;padding:5px 0;border-bottom:1px dashed rgba(150,170,190,.14)}
.kv b{color:#ffc21a;font-weight:800;letter-spacing:.05em}
.kv span{color:#93a3b2;letter-spacing:.1em;font-size:10.5px;font-weight:700;text-transform:uppercase}
button{cursor:pointer;border:0;font-family:inherit;background:linear-gradient(100deg,#ffc21a,#ff8a1f);
color:#0a0d12;font-size:22px;letter-spacing:.06em;padding:15px 30px;text-transform:uppercase;font-weight:800;
box-shadow:0 10px 0 -2px #7a4a05,0 20px 40px rgba(255,140,30,.28);transition:transform .12s,box-shadow .12s}
button:hover{transform:translateY(-2px);box-shadow:0 12px 0 -2px #7a4a05,0 26px 50px rgba(255,140,30,.4)}
button:active{transform:translateY(4px);box-shadow:0 4px 0 -2px #7a4a05}
.foot{margin-top:14px;font-size:10.5px;letter-spacing:.2em;color:#66757f;font-weight:700}
#photo{position:fixed;inset:0;z-index:8;display:none;pointer-events:none}
#photo.on{display:block}
.pbar{position:absolute;left:0;right:0;height:9vh;background:#04060a}
.pbar.t{top:0}.pbar.b{bottom:0;display:flex;align-items:flex-start;justify-content:center}
#pcard{position:absolute;left:50%;bottom:14vh;transform:translateX(-50%);text-align:center;padding:18px 40px 22px;
background:linear-gradient(150deg,rgba(14,18,24,.9),rgba(6,9,13,.82));box-shadow:inset 0 0 0 1px rgba(255,194,26,.45);
animation:pc .45s cubic-bezier(.2,1.5,.4,1) both}
@keyframes pc{from{opacity:0;transform:translateX(-50%) translateY(24px) scale(.94)}}
#pcard .tag{font-size:9.5px;letter-spacing:.5em;color:#ff8a1f;font-weight:800}
#pName{font-size:44px;line-height:1;color:#fff;margin:8px 0 6px}
#pScore{font-size:66px;line-height:.9;color:#ffc21a}
#pMeta{font-size:11px;letter-spacing:.26em;color:#9fb0c0;font-weight:700;margin-top:8px}
#pHint{position:absolute;left:50%;bottom:3.4vh;transform:translateX(-50%);font-size:10.5px;letter-spacing:.36em;
color:#c4d2de;font-weight:800;animation:blink 1.8s infinite}
#boot{position:fixed;inset:0;z-index:20;display:flex;flex-direction:column;align-items:center;justify-content:center;
gap:12px;background:#04060a;transition:opacity .5s}
#boot.gone{opacity:0;pointer-events:none}
#boot b{font-size:34px;letter-spacing:.1em;color:#ffc21a}
#boot span{font-size:11px;letter-spacing:.3em;color:#7f8d9c;max-width:520px;text-align:center;line-height:2}
@media(max-width:760px){#pArena,#pClock{display:none}#score{font-size:34px}#pGauge{display:none}}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div id="vig"></div><div id="scan"></div>
<div id="hud">
<div class="plate ch" id="pArena"><div class="in">
<div class="dsp" id="arenaName">THE <em>RUSTDOME</em></div>
<div id="arenaSub"><span id="live"></span>CYBERTRUCK MONSTER RALLY · HEAT 7 · NIGHT SHOW</div>
</div></div>
<div class="plate ch2" id="pClock"><div class="lbl">CLOCK</div><div class="mono" id="clock">1:30</div></div>
<div class="plate ch" id="pScore"><div class="lbl">SCORE</div><div class="dsp" id="score">0</div>
<div id="combo">RAM CHAIN ×2</div><div id="demoTag">◤ DEMO HEAT · 0:20 · 5000 STAKE</div></div>
<div class="plate ch2" id="pKeys"><div class="row">
<kbd>W</kbd><kbd>A</kbd><kbd>S</kbd><kbd>D</kbd> or <kbd>↑</kbd><kbd>←</kbd><kbd>↓</kbd><kbd>→</kbd> drive ·
<kbd>SPACE</kbd> hop · <kbd>SHIFT</kbd> nitro<br>
<kbd>P</kbd> photo mode · <kbd>M</kbd> mute · <kbd>R</kbd> new heat
</div></div>
<div class="plate ch" id="pGauge">
<div id="gRow"><div class="lbl">TRUCK INTEGRITY</div><div class="dsp" id="spd">0<span> KM/H</span></div></div>
<div class="bar"><i id="hpFill"></i></div>
<div class="lbl" style="margin-bottom:2px">NITRO</div><div class="bar"><i id="nzFill"></i></div>
</div>
<div id="toasts"></div>
</div>
<div id="photo">
<div class="pbar t"></div><div class="pbar b"></div>
<div id="pcard">
<div class="tag">PHOTO MODE · SIMULATION FROZEN</div>
<div class="dsp" id="pName">THE RUSTDOME</div>
<div class="dsp" id="pScore">0</div>
<div class="mono" id="pMeta">CLOCK 0:00 · 5 TRUCKS ON TRACK</div>
</div>
<div id="pHint" class="dsp">PRESS P TO RESUME</div>
</div>
<div class="screen" id="start"><div class="card ch">
<div class="hz"></div>
<div class="kicker">LIVE FROM · SEASON III</div>
<h1 class="dsp">THE<br><em>RUSTDOME</em></h1>
<div class="sub">TESLA CYBERTRUCK MONSTER RALLY<br>FIVE TRUCKS · NINETY SECONDS · NO RULES</div>
<div class="grid2">
<div class="kv"><span>Player truck</span><b>#1 STAINLESS</b></div>
<div class="kv"><span>Rivals</span><b>4 AI · HUNTING</b></div>
<div class="kv"><span>Steering</span><b>WASD / ARROWS</b></div>
<div class="kv"><span>Hop · Nitro</span><b>SPACE · SHIFT</b></div>
<div class="kv"><span>Points</span><b>RAMS · AIR · DRUMS</b></div>
<div class="kv"><span>Photo mode</span><b>P</b></div>
</div>
<button id="go" class="dsp">LAUNCH THE HEAT ▸</button>
<div class="foot">TIP: HIT A RIVAL WHILE YOU'RE FAST — THEY SPIN, YOU SCORE. CHAIN IT FOR MULTIPLIERS.</div>
</div></div>
<div class="screen hide" id="over"><div class="card ch">
<div class="hz"></div>
<div class="kicker">CHECKERED FLAG · THE RUSTDOME</div>
<h1 class="dsp" id="rank">SCRAP KING</h1>
<div class="sub" id="finalLine">FINAL SCORE</div>
<div class="grid2">
<div class="kv"><span>Rivals totaled</span><b id="sWreck">0</b></div>
<div class="kv"><span>Hits landed</span><b id="sHits">0</b></div>
<div class="kv"><span>Best chain</span><b id="sChain">×1</b></div>
<div class="kv"><span>Biggest air</span><b id="sAir">0.0s</b></div>
</div>
<button id="again" class="dsp">RUN IT BACK ▸</button>
<div class="foot">PRESS R · OR PRESS P ANY TIME FOR A STILL</div>
</div></div>
<div id="boot"><b class="dsp">BUILDING THE RUSTDOME…</b><span id="bootMsg">POURING CONCRETE · BOLTING FLOODLIGHTS · SEATING THE CROWD</span></div>
<script src="https://unpkg.com/three@0.158.0/build/three.min.js"></script>
<script>
(function(){
'use strict';
var boot=document.getElementById('boot');
if(typeof THREE==='undefined'){
boot.innerHTML='<b class="dsp">NO THREE.JS, NO RALLY</b><span>The single pinned CDN request (three@0.158.0) was blocked. Reconnect, reload.</span>';
return;
}
try{ build(); }catch(err){
boot.className=''; boot.innerHTML='<b class="dsp">RALLY HALTED</b><span>'+ (err && err.message ? err.message : err) +'</span>';
console.error(err);
}
function build(){
/* ══════════════ CORE ══════════════ */
var DEMO=/^#demo\b/i.test(location.hash);
var ARENA={name:'THE RUSTDOME',rx:52,rz:38};
var BRX=ARENA.rx-3.6, BRZ=ARENA.rz-3.4;
var clamp=function(v,a,b){return v<a?a:v>b?b:v;}, lerp=function(a,b,t){return a+(b-a)*t;};
var rnd=function(a,b){return a+Math.random()*(b-a);}, TAU=Math.PI*2;
function angDiff(a,b){var d=(b-a)%TAU; if(d>Math.PI)d-=TAU; if(d<-Math.PI)d+=TAU; return d;}
var seed=20317; function sr(){seed=(seed*1664525+1013904223)&0x7fffffff; return seed/0x7fffffff;}
function hash2(x,y){var n=Math.sin(x*127.1+y*311.7)*43758.5453; return n-Math.floor(n);}
function vnoise(x,y){var xi=Math.floor(x),yi=Math.floor(y),xf=x-xi,yf=y-yi;
var u=xf*xf*(3-2*xf),v=yf*yf*(3-2*yf);
return lerp(lerp(hash2(xi,yi),hash2(xi+1,yi),u),lerp(hash2(xi,yi+1),hash2(xi+1,yi+1),u),v);}
var canvas=document.getElementById('c');
var renderer=new THREE.WebGLRenderer({canvas:canvas,antialias:true,powerPreference:'high-performance'});
renderer.setPixelRatio(Math.min(window.devicePixelRatio||1,1.7));
renderer.setSize(innerWidth,innerHeight,false);
renderer.shadowMap.enabled=true; renderer.shadowMap.type=THREE.PCFSoftShadowMap;
renderer.outputColorSpace=THREE.SRGBColorSpace;
renderer.toneMapping=THREE.ACESFilmicToneMapping; renderer.toneMappingExposure=1.06;
var scene=new THREE.Scene();
scene.fog=new THREE.FogExp2(0x0a1018,0.0072);
var camera=new THREE.PerspectiveCamera(60,innerWidth/innerHeight,0.4,1400);
camera.position.set(0,26,74);
/* ══════════════ TEXTURES (generated) ══════════════ */
function dotTexture(){
var c=document.createElement('canvas'); c.width=c.height=64; var x=c.getContext('2d');
var g=x.createRadialGradient(32,32,0,32,32,32); g.addColorStop(0,'rgba(255,255,255,1)');
g.addColorStop(.35,'rgba(255,255,255,.55)'); g.addColorStop(1,'rgba(255,255,255,0)');
x.fillStyle=g; x.fillRect(0,0,64,64); var t=new THREE.CanvasTexture(c); t.colorSpace=THREE.SRGBColorSpace; return t;
}
function dirtTexture(){
var c=document.createElement('canvas'); c.width=c.height=512; var x=c.getContext('2d');
x.fillStyle='#6d5741'; x.fillRect(0,0,512,512);
for(var i=0;i<26000;i++){var px=Math.random()*512,py=Math.random()*512,n=vnoise(px*.028,py*.028);
var v=Math.floor(70+n*80+Math.random()*22);
x.fillStyle='rgba('+v+','+Math.floor(v*.84)+','+Math.floor(v*.62)+',.22)';
x.fillRect(px,py,1+Math.random()*3,1+Math.random()*3);}
x.strokeStyle='rgba(40,30,22,.35)'; x.lineWidth=3;
for(var k=0;k<26;k++){x.beginPath(); x.arc(Math.random()*512,Math.random()*512,30+Math.random()*180,Math.random()*6,Math.random()*6+2); x.stroke();}
var t=new THREE.CanvasTexture(c); t.wrapS=t.wrapT=THREE.RepeatWrapping; t.repeat.set(9,7); t.colorSpace=THREE.SRGBColorSpace;
t.anisotropy=4; return t;
}
function skyTexture(){
var c=document.createElement('canvas'); c.width=1024; c.height=512; var x=c.getContext('2d');
var g=x.createLinearGradient(0,0,0,512); g.addColorStop(0,'#03040a'); g.addColorStop(.55,'#0a1120');
g.addColorStop(.78,'#1d2334'); g.addColorStop(1,'#3a2a20'); x.fillStyle=g; x.fillRect(0,0,1024,512);
for(var i=0;i<620;i++){var px=Math.random()*1024,py=Math.random()*300,a=Math.random()*.8;
x.fillStyle='rgba(200,220,255,'+a+')'; x.fillRect(px,py,1.4,1.4);}
var t=new THREE.CanvasTexture(c); t.colorSpace=THREE.SRGBColorSpace; return t;
}
function bannerTexture(){
var c=document.createElement('canvas'); c.width=1024; c.height=128; var x=c.getContext('2d');
x.fillStyle='#0e1218'; x.fillRect(0,0,1024,128);
var words=['RUSTDOME','MONSTER RALLY','SCRAP OR GLASS','STEEL 4 LIFE','CLANG','MEGA SHRED','NO AIRBAGS','HYPER-LOAD'];
for(var i=0;i<words.length;i++){
x.fillStyle=i%2?'#ffc21a':'#3fe0ff'; x.font='700 54px Impact, Arial Black, sans-serif';
x.fillText(words[i],18+i*128,86);
x.fillStyle='rgba(255,90,31,.9)'; x.fillRect(i*128,0,6,128);
}
var t=new THREE.CanvasTexture(c); t.wrapS=THREE.RepeatWrapping; t.repeat.set(3,1); t.colorSpace=THREE.SRGBColorSpace; return t;
}
var DOT=dotTexture();
/* ══════════════ ENV MAP for the stainless steel ══════════════ */
(function(){
var es=new THREE.Scene();
var box=new THREE.Mesh(new THREE.BoxGeometry(90,70,90),new THREE.MeshBasicMaterial({color:0x0d1420,side:THREE.BackSide}));
es.add(box);
var p=new THREE.PlaneGeometry(60,3);
[[0,30,-40,0],[0,30,40,Math.PI],[-30,30,0,Math.PI/2],[30,30,0,-Math.PI/2]].forEach(function(a){
var m=new THREE.Mesh(p,new THREE.MeshBasicMaterial({color:0xffffff}));
m.position.set(a[0],a[1],a[2]); m.rotation.y=a[3]; es.add(m);});
var floor=new THREE.Mesh(new THREE.PlaneGeometry(90,90),new THREE.MeshBasicMaterial({color:0x5a422c}));
floor.rotation.x=-Math.PI/2; floor.position.y=-30; es.add(floor);
var roof=new THREE.Mesh(new THREE.PlaneGeometry(90,90),new THREE.MeshBasicMaterial({color:0x2b3d5c}));
roof.rotation.x=Math.PI/2; roof.position.y=34; es.add(roof);
var pm=new THREE.PMREMGenerator(renderer);
scene.environment=pm.fromScene(es,0.03).texture; pm.dispose();
})();
/* ══════════════ LIGHTS ══════════════ */
scene.add(new THREE.HemisphereLight(0x53709c,0x2a1d12,0.55));
scene.add(new THREE.AmbientLight(0x24303e,0.5));
var key=new THREE.DirectionalLight(0xe4eeff,2.3); key.position.set(58,86,34); key.castShadow=true;
key.shadow.mapSize.set(2048,2048); var sc=key.shadow.camera; sc.left=-72; sc.right=72; sc.top=62; sc.bottom=-62; sc.near=20; sc.far=230;
key.shadow.bias=-0.0006; key.shadow.normalBias=0.7; scene.add(key);
var rim=new THREE.DirectionalLight(0xff7a2a,0.85); rim.position.set(-52,22,-64); scene.add(rim);
var fill=new THREE.DirectionalLight(0x3f7dff,0.5); fill.position.set(-20,40,70); scene.add(fill);
/* ══════════════ TERRAIN FIELD ══════════════ */
var HILLS=[{x:-21,z:-11,r:15,h:5.4},{x:19,z:13,r:13,h:4.4},{x:5,z:-21,r:10,h:3.5},
{x:-30,z:15,r:12,h:3.1},{x:31,z:-9,r:9,h:2.7}];
function groundH(x,z){
var h=0,i,m;
for(i=0;i<HILLS.length;i++){m=HILLS[i];var dx=x-m.x,dz=z-m.z;h+=m.h*Math.exp(-(dx*dx+dz*dz)/(2*m.r*m.r*0.62));}
var r=(x/ARENA.rx)*(x/ARENA.rx)+(z/ARENA.rz)*(z/ARENA.rz);
h+=Math.min(2.4,Math.pow(Math.max(0,r),3.2)*1.9);
h+=vnoise(x*0.09,z*0.09)*0.55+vnoise(x*0.31,z*0.31)*0.22;
return h;
}
function terrainNormal(x,z,out){
var e=1.1,hL=groundH(x-e,z),hR=groundH(x+e,z),hD=groundH(x,z-e),hU=groundH(x,z+e);
out.set(hL-hR,2*e,hD-hU).normalize(); return out;
}
function ellipsePoint(f,ang,out){out.x=ARENA.rx*f*Math.cos(ang);out.z=ARENA.rz*f*Math.sin(ang);out.y=groundH(out.x,out.z);return out;}
/* ══════════════ SKY ══════════════ */
var sky=new THREE.Mesh(new THREE.SphereGeometry(760,32,18),
new THREE.MeshBasicMaterial({map:skyTexture(),side:THREE.BackSide,fog:false,depthWrite:false}));
scene.add(sky);
/* ══════════════ FLOOR ══════════════ */
(function(){
var SEG=150,RING=30,pos=[],uv=[],idx=[],col=[],i,j;
for(j=0;j<=RING;j++){var f=j/RING;
for(i=0;i<=SEG;i++){var a=i/SEG*TAU,ca=Math.cos(a),sa=Math.sin(a);
var x=ARENA.rx*f*ca,z=ARENA.rz*f*sa;
pos.push(x,groundH(x,z),z); uv.push(i/SEG*7,j/RING*5);
var d=Math.hypot(x,z)/58,n=vnoise(x*.13,z*.13);
col.push(.42+n*.2-d*.16,.33+n*.16-d*.14,.22+n*.12-d*.1);
}}
for(j=0;j<RING;j++)for(i=0;i<SEG;i++){var A=j*(SEG+1)+i,B=A+1,C=(j+1)*(SEG+1)+i,D=C+1;
idx.push(A,C,B, B,C,D);}
var g=new THREE.BufferGeometry();
g.setAttribute('position',new THREE.Float32BufferAttribute(pos,3));
g.setAttribute('uv',new THREE.Float32BufferAttribute(uv,2));
g.setAttribute('color',new THREE.Float32BufferAttribute(col,3));
g.setIndex(idx); g.computeVertexNormals();
var m=new THREE.Mesh(g,new THREE.MeshStandardMaterial({map:dirtTexture(),vertexColors:true,roughness:.97,metalness:0}));
m.receiveShadow=true; scene.add(m);
})();
/* ══════════════ BOWL / STANDS ══════════════ */
var ROWS=9, rowMeta=[];
(function(){
var prof=[[1.00,0],[1.00,3.4],[1.07,3.4]];
for(var i=0;i<ROWS;i++){var y=3.4+i*2.45,f=1.07+i*0.082;
prof.push([f,y]); prof.push([f+0.082,y]); prof.push([f+0.082,y+2.45]); rowMeta.push({y:y,f0:f,f1:f+0.082});}
prof.push([1.07+ROWS*0.082,3.4+ROWS*2.45]);
var SEG=140,pos=[],col=[],idx=[],i,j;
for(j=0;j<prof.length;j++){var f=prof[j][0],y=prof[j][1];
for(i=0;i<=SEG;i++){var a=i/SEG*TAU;
pos.push(ARENA.rx*f*Math.cos(a),y,ARENA.rz*f*Math.sin(a));
var tread=(j%2===1), base=tread?[.30,.31,.34]:[.19,.20,.23];
var n=vnoise(f*7,a*40)*.09; col.push(base[0]+n,base[1]+n,base[2]+n*.8);}
}
for(j=0;j<prof.length-1;j++)for(i=0;i<SEG;i++){var A=j*(SEG+1)+i,B=A+1,C=(j+1)*(SEG+1)+i,D=C+1;
idx.push(A,C,B, B,C,D);}
var g=new THREE.BufferGeometry();
g.setAttribute('position',new THREE.Float32BufferAttribute(pos,3));
g.setAttribute('color',new THREE.Float32BufferAttribute(col,3));
g.setIndex(idx); g.computeVertexNormals();
var m=new THREE.Mesh(g,new THREE.MeshStandardMaterial({vertexColors:true,roughness:.95,metalness:.05}));
m.receiveShadow=true; scene.add(m);
// sponsor ring at the wall base
var wall=new THREE.Mesh(new THREE.BoxGeometry(1,1,1),new THREE.MeshStandardMaterial({map:bannerTexture(),roughness:.7}));
wall.geometry=new THREE.CylinderGeometry(1,1,1,140,1,true);
wall.scale.set(ARENA.rx*1.035,1.55,ARENA.rz*1.035); wall.position.y=1.9;
wall.material.side=THREE.BackSide; scene.add(wall);
var curb=new THREE.Mesh(new THREE.CylinderGeometry(1,1,1,140,1,true),
new THREE.MeshStandardMaterial({color:0x16191f,roughness:.85,side:THREE.BackSide}));
curb.scale.set(ARENA.rx*1.012,1.2,ARENA.rz*1.012); curb.position.y=0.6; scene.add(curb);
})();
/* ══════════════ CROWD ══════════════ */
var CROWD=1650, crowdBody, crowdHead, crowdData=[];
(function(){
var bg=new THREE.BoxGeometry(0.44,0.8,0.3); bg.translate(0,0.4,0);
var hg=new THREE.IcosahedronGeometry(0.19,0); hg.translate(0,1.0,0);
var mat=new THREE.MeshStandardMaterial({roughness:.88,metalness:.04,flatShading:true});
crowdBody=new THREE.InstancedMesh(bg,mat,CROWD); crowdBody.frustumCulled=false;
crowdHead=new THREE.Mesh(hg,new THREE.MeshStandardMaterial({roughness:.9,metalness:0,color:0xffffff}));
crowdHead.geometry=hg; crowdHead.geometry.translate(0,0,0);
crowdHead=new THREE.InstancedMesh(hg,mat.clone(),CROWD); crowdHead.frustumCulled=false;
var d=new THREE.Object3D(),c=new THREE.Color(),i;
var palette=[0xffc21a,0xff5a3c,0x3fe0ff,0x2fd07a,0xffffff,0xd63ce0,0xff8a1f,0x4f7dff,0xe0e6ee,0x9a5cff];
for(i=0;i<CROWD;i++){
var row=rowMeta[Math.floor(Math.pow(sr(),0.6)*ROWS)];
var ang=sr()*TAU, f=lerp(row.f0+0.008,row.f1-0.008,sr());
var x=ARENA.rx*f*Math.cos(ang), z=ARENA.rz*f*Math.sin(ang);
var e={x:x,y:row.y+0.12,z:z,ang:ang+Math.PI/2+(sr()-.5)*.5,ph:sr()*TAU,sp:.7+sr()*1.5,s:.85+sr()*.42};
crowdData.push(e);
d.position.set(e.x,e.y,e.z); d.rotation.set(0,e.ang,0); d.scale.setScalar(e.s); d.updateMatrix();
crowdBody.setMatrixAt(i,d.matrix); crowdHead.setMatrixAt(i,d.matrix);
c.setHex(palette[Math.floor(sr()*palette.length)]);
if(sr()<.12)c.setHex(0x11141a);
crowdBody.setColorAt(i,c);
c.multiplyScalar(.55+sr()*.5); crowdHead.setColorAt(i,c);
}
crowdBody.instanceMatrix.needsUpdate=true; crowdHead.instanceMatrix.needsUpdate=true;
scene.add(crowdBody); scene.add(crowdHead);
})();
var cDummy=new THREE.Object3D();
function updateCrowd(){
var amp=0.05+G.hype*0.62,i,e,w;
for(i=0;i<CROWD;i++){ e=crowdData[i];
w=Math.sin(G.simTime*(2.1+e.sp)+e.ph)*amp + Math.sin(e.ang*5-G.simTime*1.35)*amp*0.55;
w=Math.max(0,w);
cDummy.position.set(e.x,e.y+w*0.5,e.z);
cDummy.rotation.set(0,e.ang+Math.sin(G.simTime*.8+e.ph)*0.3,w*0.5);
cDummy.scale.set(e.s,e.s*(1+w*0.3),e.s); cDummy.updateMatrix();
crowdBody.setMatrixAt(i,cDummy.matrix); crowdHead.setMatrixAt(i,cDummy.matrix);
}
crowdBody.instanceMatrix.needsUpdate=true; crowdHead.instanceMatrix.needsUpdate=true;
}
/* ══════════════ FLOODLIGHTS / TOWERS / JUMBOTRON ══════════════ */
var jumboTex,jumboCtx,jumboMesh;
(function(){
var chrome=new THREE.MeshStandardMaterial({color:0x5a6470,metalness:.9,roughness:.45});
var lamp=new THREE.MeshStandardMaterial({color:0xfff2cf,emissive:0xfff0c8,emissiveIntensity:2.6});
var beamG=new THREE.ConeGeometry(15,52,14,1,true);
var beamM=new THREE.MeshBasicMaterial({color:0xbfd8ff,transparent:true,opacity:.045,side:THREE.DoubleSide,
depthWrite:false,blending:THREE.AdditiveBlending,fog:false});
for(var i=0;i<8;i++){
var a=i/8*TAU+0.39, f=1.98;
var x=ARENA.rx*f*Math.cos(a), z=ARENA.rz*f*Math.sin(a), y=3.4+ROWS*2.45;
var mast=new THREE.Mesh(new THREE.CylinderGeometry(.5,1.1,26,6),chrome);
mast.position.set(x,y+13,z); mast.castShadow=true; scene.add(mast);
var head=new THREE.Mesh(new THREE.BoxGeometry(7.5,3.4,1.1),chrome);
head.position.set(x,y+26,z); scene.add(head);
for(var r0=0;r0<2;r0++)for(var cc=0;cc<5;cc++){
var lp=new THREE.Mesh(new THREE.BoxGeometry(1.25,1.3,.5),lamp);
lp.position.set(x-2.9+cc*1.45,y+25.2+r0*1.5,z+0.7); scene.add(lp);}
var beam=new THREE.Mesh(beamG,beamM);
beam.position.set(x,y+24,z); beam.lookAt(0,0,0); beam.rotateX(-Math.PI/2);
scene.add(beam);
}
// jumbotron
var jc=document.createElement('canvas'); jc.width=768; jc.height=300; jumboCtx=jc.getContext('2d');
jumboTex=new THREE.CanvasTexture(jc); jumboTex.colorSpace=THREE.SRGBColorSpace;
var jm=new THREE.Mesh(new THREE.PlaneGeometry(26,10.2),
new THREE.MeshBasicMaterial({map:jumboTex,toneMapped:false}));
jm.position.set(0,26,-ARENA.rz*1.16); jm.rotation.y=0; scene.add(jm);
var frame=new THREE.Mesh(new THREE.BoxGeometry(27.6,11.6,1.2),chrome);
frame.position.set(0,26,-ARENA.rz*1.16-0.9); scene.add(frame);
var leg=new THREE.Mesh(new THREE.BoxGeometry(1.4,14,1.4),chrome); leg.position.set(0,15,-ARENA.rz*1.16-1.2); scene.add(leg);
jumboMesh=jm; drawJumbo(true);
})();
function drawJumbo(force){
var x=jumboCtx;
x.fillStyle='#05070b'; x.fillRect(0,0,768,300);
x.fillStyle='#ffc21a'; x.fillRect(0,0,768,7);
x.fillStyle='#ff5a3c'; x.fillRect(0,293,768,7);
x.font='700 46px Impact, Arial Black, sans-serif'; x.fillStyle='#e9eff5';
x.fillText('THE RUSTDOME',26,74);
x.font='700 13px Segoe UI, sans-serif'; x.fillStyle='#ff8a1f';
x.fillText('CYBERTRUCK MONSTER RALLY · HEAT 7',28,98);
x.fillStyle='#0f141b'; x.fillRect(20,116,728,86);
x.font='700 74px Impact, Arial Black, sans-serif'; x.fillStyle='#ffc21a';
x.fillText(String(G.score).padStart(5,'0'),26,186);
x.font='700 12px Segoe UI, sans-serif'; x.fillStyle='#7f8d9c'; x.fillText('SCORE',28,132);
x.font='700 62px Menlo, monospace'; x.fillStyle=G.time<10?'#ff5a3c':'#3fe0ff';
var ts=fmtClock(G.time); x.fillText(ts,740-x.measureText(ts).width,182);
x.font='700 12px Segoe UI, sans-serif'; x.fillStyle='#7f8d9c'; x.fillText('CLOCK',700-x.measureText('CLOCK').width,132);
x.font='700 30px Impact, Arial Black, sans-serif';
var msg=G.hype>0.45?'CROWD GOES NUCLEAR':'RAM SOMETHING';
x.fillStyle=G.hype>0.45?'#ff5a3c':'#4b5a68'; x.fillText(msg,28,258);
for(var i=0;i<14;i++){x.fillStyle=i%2?'#ffc21a':'#0f141b'; x.fillRect(28+i*24,272,22,10);}
jumboTex.needsUpdate=true;
}
/* ══════════════ PARTICLES ══════════════ */
function Puff(size,max,op){
this.n=0;this.max=max;this.size=size;
this.p=new Float32Array(max*3);this.c=new Float32Array(max*3);this.b=new Float32Array(max*3);
this.v=new Float32Array(max*3);this.l=new Float32Array(max);this.L=new Float32Array(max);
var g=new THREE.BufferGeometry();
g.setAttribute('position',new THREE.BufferAttribute(this.p,3));
g.setAttribute('color',new THREE.BufferAttribute(this.c,3));
g.setDrawRange(0,0);
this.m=new THREE.PointsMaterial({size:size,map:DOT,vertexColors:true,transparent:true,depthWrite:false,
blending:THREE.AdditiveBlending,opacity:op||1,sizeAttenuation:true});
this.o=new THREE.Points(g,this.m); this.o.frustumCulled=false; scene.add(this.o); this.g=g;
}
Puff.prototype.spawn=function(x,y,z,vx,vy,vz,r,g,b,life){
var i=this.n<this.max?this.n++:Math.floor(Math.random()*this.max);
this.p[i*3]=x;this.p[i*3+1]=y;this.p[i*3+2]=z;
this.v[i*3]=vx;this.v[i*3+1]=vy;this.v[i*3+2]=vz;
this.b[i*3]=r;this.b[i*3+1]=g;this.b[i*3+2]=b;
this.c[i*3]=r;this.c[i*3+1]=g;this.c[i*3+2]=b;
this.l[i]=life;this.L[i]=life;
};
Puff.prototype.step=function(dt,grav,drag){
for(var i=0;i<this.n;i++){
if(this.l[i]<=0){var j=--this.n;
for(var k=0;k<3;k++){this.p[i*3+k]=this.p[j*3+k];this.v[i*3+k]=this.v[j*3+k];this.b[i*3+k]=this.b[j*3+k];}
this.l[i]=this.l[j];this.L[i]=this.L[j];i--;continue;}
this.l[i]-=dt;
this.v[i*3+1]-=grav*dt;
var d=Math.max(0,1-drag*dt);
this.v[i*3]*=d;this.v[i*3+1]*=d;this.v[i*3+2]*=d;
this.p[i*3]+=this.v[i*3]*dt;this.p[i*3+1]+=this.v[i*3+1]*dt;this.p[i*3+2]+=this.v[i*3+2]*dt;
var f=this.l[i]/this.L[i]; f=f*f;
this.c[i*3]=this.b[i*3]*f;this.c[i*3+1]=this.b[i*3+1]*f;this.c[i*3+2]=this.b[i*3+2]*f;
}
this.g.setDrawRange(0,this.n);
this.g.attributes.position.needsUpdate=true; this.g.attributes.color.needsUpdate=true;
};
var fx={spark:new Puff(.55,900,1),dust:new Puff(3.4,520,.55),flame:new Puff(2.1,420,.95)};
function burstSpark(x,y,z,n,pw){for(var i=0;i<n;i++)fx.spark.spawn(x,y,z,rnd(-1,1)*pw,rnd(.2,1)*pw,rnd(-1,1)*pw,1,rnd(.55,.85),rnd(.1,.3),rnd(.25,.6));}
function burstDust(x,y,z,n,pw){for(var i=0;i<n;i++)fx.dust.spawn(x+rnd(-1,1),y+rnd(0,1),z+rnd(-1,1),rnd(-1,1)*pw,rnd(.2,1.1)*pw*.6,rnd(-1,1)*pw,.42,.34,.25,rnd(.5,1.3));}
function burstFlame(x,y,z,n){for(var i=0;i<n;i++)fx.flame.spawn(x,y,z,rnd(-1,1)*7,rnd(.5,1.6)*8,rnd(-1,1)*7,1,rnd(.4,.7),rnd(.05,.18),rnd(.35,.8));}
/* ══════════════ TRUCKS ══════════════ */
function extrude(pts,depth,mat){
var s=new THREE.Shape(); s.moveTo(pts[0][0],pts[0][1]);
for(var i=1;i<pts.length;i++)s.lineTo(pts[i][0],pts[i][1]); s.closePath();
var g=new THREE.ExtrudeGeometry(s,{depth:depth,bevelEnabled:false}); g.translate(0,0,-depth/2);
var m=new THREE.Mesh(g,mat); m.castShadow=true; return m;
}
function makeTruck(tint,accent,glassy){
var g=new THREE.Group(), body=new THREE.Group(); g.add(body);
var steel=new THREE.MeshStandardMaterial({color:0xb9c3cd,metalness:1,roughness:.19,flatShading:true});
var skin =new THREE.MeshStandardMaterial({color:tint,metalness:.96,roughness:.24,flatShading:true});
var dark =new THREE.MeshStandardMaterial({color:0x101318,metalness:.5,roughness:.72});
var rub =new THREE.MeshStandardMaterial({color:0x0c0e11,metalness:.05,roughness:.95,flatShading:true});
var glass=new THREE.MeshStandardMaterial({color:0x080d14,metalness:.9,roughness:.06});
var glow =new THREE.MeshStandardMaterial({color:accent,emissive:accent,emissiveIntensity:2.6,roughness:.4});
var tub=extrude([[-3.15,1.12],[3.15,1.12],[3.2,1.78],[2.2,2.0],[-3.15,2.18]],2.5,steel); body.add(tub);
var bed=extrude([[-3.15,2.18],[-1.9,2.2],[-1.9,2.0],[-3.15,1.9]],2.5,skin); body.add(bed);
var gh=extrude([[2.32,1.98],[0.32,3.38],[-1.82,2.42],[-1.92,1.98]],2.18,glassy?glass:glass); body.add(gh);
var roof=extrude([[0.62,3.28],[0.3,3.42],[-1.78,2.46],[-1.62,2.34]],2.28,steel); body.add(roof);
var skirt=extrude([[-3.1,0.9],[3.1,0.9],[3.1,1.16],[-3.1,1.16]],2.62,dark); body.add(skirt);
var nose=new THREE.Mesh(new THREE.BoxGeometry(.22,.5,2.3),dark); nose.position.set(3.14,1.5,0); body.add(nose);
var hl=new THREE.Mesh(new THREE.BoxGeometry(.14,.18,2.16),glow); hl.position.set(3.22,1.72,0); body.add(hl);
var tl=new THREE.Mesh(new THREE.BoxGeometry(.12,.2,2.3),new THREE.MeshStandardMaterial({color:0xff2d1a,emissive:0xff2d1a,emissiveIntensity:2.4}));
tl.position.set(-3.2,1.85,0); body.add(tl);
var lb=new THREE.Mesh(new THREE.BoxGeometry(.3,.24,1.5),glow); lb.position.set(1.5,3.5,0); body.add(lb);
for(var s2=0;s2<3;s2++){var pod=new THREE.Mesh(new THREE.CylinderGeometry(.16,.16,.3,6),glow);
pod.rotation.x=Math.PI/2; pod.position.set(1.5,3.5,-.5+s2*.5); body.add(pod);}
[[-2.55,1.35],[-2.55,-1.35]].forEach(function(p){var e=new THREE.Mesh(new THREE.CylinderGeometry(.17,.21,1.5,7),steel);
e.position.set(p[0],2.6,p[1]); e.rotation.x=.12; body.add(e);});
[[-2.05,1.42],[2.05,1.42],[-2.05,-1.42],[2.05,-1.42]].forEach(function(p){
var fl=new THREE.Mesh(new THREE.BoxGeometry(2.1,.16,.55),dark);
fl.position.set(p[0],2.62,p[1]); fl.rotation.z=p[0]>0?-.28:.28; body.add(fl);
var sp=new THREE.Mesh(new THREE.CylinderGeometry(.15,.15,1.25,6),new THREE.MeshStandardMaterial({color:accent,metalness:.8,roughness:.3}));
sp.position.set(p[0],2.05,p[1]*.72); sp.rotation.z=.18*Math.sign(p[1]); body.add(sp);
});
var wheels=[],steers=[];
[[2.02,1.42],[2.02,-1.42],[-2.02,1.42],[-2.02,-1.42]].forEach(function(p,i){
var piv=new THREE.Group(); piv.position.set(p[0],1.62,p[1]); g.add(piv);
var w=new THREE.Group(); piv.add(w);
var t=new THREE.Mesh(new THREE.CylinderGeometry(1.62,1.55,.98,16,1),rub); t.geometry.rotateX(Math.PI/2); t.castShadow=true; w.add(t);
var sw=new THREE.Mesh(new THREE.CylinderGeometry(1.2,1.2,1.04,16,1),new THREE.MeshStandardMaterial({color:0x191d23,metalness:.3,roughness:.8,flatShading:true}));
sw.geometry.rotateX(Math.PI/2); w.add(sw);
var hb=new THREE.Mesh(new THREE.CylinderGeometry(.6,.6,1.1,6),steel); hb.geometry.rotateX(Math.PI/2); w.add(hb);
wheels.push(w); if(i<2)steers.push(piv);
});
return {g:g,body:body,wheels:wheels,steers:steers,steel:steel,skin:skin,glow:glow};
}
function labelSprite(num,name,accent){
var c=document.createElement('canvas'); c.width=256; c.height=72; var x=c.getContext('2d');
x.fillStyle='rgba(6,9,13,.82)'; x.fillRect(0,0,256,72);
x.fillStyle=accent; x.fillRect(0,0,10,72); x.fillRect(0,66,256,6);
x.font='700 40px Impact, Arial Black, sans-serif'; x.fillStyle='#fff'; x.fillText('#'+num,20,47);
x.font='700 19px Segoe UI, sans-serif'; x.fillStyle='#a9b8c6'; x.fillText(name,86,42);
var t=new THREE.CanvasTexture(c); t.colorSpace=THREE.SRGBColorSpace;
var sp=new THREE.Sprite(new THREE.SpriteMaterial({map:t,transparent:true,depthWrite:false}));
sp.scale.set(4.6,1.3,1); return sp;
}
var DEFS=[
{num:1, name:'STAINLESS', tint:0xc3ccd6, acc:0x3fe0ff, player:true},
{num:7, name:'GLASSHOUSE', tint:0x8f9aa6, acc:0xff3b30, lane:0.62, aggr:0.80, rate:0.20, skill:1.02},
{num:13,name:'RUSTLORD', tint:0x6d5b46, acc:0xff8a1f, lane:0.78, aggr:0.50, rate:0.175,skill:0.97},
{num:21,name:'SANDVIPER', tint:0x7f8c6a, acc:0x2fd07a, lane:0.90, aggr:0.22, rate:0.235,skill:1.06},
{num:44,name:'CHROME WIDOW',tint:0x5a6b8c,acc:0xd63ce0, lane:1.00, aggr:0.62, rate:0.19, skill:1.0}
];
var trucks=[], player;
(function(){
var sx=[-30,-30,-22,-22,-14], sz=[-8,8,-15,15,0];
for(var i=0;i<DEFS.length;i++){
var d=DEFS[i], vis=makeTruck(d.tint,d.acc,false);
var lab=labelSprite(d.num,d.name,'#'+d.acc.toString(16).padStart(6,'0'));
lab.position.set(0,5.2,0); vis.g.add(lab);
scene.add(vis.g);
var t={id:i,name:d.name,num:d.num,isPlayer:!!d.player,rad:2.55,mass:d.player?1.15:1.0+ (i*0.03),
pos:new THREE.Vector3(sx[i],0,sz[i]),vel:new THREE.Vector3(),yaw:0,yawVel:0,vy:0,grounded:true,
integ:100,state:'drive',wt:0,air:0,boost:1,susp:0,roll:0,pitch:0,vf:0,
group:vis.g,body:vis.body,wheels:vis.wheels,steers:vis.steers,label:lab,acc:d.acc,
ai:d.player?null:{lane:d.lane,aggr:d.aggr,rate:d.rate,skill:d.skill,phi:Math.PI+i*0.4,t:0,hunt:false,victim:null}};
t.pos.y=groundH(t.pos.x,t.pos.z);
trucks.push(t); if(t.isPlayer)player=t;
}
})();
/* ══════════════ OBSTACLES ══════════════ */
var obstacles=[];
function addObstacle(o){
if(o.kind==='barrier'){
var m=new THREE.Mesh(new THREE.BoxGeometry(o.hx*2,o.hy*2,o.hz*2),
new THREE.MeshStandardMaterial({color:0x9aa0a2,roughness:.95,metalness:.02,flatShading:true}));
m.castShadow=true;m.receiveShadow=true;
var cap=new THREE.Mesh(new THREE.BoxGeometry(o.hx*2+.1,.18,o.hz*2+.1),new THREE.MeshStandardMaterial({color:0xffc21a,roughness:.6}));
cap.position.y=o.hy;m.add(cap);
o.mesh=m;
} else if(o.kind==='sedan'){
var g=new THREE.Group();
var paint=new THREE.MeshStandardMaterial({color:o.color,metalness:.5,roughness:.55,flatShading:true});
var b=new THREE.Mesh(new THREE.BoxGeometry(4.1,1.0,1.9),paint); b.position.y=.75; b.castShadow=true; g.add(b);
var c=new THREE.Mesh(new THREE.BoxGeometry(2.1,.9,1.75),new THREE.MeshStandardMaterial({color:0x2b2f36,roughness:.8}));
c.position.set(-.2,1.6,0); c.castShadow=true; g.add(c);
var w=new THREE.MeshStandardMaterial({color:0x0c0e11,roughness:.9});
[[1.3,.35,.95],[1.3,.35,-.95],[-1.3,.35,.95],[-1.3,.35,-.95]].forEach(function(p){
var t=new THREE.Mesh(new THREE.CylinderGeometry(.42,.42,.3,8),w); t.rotation.x=Math.PI/2; t.position.set(p[0],p[1],p[2]); g.add(t);});
o.mesh=g;
} else if(o.kind==='barrel'){
var bg=new THREE.Group();
var cyl=new THREE.Mesh(new THREE.CylinderGeometry(.85,.85,2.1,12),
new THREE.MeshStandardMaterial({color:0xb8341f,metalness:.6,roughness:.5,flatShading:true}));
cyl.position.y=1.05;cyl.castShadow=true;bg.add(cyl);
[0.55,1.55].forEach(function(y){var r=new THREE.Mesh(new THREE.CylinderGeometry(.9,.9,.22,12),
new THREE.MeshStandardMaterial({color:0xffc21a,metalness:.4,roughness:.6}));r.position.y=y;bg.add(r);});
var fl=new THREE.Mesh(new THREE.CylinderGeometry(.3,.3,2.2,8),new THREE.MeshStandardMaterial({color:0x2fd07a,emissive:0x2fd07a,emissiveIntensity:1.2}));
fl.position.y=1.06;fl.rotation.z=Math.PI/2;fl.rotation.x=Math.PI/2;bg.add(fl);
o.mesh=bg; o.light=fl;
} else if(o.kind==='tires'){
var tg=new THREE.Group();
var tr=new THREE.MeshStandardMaterial({color:0x111417,roughness:.95,metalness:.02});
for(var i=0;i<3;i++){var tm=new THREE.Mesh(new THREE.CylinderGeometry(1.15,1.15,.55,12),tr);
tm.position.y=.3+i*.56;tm.rotation.y=i*.4;tm.castShadow=true;tg.add(tm);}
o.mesh=tg;
} else if(o.kind==='ramp'){
var shape=new THREE.Shape(); shape.moveTo(-2.6,-1.1); shape.lineTo(2.6,-1.1); shape.lineTo(2.6,1.1); shape.lineTo(-2.6,1.1); shape.closePath();
var rg=new THREE.ExtrudeGeometry(shape,{depth:2.0,bevelEnabled:false}); rg.translate(0,0,-1.0);
var ramp=new THREE.Group();
var surf=new THREE.Mesh(rg,new THREE.MeshStandardMaterial({color:0x3c4048,metalness:.7,roughness:.5}));
surf.rotation.z=.34; surf.position.set(0,1.55,0); surf.castShadow=true; surf.receiveShadow=true; ramp.add(surf);
for(var k=0;k<4;k++){var post=new THREE.Mesh(new THREE.BoxGeometry(.3,1.6+k*.42,.3),
new THREE.MeshStandardMaterial({color:k%2?0xffc21a:0x14181e,roughness:.8}));
post.position.set(-1.9+k*1.3,.8+k*.21,0); ramp.add(post);}
var edge=new THREE.Mesh(new THREE.BoxGeometry(.2,.2,2.1),new THREE.MeshStandardMaterial({color:0xff8a1f,emissive:0xff8a1f,emissiveIntensity:1.6}));
edge.position.set(2.35,2.62,0); edge.rotation.z=.34; ramp.add(edge);
o.mesh=ramp;
}
o.mesh.position.set(o.x,0,o.z); o.mesh.rotation.y=o.rot||0;
o.y=groundH(o.x,o.z); o.mesh.position.y=o.y;
o.vx=0;o.vz=0;o.vy=0;o.yawVel=0;o.alive=true;o.fuse=0;
scene.add(o.mesh); obstacles.push(o); return o;
}
(function layout(){
var i,a,x,z;
function clearOfSpawn(x,z){return Math.abs(x+26)<16&&Math.abs(z)<20?false:true;}
// concrete barriers: three chicane clusters + a center island
var clusters=[[-4,-19,0],[16,4,1.1],[-16,16,-.7],[30,-14,.4]];
for(i=0;i<clusters.length;i++){
for(var b=0;b<4;b++){
x=clusters[i][0]+Math.cos(clusters[i][2])*b*4.4+ (b%2?1.2:-1.2);
z=clusters[i][1]+Math.sin(clusters[i][2])*b*4.4;
if(!clearOfSpawn(x,z))continue;
addObstacle({kind:'barrier',x:x,z:z,rot:clusters[i][2]+rnd(-.2,.2),hx:2.2,hy:.95,hz:.75,mass:1e9,shape:'box',
hxw:2.2,hzw:.75,score:0});
}
}
// ramps
[[-34,2,0.5],[8,-26,1.9],[34,10,-1.1],[-8,26,-2.6],[24,-2,2.9]].forEach(function(r){
addObstacle({kind:'ramp',x:r[0],z:r[1],rot:r[2],shape:'box',hx:2.7,hz:1.2,hy:2.4,mass:1e9});});
// wrecked sedans
var cols=[0x7a2f2f,0x2f4f7a,0x6a6f2f,0x777c82,0x5a2f6f,0x2f6f5f];
for(i=0;i<12;i++){
a=sr()*TAU; var f=.25+sr()*.68; x=ARENA.rx*f*Math.cos(a); z=ARENA.rz*f*Math.sin(a);
if(!clearOfSpawn(x,z)){i--;continue;}
addObstacle({kind:'sedan',x:x,z:z,rot:sr()*TAU,shape:'circ',rad:2.0,mass:.42,color:cols[i%cols.length]});
}
// fuel drums
for(i=0;i<11;i++){
a=sr()*TAU; var f2=.3+sr()*.6; x=ARENA.rx*f2*Math.cos(a); z=ARENA.rz*f2*Math.sin(a);
if(!clearOfSpawn(x,z)){i--;continue;}
addObstacle({kind:'barrel',x:x,z:z,rot:sr()*TAU,shape:'circ',rad:.95,mass:.25});
}
// tire stacks
for(i=0;i<8;i++){
a=sr()*TAU; x=ARENA.rx*(.4+sr()*.5)*Math.cos(a); z=ARENA.rz*(.4+sr()*.5)*Math.sin(a);
if(!clearOfSpawn(x,z)){i--;continue;}
addObstacle({kind:'tires',x:x,z:z,rot:sr()*TAU,shape:'circ',rad:1.35,mass:1e9});
}
// decorative debris field (instanced, no physics)
var dg=new THREE.IcosahedronGeometry(.62,0);
var dm=new THREE.MeshStandardMaterial({color:0x6f6255,roughness:.98,flatShading:true});
var di=new THREE.InstancedMesh(dg,dm,260); di.frustumCulled=false;
var d3=new THREE.Object3D();
for(i=0;i<260;i++){ a=sr()*TAU; var rf=.15+sr()*.92;
x=ARENA.rx*rf*Math.cos(a); z=ARENA.rz*rf*Math.sin(a);
d3.position.set(x,groundH(x,z)+.1,z); d3.rotation.set(sr()*TAU,sr()*TAU,sr()*TAU);
d3.scale.setScalar(.35+sr()*1.1); d3.updateMatrix(); di.setMatrixAt(i,d3.matrix);}
di.castShadow=false; di.receiveShadow=true; scene.add(di);
// chained tire wall at the arena lip
var tw=new THREE.InstancedMesh(new THREE.CylinderGeometry(1.05,1.05,.7,10),
new THREE.MeshStandardMaterial({color:0x0d1013,roughness:.98,flatShading:true}),150);
tw.frustumCulled=false;
for(i=0;i<150;i++){a=i/150*TAU;
d3.position.set(ARENA.rx*1.005*Math.cos(a),.35+groundH(ARENA.rx*1.005*Math.cos(a),ARENA.rz*1.005*Math.sin(a))*.5,ARENA.rz*1.005*Math.sin(a));
d3.rotation.set(Math.PI/2,a+Math.PI/2,0); d3.scale.setScalar(1); d3.updateMatrix(); tw.setMatrixAt(i,d3.matrix);}
tw.castShadow=true; scene.add(tw);
})();
/* ══════════════ AUDIO (Web Audio only) ══════════════ */
var Snd={ok:false,muted:false,
init:function(){ if(this.ok)return; try{
var AC=window.AudioContext||window.webkitAudioContext; if(!AC)return;
var c=new AC(); this.c=c; this.mst=c.createGain(); this.mst.gain.value=.55; this.mst.connect(c.destination);
var nb=c.createBuffer(1,c.sampleRate*2,c.sampleRate),nd=nb.getChannelData(0);
for(var i=0;i<nd.length;i++)nd[i]=(Math.random()*2-1);
this.noise=nb;
// engine
this.eG=c.createGain(); this.eG.gain.value=0; this.eF=c.createBiquadFilter(); this.eF.type='lowpass'; this.eF.frequency.value=520;
this.o1=c.createOscillator(); this.o1.type='sawtooth'; this.o1.frequency.value=42;
this.o2=c.createOscillator(); this.o2.type='square'; this.o2.frequency.value=21;
this.o1.connect(this.eF); this.o2.connect(this.eF); this.eF.connect(this.eG); this.eG.connect(this.mst);
this.o1.start(); this.o2.start();
// crowd bed
this.cr=c.createBufferSource(); this.cr.buffer=nb; this.cr.loop=true;
this.crF=c.createBiquadFilter(); this.crF.type='bandpass'; this.crF.frequency.value=760; this.crF.Q.value=.6;
this.crG=c.createGain(); this.crG.gain.value=.012;
this.cr.connect(this.crF); this.crF.connect(this.crG); this.crG.connect(this.mst); this.cr.start();
this.ok=true;
}catch(e){} },
engine:function(sp,thr){ if(!this.ok||this.muted)return; var t=this.c.currentTime;
this.eG.gain.setTargetAtTime(clamp(.02+sp*.0032+thr*.03,0,.13),t,.08);
this.o1.frequency.setTargetAtTime(38+sp*3.4,t,.09); this.o2.frequency.setTargetAtTime(19+sp*1.7,t,.09);
this.eF.frequency.setTargetAtTime(360+sp*34,t,.1); },
hit:function(v){ if(!this.ok||this.muted)return; var c=this.c,t=c.currentTime;
var s=c.createBufferSource(); s.buffer=this.noise;
var f=c.createBiquadFilter(); f.type='bandpass'; f.frequency.value=190+v*260; f.Q.value=1.1;
var g=c.createGain(); g.gain.setValueAtTime(Math.min(.5,.08+v*.42),t); g.gain.exponentialRampToValueAtTime(.0008,t+.28+v*.2);
s.connect(f); f.connect(g); g.connect(this.mst); s.start(t); s.stop(t+.5);
var o=c.createOscillator(); o.type='sine'; o.frequency.setValueAtTime(150,t); o.frequency.exponentialRampToValueAtTime(38,t+.3);
var og=c.createGain(); og.gain.setValueAtTime(.32*v+.05,t); og.gain.exponentialRampToValueAtTime(.001,t+.34);
o.connect(og); og.connect(this.mst); o.start(t); o.stop(t+.4); },
boom:function(){ if(!this.ok||this.muted)return; var c=this.c,t=c.currentTime;
var s=c.createBufferSource(); s.buffer=this.noise; s.playbackRate.value=.6;
var f=c.createBiquadFilter(); f.type='lowpass'; f.frequency.setValueAtTime(2400,t); f.frequency.exponentialRampToValueAtTime(180,t+.9);
var g=c.createGain(); g.gain.setValueAtTime(.6,t); g.gain.exponentialRampToValueAtTime(.001,t+1.1);
s.connect(f); f.connect(g); g.connect(this.mst); s.start(t); s.stop(t+1.2); },
cheer:function(v){ if(!this.ok||this.muted)return; var c=this.c,t=c.currentTime;
var s=c.createBufferSource(); s.buffer=this.noise; s.playbackRate.value=1.4;
var f=c.createBiquadFilter(); f.type='bandpass'; f.frequency.value=1150; f.Q.value=.8;
var g=c.createGain(); g.gain.setValueAtTime(.0001,t); g.gain.exponentialRampToValueAtTime(.16*v,t+.15);
g.gain.exponentialRampToValueAtTime(.0001,t+1.8);
s.connect(f); f.connect(g); g.connect(this.mst); s.start(t); s.stop(t+2); },
blip:function(fr){ if(!this.ok||this.muted)return; var c=this.c,t=c.currentTime;
var o=c.createOscillator(); o.type='triangle'; o.frequency.setValueAtTime(fr||660,t);
o.frequency.exponentialRampToValueAtTime((fr||660)*1.9,t+.09);
var g=c.createGain(); g.gain.setValueAtTime(.12,t); g.gain.exponentialRampToValueAtTime(.001,t+.16);
o.connect(g); g.connect(this.mst); o.start(t); o.stop(t+.2); },
shutter:function(){ if(!this.ok||this.muted)return; var c=this.c,t=c.currentTime;
var s=c.createBufferSource(); s.buffer=this.noise;
var f=c.createBiquadFilter(); f.type='highpass'; f.frequency.value=2600;
var g=c.createGain(); g.gain.setValueAtTime(.22,t); g.gain.exponentialRampToValueAtTime(.001,t+.07);
s.connect(f); f.connect(g); g.connect(this.mst); s.start(t); s.stop(t+.1); }
};
/* ══════════════ GAME STATE ══════════════ */
var G={running:false,over:false,photo:false,simTime:0,time:90,score:0,disp:0,
combo:0,comboT:0,bestChain:1,hype:0,shake:0,hits:0,wrecks:0,bestAir:0,frames:0};
function fmtClock(t){t=Math.max(0,t);var m=Math.floor(t/60),s=Math.floor(t%60);return m+':'+(s<10?'0':'')+s;}
function addScore(v,label,cls){
G.score+=Math.round(v);
var e=document.getElementById('score'); e.classList.remove('bump'); void e.offsetWidth; e.classList.add('bump');
if(label)toast(label,cls);
}
var toastBox=document.getElementById('toasts');
function toast(main,cls,sub){
var d=document.createElement('div'); d.className='dsp toast '+(cls||'');
d.innerHTML=main+(sub?'<small class="mono">'+sub+'</small>':'');
toastBox.appendChild(d);
while(toastBox.children.length>4)toastBox.removeChild(toastBox.firstChild);
setTimeout(function(){d.classList.add('out');},900);
setTimeout(function(){if(d.parentNode)d.parentNode.removeChild(d);},1400);
}
function hype(v){G.hype=Math.min(1,G.hype+v);}
/* ══════════════ INPUT ══════════════ */
var K={},hopQ=false;
var HAND={ArrowUp:1,ArrowDown:1,ArrowLeft:1,ArrowRight:1,Space:1,KeyW:1,KeyA:1,KeyS:1,KeyD:1,KeyP:1,KeyR:1,KeyM:1,Enter:1};
addEventListener('keydown',function(e){
if(HAND[e.code])e.preventDefault();
Snd.init();
if(!e.repeat){
if(e.code==='KeyP'){togglePhoto();}
if(e.code==='KeyM'){Snd.muted=!Snd.muted;toast(Snd.muted?'MUTED':'SOUND ON','cy');}
if(e.code==='Space')hopQ=true;
if(e.code==='Enter'){ if(!G.running&&!G.over)startHeat(); else if(G.over)restart(); }
if(e.code==='KeyR'&&G.over)restart();
}
K[e.code]=true;
},{passive:false});
addEventListener('keyup',function(e){K[e.code]=false;});
addEventListener('blur',function(){K={};});
function readInput(){
var up=K.ArrowUp||K.KeyW,dn=K.ArrowDown||K.KeyS,lf=K.ArrowLeft||K.KeyA,rt=K.ArrowRight||K.KeyD;
return {throttle:up?1:0,brake:dn?1:0,steer:(rt?1:0)-(lf?1:0),hop:hopQ,boost:(K.ShiftLeft||K.ShiftRight)};
}
/* ══════════════ COLLISION HELPERS ══════════════ */
var tmpV=new THREE.Vector3(),tmpU=new THREE.Vector3(),tmpF=new THREE.Vector3(),
tmpR=new THREE.Vector3(),tmpZ=new THREE.Vector3(),tmpM=new THREE.Matrix4(),tmpQ=new THREE.Quaternion();
function circleVsBox(cx,cz,cr,o){
var c=Math.cos(o.rot),s=Math.sin(o.rot),dx=cx-o.x,dz=cz-o.z;
var lx=dx*c-dz*s, lz=dx*s+dz*c;
var qx=clamp(lx,-o.hx,o.hx), qz=clamp(lz,-o.hz,o.hz);
var nx=lx-qx, nz=lz-qz, d2=nx*nx+nz*nz, lnx,lnz,pen;
if(d2>cr*cr)return null;
var d=Math.sqrt(d2);
if(d>1e-5){ lnx=nx/d; lnz=nz/d; pen=cr-d; }
else{ var px=o.hx-Math.abs(lx), pz=o.hz-Math.abs(lz);
if(px<pz){lnx=lx<0?-1:1;lnz=0;pen=cr+px;} else {lnx=0;lnz=lz<0?-1:1;pen=cr+pz;} }
return {nx:lnx*c+lnz*s, nz:-lnx*s+lnz*c, pen:pen};
}
function pushObstacle(o,ix,iz,fy){
o.x+=ix; o.z+=iz; o.vx=(o.vx||0)+ix*fy; o.vz=(o.vz||0)+iz*fy;
o.yawVel=(o.yawVel||0)+rnd(-2,2)*fy*.4;
}
function landTruck(t,impact){
if(impact>4){
var x=t.pos.x,z=t.pos.z;
burstDust(x,groundH(x,z)+.3,z,Math.min(26,6+impact*1.6),3.4);
G.shake=Math.max(G.shake,Math.min(1.1,impact*.05)); Snd.hit(Math.min(.7,impact*.05));
if(t.isPlayer&&t.air>.55){var bonus=Math.round(t.air*140);
addScore(bonus,'BIG AIR <b style="color:#fff">+'+bonus+'</b>','gold',0);
t.air=0; hype(.2);}
}
t.air=0;
}
function wreck(t,byPlayer){
if(t.state==='wreck')return;
t.state='wreck'; t.wt=4.2; t.integ=100; t.yawVel=rnd(-7,7); t.vel.y=Math.max(t.vel.y,7);
t.vel.x*=1.25; t.vel.z*=1.25;
burstFlame(t.pos.x,t.pos.y+1.8,t.pos.z,26); burstSpark(t.pos.x,t.pos.y+1.6,t.pos.z,34,9);
burstDust(t.pos.x,t.pos.y+1,t.pos.z,24,5);
G.shake=Math.max(G.shake,1.25); hype(.6); Snd.boom(); Snd.cheer(1);
if(byPlayer){G.wrecks++; addScore(1000,'<span class="dsp">TOTALLED '+t.name+'</span> +1000','red');}
else if(t.isPlayer){toast('YOU ARE WRECKED','red','BROOM BROOM IN 4s'); G.combo=0;}
}
function respawn(t){
var a=Math.atan2(t.pos.z/ARENA.rz,t.pos.x/ARENA.rx),p=ellipsePoint(.7,a,tmpV);
t.pos.set(p.x,0,p.z); t.pos.y=groundH(p.x,p.z); t.vel.set(0,0,0); t.vy=0; t.yaw=a+Math.PI/2;
t.yawVel=0; t.state='drive'; t.integ=100; t.group.rotation.set(0,0,0);
burstDust(t.pos.x,t.pos.y+1,t.pos.z,18,4);
}
/* ══════════════ SIM ══════════════ */
var POWER=27,BRAKE=34,TURN=2.55,MAXV=25,GRAV=27,HSTEP=1/120;
function stepTruck(t,dt){
var f=tmpF.set(Math.cos(t.yaw),0,Math.sin(t.yaw)), vf=t.vel.x*f.x+t.vel.z*f.z,
rx=-f.z, rz=f.x, vr=t.vel.x*rx+t.vel.z*rz, c;
if(t.state==='wreck'){
t.wt-=dt;
vf*=Math.pow(.35,dt); vr*=Math.pow(.35,dt);
t.vel.x=vf*f.x+vr*rx; t.vel.z=vf*f.z+vr*rz;
t.pos.x+=t.vel.x*dt; t.pos.z+=t.vel.z*dt;
t.vy-=GRAV*dt; t.pos.y+=t.vy*dt;
var gh=groundH(t.pos.x,t.pos.z);
if(t.pos.y<=gh){t.pos.y=gh;t.vy=Math.abs(t.vy)*.25;if(t.vy<1)t.vy=0;}
t.yaw+=t.yawVel*dt; t.yawVel*=Math.pow(.5,dt);
if(Math.random()<dt*22)burstDust(t.pos.x,t.pos.y+.6,t.pos.z,2,2.2);
if(Math.random()<dt*14)fx.flame.spawn(t.pos.x+rnd(-1,1),t.pos.y+1.4,t.pos.z+rnd(-1,1),rnd(-1,1),rnd(1.5,3.5),rnd(-1,1),.9,.35,.1,.6);
if(t.wt<=0)respawn(t);
clampArena(t); return;
}
c=t.isPlayer?readInput():aiControl(t,dt);
var boosting=c.boost&&t.boost>.05&&t.isPlayer?true:(c.boost&&t.boost>.25);
if(boosting){t.boost=Math.max(0,t.boost-dt*.42);} else {t.boost=Math.min(1,t.boost+dt*.11);}
var mul=1+(boosting?.75:0);
vf+=c.throttle*POWER*mul*dt;
if(c.brake){ if(vf>0.4)vf-=BRAKE*dt; else vf=Math.max(-9,vf-POWER*.55*dt); }
vf*=Math.pow(.62,dt); vr*=Math.pow(t.grounded?(boosting?.28:.09):.6,dt);
t.vel.x=vf*f.x+vr*rx; t.vel.z=vf*f.z+vr*rz;
var grip=clamp(Math.abs(vf)/13,0,1)*(1-clamp(Math.abs(vf)/46,0,.45));
t.yaw+=c.steer*TURN*grip*dt*(vf<-0.5?-1:1)+t.yawVel*dt;
t.yawVel*=Math.pow(.12,dt);
var sp=Math.hypot(t.vel.x,t.vel.z),cap=MAXV*(t.ai?t.ai.skill:1);
if(sp>cap){t.vel.x*=cap/sp;t.vel.z*=cap/sp;}
t.pos.x+=t.vel.x*dt; t.pos.z+=t.vel.z*dt;
if(c.hop&&t.grounded){t.vy=10.5;t.grounded=false;burstDust(t.pos.x,t.pos.y+.2,t.pos.z,14,4);Snd.hit(.2);}
t.vy-=GRAV*dt; t.pos.y+=t.vy*dt;
var g2=groundH(t.pos.x,t.pos.z);
if(t.pos.y<=g2){ var imp=-t.vy; t.pos.y=g2; t.vy=0; if(!t.grounded){t.grounded=true;landTruck(t,imp);} }
else { t.grounded=false; t.air+=dt; if(t.air>G.bestAir&&t.isPlayer)G.bestAir=t.air; }
if(t.grounded&&Math.abs(vr)>7&&Math.random()<dt*26)
burstDust(t.pos.x-f.x*1.8,t.pos.y+.3,t.pos.z-f.z*1.8,3,3.2);
if(boosting&&Math.random()<dt*90)
fx.flame.spawn(t.pos.x-f.x*3.2,t.pos.y+1.5,t.pos.z-f.z*3.2,-f.x*rnd(6,12),rnd(-.5,1.2),-f.z*rnd(6,12),.35,.75,1,.28);
t.vf=vf;
clampArena(t);
collideObstacles(t,dt);
}
function clampArena(t){
var k=(t.pos.x/BRX)*(t.pos.x/BRX)+(t.pos.z/BRZ)*(t.pos.z/BRZ);
if(k>1){
var s=Math.sqrt(k); t.pos.x/=s; t.pos.z/=s;
var nx=(t.pos.x/BRX)/Math.hypot(t.pos.x/(BRX*BRX),t.pos.z/(BRZ*BRZ));
var nz=(t.pos.z/BRZ)/Math.hypot(t.pos.x/(BRX*BRX),t.pos.z/(BRZ*BRZ));
var vn=t.vel.x*nx+t.vel.z*nz;
if(vn<0){ t.vel.x-=1.55*vn*nx; t.vel.z-=1.55*vn*nz;
if(-vn>7){burstSpark(t.pos.x,t.pos.y+1.4,t.pos.z,14,5);G.shake=Math.max(G.shake,.5);Snd.hit(.35);
if(t.isPlayer)G.combo=0; t.integ-=-vn*.35;} }
}
}
function collideObstacles(t,dt){
for(var i=0;i<obstacles.length;i++){
var o=obstacles[i]; if(!o.alive)continue;
var hit=null,dx=t.pos.x-o.x,dz=t.pos.z-o.z,d=0;
if(o.shape==='box'){ hit=circleVsBox(t.pos.x,t.pos.z,t.rad*.86,o); if(hit){dx=hit.nz;-0;} }
else { d=Math.hypot(dx,dz); if(d<t.rad+o.rad){ var pen=t.rad+o.rad-d, nx=d>1e-4?dx/d:1, nz=d>1e-4?dz/d:0;
hit={nx:nx,nz:nz,pen:pen}; } }
if(!hit)continue;
var vn=-(t.vel.x*hit.nx+t.vel.z*hit.nz);
if(o.mass>1e8){ // immovable
t.pos.x+=hit.nx*hit.pen; t.pos.z+=hit.nz*hit.pen;
if(vn>0){ t.vel.x+=(1+.28)*vn*hit.nx; t.vel.z+=(1+.28)*vn*hit.nz; }
if(o.kind==='ramp'){
var rf=Math.cos(o.rot),rfz=Math.sin(o.rot),dotf=rf*Math.cos(t.yaw)+rfz*Math.sin(t.yaw);
var spd=Math.hypot(t.vel.x,t.vel.z);
if(dotf>.5&&spd>7&&t.grounded){ t.vy=8.5+spd*.34; t.grounded=false;
burstDust(t.pos.x,t.pos.y+.4,t.pos.z,16,5); hype(.25); Snd.hit(.3);
if(t.isPlayer){addScore(60,'LAUNCH','cy');} }
} else if(vn>6){
burstSpark(t.pos.x+hit.nx*t.rad*.6,t.pos.y+1.3,t.pos.z+hit.nz*t.rad*.6,12,5);
t.integ-=vn*.22; G.shake=Math.max(G.shake,Math.min(.6,vn*.04)); Snd.hit(.28);
if(t.isPlayer&&vn>10)G.combo=0;
}
} else { // shovable
var inv=1/t.mass+1/o.mass, j=(1+.4)*Math.max(0,vn)/inv;
t.vel.x-=j*hit.nx/t.mass; t.vel.z-=j*hit.nz/t.mass;
o.vx=(o.vx||0)+j*hit.nx/o.mass*-1; o.vz=(o.vz||0)+j*hit.nz/o.mass*-1;
o.x-=hit.nx*hit.pen*(o.mass/(o.mass+t.mass)); o.z-=hit.nz*hit.pen*(o.mass/(o.mass+t.mass));
t.pos.x+=hit.nx*hit.pen*(t.mass/(o.mass+t.mass))*0; t.pos.z+=hit.nz*hit.pen*(t.mass/(o.mass+t.mass));
o.yawVel+=rnd(-3,3)*(vn*.12);
if(vn>5.5){
burstSpark(o.x,1.4,o.z,10,4); burstDust(o.x,1,o.z,8,3); Snd.hit(Math.min(.5,vn*.04));
if(o.kind==='barrel'){ o.fuse=Math.min(o.fuse||9,0.18); }
else if(o.kind==='sedan'&&t.isPlayer&&vn>8){ addScore(75,'SCRAPPED +75','gold'); hype(.12); }
}
}
}
}
function truckVsTruck(a,b){
var dx=b.pos.x-a.pos.x,dz=b.pos.z-a.pos.z,rr=a.rad+b.rad,d2=dx*dx+dz*dz;
if(d2>rr*rr||d2<1e-6)return;
var d=Math.sqrt(d2),nx=dx/d,nz=dz/d,pen=rr-d;
var ia=1/a.mass,ib=1/b.mass,s=ia+ib;
a.pos.x-=nx*pen*(ia/s); a.pos.z-=nz*pen*(ia/s);
b.pos.x+=nx*pen*(ib/s); b.pos.z+=nz*pen*(ib/s);
var rvx=b.vel.x-a.vel.x,rvz=b.vel.z-a.vel.z,vn=rvx*nx+rvz*nz;
if(vn>0)return;
var j=-(1+.45)*vn/s;
a.vel.x-=j*nx*ia; a.vel.z-=j*nz*ia; b.vel.x+=j*nx*ib; b.vel.z+=j*nz*ib;
var tx=-nz,tz=nx,vt=rvx*tx+rvz*tz,jt=clamp(-vt/s,-Math.abs(j)*.55,Math.abs(j)*.55);
a.vel.x-=jt*tx*ia; a.vel.z-=jt*tz*ia; b.vel.x+=jt*tx*ib; b.vel.z+=jt*tz*ib;
a.yawVel-=jt*.34*ia+(rnd(-.4,.4))*Math.abs(vn)*.05; b.yawVel+=jt*.34*ib+(rnd(-.4,.4))*Math.abs(vn)*.05;
var cv=Math.abs(vn)*Math.min(a.mass,b.mass);
if(cv<1.6)return;
var cx=a.pos.x+nx*a.rad,cz=a.pos.z+nz*a.rad;
burstSpark(cx,a.pos.y+1.6,cz,Math.min(26,6+cv*1.6),5+cv*.5);
burstDust(cx,a.pos.y+1,cz,Math.min(14,3+cv),3);
G.shake=Math.max(G.shake,Math.min(1.2,cv*.075)); hype(Math.min(.5,cv*.035)); Snd.hit(Math.min(.85,cv*.09));
var aFast=a.vel.x*nx+a.vel.z*nz, bFast=-(b.vel.x*nx+b.vel.z*nz);
var attacker=null,victim=null;
if(aFast>bFast+.5)attacker=a,victim=b; else if(bFast>aFast+.5)attacker=b,victim=a;
var dmg=cv*3.4;
victim.integ-=dmg*1.15; victim.yawVel+=rnd(-1,1)*(2.4+cv*.4);
victim.vel.y=Math.max(victim.vel.y,Math.min(6,cv*.42));
if(attacker&&attacker.isPlayer&&victim!==player){
G.hits++;
if(G.comboT>0)G.combo++; else G.combo=1;
G.comboT=3.0; G.bestChain=Math.max(G.bestChain,G.combo);
var pts=Math.round((140+cv*26)*G.combo);
addScore(pts,'RAM +'+pts+(G.combo>1?' <span class="dsp" style="color:#3fe0ff">×'+G.combo+'</span>':''),'gold',0);
if(G.combo>=3)Snd.blip(880);
} else if(attacker&&attacker===player&&victim&&!victim.isPlayer){
victim.integ-=dmg;
}
if(victim.isPlayer){ G.combo=0; }
if(attacker&&!attacker.isPlayer&&victim&&victim.isPlayer){
toast('CLANG! '+attacker.name+' GOT YOU','red');
}
for(var i=0;i<2;i++){var t2=i?b:a; if(t2.integ<=0)wreck(t2,(i?b:a)!==t2&&((i?a:b).isPlayer));}
}
function aiControl(t,dt){
var a=t.ai,dx,dz;
a.t-=dt;
if(a.t<=0){ a.t=rnd(2.4,5.4);
a.hunt=Math.random()<a.aggr;
if(a.hunt){ a.victim=(Math.random()<.62)?player:trucks[1+Math.floor(sr()*4)]; if(a.victim===t)a.victim=player; }
a.lane=rnd(a.lane*.94,Math.min(1.02,a.lane*1.06));
}
if(a.hunt&&a.victim&&a.victim.state==='drive'){
dx=a.victim.pos.x+a.victim.vel.x*.4-t.pos.x; dz=a.victim.pos.z+a.victim.vel.z*.4-t.pos.z;
} else {
a.phi+=dt*a.rate*6;
var p=ellipsePoint(a.lane,a.phi,tmpV); dx=p.x-t.pos.x; dz=p.z-t.pos.z;
}
var dl=Math.hypot(dx,dz)||1; dx/=dl; dz/=dl;
for(var i=0;i<trucks.length;i++){ var o=trucks[i]; if(o===t)continue;
var ox=t.pos.x-o.pos.x,oz=t.pos.z-o.pos.z,d=Math.hypot(ox,oz);
if(d<11&&d>.01){var w=(1-d/11)*2.3; dx+=ox/d*w; dz+=oz/d*w;} }
var rr=(t.pos.x/BRX)*(t.pos.x/BRX)+(t.pos.z/BRZ)*(t.pos.z/BRZ);
if(rr>.72){var w2=(rr-.72)*7; dx+=-t.pos.x/BRX*w2; dz+=-t.pos.z/BRZ*w2;}
dl=Math.hypot(dx,dz)||1;
var want=Math.atan2(dz/dl,dx/dl), err=angDiff(t.yaw,want);
var distP=Math.hypot(t.pos.x-player.pos.x,t.pos.z-player.pos.z);
return {throttle:Math.abs(err)<1.3?1:.35, brake:Math.abs(err)>2.5?1:0,
steer:clamp(err*2.0,-1,1), hop:false, boost:(distP>34&&Math.abs(err)<.5)||(a.hunt&&Math.abs(err)<.25&&t.boost>.4)};
}
function updateObstacles(dt){
for(var i=0;i<obstacles.length;i++){
var o=obstacles[i];
if(!o.alive){ if(G.simTime>o.respawn){o.alive=true;o.mesh.visible=true;o.fuse=0;} continue; }
if(o.fuse>0){ o.fuse-=dt; if(o.light)o.light.material.emissiveIntensity=1+Math.sin(G.simTime*40)*6;
if(o.fuse<=0){ explodeBarrel(o); continue; } }
if(o.mass>1e8)continue;
var fr=Math.pow(.12,dt);
o.x+=o.vx*dt; o.z+=o.vz*dt; o.vx*=fr; o.vz*=fr;
o.rot=(o.rot||0)+o.yawVel*dt; o.yawVel*=Math.pow(.1,dt);
var k=(o.x/BRX)*(o.x/BRX)+(o.z/BRZ)*(o.z/BRZ);
if(k>1){var s=Math.sqrt(k);o.x/=s;o.z/=s;o.vx*=-.3;o.vz*=-.3;}
o.y=groundH(o.x,o.z);
o.mesh.position.set(o.x,o.y,o.z); o.mesh.rotation.y=o.rot;
if(o.kind==='sedan'&&Math.hypot(o.vx,o.vz)>6&&Math.random()<dt*18)burstDust(o.x,o.y+.6,o.z,2,2);
}
}
function explodeBarrel(o){
o.alive=false; o.mesh.visible=false; o.respawn=G.simTime+rnd(9,15); o.fuse=0;
if(o.light)o.light.material.emissiveIntensity=1.2;
burstFlame(o.x,o.y+1.2,o.z,30); burstSpark(o.x,o.y+1.4,o.z,40,11); burstDust(o.x,o.y+1,o.z,18,6);
G.shake=Math.max(G.shake,1.0); hype(.45); Snd.boom();
for(var i=0;i<trucks.length;i++){ var t=trucks[i],dx=t.pos.x-o.x,dz=t.pos.z-o.z,d=Math.hypot(dx,dz);
if(d<11){ var f=(1-d/11)*26; t.vel.x+=dx/d*f; t.vel.z+=dz/d*f;
t.vy=Math.max(t.vy,(1-d/11)*10); t.grounded=false; t.yawVel+=rnd(-4,4);
t.integ-=(1-d/11)*20; if(t.integ<=0)wreck(t,false); } }
for(var j=0;j<obstacles.length;j++){ var b=obstacles[j];
if(b.kind==='barrel'&&b.alive){ var dd=Math.hypot(b.x-o.x,b.z-o.z); if(dd<10)b.fuse=Math.min(b.fuse||9,.2+sr()*.25); }
if(b.mass<1e8&&b.alive){ var d2=Math.hypot(b.x-o.x,b.z-o.z); if(d2<10){b.vx+=(b.x-o.x)/d2*16;b.vz+=(b.z-o.z)/d2*16;b.yawVel+=rnd(-5,5);} } }
if(player.pos.distanceTo(o.pos?player.pos:new THREE.Vector3(o.x,o.y,o.z))<400){}
var pd=Math.hypot(player.pos.x-o.x,player.pos.z-o.z);
if(pd<13){ addScore(150,'DRUM POP +150','cy'); }
}
function simStep(dt){
G.simTime+=dt;
G.time-=dt;
if(G.time<=0){G.time=0;endHeat();return;}
G.hype=Math.max(0,G.hype-dt*.35);
G.shake=Math.max(0,G.shake-dt*3.2);
if(G.comboT>0){G.comboT-=dt; if(G.comboT<=0)G.combo=0;}
for(var i=0;i<trucks.length;i++)stepTruck(trucks[i],dt);
for(var a=0;a<trucks.length;a++)for(var b=a+1;b<trucks.length;b++)truckVsTruck(trucks[a],trucks[b]);
updateObstacles(dt);
hopQ=false;
}
/* ══════════════ VISUALS ══════════════ */
function updateTruckVisual(t,dt){
var g=t.group;
g.position.set(t.pos.x,t.pos.y,t.pos.z);
terrainNormal(t.pos.x,t.pos.z,tmpU);
if(t.state==='wreck'){ tmpU.y=1; tmpU.normalize(); }
tmpF.set(Math.cos(t.yaw),0,Math.sin(t.yaw));
tmpF.addScaledVector(tmpU,-tmpF.dot(tmpU)).normalize();
tmpR.crossVectors(tmpF,tmpU).normalize();
tmpZ.crossVectors(tmpF,tmpR).normalize();
tmpM.makeBasis(tmpF,tmpU,tmpZ); tmpQ.setFromRotationMatrix(tmpM);
g.quaternion.slerp(tmpQ,Math.min(1,dt*10));
if(t.state==='wreck'){ g.rotateZ(Math.min(1.45,(4.2-t.wt)*1.6)); g.rotateX(.3); }
var roll=clamp(-t.steerVis*.22,-.2,.2), pitch=clamp((t.vf-t.vfOld)*.02,-.14,.14);
t.vfOld=t.vf;
if(!t.grounded)pitch=clamp(-t.vy*.02,-.3,.3);
t.roll=lerp(t.roll,roll,Math.min(1,dt*7)); t.pitch=lerp(t.pitch,pitch,Math.min(1,dt*7));
t.body.rotation.set(t.pitch,0,t.roll);
t.susp=lerp(t.susp,t.grounded?0:.16,Math.min(1,dt*8));
t.body.position.y=-t.susp;
var wr=t.vf/Math.max(1,1.62);
for(var i=0;i<t.wheels.length;i++){ t.wheels[i].rotation.z-=wr*dt; }
var sv=clamp(t.steerVis,-.5,.5);
for(var j=0;j<t.steers.length;j++)t.steers[j].rotation.y=sv;
var hurt=t.integ<40;
t.body.children[0].material=(hurt&&Math.sin(G.simTime*14)>0)?t.flashMat:t.steel;
t.label.material.opacity=(t.state==='wreck')?.35:.92;
}
function updateCamera(dt){
if(G.photo){
var pa=G.photoAng;
var r=68;
camera.position.set(Math.cos(pa)*r,26+Math.sin(pa*.7)*8,Math.sin(pa)*r*0.78);
camera.lookAt(0,2.5,0); camera.fov=lerp(camera.fov,42,Math.min(1,dt*4));
camera.updateProjectionMatrix(); return;
}
if(!G.running){
var a2=G.simTime*0.11;
camera.position.set(Math.cos(a2)*72,30+Math.sin(a2*.8)*9,Math.sin(a2)*58);
camera.lookAt(0,4,0); camera.fov=lerp(camera.fov,52,Math.min(1,dt*3)); camera.updateProjectionMatrix(); return;
}
var t=player;
var f=tmpF.set(Math.cos(t.yaw),0,Math.sin(t.yaw));
var sp=Math.hypot(t.vel.x,t.vel.z);
var back=12.6+sp*.16, up=6.4+sp*.08;
tmpV.set(t.pos.x-f.x*back,t.pos.y+up,t.pos.z-f.z*back);
var k=Math.min(1,dt*(5.2+sp*.06));
camera.position.lerp(tmpV,k);
var gh=groundH(camera.position.x,camera.position.z)+2.4;
if(camera.position.y<gh)camera.position.y=gh;
camLook.lerp(tmpU.set(t.pos.x+f.x*7.5,t.pos.y+2.4,t.pos.z+f.z*7.5),Math.min(1,dt*7));
var sh=G.shake;
camera.position.x+=rnd(-1,1)*sh*.5; camera.position.y+=rnd(-1,1)*sh*.5; camera.position.z+=rnd(-1,1)*sh*.5;
camera.lookAt(camLook);
camera.fov=lerp(camera.fov,57+clamp(sp/MAXV,0,1)*11+(t.boost>.02&&K.ShiftLeft?5:0),Math.min(1,dt*5));
camera.updateProjectionMatrix();
}
var camLook=new THREE.Vector3(0,2,0);
/* ══════════════ HUD ══════════════ */
var elScore=document.getElementById('score'),elClock=document.getElementById('clock'),
elCombo=document.getElementById('combo'),elHp=document.getElementById('hpFill'),
elNz=document.getElementById('nzFill'),elSpd=document.getElementById('spd'),
elDemo=document.getElementById('demoTag');
function updateHUD(dt){
G.disp+=(G.score-G.disp)*Math.min(1,dt*9);
if(Math.abs(G.score-G.disp)<1.2)G.disp=G.score;
elScore.textContent=Math.round(G.disp);
elClock.textContent=fmtClock(G.time);
elClock.classList.toggle('warn',G.time<10);
elCombo.classList.toggle('on',G.combo>1);
if(G.combo>1)elCombo.textContent='RAM CHAIN ×'+G.combo;
elHp.style.width=clamp(player.integ,0,100)+'%';
elNz.style.width=clamp(player.boost*100,0,100)+'%';
elSpd.innerHTML=Math.round(Math.hypot(player.vel.x,player.vel.z)*3.4)+'<span> KM/H</span>';
document.getElementById('pName').textContent=ARENA.name;
}
/* ══════════════ PHOTO MODE ══════════════ */
var photoEl=document.getElementById('photo');
function togglePhoto(){
if(!G.running&&!G.over)return;
G.photo=!G.photo;
photoEl.classList.toggle('on',G.photo);
canvas.classList.toggle('photo',G.photo);
document.getElementById('hud').classList.toggle('on',!G.photo);
if(G.photo){
G.disp=G.score;
document.getElementById('pScore').textContent=G.score;
document.getElementById('pMeta').textContent='CLOCK '+fmtClock(G.time)+' · 5 TRUCKS ON TRACK · SIM FROZEN';
G.photoAng=Math.atan2(camera.position.z,camera.position.x);
Snd.shutter();
} else { last=performance.now(); }
}
/* ══════════════ HEAT CONTROL ══════════════ */
function newHeat(){
G.score = DEMO?5000:0; /* ← real game state, read by the same code path every frame */
G.time = DEMO?20:90; /* ← ditto: the HUD clock counts down from here */
G.disp=G.score; G.simTime=0; G.combo=0;G.comboT=0;G.bestChain=1;G.hype=0;G.shake=0;
G.hits=0;G.wrecks=0;G.bestAir=0;G.over=false;
elDemo.classList.toggle('on',DEMO);
var sx=[-30,-30,-22,-22,-14],sz=[-8,8,-15,15,0];
for(var i=0;i<trucks.length;i++){var t=trucks[i];
t.pos.set(sx[i],0,sz[i]); t.pos.y=groundH(t.pos.x,t.pos.z);
t.vel.set(0,0,0); t.vy=0; t.yaw=0; t.yawVel=0; t.state='drive'; t.wt=0; t.integ=100;
t.boost=1; t.air=0; t.grounded=true; t.vf=0; t.vfOld=0; t.roll=0;t.pitch=0;t.steerVis=0;
t.group.rotation.set(0,0,0); t.group.position.copy(t.pos);
if(t.ai){t.ai.t=0;t.ai.phi=Math.PI+i*0.55;t.ai.hunt=false;}
}
for(var j=0;j<obstacles.length;j++){var o=obstacles[j];
o.alive=true;o.mesh.visible=true;o.fuse=0;o.vx=0;o.vz=0;o.yawVel=0;
o.mesh.position.set(o.x,groundH(o.x,o.z),o.z);}
toast('HEAT 7 · GO GO GO','gold','THE RUSTDOME');
hype(1); Snd.cheer(1);
}
function startHeat(){
document.getElementById('start').classList.add('hide');
document.getElementById('over').classList.add('hide');
document.getElementById('hud').classList.add('on');
Snd.init(); newHeat(); G.running=true; last=performance.now();
}
function endHeat(){
G.running=false; G.over=true;
document.getElementById('hud').classList.remove('on');
var r=G.score>26000?'SCRAP KING':G.score>16000?'DEMOLITION DERBY LORD':G.score>9000?'STEEL LEGEND':G.score>3500?'RUST ROOKIE':'SCRAP HEAP';
document.getElementById('rank').textContent=r;
document.getElementById('finalLine').innerHTML='FINAL SCORE <b style="color:#ffc21a;font-size:34px" class="mono">'+G.score+'</b>';
document.getElementById('sWreck').textContent=G.wrecks;
document.getElementById('sHits').textContent=G.hits;
document.getElementById('sChain').textContent='×'+G.bestChain;
document.getElementById('sAir').textContent=G.bestAir.toFixed(1)+'s';
document.getElementById('over').classList.remove('hide');
toast('CHECKERED FLAG','gold');
Snd.cheer(1); hype(1);
}
function restart(){ if(G.photo)togglePhoto(); startHeat(); }
document.getElementById('go').addEventListener('click',function(){Snd.init();startHeat();this.blur();});
document.getElementById('again').addEventListener('click',function(){Snd.init();restart();this.blur();});
/* ══════════════ LOOP ══════════════ */
var last=performance.now(),acc=0;
function loop(now){
requestAnimationFrame(loop);
var dt=(now-last)/1000; last=now;
if(!(dt>0))dt=0.016; dt=Math.min(.05,dt);
G.frames++;
if(G.running&&!G.photo){
acc+=dt; var n=0;
while(acc>=HSTEP&&n<7){ simStep(HSTEP); acc-=HSTEP; n++; }
if(n>=7)acc=0;
var c=readInput(); player.steerVis=lerp(player.steerVis,c.steer,Math.min(1,dt*12));
} else if(!G.photo){ G.simTime+=dt; }
if(!G.photo){
fx.spark.step(dt,26,1.1); fx.dust.step(dt,1.6,2.2); fx.flame.step(dt,-3.5,1.6);
updateCrowd();
G.shake=Math.max(0,G.shake-dt*2.2);
}
for(var i=0;i<trucks.length;i++)updateTruckVisual(trucks[i],G.photo?0:dt);
updateCamera(dt);
if(G.running&&Snd.ok&&!G.photo)Snd.engine(Math.hypot(player.vel.x,player.vel.z),readInput().throttle);
if(Snd.ok)Snd.crG.gain.setTargetAtTime(.010+G.hype*.075,Snd.c.currentTime,.25);
if(G.running)updateHUD(dt);
if(G.frames%14===0&&!G.photo)drawJumbo();
renderer.render(scene,camera);
}
requestAnimationFrame(loop);
addEventListener('resize',function(){
camera.aspect=innerWidth/innerHeight; camera.updateProjectionMatrix();
renderer.setSize(innerWidth,innerHeight,false);
});
boot.classList.add('gone');
setTimeout(function(){boot.style.display='none';},600);
if(DEMO){ startHeat(); }
document.getElementById('pName').textContent=ARENA.name;
drawJumbo(true);
}
})();
</script>
</body>
</html>
r² > 0.72 says it's drifting into the wall — so contact happens in bursts, not as a permanent pile-up.newHeat() assigns G.score = DEMO ? 5000 : 0 and G.time = DEMO ? 20 : 90 on the live state object that simStep() decrements and updateHUD()/drawJumbo() read every frame — the same variables a normal heat runs on, so the clock really ticks down from 0:20, really ends the heat at zero, and the 5000 really carries into ram bonuses and the results card. There is no overlay or patched DOM text; #demo also skips the start screen and calls startHeat() immediately.P flips G.photo, which gates the simulation — the fixed-step physics loop, the AI, the clock decrement, hype/shake decay and particle stepping are all skipped, and every animated thing (crowd bob, barrel strobe, wheel spin) reads G.simTime, which stops advancing, so the scene is genuinely frozen rather than playing behind the card. The requestAnimationFrame render loop deliberately stays alive so the camera can orbit the still frame and the letterboxed card can show the arena name plus the exact current G.score; un-pressing P resets the frame timer so nothing fast-forwards.j = -(1+e)·v_n / (1/m_a+1/m_b) with e ≈ 0.45, plus a clamped tangential friction impulse and an offset-contact torque that feeds yawVel, so a hard side hit spins the victim out and pops it airborne. Static furniture (barriers, ramps) uses circle-vs-OBB: the truck centre is transformed into the box's local frame, clamped to half-extents, and pushed out along the minimum-penetration normal; movable props (sedans, drums) are light circles that receive the reaction impulse and keep sliding/spinning with exponential friction. Vertical motion is a height-field test against the analytic terrain function groundH(), and impact energy |v_n|·min(mass) drives damage, sparks, dust, screen shake, crowd hype and score — integrity at zero flips the truck into a timed wreck state with smoke, flame and a 1000-point bounty.