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.SCRAPVANA SMASH-O-RAMA — five wrapped Cybertrucks trading paint under floodlights in a night-time salvage-yard coliseum, ringed the whole way round by ~2,400 instanced fans who roar every time you launch off a dirt mound, steamroll a junker, or put a rival out on a flatbed.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>SCRAPVANA SMASH-O-RAMA</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
html,body{height:100%;overflow:hidden;background:#0c0f15}
canvas{display:block;position:fixed;inset:0}
#hud{position:fixed;inset:0;pointer-events:none;color:#f2ede4;
font-family:Impact,'Arial Narrow Bold','Franklin Gothic Medium',sans-serif;user-select:none}
#plate{position:absolute;left:18px;top:16px;padding:10px 16px 9px;background:rgba(11,12,15,.8);
border:1px solid rgba(242,237,228,.14);border-left:4px solid #ff6a1a}
#arenaName{font-size:22px;letter-spacing:2.5px;line-height:1}
#arenaSub{font-family:Consolas,Menlo,monospace;font-size:10px;letter-spacing:2px;color:#9b948a;margin-top:5px}
#stats{position:absolute;right:18px;top:16px;display:flex;background:rgba(11,12,15,.8);
border:1px solid rgba(242,237,228,.14);transform:skewX(-8deg)}
.stat{padding:8px 20px 7px}
.stat+.stat{border-left:1px solid rgba(242,237,228,.14)}
.stat>*{display:block;transform:skewX(8deg)}
.lab{font-family:Consolas,Menlo,monospace;font-size:10px;letter-spacing:3px;color:#9b948a}
#score{font-size:40px;line-height:1;letter-spacing:1px;display:inline-block}
#score.tick{animation:scoretick .22s ease-out}
@keyframes scoretick{30%{transform:skewX(8deg) scale(1.13);color:#ff6a1a}}
#clock{font-size:30px;line-height:1.25}
#clock.low{color:#ff6a1a;animation:blink .5s steps(2) infinite}
@keyframes blink{50%{opacity:.35}}
#hint{position:absolute;left:18px;bottom:14px;font-family:Consolas,Menlo,monospace;
font-size:11px;letter-spacing:1.5px;color:#8d867c}
#feed{position:absolute;left:50%;bottom:22%;width:0;text-align:center}
.pop{position:absolute;left:0;bottom:0;transform:translateX(-50%);white-space:nowrap;
font-size:30px;letter-spacing:2px;text-shadow:0 2px 0 rgba(0,0,0,.65);
animation:rise 1.4s ease-out forwards}
.pop b{color:#ff6a1a;font-weight:normal}
.pop.minor{font-size:15px;color:#cfc8bd;letter-spacing:1.5px}
@keyframes rise{0%{opacity:0;transform:translate(-50%,10px) scale(.75)}
14%{opacity:1;transform:translate(-50%,-8px) scale(1.05)}75%{opacity:1}
100%{opacity:0;transform:translate(-50%,-64px)}}
.card{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:rgba(7,8,10,.55)}
.card .inner{background:#0d0f13;border:2px solid #f2ede4;padding:34px 56px;text-align:center;
box-shadow:0 0 0 6px #0d0f13,0 24px 60px rgba(0,0,0,.5)}
.kicker{font-family:Consolas,Menlo,monospace;font-size:11px;letter-spacing:4px;color:#ff6a1a;margin-bottom:10px}
.bigname{font-size:44px;letter-spacing:3px;line-height:1.05}
.rule{width:64px;height:3px;background:#ff6a1a;margin:14px auto}
.cardscore{font-size:62px;color:#ff6a1a;letter-spacing:2px;line-height:1}
.sub{font-family:Consolas,Menlo,monospace;font-size:11px;letter-spacing:2px;color:#9b948a;margin-top:12px}
.hidden{display:none}
#intro{position:absolute;left:50%;bottom:56px;transform:translateX(-50%);text-align:center;
background:rgba(11,12,15,.85);border:1px solid rgba(242,237,228,.16);
border-bottom:3px solid #ff6a1a;padding:16px 36px 14px;transition:opacity .6s,transform .6s}
#intro.gone{opacity:0;transform:translate(-50%,14px)}
#intro h1{font-size:24px;letter-spacing:3px}
#intro p{font-family:Consolas,Menlo,monospace;font-size:11px;letter-spacing:2px;color:#cfc8bd;margin-top:7px}
#dmg{position:absolute;inset:0;opacity:0;box-shadow:inset 0 0 140px 50px rgba(220,60,35,.6)}
#dmg.on{animation:dmgf .55s ease-out}
@keyframes dmgf{0%{opacity:1}100%{opacity:0}}
</style>
</head>
<body>
<div id="hud">
<div id="plate">
<div id="arenaName"></div>
<div id="arenaSub"></div>
</div>
<div id="stats">
<div class="stat"><span class="lab">SCORE</span><span id="score">0</span></div>
<div class="stat"><span class="lab">CLOCK</span><span id="clock">1:30</span></div>
</div>
<div id="hint">WASD / ARROW KEYS — DRIVE · SPACE — HANDBRAKE · P — PHOTO MODE · R — RESTART</div>
<div id="feed"></div>
<div id="intro">
<h1 id="introName"></h1>
<p>FIVE TRUCKS · ONE PIT · NO INSURANCE</p>
<p>RAM RIVALS · CRUSH JUNKERS · CATCH AIR — WASD / ARROWS, SPACE TO SLIDE</p>
</div>
<div id="photoCard" class="card hidden"><div class="inner">
<div class="kicker">PHOTO MODE</div>
<div class="bigname" id="photoName"></div>
<div class="rule"></div>
<div class="lab">SCORE</div>
<div class="cardscore" id="photoScore">0</div>
<div class="sub">PRESS P TO RESUME</div>
</div></div>
<div id="endCard" class="card hidden"><div class="inner">
<div class="kicker">TIME</div>
<div class="bigname">FINAL TALLY</div>
<div class="rule"></div>
<div class="cardscore" id="finalScore">0</div>
<div class="sub">PRESS R FOR A FRESH ROUND</div>
</div></div>
<div id="dmg"></div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/0.149.0/three.min.js"></script>
<script>
'use strict';
if(!window.THREE){
document.getElementById('hud').innerHTML='<div class="card"><div class="inner">'+
'<div class="bigname">COULD NOT REACH THE PITS</div>'+
'<div class="sub">three.js failed to load — this page needs its one CDN request to run.</div></div></div>';
throw new Error('three.js missing');
}
/* ---------------- helpers ---------------- */
const TAU=Math.PI*2;
const clamp=(v,a,b)=>v<a?a:v>b?b:v;
const lerp=(a,b,t)=>a+(b-a)*t;
const rnd=(a,b)=>a+Math.random()*(b-a);
const angNorm=a=>{while(a>Math.PI)a-=TAU;while(a<-Math.PI)a+=TAU;return a;};
const fmt=n=>String(n).replace(/\B(?=(\d{3})+(?!\d))/g,',');
/* ---------------- state ---------------- */
const ARENA_NAME='SCRAPVANA SMASH-O-RAMA';
const ROUND_TIME=90, BOUND=40;
const DEMO=/^#demo$/i.test(location.hash);
const app={t:0,score:0,time:ROUND_TIME,over:false,photo:false,hype:0,shake:0};
if(DEMO){ app.time=20; app.score=5000; } /* real game state, ticked by the loop */
let lastThr=0,lastClock=null,lastFeedT=-9,introGone=false;
/* ---------------- dom ---------------- */
const $=id=>document.getElementById(id);
const scoreEl=$('score'),clockEl=$('clock'),feedEl=$('feed'),dmgEl=$('dmg'),
introEl=$('intro'),photoEl=$('photoCard'),endEl=$('endCard');
$('arenaName').textContent=ARENA_NAME;
$('arenaSub').textContent='NIGHT SESSION · FAIRGROUNDS COLISEUM · FIVE TRUCKS';
$('photoName').textContent=ARENA_NAME;
$('introName').textContent=ARENA_NAME;
/* ---------------- three setup ---------------- */
const scene=new THREE.Scene();
scene.background=new THREE.Color(0x0c0f15);
scene.fog=new THREE.Fog(0x0c0f15,90,230);
const camera=new THREE.PerspectiveCamera(62,innerWidth/innerHeight,0.1,500);
const renderer=new THREE.WebGLRenderer({antialias:true});
renderer.setPixelRatio(Math.min(devicePixelRatio,2));
renderer.setSize(innerWidth,innerHeight);
renderer.shadowMap.enabled=true;
renderer.shadowMap.type=THREE.PCFSoftShadowMap;
renderer.outputEncoding=THREE.sRGBEncoding;
renderer.toneMapping=THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure=1.25;
document.body.appendChild(renderer.domElement);
scene.add(new THREE.HemisphereLight(0x33405a,0x2a1f14,1.15));
const key=new THREE.DirectionalLight(0xffe9c4,2.2);
key.position.set(35,55,18); key.castShadow=true;
key.shadow.mapSize.set(2048,2048);
Object.assign(key.shadow.camera,{left:-55,right:55,top:55,bottom:-55,far:150});
key.shadow.bias=-0.0006;
scene.add(key);
const fill=new THREE.DirectionalLight(0x8fa3c8,0.55);
fill.position.set(-40,30,-30); scene.add(fill);
/* ---------------- arena analytics (the height field) ---------------- */
const mounds=[{x:-13,z:9,r:11,h:2.5},{x:15,z:-11,r:9,h:2.0}];
const ramps=[
{x:-3,z:-21,yaw:Math.atan2(3,27),len:9,wid:6.4,h:2.4},
{x:19,z:14,yaw:Math.atan2(-25,-18),len:8.5,wid:6,h:2.1},
{x:-22,z:16,yaw:Math.atan2(26,-14),len:8,wid:6,h:1.9}
];
function groundHeight(x,z){
let h=0;
for(const m of mounds){
const d=Math.hypot(x-m.x,z-m.z);
if(d<m.r)h=Math.max(h,m.h*0.5*(1+Math.cos(Math.PI*d/m.r)));
}
for(const r of ramps){
const dx=x-r.x,dz=z-r.z;
const u=dx*Math.sin(r.yaw)+dz*Math.cos(r.yaw);
const v=dx*Math.cos(r.yaw)-dz*Math.sin(r.yaw);
if(u>=0&&u<=r.len&&Math.abs(v)<=r.wid/2)h=Math.max(h,r.h*u/r.len);
}
return h;
}
const stuntSpots=[];
ramps.forEach(r=>stuntSpots.push({x:r.x+Math.sin(r.yaw)*(r.len+2),z:r.z+Math.cos(r.yaw)*(r.len+2)}));
mounds.forEach(m=>stuntSpots.push({x:m.x,z:m.z}));
/* ---------------- generated textures ---------------- */
function makeDirtTex(){
const c=document.createElement('canvas');c.width=c.height=1024;const g=c.getContext('2d');
g.fillStyle='#6b5030';g.fillRect(0,0,1024,1024);
for(let i=0;i<260;i++){
g.fillStyle=Math.random()<0.5?'rgba(30,20,10,'+rnd(0.04,0.1)+')':'rgba(120,90,55,'+rnd(0.04,0.1)+')';
g.beginPath();g.ellipse(rnd(0,1024),rnd(0,1024),rnd(20,90),rnd(14,60),rnd(0,TAU),0,TAU);g.fill();
}
for(let i=0;i<9000;i++){
g.fillStyle=Math.random()<0.5?'rgba(25,16,8,'+rnd(0.05,0.2)+')':'rgba(190,150,100,'+rnd(0.04,0.12)+')';
g.fillRect(rnd(0,1024),rnd(0,1024),rnd(1,2.6),rnd(1,2.6));
}
g.lineCap='round';
for(let i=0;i<46;i++){
g.strokeStyle='rgba(28,18,10,'+rnd(0.1,0.28)+')';g.lineWidth=rnd(5,13);
const x=rnd(0,1024),y=rnd(0,1024),r=rnd(40,220),a0=rnd(0,TAU);
g.beginPath();g.arc(x,y,r,a0,a0+rnd(0.4,1.6));g.stroke();
}
g.strokeStyle='rgba(235,228,212,0.5)';g.lineWidth=9;
g.beginPath();g.arc(512,512,86,0,TAU);g.stroke();
g.setLineDash([26,20]);g.lineWidth=5;
g.beginPath();g.arc(512,512,210,0,TAU);g.stroke();g.setLineDash([]);
g.fillStyle='rgba(235,228,212,0.34)';g.textAlign='center';g.textBaseline='middle';
g.save();g.translate(512,512);g.rotate(-0.06);
g.font='96px Impact,sans-serif';g.fillText('SCRAPVANA',0,-52);
g.font='40px Impact,sans-serif';g.fillText('SMASH - O - RAMA',0,58);
g.restore();
const t=new THREE.CanvasTexture(c);t.encoding=THREE.sRGBEncoding;t.anisotropy=4;return t;
}
function makeWallTex(){
const c=document.createElement('canvas');c.width=1024;c.height=128;const g=c.getContext('2d');
g.fillStyle='#141519';g.fillRect(0,0,1024,128);
for(let x=-24;x<1048;x+=26){
g.fillStyle='#ff6a1a';
g.beginPath();g.moveTo(x,0);g.lineTo(x+13,0);g.lineTo(x+5,12);g.lineTo(x-8,12);g.closePath();g.fill();
g.beginPath();g.moveTo(x,116);g.lineTo(x+13,116);g.lineTo(x+5,128);g.lineTo(x-8,128);g.closePath();g.fill();
}
g.fillStyle='#e9e3d6';g.font='54px Impact,sans-serif';g.textAlign='center';g.textBaseline='middle';
g.fillText('SCRAPVANA SMASH-O-RAMA',512,64);
g.fillStyle='#ff6a1a';
[[40,64],[984,64]].forEach(p=>{g.save();g.translate(p[0],p[1]);g.rotate(Math.PI/4);g.fillRect(-9,-9,18,18);g.restore();});
const t=new THREE.CanvasTexture(c);t.encoding=THREE.sRGBEncoding;
t.wrapS=THREE.RepeatWrapping;t.repeat.set(-5,1);return t;
}
function makeSignTex(){
const c=document.createElement('canvas');c.width=1024;c.height=256;const g=c.getContext('2d');
g.fillStyle='#0e1013';g.fillRect(0,0,1024,256);
g.strokeStyle='#ff6a1a';g.lineWidth=10;g.strokeRect(8,8,1008,240);
g.fillStyle='#f2ede4';g.font='120px Impact,sans-serif';g.textAlign='center';g.textBaseline='middle';
g.fillText('SCRAPVANA',512,108);
g.fillStyle='#ff6a1a';g.font='46px Impact,sans-serif';g.fillText('SMASH-O-RAMA',512,196);
const t=new THREE.CanvasTexture(c);t.encoding=THREE.sRGBEncoding;return t;
}
function makeHaloTex(){
const c=document.createElement('canvas');c.width=c.height=128;const g=c.getContext('2d');
const gr=g.createRadialGradient(64,64,4,64,64,62);
gr.addColorStop(0,'rgba(255,240,214,0.9)');gr.addColorStop(0.4,'rgba(255,230,190,0.28)');gr.addColorStop(1,'rgba(255,230,190,0)');
g.fillStyle=gr;g.fillRect(0,0,128,128);
const t=new THREE.CanvasTexture(c);t.encoding=THREE.sRGBEncoding;return t;
}
const dirtTex=makeDirtTex(),wallTex=makeWallTex(),signTex=makeSignTex(),haloTex=makeHaloTex();
const dirtSolid=new THREE.MeshStandardMaterial({color:0x6b5030,roughness:1,flatShading:true});
/* ---------------- stadium ---------------- */
(function buildStadium(){
const floor=new THREE.Mesh(new THREE.CircleGeometry(47,72),
new THREE.MeshStandardMaterial({map:dirtTex,roughness:1}));
floor.rotation.x=-Math.PI/2;floor.receiveShadow=true;scene.add(floor);
const under=new THREE.Mesh(new THREE.CircleGeometry(64,48),
new THREE.MeshStandardMaterial({color:0x14110c,roughness:1}));
under.rotation.x=-Math.PI/2;under.position.y=-0.06;scene.add(under);
const wall=new THREE.Mesh(new THREE.CylinderGeometry(43,43,2.6,72,1,true),
new THREE.MeshStandardMaterial({map:wallTex,roughness:0.85,side:THREE.BackSide}));
wall.position.y=1.3;wall.receiveShadow=true;scene.add(wall);
const rim=new THREE.Mesh(new THREE.RingGeometry(42.6,43.4,72),
new THREE.MeshBasicMaterial({color:0xff6a1a,side:THREE.DoubleSide}));
rim.rotation.x=-Math.PI/2;rim.position.y=2.62;scene.add(rim);
/* stands: one stepped lathe, all the way round */
const pts=[new THREE.Vector2(42.8,0),new THREE.Vector2(44.2,2.3)];
for(let k=0;k<8;k++){
pts.push(new THREE.Vector2(46.2+2*k,2.3+1.15*k));
if(k<7)pts.push(new THREE.Vector2(46.2+2*k,2.3+1.15*(k+1)));
}
pts.push(new THREE.Vector2(62.4,10.35),new THREE.Vector2(62.4,13.6));
const stands=new THREE.Mesh(new THREE.LatheGeometry(pts,84),
new THREE.MeshStandardMaterial({color:0x33373d,roughness:0.95,flatShading:true}));
stands.receiveShadow=true;scene.add(stands);
/* signage on all four sides */
const sg=new THREE.PlaneGeometry(30,7.5);
const sMat=new THREE.MeshBasicMaterial({map:signTex});
[[0,-63.5,0],[0,63.5,Math.PI],[-63.5,0,Math.PI/2],[63.5,0,-Math.PI/2]].forEach(a=>{
const g=new THREE.Group();
const back=new THREE.Mesh(new THREE.BoxGeometry(31,8,0.7),
new THREE.MeshStandardMaterial({color:0x101216,roughness:0.9}));
back.position.z=-0.4;
g.add(back,new THREE.Mesh(sg,sMat));
g.position.set(a[0],15.2,a[1]);g.rotation.y=a[2];scene.add(g);
});
/* floodlight towers + halos */
[Math.PI/4,3*Math.PI/4,5*Math.PI/4,7*Math.PI/4].forEach(a=>{
const x=Math.sin(a)*60,z=Math.cos(a)*60;
const pole=new THREE.Mesh(new THREE.CylinderGeometry(0.5,0.7,14,8),
new THREE.MeshStandardMaterial({color:0x1c1e23,roughness:0.8}));
pole.position.set(x,17.35,z);scene.add(pole);
const head=new THREE.Group();
const panel=new THREE.Mesh(new THREE.PlaneGeometry(6.4,2.6),
new THREE.MeshBasicMaterial({color:0xfff3d8}));
const frame=new THREE.Mesh(new THREE.BoxGeometry(7,3.2,0.5),
new THREE.MeshStandardMaterial({color:0x191b20}));
frame.position.z=-0.3;head.add(frame,panel);
head.position.set(x,24.95,z);head.rotation.y=a+Math.PI;scene.add(head);
const halo=new THREE.Sprite(new THREE.SpriteMaterial({map:haloTex,color:0xffe6b8,
transparent:true,opacity:0.5,blending:THREE.AdditiveBlending,depthWrite:false}));
halo.scale.set(13,9,1);halo.position.set(x,24.95,z);scene.add(halo);
});
/* stars */
const arr=new Float32Array(320*3);
for(let i=0;i<320;i++){
const a=rnd(0,TAU),e=rnd(0.15,1.35),r=265;
arr[i*3]=Math.cos(a)*Math.cos(e)*r;arr[i*3+1]=Math.sin(e)*r;arr[i*3+2]=Math.sin(a)*Math.cos(e)*r;
}
const sg2=new THREE.BufferGeometry();
sg2.setAttribute('position',new THREE.BufferAttribute(arr,3));
scene.add(new THREE.Points(sg2,new THREE.PointsMaterial({color:0xc4cddd,size:1.7,
sizeAttenuation:false,transparent:true,opacity:0.8,fog:false})));
})();
(function buildStaticObstacles(){
ramps.forEach(r=>{
const shp=new THREE.Shape();
shp.moveTo(0,0);shp.lineTo(r.len,0);shp.lineTo(r.len,r.h);shp.lineTo(0,0);
const geo=new THREE.ExtrudeGeometry(shp,{depth:r.wid,bevelEnabled:false});
geo.rotateY(-Math.PI/2);geo.translate(r.wid/2,0,0);
const m=new THREE.Mesh(geo,dirtSolid);m.castShadow=true;m.receiveShadow=true;
const g=new THREE.Group();g.add(m);
const poleMat=new THREE.MeshStandardMaterial({color:0x2a2c31});
[-r.wid/2,r.wid/2].forEach(px=>{
const pole=new THREE.Mesh(new THREE.CylinderGeometry(0.05,0.05,2.3,6),poleMat);
pole.position.set(px,1.15,-0.15);g.add(pole);
const pen=new THREE.Mesh(new THREE.PlaneGeometry(0.62,0.36),
new THREE.MeshBasicMaterial({color:0xff6a1a,side:THREE.DoubleSide}));
pen.position.set(px,2.1,-0.15);g.add(pen);
});
g.position.set(r.x,0,r.z);g.rotation.y=r.yaw;scene.add(g);
});
mounds.forEach(mn=>{
const pts=[];
for(let i=0;i<=22;i++){
const d=i/22*mn.r;
pts.push(new THREE.Vector2(d,mn.h*0.5*(1+Math.cos(Math.PI*d/mn.r))));
}
const m=new THREE.Mesh(new THREE.LatheGeometry(pts,26),dirtSolid);
m.castShadow=true;m.receiveShadow=true;
m.position.set(mn.x,0,mn.z);scene.add(m);
});
})();
/* ---------------- crowd (instanced, all the way round) ---------------- */
let crowdBody,crowdHead,bodyArr,headArr;
const crowdData=[];
(function buildCrowd(){
const dummy=new THREE.Object3D();
for(let k=0;k<8&&crowdData.length<2500;k++){
const rMid=45.2+2*k,y=2.3+1.15*k;
const n=Math.floor(TAU*rMid/0.95);
for(let j=0;j<n&&crowdData.length<2500;j++){
if(Math.random()<0.16)continue;
const ang=(j+rnd(-0.25,0.25))/n*TAU,r=rMid+rnd(-0.6,0.7);
crowdData.push({x:Math.sin(ang)*r,z:Math.cos(ang)*r,y:y+0.39,s:rnd(0.85,1.2),
ry:rnd(0,TAU),sp:rnd(3.4,7.5),ph:rnd(0,TAU),resp:rnd(0.5,1.6)});
}
}
const N=crowdData.length;
crowdBody=new THREE.InstancedMesh(new THREE.BoxGeometry(0.52,0.78,0.36),
new THREE.MeshStandardMaterial({roughness:0.9,flatShading:true}),N);
crowdHead=new THREE.InstancedMesh(new THREE.IcosahedronGeometry(0.17,0),
new THREE.MeshStandardMaterial({roughness:0.8,flatShading:true}),N);
crowdBody.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
crowdHead.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
const clothes=[0xc9b28a,0x8a2f2b,0x3f4a52,0x6b7448,0xa0522d,0x2e2a26,0x7d6b5d,0xb0413e,0x44584f,0xd9cfc0,0xff6a1a,0xe8b13c];
const skins=[0xe8b48c,0xc98d5f,0x8c5a3c,0x5f3d28,0xf0c9a2];
const col=new THREE.Color();
crowdData.forEach((s,i)=>{
dummy.position.set(s.x,s.y,s.z);dummy.rotation.set(0,s.ry,0);dummy.scale.setScalar(s.s);
dummy.updateMatrix();crowdBody.setMatrixAt(i,dummy.matrix);
dummy.position.y=s.y+0.6*s.s;dummy.updateMatrix();crowdHead.setMatrixAt(i,dummy.matrix);
crowdBody.setColorAt(i,col.setHex(clothes[(Math.random()*clothes.length)|0]));
crowdHead.setColorAt(i,col.setHex(skins[(Math.random()*skins.length)|0]));
});
crowdBody.instanceColor.needsUpdate=true;
crowdHead.instanceColor.needsUpdate=true;
crowdBody.frustumCulled=false;crowdHead.frustumCulled=false;
scene.add(crowdBody,crowdHead);
bodyArr=crowdBody.instanceMatrix.array;
headArr=crowdHead.instanceMatrix.array;
})();
function crowdStep(){
const t=app.t,h=app.hype;
for(let i=0;i<crowdData.length;i++){
const s=crowdData[i];
const bob=Math.sin(t*s.sp+s.ph)*(0.05+h*0.12*s.resp)
+Math.max(0,Math.sin(t*s.sp*2.13+s.ph*1.7))*h*s.resp*0.5;
bodyArr[i*16+13]=s.y+bob;
headArr[i*16+13]=s.y+0.6*s.s+bob*1.15;
}
crowdBody.instanceMatrix.needsUpdate=true;
crowdHead.instanceMatrix.needsUpdate=true;
}
/* ---------------- props (junkers, tire pyramids, cones) ---------------- */
let props=[];
const junkerSpots=[[7,-3],[2,-15],[22,2],[10,20],[-16,-12],[-24,6]];
const tireSpots=[[0,10],[-14,-18],[24,-6],[12,-22],[-26,-14]];
function buildProps(){
props=[];
junkerSpots.forEach((s,i)=>{
const g=new THREE.Group();
const col=[0x7a3b2a,0x5d6157,0x8a3b30,0x4d4a41,0x6b5a2f,0x744a3d][i%6];
const m=new THREE.MeshStandardMaterial({color:col,roughness:0.9,metalness:0.15,flatShading:true});
const b=new THREE.Mesh(new THREE.BoxGeometry(4.3,1.0,2.0),m);b.position.y=0.95;b.castShadow=true;
const cab=new THREE.Mesh(new THREE.BoxGeometry(2.1,0.72,1.8),m);cab.position.set(-0.3,1.72,0);cab.castShadow=true;
g.add(b,cab);
const wg=new THREE.CylinderGeometry(0.42,0.42,0.35,10);wg.rotateZ(Math.PI/2);
const wm=new THREE.MeshStandardMaterial({color:0x161616,roughness:1});
[[-1.45,1],[1.45,1],[-1.45,-1],[1.45,-1]].forEach(p=>{
const w=new THREE.Mesh(wg,wm);w.position.set(p[0],0.42,p[1]);g.add(w);
});
g.position.set(s[0],groundHeight(s[0],s[1]),s[1]);g.rotation.y=rnd(0,TAU);
scene.add(g);
props.push({type:'junker',x:s[0],z:s[1],y:g.position.y,vx:0,vz:0,vy:0,spin:0,
radius:2.3,mass:1.3,group:g,hitCD:0,crush:0});
});
tireSpots.forEach(s=>{
const g=new THREE.Group();
const tm=new THREE.MeshStandardMaterial({color:0x1b1b1f,roughness:0.95,flatShading:true});
const chalk=new THREE.MeshStandardMaterial({color:0xcfc9ba,roughness:0.9,flatShading:true});
const t1=new THREE.TorusGeometry(0.85,0.34,8,16),t2=new THREE.TorusGeometry(0.72,0.3,8,14);
[[t1,0,tm],[t1,0.66,tm],[t2,1.28,chalk]].forEach(a=>{
const m=new THREE.Mesh(a[0],a[2]);m.rotation.x=Math.PI/2;m.position.y=a[1];m.castShadow=true;g.add(m);
});
g.position.set(s[0],groundHeight(s[0],s[1]),s[1]);g.rotation.y=rnd(0,TAU);
scene.add(g);
props.push({type:'tires',x:s[0],z:s[1],y:g.position.y,radius:1.75,group:g,wob:0,hitCD:0});
});
const coneGeo=new THREE.ConeGeometry(0.36,0.95,10);
const bandGeo=new THREE.CylinderGeometry(0.235,0.29,0.16,10);
const baseGeo=new THREE.BoxGeometry(0.62,0.07,0.62);
const cm=new THREE.MeshStandardMaterial({color:0xff6a1a,roughness:0.6});
const wm2=new THREE.MeshStandardMaterial({color:0xf2ede4,roughness:0.6});
[{r:27,a0:-0.5,a1:0.7,n:6},{r:31,a0:2.3,a1:3.6,n:6}].forEach(arc=>{
for(let i=0;i<arc.n;i++){
const ang=arc.a0+(arc.a1-arc.a0)*i/(arc.n-1);
const x=Math.sin(ang)*arc.r,z=Math.cos(ang)*arc.r;
const g=new THREE.Group();
const c=new THREE.Mesh(coneGeo,cm);c.position.y=0.51;c.castShadow=true;
const band=new THREE.Mesh(bandGeo,wm2);band.position.y=0.55;
const base=new THREE.Mesh(baseGeo,cm);base.position.y=0.035;
g.add(c,band,base);g.position.set(x,groundHeight(x,z),z);scene.add(g);
props.push({type:'cone',x,z,y:g.position.y,vx:0,vz:0,vy:0,spin:0,
radius:0.55,mass:0.08,group:g,hitCD:0,scored:false,knocked:false});
}
});
}
/* ---------------- particles ---------------- */
function makePSys(cap,size){
const geo=new THREE.BufferGeometry();
const pos=new Float32Array(cap*3),col=new Float32Array(cap*3);
for(let i=0;i<cap;i++)pos[i*3+1]=-999;
geo.setAttribute('position',new THREE.BufferAttribute(pos,3));
geo.setAttribute('color',new THREE.BufferAttribute(col,3));
const pts=new THREE.Points(geo,new THREE.PointsMaterial({size,vertexColors:true,
transparent:true,opacity:0.9,depthWrite:false,blending:THREE.AdditiveBlending}));
pts.frustumCulled=false;scene.add(pts);
return {geo,pos,col,vel:new Float32Array(cap*3),base:new Float32Array(cap*3),
life:new Float32Array(cap),maxl:new Float32Array(cap),head:0,cap};
}
const sparksS=makePSys(300,0.42),dustS=makePSys(360,1.7),smokeS=makePSys(160,2.6);
function pspawn(s,x,y,z,vx,vy,vz,life,r,g,b){
const i=s.head;s.head=(i+1)%s.cap;
s.pos[i*3]=x;s.pos[i*3+1]=y;s.pos[i*3+2]=z;
s.vel[i*3]=vx;s.vel[i*3+1]=vy;s.vel[i*3+2]=vz;
s.base[i*3]=r;s.base[i*3+1]=g;s.base[i*3+2]=b;
s.col[i*3]=r;s.col[i*3+1]=g;s.col[i*3+2]=b;
s.life[i]=s.maxl[i]=life;
}
function stepSys(s,dt,grav,drag){
for(let i=0;i<s.cap;i++){
if(s.life[i]<=0)continue;
s.life[i]-=dt;
if(s.life[i]<=0){s.pos[i*3+1]=-999;continue;}
s.vel[i*3+1]-=grav*dt;
const f=Math.exp(-drag*dt);
s.vel[i*3]*=f;s.vel[i*3+1]*=f;s.vel[i*3+2]*=f;
s.pos[i*3]+=s.vel[i*3]*dt;s.pos[i*3+1]+=s.vel[i*3+1]*dt;s.pos[i*3+2]+=s.vel[i*3+2]*dt;
const k=s.life[i]/s.maxl[i];
s.col[i*3]=s.base[i*3]*k;s.col[i*3+1]=s.base[i*3+1]*k;s.col[i*3+2]=s.base[i*3+2]*k;
}
s.geo.attributes.position.needsUpdate=true;
s.geo.attributes.color.needsUpdate=true;
}
function sparksAt(x,y,z,n){
for(let i=0;i<n;i++){
const a=rnd(0,TAU),sp=rnd(3,11);
pspawn(sparksS,x,y,z,Math.cos(a)*sp,rnd(2,8),Math.sin(a)*sp,rnd(0.25,0.6),1,rnd(0.6,0.9),rnd(0.15,0.4));
}
}
function dustAt(x,y,z,n){
for(let i=0;i<n;i++){
const a=rnd(0,TAU),r2=rnd(0.2,1.6);
pspawn(dustS,x+Math.cos(a)*r2,y+0.2,z+Math.sin(a)*r2,
Math.cos(a)*rnd(1,3),rnd(1,3),Math.sin(a)*rnd(1,3),rnd(0.5,1.1),0.42,0.33,0.22);
}
}
function smokeAt(x,y,z){
pspawn(smokeS,x+rnd(-0.5,0.5),y,z+rnd(-0.5,0.5),rnd(-0.4,0.4),rnd(1.6,2.6),rnd(-0.4,0.4),rnd(0.9,1.5),0.28,0.28,0.3);
}
function confettiAt(x,y,z){
const pal=[[1,0.42,0.1],[0.95,0.93,0.88],[0.16,0.35,0.38],[0.72,0.54,0.18]];
for(let i=0;i<44;i++){
const a=rnd(0,TAU),c=pal[(Math.random()*pal.length)|0];
pspawn(sparksS,x+Math.cos(a)*rnd(0,2),y,z+Math.sin(a)*rnd(0,2),
Math.cos(a)*rnd(2,7),rnd(5,13),Math.sin(a)*rnd(2,7),rnd(0.8,1.4),c[0],c[1],c[2]);
}
}
/* ---------------- trucks ---------------- */
function drawPlate(t){
const g=t.plateCanvas.getContext('2d');
g.clearRect(0,0,256,64);
g.textAlign='center';g.font='30px Impact,sans-serif';
if(t.wrecked){
g.fillStyle='#e0512f';g.fillText('WRECKED',128,26);
g.fillStyle='rgba(0,0,0,.6)';g.fillRect(64,38,128,9);
}else{
g.lineWidth=4;g.strokeStyle='rgba(0,0,0,.7)';
g.fillStyle=t.isPlayer?'#ffb27a':'#f2ede4';
g.strokeText(t.name,128,26);g.fillText(t.name,128,26);
g.fillStyle='rgba(0,0,0,.6)';g.fillRect(64,38,128,9);
g.fillStyle=t.hp>50?'#a7d05a':(t.hp>25?'#e8b13c':'#e0512f');
g.fillRect(65.5,39.5,125*Math.max(0,t.hp)/100,6);
}
t.plateTex.needsUpdate=true;
}
function buildTruck(cfg){
const root=new THREE.Group(),tilt=new THREE.Group(),bodyG=new THREE.Group();
root.add(tilt);tilt.add(bodyG);
const bodyMat=new THREE.MeshStandardMaterial({color:cfg.color,metalness:0.55,roughness:0.38,flatShading:true});
bodyMat.emissive=new THREE.Color(0xff3014);bodyMat.emissiveIntensity=0;
const darkMat=new THREE.MeshStandardMaterial({color:0x17181c,roughness:0.7,flatShading:true});
const glassMat=new THREE.MeshStandardMaterial({color:0x0d1218,roughness:0.2,flatShading:true});
const shp=new THREE.Shape();
shp.moveTo(2.9,0.62);shp.lineTo(2.95,0.95);shp.lineTo(0.75,2.05);shp.lineTo(-1.5,1.78);
shp.lineTo(-2.95,1.5);shp.lineTo(-2.95,0.62);shp.lineTo(2.9,0.62);
const hullGeo=new THREE.ExtrudeGeometry(shp,{depth:2.3,bevelEnabled:false});
hullGeo.rotateY(-Math.PI/2);hullGeo.translate(1.15,0,0);
const hull=new THREE.Mesh(hullGeo,bodyMat);hull.position.y=1.55;hull.castShadow=true;bodyG.add(hull);
const ws=new THREE.Mesh(new THREE.BoxGeometry(2.02,1.9,0.08),glassMat);
ws.rotation.x=-1.107;ws.position.set(0,3.09,1.9);bodyG.add(ws);
const band=new THREE.Mesh(new THREE.BoxGeometry(2.42,0.5,2.35),glassMat);
band.position.set(0,2.55,-0.2);bodyG.add(band);
const chassis=new THREE.Mesh(new THREE.BoxGeometry(2.05,0.72,5.1),darkMat);
chassis.position.y=1.62;chassis.castShadow=true;bodyG.add(chassis);
const nose=new THREE.Mesh(new THREE.BoxGeometry(2.24,0.15,0.1),new THREE.MeshBasicMaterial({color:0xfff6e2}));
nose.position.set(0,2.42,2.97);bodyG.add(nose);
const tail=new THREE.Mesh(new THREE.BoxGeometry(2.24,0.13,0.1),new THREE.MeshBasicMaterial({color:0xff2a1a}));
tail.position.set(0,2.9,-2.98);bodyG.add(tail);
const bar=new THREE.Mesh(new THREE.BoxGeometry(1.7,0.12,0.32),darkMat);
bar.position.set(0,3.68,0.62);bodyG.add(bar);
const podMat=new THREE.MeshBasicMaterial({color:cfg.isPlayer?0xff8a3c:0xffd9a2});
for(let i=0;i<4;i++){
const p=new THREE.Mesh(new THREE.BoxGeometry(0.2,0.13,0.16),podMat);
p.position.set(-0.63+i*0.42,3.8,0.62);bodyG.add(p);
}
const tireGeo=new THREE.CylinderGeometry(1.05,1.05,0.85,14);tireGeo.rotateZ(Math.PI/2);
const hubGeo=new THREE.CylinderGeometry(0.52,0.52,0.95,8);hubGeo.rotateZ(Math.PI/2);
const tireMat=new THREE.MeshStandardMaterial({color:0x141416,roughness:0.95,flatShading:true});
const hubMat=new THREE.MeshStandardMaterial({color:0xb9bcc2,metalness:0.7,roughness:0.35,flatShading:true});
const wheels=[];
[[-1.85,1.95,true],[1.85,1.95,true],[-1.85,-1.95,false],[1.85,-1.95,false]].forEach(w=>{
const grp=new THREE.Group();grp.position.set(w[0],1.05,w[1]);
const tire=new THREE.Mesh(tireGeo,tireMat);tire.castShadow=true;
grp.add(tire,new THREE.Mesh(hubGeo,hubMat));
tilt.add(grp);wheels.push({grp,tire,front:w[2]});
});
const ant=new THREE.Group();ant.position.set(1.0,1.6,-2.55);
const rod=new THREE.Mesh(new THREE.CylinderGeometry(0.03,0.03,2.1,5),darkMat);rod.position.y=1.05;
const pen=new THREE.Mesh(new THREE.PlaneGeometry(0.6,0.34),
new THREE.MeshBasicMaterial({color:cfg.isPlayer?0xff6a1a:cfg.color,side:THREE.DoubleSide}));
pen.position.set(0,1.95,0);ant.add(rod,pen);tilt.add(ant);
const pc=document.createElement('canvas');pc.width=256;pc.height=64;
const ptex=new THREE.CanvasTexture(pc);ptex.encoding=THREE.sRGBEncoding;
const spr=new THREE.Sprite(new THREE.SpriteMaterial({map:ptex,transparent:true,depthWrite:false}));
spr.scale.set(6,1.5,1);spr.position.y=5.2;root.add(spr);
scene.add(root);
const t={
name:cfg.name,isPlayer:!!cfg.isPlayer,
x:0,z:0,y:0,vx:0,vz:0,vy:0,vf:0,vl:0,lastVf:0,heading:0,radius:2.5,
accel:26*cfg.power,maxSpeed:30*cfg.power,
grounded:true,launchVy:0,airT:0,sus:0,susV:0,pitch:0,roll:0,visSteer:0,
hp:100,wrecked:false,wreckT:0,invuln:0,flash:0,tipDir:1,smokeT:0,wallCD:0,
brain:{retarget:rnd(0.5,2),mode:'charge',targetTruck:null,targetProp:null,spot:null,
stuck:0,revT:0,seed:Math.random()*10,style:cfg.style,aggr:0.6},
root,tilt,bodyG,bodyMat,wheels,ant,antPh:rnd(0,9),
plateCanvas:pc,plateTex:ptex
};
drawPlate(t);
return t;
}
const spawnList=[
{x:0,z:26,h:Math.PI},
{x:-26,z:-10,h:1.2},
{x:26,z:-16,h:-1.0},
{x:-8,z:-28,h:0.3},
{x:15,z:31,h:-2.7}
];
const player=buildTruck({name:'YOU',color:0xd6dade,isPlayer:true,power:1});
const rivals=[
buildTruck({name:'THE JUDGE',color:0x8c2f26,style:'hunter',power:0.98}),
buildTruck({name:'CHOP TOP',color:0xb98a2f,style:'farm',power:0.94}),
buildTruck({name:'NITRO HAZARD',color:0x1f4e55,style:'stunt',power:0.96}),
buildTruck({name:'MOONSHINE MISSILE',color:0x2b2b31,style:'wild',power:0.97})
];
const trucks=[player].concat(rivals);
const pairs=[];
for(let i=0;i<trucks.length;i++)for(let j=i+1;j<trucks.length;j++)pairs.push({a:i,b:j,cd:0});
function placeTrucksAtSpawns(){
trucks.forEach((t,i)=>{
const s=spawnList[i];
t.x=s.x;t.z=s.z;t.heading=s.h;
t.vx=t.vz=t.vy=t.vf=t.vl=t.lastVf=0;
t.y=groundHeight(s.x,s.z);t.grounded=true;t.airT=0;t.launchVy=0;
t.hp=100;t.wrecked=false;t.wreckT=0;t.invuln=0;t.flash=0;
t.sus=0;t.susV=0;t.pitch=0;t.roll=0;t.visSteer=0;t.wallCD=0;
t.tilt.rotation.set(0,0,0);t.bodyG.visible=true;
drawPlate(t);
});
}
/* ---------------- audio (all synthesized) ---------------- */
let AC=null,master=null,crowdGain=null,engOsc=null,engGain=null,noiseBuf=null;
function initAudio(){
if(AC)return;
const Ctx=window.AudioContext||window.webkitAudioContext;
if(!Ctx)return;
try{
AC=new Ctx();
master=AC.createGain();master.gain.value=0.55;master.connect(AC.destination);
noiseBuf=AC.createBuffer(1,Math.floor(AC.sampleRate*1.2),AC.sampleRate);
const d=noiseBuf.getChannelData(0);
for(let i=0;i<d.length;i++)d[i]=Math.random()*2-1;
const src=AC.createBufferSource();src.buffer=noiseBuf;src.loop=true;
const bp=AC.createBiquadFilter();bp.type='bandpass';bp.frequency.value=520;bp.Q.value=0.6;
crowdGain=AC.createGain();crowdGain.gain.value=0;
src.connect(bp);bp.connect(crowdGain);crowdGain.connect(master);src.start();
const lfo=AC.createOscillator();lfo.frequency.value=0.13;
const lg=AC.createGain();lg.gain.value=0.015;
lfo.connect(lg);lg.connect(crowdGain.gain);lfo.start();
engOsc=AC.createOscillator();engOsc.type='sawtooth';engOsc.frequency.value=50;
const ef=AC.createBiquadFilter();ef.type='lowpass';ef.frequency.value=520;
engGain=AC.createGain();engGain.gain.value=0;
engOsc.connect(ef);ef.connect(engGain);engGain.connect(master);engOsc.start();
}catch(e){AC=null;}
}
function sfxSlam(v){
if(!AC)return;
const t0=AC.currentTime,vol=Math.min(0.85,0.12+v*0.035);
const s=AC.createBufferSource();s.buffer=noiseBuf;s.playbackRate.value=0.45;
const f=AC.createBiquadFilter();f.type='lowpass';
f.frequency.setValueAtTime(900,t0);f.frequency.exponentialRampToValueAtTime(120,t0+0.3);
const g=AC.createGain();g.gain.setValueAtTime(vol,t0);g.gain.exponentialRampToValueAtTime(0.001,t0+0.38);
s.connect(f);f.connect(g);g.connect(master);s.start(t0);s.stop(t0+0.4);
const o=AC.createOscillator();o.type='sine';
o.frequency.setValueAtTime(110,t0);o.frequency.exponentialRampToValueAtTime(36,t0+0.28);
const og=AC.createGain();og.gain.setValueAtTime(vol*0.9,t0);og.gain.exponentialRampToValueAtTime(0.001,t0+0.32);
o.connect(og);og.connect(master);o.start(t0);o.stop(t0+0.34);
}
function sfxThud(v){
if(!AC)return;
const t0=AC.currentTime;
const s=AC.createBufferSource();s.buffer=noiseBuf;s.playbackRate.value=0.3;
const f=AC.createBiquadFilter();f.type='lowpass';f.frequency.value=240;
const g=AC.createGain();g.gain.setValueAtTime(0.25*v,t0);g.gain.exponentialRampToValueAtTime(0.001,t0+0.16);
s.connect(f);f.connect(g);g.connect(master);s.start(t0);s.stop(t0+0.18);
}
function sfxClang(v){
if(!AC)return;
const t0=AC.currentTime;
[523,741].forEach(fr=>{
const o=AC.createOscillator();o.type='triangle';o.frequency.value=fr;
const f=AC.createBiquadFilter();f.type='lowpass';f.frequency.value=2600;
const g=AC.createGain();g.gain.setValueAtTime(0.11*Math.min(1.4,v/8),t0);
g.gain.exponentialRampToValueAtTime(0.001,t0+0.3);
o.connect(f);f.connect(g);g.connect(master);o.start(t0);o.stop(t0+0.32);
});
}
function sfxLand(v){
if(!AC)return;
const t0=AC.currentTime;
const o=AC.createOscillator();o.type='sine';
o.frequency.setValueAtTime(75,t0);o.frequency.exponentialRampToValueAtTime(40,t0+0.16);
const g=AC.createGain();g.gain.setValueAtTime(0.3*v,t0);g.gain.exponentialRampToValueAtTime(0.001,t0+0.2);
o.connect(g);g.connect(master);o.start(t0);o.stop(t0+0.22);
}
function cheer(){
if(!AC)return;
const t0=AC.currentTime;
const s=AC.createBufferSource();s.buffer=noiseBuf;s.loop=true;s.playbackRate.value=1.4;
const f=AC.createBiquadFilter();f.type='bandpass';f.frequency.value=1100;f.Q.value=0.7;
const g=AC.createGain();
g.gain.setValueAtTime(0.001,t0);
g.gain.exponentialRampToValueAtTime(0.28,t0+0.15);
g.gain.exponentialRampToValueAtTime(0.001,t0+1.9);
s.connect(f);f.connect(g);g.connect(master);s.start(t0);s.stop(t0+2);
}
/* ---------------- driving model ---------------- */
function driveTruck(t,thr,steer,hb,dt){
if(t.wrecked){thr=0;steer=0;hb=true;}
const sin=Math.sin(t.heading),cos=Math.cos(t.heading);
let vf=t.vx*sin+t.vz*cos;
let vl=t.vx*cos-t.vz*sin;
if(thr>0)vf+=t.accel*thr*dt*Math.max(0,1-vf/t.maxSpeed);
else if(thr<0){
if(vf>0.6)vf+=thr*42*dt;
else vf+=thr*16*dt*(1+vf/14);
}
vf*=Math.exp(-dt*(hb?1.6:0.4));
if(Math.abs(vf)<0.02&&thr===0)vf=0;
vl*=Math.exp(-dt*(hb?1.7:8.5));
const steerEff=steer*2.35*clamp(Math.abs(vf)/7,0,1)*(vf<0?-1:1);
t.heading+=steerEff*dt;
t.vx=sin*vf+cos*vl;t.vz=cos*vf-sin*vl;
const sp=Math.hypot(t.vx,t.vz);
if(sp>34){t.vx*=34/sp;t.vz*=34/sp;}
t.x+=t.vx*dt;t.z+=t.vz*dt;
t.vf=vf;t.vl=vl;
/* boundary wall */
const pr=Math.hypot(t.x,t.z);
t.wallCD-=dt;
if(pr>BOUND){
const nx=t.x/pr,nz=t.z/pr;
t.x=nx*BOUND;t.z=nz*BOUND;
const vn=t.vx*nx+t.vz*nz;
if(vn>0){
t.vx-=nx*vn*1.6;t.vz-=nz*vn*1.6;
if(vn>5){
sparksAt(t.x+nx*2,t.y+1,t.z+nz*2,Math.min(14,vn|0));
sfxThud(Math.min(1,vn/18));
app.hype=Math.min(1,app.hype+0.05);
if(t.isPlayer){
app.shake=Math.min(0.8,app.shake+vn*0.02);
if(vn>9&&t.wallCD<=0){t.wallCD=1;addScore(80,'WALL SLAM');}
}
}
}
}
/* vertical: height field, launches and landings */
const gh=groundHeight(t.x,t.z);
if(t.grounded){
if(gh<t.y-0.14){t.grounded=false;t.vy=clamp(t.launchVy,0,26);}
else{
t.launchVy=clamp((gh-t.y)/Math.max(dt,1e-4),0,26);
if(gh-t.y>0.6)dustAt(t.x,gh,t.z,4);
t.y=gh;
}
}else{
t.vy-=32*dt;t.y+=t.vy*dt;
if(t.y<=gh){
const fall=-t.vy,air=t.airT;
t.y=gh;t.vy=0;t.launchVy=0;t.airT=0;t.grounded=true;
if(air>0.15||fall>4){
dustAt(t.x,t.y,t.z,Math.min(20,(4+fall)|0));
sfxLand(Math.min(1,fall/16));
t.susV-=Math.min(9,fall*0.45);
if(t.isPlayer){
if(air>0.65)addScore(Math.round(air*150),'BIG AIR');
app.hype=Math.min(1,app.hype+air*0.1);
app.shake=Math.min(0.9,app.shake+fall*0.015);
}else if(air>0.8)app.hype=Math.min(1,app.hype+0.06);
}
}
}
if(!t.grounded)t.airT+=dt;
t.susV+=(-t.sus*130-t.susV*10)*dt;
t.sus+=t.susV*dt;
const acc=(vf-t.lastVf)/Math.max(dt,1e-4);t.lastVf=vf;
t.pitch=lerp(t.pitch,clamp(-acc*0.0045,-0.14,0.16),1-Math.exp(-6*dt));
t.roll=lerp(t.roll,clamp(steerEff*Math.abs(vf)*0.006,-0.15,0.15),1-Math.exp(-6*dt));
t.visSteer=lerp(t.visSteer,steer*0.42,1-Math.exp(-10*dt));
if(t.grounded&&Math.abs(vl)>4.2&&Math.random()<0.7){
const bx=t.x-sin*1.95,bz=t.z-cos*1.95,sx=cos*1.85,sz=-sin*1.85;
dustAt(bx+sx,t.y+0.3,bz+sz,2);dustAt(bx-sx,t.y+0.3,bz-sz,2);
}
}
/* ---------------- AI: four personalities, never a wall ---------------- */
function pickTruckTarget(t){
let best=null,bd=1e9;
for(const o of trucks){
if(o===t||o.wrecked)continue;
let d=Math.hypot(o.x-t.x,o.z-t.z);
if(o.isPlayer)d*=0.75;
if(d<bd){bd=d;best=o;}
}
return best;
}
function aiThink(t,dt){
const b=t.brain;
b.retarget-=dt;
if(b.targetTruck&&b.targetTruck.wrecked)b.retarget=Math.min(b.retarget,0);
if(b.targetProp&&b.targetProp.crush>4)b.retarget=Math.min(b.retarget,0);
if(b.retarget<=0){
b.retarget=rnd(2.2,4.6);
const r=Math.random();
if(b.style==='hunter')b.mode=r<0.85?'charge':'prop';
else if(b.style==='farm')b.mode=r<0.62?'prop':'charge';
else if(b.style==='stunt')b.mode=r<0.55?'stunt':(r<0.8?'charge':'prop');
else{
b.aggr=0.2+0.75*(0.5+0.5*Math.sin(app.t*0.33+b.seed));
b.mode=r<b.aggr?'charge':(r<b.aggr+0.25?'stunt':'prop');
}
b.targetTruck=null;b.targetProp=null;b.spot=null;
if(b.mode==='charge'){
b.targetTruck=pickTruckTarget(t);
if(!b.targetTruck){b.mode='stunt';b.spot=stuntSpots[(Math.random()*stuntSpots.length)|0];}
}else if(b.mode==='prop'){
const pool=props.filter(p=>(p.type==='junker'&&p.crush<4)||(p.type==='cone'&&!p.knocked));
if(pool.length)b.targetProp=pool[(Math.random()*pool.length)|0];
else{b.mode='charge';b.targetTruck=pickTruckTarget(t);}
}else b.spot=stuntSpots[(Math.random()*stuntSpots.length)|0];
}
let ax=t.x+Math.sin(t.heading)*10,az=t.z+Math.cos(t.heading)*10;
if(b.mode==='charge'&&b.targetTruck){
ax=b.targetTruck.x+b.targetTruck.vx*0.35;
az=b.targetTruck.z+b.targetTruck.vz*0.35;
}else if(b.mode==='prop'&&b.targetProp){
ax=b.targetProp.x;az=b.targetProp.z;
if(Math.hypot(ax-t.x,az-t.z)<5)b.retarget=Math.min(b.retarget,0.3);
}else if(b.spot){
ax=b.spot.x;az=b.spot.z;
if(Math.hypot(ax-t.x,az-t.z)<6)b.retarget=0;
}
/* separation: aim away from any other truck within 7u */
for(const o of trucks){
if(o===t)continue;
const dx=t.x-o.x,dz=t.z-o.z,d=Math.hypot(dx,dz);
if(d<7&&d>0.01){const w=(7-d)/7*9;ax+=dx/d*w;az+=dz/d*w;}
}
/* wall pull-back */
const pr=Math.hypot(t.x,t.z);
if(pr>BOUND-9){const w=(pr-(BOUND-9))*1.7;ax-=t.x/pr*w;az-=t.z/pr*w;}
/* soft avoid tire pyramids */
const fx=t.x+Math.sin(t.heading)*6,fz=t.z+Math.cos(t.heading)*6;
for(const p of props){
if(p.type!=='tires')continue;
if(Math.hypot(p.x-fx,p.z-fz)<3.6){ax+=(t.x-p.x)*0.8;az+=(t.z-p.z)*0.8;}
}
const diff=angNorm(Math.atan2(ax-t.x,az-t.z)-t.heading);
let thr=1,steer=clamp(diff*2.1,-1,1),hb=false;
if(Math.abs(diff)>2.05)thr=0.3;
if(Math.abs(diff)>2.6&&t.vf>9)hb=true;
if(b.mode==='charge'&&b.targetTruck&&
Math.hypot(b.targetTruck.x-t.x,b.targetTruck.z-t.z)<5.5&&t.vf<3)b.revT=0.7;
if(Math.abs(t.vf)<1.1)b.stuck+=dt;else b.stuck=0;
if(b.stuck>1.4){b.revT=0.9;b.stuck=0;b.retarget=0;}
if(b.revT>0){b.revT-=dt;thr=-1;steer=diff>0?-1:1;}
return {thr,steer,hb};
}
/* ---------------- collisions ---------------- */
const CA=[[0,0],[0,0]],CB=[[0,0],[0,0]];
function truckCircles(t,out){
const s=Math.sin(t.heading),c=Math.cos(t.heading);
out[0][0]=t.x+s*1.55;out[0][1]=t.z+c*1.55;
out[1][0]=t.x-s*1.55;out[1][1]=t.z-c*1.55;
}
function truckCollisions(dt){
for(const pr of pairs){
pr.cd-=dt;
const a=trucks[pr.a],b=trucks[pr.b];
const cdx=b.x-a.x,cdz=b.z-a.z;
if(cdx*cdx+cdz*cdz>64)continue;
truckCircles(a,CA);truckCircles(b,CB);
for(let i=0;i<2;i++)for(let j=0;j<2;j++){
const dx=CB[j][0]-CA[i][0],dz=CB[j][1]-CA[i][1];
const d2=dx*dx+dz*dz;
if(d2>=16||d2===0)continue;
const d=Math.sqrt(d2),nx=dx/d,nz=dz/d,ov=4-d;
a.x-=nx*ov*0.5;a.z-=nz*ov*0.5;b.x+=nx*ov*0.5;b.z+=nz*ov*0.5;
const rel=(b.vx-a.vx)*nx+(b.vz-a.vz)*nz;
if(rel>=0)continue;
const jimp=-1.35*rel*0.5;
a.vx-=nx*jimp;a.vz-=nz*jimp;b.vx+=nx*jimp;b.vz+=nz*jimp;
const impact=-rel;
if(pr.cd>0)continue;
pr.cd=0.28;
sparksAt((CA[i][0]+CB[j][0])/2,Math.max(a.y,b.y)+1.7,(CA[i][1]+CB[j][1])/2,
Math.min(26,(6+impact*1.4)|0));
if(impact>3.5){
sfxSlam(impact);
app.hype=Math.min(1,app.hype+impact*0.012);
const pa=a.vx*nx+a.vz*nz,pb=-(b.vx*nx+b.vz*nz);
const rammer=pa>=pb?a:b,victim=rammer===a?b:a;
if(!rammer.wrecked&&!victim.wrecked&&!victim.invuln){
victim.hp-=impact*3.1;victim.flash=0.25;drawPlate(victim);
if(rammer.isPlayer)addScore(Math.round(impact*45),'SLAM');
if(victim.isPlayer){app.shake=Math.min(1,app.shake+impact*0.035);flashDmg();}
else if(!rammer.isPlayer&&impact>10&&app.t-lastFeedT>2.5){
lastFeedT=app.t;popup(rammer.name+' rocked '+victim.name,true);
}
if(victim.hp<=0)doWreck(victim,rammer);
}
}else sfxThud(0.35);
}
}
}
function doWreck(t,by){
t.wrecked=true;t.wreckT=4.5;t.hp=0;t.smokeT=0;
t.tipDir=Math.random()<0.5?-1:1;
drawPlate(t);
app.hype=1;cheer();confettiAt(t.x,t.y+3,t.z);
if(by&&by.isPlayer&&!t.isPlayer){addScore(2500,'WRECK');popup('YOU put '+t.name+' out',true);}
else if(t.isPlayer){flashDmg();app.shake=1;popup(by?by.name+' put YOU out':'WRECKED',true);}
else popup((by?by.name:'THE WALL')+' wrecked '+t.name,true);
}
function respawn(t){
const a=Math.random()*TAU;
t.x=Math.cos(a)*33;t.z=Math.sin(a)*33;
t.heading=Math.atan2(-t.x,-t.z);
t.vx=t.vz=t.vy=t.vf=t.vl=0;
t.hp=100;t.wrecked=false;t.invuln=2.2;t.airT=0;
t.sus=0;t.susV=0;t.grounded=true;t.launchVy=0;
t.y=groundHeight(t.x,t.z);
t.tilt.rotation.set(0,0,0);t.bodyG.visible=true;
drawPlate(t);
}
function propsStep(dt){
for(const p of props){
p.hitCD-=dt;
if(p.type==='tires'){
if(p.wob>0){
p.wob-=dt;
p.group.rotation.z=Math.sin(p.wob*24)*0.1*p.wob;
p.group.rotation.x=Math.cos(p.wob*20)*0.08*p.wob;
}
continue;
}
if(p.type==='cone'){
if(p.vy!==0||p.y>groundHeight(p.x,p.z)+0.01){
p.vy-=32*dt;p.y+=p.vy*dt;p.x+=p.vx*dt;p.z+=p.vz*dt;
p.group.rotation.x+=p.spin*dt;p.group.rotation.z+=p.spin*0.7*dt;
if(p.vy<0&&p.y<=groundHeight(p.x,p.z)){
p.y=groundHeight(p.x,p.z);p.vy=0;p.vx=0;p.vz=0;p.spin=0;p.knocked=true;
}
}else if(p.vx||p.vz){
p.x+=p.vx*dt;p.z+=p.vz*dt;
const f=Math.exp(-dt*3);p.vx*=f;p.vz*=f;
}
}else{ /* junker */
const gh=groundHeight(p.x,p.z);
p.y+=(gh-p.y)*Math.min(1,dt*8);
p.x+=p.vx*dt;p.z+=p.vz*dt;
const f=Math.exp(-dt*1.9);p.vx*=f;p.vz*=f;
p.group.rotation.y+=p.spin*dt;p.spin*=Math.exp(-dt*2.5);
const pr2=Math.hypot(p.x,p.z);
if(pr2>BOUND-1.5){
const nx=p.x/pr2,nz=p.z/pr2;
p.x=nx*(BOUND-1.5);p.z=nz*(BOUND-1.5);
const vn=p.vx*nx+p.vz*nz;
if(vn>0){p.vx-=nx*vn*1.7;p.vz-=nz*vn*1.7;p.spin+=rnd(-1,1);}
}
}
p.group.position.set(p.x,p.y,p.z);
}
/* trucks vs props */
for(const t of trucks)for(const p of props){
const dx=p.x-t.x,dz=p.z-t.z,rr=p.radius+t.radius;
if(dx*dx+dz*dz>rr*rr)continue;
const d=Math.max(0.01,Math.hypot(dx,dz)),nx=dx/d,nz=dz/d;
if(p.type==='tires'){
t.x-=nx*(rr-d);t.z-=nz*(rr-d);
const vn=t.vx*nx+t.vz*nz;
if(vn>0){
t.vx-=nx*vn*1.8;t.vz-=nz*vn*1.8;
if(vn>6){
p.wob=0.8;sfxClang(vn);dustAt(p.x,p.y+1,p.z,6);
if(t.isPlayer){app.shake=Math.min(0.7,app.shake+0.12);app.hype=Math.min(1,app.hype+0.05);}
}
}
}else{
const m=p.mass,tot=1+m,ov=rr-d;
t.x-=nx*ov*(m/tot);t.z-=nz*ov*(m/tot);
p.x+=nx*ov*(1/tot);p.z+=nz*ov*(1/tot);
const rvn=(t.vx-p.vx)*nx+(t.vz-p.vz)*nz;
if(rvn>0){
const j=1.35*rvn/(1+1/m);
t.vx-=nx*j;t.vz-=nz*j;p.vx+=nx*j/m;p.vz+=nz*j/m;
p.spin+=(Math.random()-0.5)*rvn*0.25;
if(p.type==='junker'&&rvn>7&&p.hitCD<=0){
p.hitCD=1.2;p.crush++;
p.group.scale.y=Math.max(0.45,p.group.scale.y*0.84);
p.group.rotation.z+=(Math.random()-0.5)*0.24;
dustAt(p.x,p.y+1,p.z,10);sfxSlam(rvn*0.7);
app.hype=Math.min(1,app.hype+0.08);
if(t.isPlayer)addScore(150,'JUNKER');
}
if(p.type==='cone'&&rvn>2.5&&p.vy===0&&p.hitCD<=0){
p.hitCD=2;p.vy=4+rvn*0.5;sfxThud(0.35);
if(t.isPlayer&&!p.scored)addScore(25,'CONE');
p.scored=true;
}
}
}
}
}
/* ---------------- visuals per truck ---------------- */
function truckVisuals(dt){
for(const t of trucks){
t.root.position.set(t.x,t.y,t.z);
t.root.rotation.y=t.heading;
if(t.wrecked){
t.wreckT-=dt;
t.tilt.rotation.z=lerp(t.tilt.rotation.z,t.tipDir*2.35,1-Math.exp(-5*dt));
t.smokeT-=dt;
if(t.smokeT<=0){t.smokeT=0.08;smokeAt(t.x,t.y+2.6,t.z);}
if(t.wreckT<=0)respawn(t);
}else{
t.invuln=Math.max(0,t.invuln-dt);
t.tilt.rotation.x=t.pitch;
t.tilt.rotation.z=t.roll;
t.bodyG.visible=t.invuln>0?(Math.floor(app.t*14)%2===0):true;
}
t.bodyG.position.y=clamp(t.sus,-0.32,0.3);
for(const w of t.wheels){
if(w.front)w.grp.rotation.y=t.visSteer;
w.tire.rotation.x+=t.vf*dt/1.05;
}
t.ant.rotation.x=-0.28-Math.abs(t.vf)*0.012+Math.sin(app.t*9+t.antPh)*0.07;
if(t.flash>0){
t.flash-=dt;
t.bodyMat.emissiveIntensity=Math.max(0,t.flash*3);
}else t.bodyMat.emissiveIntensity=0;
}
}
/* ---------------- camera ---------------- */
const camP=new THREE.Vector3(),camT=new THREE.Vector3();
function updateCamera(dt){
const dx=Math.sin(player.heading),dz=Math.cos(player.heading);
const back=12.8+Math.abs(player.vf)*0.1;
camT.set(player.x-dx*back,player.y+7.4+Math.abs(player.vf)*0.04,player.z-dz*back);
camP.lerp(camT,1-Math.exp(-4.5*dt));
camera.position.copy(camP);
if(app.shake>0.002){
camera.position.x+=rnd(-1,1)*app.shake*0.5;
camera.position.y+=rnd(-1,1)*app.shake*0.35;
camera.position.z+=rnd(-1,1)*app.shake*0.5;
app.shake*=Math.exp(-5.5*dt);
}
camera.lookAt(player.x+dx*7,player.y+2.1,player.z+dz*7);
}
/* ---------------- HUD / scoring ---------------- */
function setScore(){
scoreEl.textContent=fmt(app.score);
scoreEl.classList.remove('tick');void scoreEl.offsetWidth;scoreEl.classList.add('tick');
}
function popup(txt,minor){
const d=document.createElement('div');
d.className='pop'+(minor?' minor':'');
d.innerHTML=txt;
d.style.marginLeft=rnd(-60,60)+'px';
feedEl.appendChild(d);
setTimeout(()=>d.remove(),1500);
while(feedEl.children.length>6)feedEl.firstChild.remove();
}
function addScore(n,label){app.score+=n;setScore();popup(label+' <b>+'+fmt(n)+'</b>');}
function flashDmg(){dmgEl.classList.remove('on');void dmgEl.offsetWidth;dmgEl.classList.add('on');}
function hudStep(){
const s=Math.max(0,Math.ceil(app.time));
const str=Math.floor(s/60)+':'+String(s%60).padStart(2,'0');
if(str!==lastClock){
lastClock=str;clockEl.textContent=str;
clockEl.classList.toggle('low',app.time<=10.5&&!app.over);
}
}
/* ---------------- modes ---------------- */
function togglePhoto(){
app.photo=!app.photo;
photoEl.classList.toggle('hidden',!app.photo);
if(app.photo){
$('photoScore').textContent=fmt(app.score);
if(AC){
engGain.gain.setTargetAtTime(0,AC.currentTime,0.05);
crowdGain.gain.setTargetAtTime(0.015,AC.currentTime,0.2);
}
}
}
function endRound(){
app.over=true;
cheer();confettiAt(0,6,0);confettiAt(0,6,0);
$('finalScore').textContent=fmt(app.score);
endEl.classList.remove('hidden');
clockEl.classList.remove('low');
}
function resetRound(){
app.score=0;app.time=ROUND_TIME;app.over=false;app.hype=0;app.shake=0;app.photo=false;
photoEl.classList.add('hidden');endEl.classList.add('hidden');
feedEl.innerHTML='';
lastClock=null;setScore();hudStep();
placeTrucksAtSpawns();
props.forEach(p=>scene.remove(p.group));
buildProps();
[sparksS,dustS,smokeS].forEach(s=>{
s.life.fill(0);
for(let i=0;i<s.cap;i++)s.pos[i*3+1]=-999;
s.geo.attributes.position.needsUpdate=true;
});
updateCamera(0.016);
}
/* ---------------- input ---------------- */
const keys={};
function readInput(){
return {
thr:(keys.KeyW||keys.ArrowUp?1:0)-(keys.KeyS||keys.ArrowDown?1:0),
steer:(keys.KeyD||keys.ArrowRight?1:0)-(keys.KeyA||keys.ArrowLeft?1:0),
hb:!!keys.Space
};
}
function firstGesture(){
initAudio();
if(AC&&AC.state==='suspended')AC.resume();
if(!introGone){introGone=true;introEl.classList.add('gone');}
}
addEventListener('keydown',e=>{
if(['ArrowUp','ArrowDown','ArrowLeft','ArrowRight','Space'].indexOf(e.code)>=0)e.preventDefault();
firstGesture();
if(e.repeat)return;
keys[e.code]=true;
if(e.code==='KeyP')togglePhoto();
if(e.code==='KeyR')resetRound();
});
addEventListener('keyup',e=>{keys[e.code]=false;});
addEventListener('pointerdown',firstGesture);
addEventListener('resize',()=>{
camera.aspect=innerWidth/innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth,innerHeight);
});
/* ---------------- audio per frame ---------------- */
function audioStep(){
if(!AC||!engOsc)return;
const now=AC.currentTime,spd=Math.abs(player.vf);
const active=!app.photo&&!app.over&&!player.wrecked;
engOsc.frequency.setTargetAtTime(46+spd*3.5+(lastThr>0?9:0),now,0.08);
engGain.gain.setTargetAtTime(active?0.05+Math.min(0.15,spd*0.0055)+Math.max(0,lastThr)*0.03:0,now,0.12);
crowdGain.gain.setTargetAtTime(app.photo?0.015:(app.over?0.12:0.045+app.hype*0.3),now,0.3);
}
/* ---------------- main loop ---------------- */
function step(dt){
app.t+=dt;
if(!app.over){
app.time-=dt;
if(app.time<=0){app.time=0;endRound();}
}
app.hype=Math.max(0,app.hype-dt*0.12);
const inp=readInput();lastThr=inp.thr;
driveTruck(player,inp.thr,inp.steer,inp.hb,dt);
for(const r of rivals){
if(r.wrecked)driveTruck(r,0,0,true,dt);
else{const c=aiThink(r,dt);driveTruck(r,c.thr,c.steer,c.hb,dt);}
}
truckCollisions(dt);
propsStep(dt);
truckVisuals(dt);
stepSys(sparksS,dt,26,0.8);
stepSys(dustS,dt,2.5,2.2);
stepSys(smokeS,dt,-1.2,1.0);
crowdStep();
updateCamera(dt);
hudStep();
audioStep();
}
let last=performance.now();
function loop(now){
requestAnimationFrame(loop);
const dt=Math.min((now-last)/1000,0.05);
last=now;
if(!app.photo&&!app.over)step(dt);
renderer.render(scene,camera);
}
/* ---------------- go ---------------- */
buildProps();
placeTrucksAtSpawns();
setScore();
hudStep();
camP.set(player.x-Math.sin(player.heading)*13,8,player.z-Math.cos(player.heading)*13);
updateCamera(0.016);
setTimeout(()=>{if(!introGone){introGone=true;introEl.classList.add('gone');}},5500);
requestAnimationFrame(loop);
</script>
</body>
</html>
if(/^#demo$/i.test(location.hash)){app.time=20;app.score=5000;} writes the exact two variables (app.time, app.score) that the loop decrements and accumulates all round — the HUD just reads them, so the clock genuinely ticks 0:20 → 0:00 and the round ends with the "FINAL TALLY" card like any other. There is no demo-specific rendering path at all.P stops the simulation step and every world animation — trucks, AI, clock, particles, crowd bob, wheel spin, even the engine/crowd audio duck down — because all of them live inside the one step(dt) call, which is gated off. The requestAnimationFrame loop itself survives purely to re-render the frozen frame so the card stays correct across window resizes; no world time advances until P is pressed again.impact×45), victim damage (impact×3.1 toward a 100 HP wreck state), spark count, and camera shake. Props resolve through mass-weighted impulses (cone 0.08, junker 1.3) so cones fly and junkers shove; tire pyramids are immovable 1.8× restitution bumpers; ramps and mounds are an analytic height field that hands the truck its climb-rate as launch velocity at the lip, with gravity, airtime scoring, and landing squash on the other side.