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.CYBERTRUCK MONSTER RALLY: NIGHT SHIFT AT THE ANGLE GRINDER — five stainless monster trucks tear around a floodlit dirt bowl ringed by a bobbing 6,000-strong crowd, smashing each other across mounds, wrecked sedans, tires and barrels.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>NIGHT SHIFT AT THE ANGLE GRINDER — Cybertruck Monster Rally</title>
<style>
:root{--cy:#41d9ff;--or:#ff9b2f;--st:#dfe6ee;--ink:#0c0f15}
*{margin:0;padding:0;box-sizing:border-box}
html,body{width:100%;height:100%;overflow:hidden;background:#05070b}
body{font-family:'Segoe UI',system-ui,-apple-system,Helvetica,Arial,sans-serif;color:var(--st);user-select:none;-webkit-user-select:none}
canvas{display:block}
.disp{font-family:Impact,Haettenschweiler,'Arial Black',sans-serif;font-style:italic;letter-spacing:.05em;text-transform:uppercase;font-weight:400}
.mono{font-family:Consolas,Menlo,'DejaVu Sans Mono',monospace}
.plate{background:linear-gradient(180deg,#182031,#0c1119);clip-path:polygon(0 0,100% 0,calc(100% - 14px) 100%,0 100%);padding:8px 24px 8px 14px;border-left:4px solid var(--cy)}
.plate.r{clip-path:polygon(14px 0,100% 0,100% 100%,0 100%);border-left:none;border-right:4px solid var(--or);text-align:right;padding:8px 14px 8px 24px}
#hud{position:fixed;top:0;left:0;right:0;display:flex;justify-content:space-between;align-items:flex-start;padding:14px 16px;pointer-events:none;z-index:10}
#arenaName{font-size:27px;line-height:1;color:#fff;text-shadow:0 2px 0 #000,0 0 18px rgba(65,217,255,.35)}
#arenaSub{font-size:10px;letter-spacing:.32em;color:#8fa3bf;margin-top:4px}
#clockBox{text-align:center;background:rgba(8,11,17,.72);border:1px solid #2a3648;border-top:3px solid var(--or);padding:6px 26px 8px;clip-path:polygon(12px 0,calc(100% - 12px) 0,100% 100%,0 100%)}
#clockLbl{font-size:9px;letter-spacing:.4em;color:#8fa3bf}
#clock{font-size:42px;line-height:1.05;color:#fff;text-shadow:0 0 14px rgba(255,155,47,.35)}
#clock.low{color:#ff5340;animation:pulse .5s steps(2) infinite}
@keyframes pulse{50%{opacity:.35}}
#scoreLbl{font-size:9px;letter-spacing:.4em;color:#8fa3bf}
#score{font-size:32px;line-height:1;color:var(--or);text-shadow:0 2px 0 #000}
#score.pop{animation:pop .25s ease-out}
@keyframes pop{0%{transform:scale(1.4);color:#fff}100%{transform:scale(1)}}
#ctrlBar{position:fixed;bottom:12px;left:50%;transform:translateX(-50%);z-index:10;font-size:11px;letter-spacing:.18em;color:#9db1cc;background:rgba(8,11,17,.66);border:1px solid #232e40;padding:7px 18px;clip-path:polygon(10px 0,calc(100% - 10px) 0,100% 100%,0 100%);pointer-events:none;white-space:nowrap}
#ctrlBar b{color:#fff;background:#242f42;padding:1px 7px;margin:0 2px;border:1px solid #3b4a63;border-radius:3px;font-weight:600}
#toasts{position:fixed;top:17%;left:50%;transform:translateX(-50%);z-index:12;pointer-events:none;display:flex;flex-direction:column;align-items:center;gap:6px}
.toast{font-size:22px;color:#fff;text-shadow:0 2px 0 #000,0 0 16px rgba(255,155,47,.5);background:rgba(10,13,20,.55);padding:3px 18px;clip-path:polygon(8px 0,100% 0,calc(100% - 8px) 100%,0 100%);animation:rise 1.5s ease-out forwards;white-space:nowrap}
@keyframes rise{0%{opacity:0;transform:translateY(14px) scale(.8)}12%{opacity:1;transform:none}75%{opacity:1}100%{opacity:0;transform:translateY(-28px)}}
#vignette{position:fixed;inset:0;pointer-events:none;z-index:8;opacity:0;background:radial-gradient(ellipse at center,transparent 52%,rgba(255,42,26,.6) 100%)}
#vignette.hit{animation:hitflash .45s ease-out}
@keyframes hitflash{0%{opacity:1}100%{opacity:0}}
.card{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;z-index:20}
.hidden{display:none !important}
#photoCard{background:rgba(4,6,10,.42)}
#photoPlate{background:linear-gradient(160deg,#141b28,#0a0e15);border:1px solid #33415a;border-top:5px solid var(--cy);padding:34px 60px 30px;text-align:center;clip-path:polygon(0 0,calc(100% - 34px) 0,100% 34px,100% 100%,34px 100%,0 calc(100% - 34px));box-shadow:0 30px 80px rgba(0,0,0,.65)}
#photoPlate .k{font-size:11px;letter-spacing:.5em;color:var(--cy);margin-bottom:10px}
#photoArena{font-size:54px;line-height:1;color:#fff;text-shadow:0 3px 0 #000,0 0 30px rgba(65,217,255,.4)}
#photoScore{font-size:30px;color:var(--or);margin-top:12px}
#photoPlate .h{font-size:11px;letter-spacing:.3em;color:#8fa3bf;margin-top:16px}
#endCard{background:rgba(3,5,9,.72)}
#endPlate{text-align:center;background:linear-gradient(160deg,#161d2b,#0a0e15);border:1px solid #33415a;border-top:5px solid var(--or);padding:36px 70px;clip-path:polygon(34px 0,100% 0,100% calc(100% - 34px),calc(100% - 34px) 100%,0 100%,0 34px)}
#endTitle{font-size:46px;color:#fff}
#endScore{font-size:64px;color:var(--or);margin:6px 0 2px;text-shadow:0 3px 0 #000}
#endRank{font-size:20px;color:var(--cy);letter-spacing:.12em}
#endHint{margin-top:18px;font-size:11px;letter-spacing:.3em;color:#8fa3bf}
#count{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;z-index:15;pointer-events:none;font-size:150px;color:#fff;text-shadow:0 0 40px rgba(65,217,255,.6),0 4px 0 #000;opacity:0}
#count.show{animation:cnt .9s ease-out}
@keyframes cnt{0%{opacity:0;transform:scale(1.7)}20%{opacity:1;transform:scale(1)}80%{opacity:1}100%{opacity:0}}
#fatal{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;z-index:50;background:#0a0d13;color:#ffb1a6;font-size:15px;letter-spacing:.1em;text-align:center;padding:20px}
</style>
</head>
<body>
<div id="hud">
<div class="plate"><div id="arenaName" class="disp">THE ANGLE GRINDER</div><div id="arenaSub">CYBERTRUCK MONSTER RALLY · NIGHT SHIFT</div></div>
<div id="clockBox"><div id="clockLbl" class="disp">TIME</div><div id="clock" class="mono">1:30</div></div>
<div class="plate r"><div id="scoreLbl" class="disp">SCORE</div><div id="score" class="mono">0</div></div>
</div>
<div id="toasts"></div>
<div id="ctrlBar" class="disp"><b>W A S D</b> / <b>▲◀▼▶</b> DRIVE · <b>SPACE</b> HANDBRAKE · <b>P</b> PHOTO · <b>M</b> SOUND · <b>R</b> RESTART</div>
<div id="vignette"></div>
<div id="photoCard" class="card hidden"><div id="photoPlate">
<div class="k disp">PHOTO MODE</div>
<div id="photoArena" class="disp">THE ANGLE GRINDER</div>
<div id="photoScore" class="mono">SCORE 0</div>
<div class="h">SIMULATION FROZEN — PRESS <b>P</b> TO RESUME</div>
</div></div>
<div id="endCard" class="card hidden"><div id="endPlate">
<div id="endTitle" class="disp">FULL TIME</div>
<div id="endScore" class="mono">0</div>
<div id="endRank" class="disp">STAINLESS LEGEND</div>
<div id="endHint">PRESS <b>R</b> TO RUN IT BACK</div>
</div></div>
<div id="count" class="disp"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
(function(){
'use strict';
var $=function(id){return document.getElementById(id);};
if(!window.THREE){$('body')&&document.body.insertAdjacentHTML('beforeend','<div id="fatal">THE ANGLE GRINDER needs one network fetch — three.js from cdnjs — then it runs offline forever. Reload once you have signal.</div>');return;}
var TAU=Math.PI*2;
function clamp(v,a,b){return v<a?a:v>b?b:v;}
function rand(a,b){return a+Math.random()*(b-a);}
function angDiff(a,b){var d=(b-a)%TAU;if(d>Math.PI)d-=TAU;if(d<-Math.PI)d+=TAU;return d;}
function fmt(t){t=Math.max(0,Math.ceil(t));return Math.floor(t/60)+':'+('0'+(t%60)).slice(-2);}
/* ---------- constants & state ---------- */
var ARENA='THE ANGLE GRINDER';
var R_WALL=39.6, TIER0=45, TIER_W=5, TIERS=6, TIER_H=2.4;
var TRUCK_R=2.55, NORMAL=90, MATCH=90;
var DEMO=(location.hash||'').toLowerCase().indexOf('#demo')>=0;
var S={phase:DEMO?'play':'count',countT:3.2,lastNum:'',timeLeft:MATCH,score:0,photo:false,muted:false,simTime:0,shake:0,cheer:0,cheerT:0};
var KEY={u:0,d:0,l:0,r:0,hb:0};
/* ---------- renderer / scene ---------- */
var renderer=new THREE.WebGLRenderer({antialias:true});
renderer.setPixelRatio(Math.min(window.devicePixelRatio||1,1.75));
renderer.setSize(innerWidth,innerHeight);
renderer.shadowMap.enabled=true;
renderer.shadowMap.type=THREE.PCFSoftShadowMap;
renderer.outputEncoding=THREE.sRGBEncoding;
renderer.toneMapping=THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure=1.12;
document.body.appendChild(renderer.domElement);
var scene=new THREE.Scene();
scene.background=new THREE.Color(0x05070c);
scene.fog=new THREE.Fog(0x05070c,110,260);
var camera=new THREE.PerspectiveCamera(62,innerWidth/innerHeight,0.5,400);
/* ---------- canvas textures ---------- */
function canvasTex(size,fn){var c=document.createElement('canvas');c.width=c.height=size;fn(c.getContext('2d'),size);var t=new THREE.CanvasTexture(c);t.encoding=THREE.sRGBEncoding;t.anisotropy=renderer.capabilities.getMaxAnisotropy();return t;}
var dirtTex=canvasTex(512,function(g,s){
g.fillStyle='#6f5a3e';g.fillRect(0,0,s,s);
var cols=['#5d4a33','#7d684a','#54432e','#8a7454'];
for(var i=0;i<2600;i++){g.fillStyle=cols[(Math.random()*4)|0];g.globalAlpha=.12+Math.random()*.25;var r=1+Math.random()*3;g.fillRect(Math.random()*s,Math.random()*s,r,r);}
g.globalAlpha=.07;g.strokeStyle='#3c2f1f';g.lineWidth=6;
for(i=0;i<14;i++){g.beginPath();var a=Math.random()*TAU,r0=8+Math.random()*26;g.arc(s/2,s/2,r0*6,a,a+rand(1,3));g.stroke();}
g.globalAlpha=.5;g.strokeStyle='#e8d9b0';g.lineWidth=3;g.beginPath();g.arc(s/2,s/2,120,0,TAU);g.stroke();
g.beginPath();g.arc(s/2,s/2,66,0,TAU);g.stroke();
g.fillStyle='#e8d9b0';g.textAlign='center';g.globalAlpha=.55;
g.font='italic 900 44px Impact,Arial Black,sans-serif';g.fillText('THE ANGLE',s/2,s/2-8);g.fillText('GRINDER',s/2,s/2+40);
g.globalAlpha=.4;g.font='italic 900 16px Impact,sans-serif';g.fillText('CYBERTRUCK MONSTER RALLY',s/2,s/2+70);
g.globalAlpha=1;
});
var wallTex=canvasTex(256,function(g,s){
g.fillStyle='#151a22';g.fillRect(0,0,s,s);
g.fillStyle='#ffb02e';
for(var x=-s;x<s*2;x+=64){g.beginPath();g.moveTo(x,s);g.lineTo(x+32,0);g.lineTo(x+64,0);g.lineTo(x+32,s);g.closePath();g.fill();}
g.fillStyle='rgba(10,12,16,.55)';g.fillRect(0,0,s,26);g.fillRect(0,s-30,s,30);
g.fillStyle='#e8edf4';g.font='italic 900 46px Impact,Arial Black,sans-serif';g.textAlign='center';
g.fillText('ANGLE GRINDER',s/2,s/2+16);
});
wallTex.wrapS=THREE.RepeatWrapping;wallTex.repeat.set(22,1);
/* ---------- lights ---------- */
scene.add(new THREE.HemisphereLight(0x39506b,0x241c12,0.85));
var sun=new THREE.DirectionalLight(0xcfe0ff,0.55);
sun.position.set(35,70,18);sun.castShadow=true;
sun.shadow.mapSize.set(2048,2048);
sun.shadow.camera.left=-55;sun.shadow.camera.right=55;sun.shadow.camera.top=55;sun.shadow.camera.bottom=-55;
sun.shadow.camera.near=10;sun.shadow.camera.far=160;sun.shadow.bias=-0.0006;
scene.add(sun);
var TOWERS=[];
for(var ti=0;ti<4;ti++){
var ta=ti*Math.PI/2+Math.PI/4;
var tx=Math.cos(ta)*78,tz=Math.sin(ta)*78;
TOWERS.push({x:tx,z:tz});
var sp=new THREE.SpotLight(0xfff1d6,1.15,0,0.62,0.75,1);
sp.position.set(tx,26,tz);
sp.target.position.set(tx*-0.18,0,tz*-0.18);
scene.add(sp);scene.add(sp.target);
}
/* ---------- floor & wall ---------- */
var floor=new THREE.Mesh(new THREE.CircleGeometry(46,64),new THREE.MeshLambertMaterial({map:dirtTex}));
floor.rotation.x=-Math.PI/2;floor.receiveShadow=true;scene.add(floor);
var wall=new THREE.Mesh(new THREE.CylinderGeometry(R_WALL+1.7,R_WALL+2.3,3.4,72,1,true),new THREE.MeshLambertMaterial({map:wallTex,side:THREE.DoubleSide}));
wall.position.y=1.7;scene.add(wall);
var rim=new THREE.Mesh(new THREE.TorusGeometry(R_WALL+1.9,0.18,8,72),new THREE.MeshBasicMaterial({color:0x41d9ff}));
rim.rotation.x=Math.PI/2;rim.position.y=3.5;scene.add(rim);
/* ---------- stands, towers, crowd ---------- */
var tierMat=new THREE.MeshLambertMaterial({color:0x252e3d});
for(var i=0;i<TIERS;i++){
var m=new THREE.Mesh(new THREE.CylinderGeometry(TIER0+TIER_W*(i+1),TIER0+TIER_W*(i+1)+0.6,TIER_H,72),tierMat);
m.position.y=TIER_H*(i+0.5);m.receiveShadow=true;scene.add(m);
}
var outer=new THREE.Mesh(new THREE.CylinderGeometry(TIER0+TIER_W*TIERS+1.5,TIER0+TIER_W*TIERS+1.5,20,72,1,true),new THREE.MeshLambertMaterial({color:0x0c1017,side:THREE.DoubleSide}));
outer.position.y=10;scene.add(outer);
var mastMat=new THREE.MeshLambertMaterial({color:0x11151c});
TOWERS.forEach(function(t){
var mast=new THREE.Mesh(new THREE.BoxGeometry(1.4,26,1.4),mastMat);
mast.position.set(t.x,13,t.z);scene.add(mast);
var head=new THREE.Mesh(new THREE.BoxGeometry(6,2.2,1.6),new THREE.MeshBasicMaterial({color:0xfff3cf}));
head.position.set(t.x,26,t.z);head.lookAt(0,2,0);scene.add(head);
var cone=new THREE.Mesh(new THREE.ConeGeometry(13,34,20,1,true),new THREE.MeshBasicMaterial({color:0xffedc2,transparent:true,opacity:0.05,blending:THREE.AdditiveBlending,depthWrite:false,side:THREE.DoubleSide}));
cone.position.set(t.x,26,t.z);cone.lookAt(0,0,0);cone.rotateX(-Math.PI/2);cone.translateY(-17);scene.add(cone);
});
var uTime={value:0};
function mergeGeos(a,b){
var A=a.toNonIndexed(),B=b.toNonIndexed(),g=new THREE.BufferGeometry();
g.setAttribute('position',new THREE.Float32BufferAttribute(A.attributes.position.array.length+B.attributes.position.array.length,3));
var p=new Float32Array(A.attributes.position.array.length+B.attributes.position.array.length);
p.set(A.attributes.position.array,0);p.set(B.attributes.position.array,A.attributes.position.array.length);
var n=new Float32Array(p.length);n.set(A.attributes.normal.array,0);n.set(B.attributes.normal.array,A.attributes.normal.array.length);
g.setAttribute('position',new THREE.BufferAttribute(p,3));
g.setAttribute('normal',new THREE.BufferAttribute(n,3));
return g;
}
var bodyG=new THREE.BoxGeometry(0.5,0.75,0.32);bodyG.translate(0,0.375,0);
var headG=new THREE.SphereGeometry(0.17,6,5);headG.translate(0,0.92,0);
var personG=mergeGeos(bodyG,headG);
var crowdMeshes=[];
function crowdMat(){
var m=new THREE.MeshLambertMaterial({color:0xffffff});
m.onBeforeCompile=function(sh){
sh.uniforms.uTime=uTime;
sh.vertexShader='uniform float uTime;\n'+sh.vertexShader.replace('#include <begin_vertex>',
'#include <begin_vertex>\n'+
'float cwPh=instanceMatrix[3][0]*0.53+instanceMatrix[3][2]*0.71;\n'+
'float cwWave=sin(uTime*1.7+instanceMatrix[3][0]*0.16+instanceMatrix[3][2]*0.11);\n'+
'transformed.y+=(0.05+0.16*max(cwWave,0.0))*sin(uTime*4.2+cwPh);\n'+
'transformed.x+=0.05*sin(uTime*2.6+cwPh*1.3);');
};
return m;
}
(function buildCrowd(){
var pts=[],ti2,ri,row;
for(ti2=0;ti2<TIERS;ti2++){
var yT=TIER_H*(ti2+1),rOut=TIER0+TIER_W*(ti2+1);
for(row=0;row<3;row++){
var rr=rOut-1.1-row*1.55,n=Math.floor(TAU*rr/0.95);
for(var k=0;k<n;k++){
var a=k/n*TAU+rand(-0.02,0.02)+ti2*0.13;
pts.push({x:Math.cos(a)*rr+rand(-0.2,0.2),z:Math.sin(a)*rr+rand(-0.2,0.2),y:yT});
}
}
}
var per=Math.ceil(pts.length/3),dummy=new THREE.Object3D(),c=new THREE.Color();
var PAL=[0xff9b2f,0x41d9ff,0xf2f4f7,0xff5a4e,0xffd23e,0x6fe08c,0xb48cff,0xff7ab0,0x8fa3bf];
for(var g=0;g<3;g++){
var slice=pts.slice(g*per,(g+1)*per);
var im=new THREE.InstancedMesh(personG,crowdMat(),slice.length);
for(var j=0;j<slice.length;j++){
dummy.position.set(slice[j].x,slice[j].y,slice[j].z);
dummy.rotation.set(0,Math.atan2(-slice[j].x,-slice[j].z)+rand(-0.35,0.35),0);
dummy.scale.setScalar(rand(0.85,1.15));
dummy.updateMatrix();im.setMatrixAt(j,dummy.matrix);
if(im.setColorAt){c.setHex(PAL[(Math.random()*PAL.length)|0]).multiplyScalar(rand(0.75,1.05));im.setColorAt(j,c);}
}
if(im.instanceColor)im.instanceColor.needsUpdate=true;
im.frustumCulled=false;scene.add(im);crowdMeshes.push(im);
}
})();
/* ---------- terrain mounds ---------- */
var MOUNDS=[{x:14,z:8,r:7,h:2.6},{x:-16,z:12,r:8,h:3.0},{x:-8,z:-18,r:6.5,h:2.3},{x:18,z:-14,r:7.5,h:2.8},{x:0,z:22,r:6,h:2.1},{x:-24,z:-4,r:5.5,h:1.9}];
var moundMat=new THREE.MeshLambertMaterial({color:0x7a6244});
MOUNDS.forEach(function(m){
var mesh=new THREE.Mesh(new THREE.SphereGeometry(1,22,10,0,TAU,0,Math.PI/2),moundMat);
mesh.scale.set(m.r,m.h,m.r);mesh.position.set(m.x,0,m.z);
mesh.castShadow=true;mesh.receiveShadow=true;scene.add(mesh);
});
function groundH(x,z){
var h=0;
for(var i=0;i<MOUNDS.length;i++){
var m=MOUNDS[i],dx=x-m.x,dz=z-m.z,d=Math.sqrt(dx*dx+dz*dz);
if(d<m.r)h+=m.h*0.5*(1+Math.cos(Math.PI*d/m.r));
}
return h;
}
/* ---------- obstacles ---------- */
var WRECKS=[],TIRES=[],BARRELS=[];
var rustCols=[0x7a4a2e,0x5e6d78,0x8a8f4a,0x6e3b3b,0x4a5a66];
for(i=0;i<5;i++){
var wa=rand(0,TAU),wr=rand(10,26);
var wx=Math.cos(wa)*wr,wz=Math.sin(wa)*wr;
var g2=new THREE.Group();
var cm=new THREE.MeshLambertMaterial({color:rustCols[i%rustCols.length]});
var b1=new THREE.Mesh(new THREE.BoxGeometry(4.2,1.0,1.9),cm);b1.position.y=0.5;b1.castShadow=true;g2.add(b1);
var b2=new THREE.Mesh(new THREE.BoxGeometry(2.1,0.75,1.7),cm);b2.position.set(-0.3,1.3,0);b2.rotation.z=rand(-0.15,0.15);b2.castShadow=true;g2.add(b2);
g2.position.set(wx,groundH(wx,wz),wz);g2.rotation.y=rand(0,TAU);g2.rotation.z=rand(-0.06,0.06);
scene.add(g2);
WRECKS.push({x:wx,z:wz,r:2.35,mesh:g2,rock:0,baseZ:0});
}
var tireGeo=new THREE.CylinderGeometry(0.85,0.85,0.55,14);
var tireMat=new THREE.MeshLambertMaterial({color:0x14171c});
for(i=0;i<12;i++){
var ta2=rand(0,TAU),tr2=rand(8,30);
var tx2=Math.cos(ta2)*tr2,tz2=Math.sin(ta2)*tr2;
var tm=new THREE.Mesh(tireGeo,tireMat);tm.castShadow=true;scene.add(tm);
TIRES.push({mesh:tm,x:tx2,z:tz2,y:0.3,vx:0,vy:0,vz:0,spin:0,cd:0});
}
var barGeo=new THREE.CylinderGeometry(0.5,0.5,1.25,12);
var barMat=new THREE.MeshLambertMaterial({color:0xc46a1d});
for(i=0;i<8;i++){
var ba=rand(0,TAU),br=rand(9,28);
var bx=Math.cos(ba)*br,bz=Math.sin(ba)*br;
var bm=new THREE.Mesh(barGeo,barMat);bm.castShadow=true;scene.add(bm);
BARRELS.push({mesh:bm,x:bx,z:bz,y:0.65,vx:0,vy:0,vz:0,cd:0});
}
/* ---------- trucks ---------- */
var truckShape=new THREE.Shape();
truckShape.moveTo(2.75,0.55);truckShape.lineTo(2.75,1.02);truckShape.lineTo(0.25,2.12);
truckShape.lineTo(-2.7,1.32);truckShape.lineTo(-2.7,0.55);truckShape.closePath();
var truckGeo=new THREE.ExtrudeGeometry(truckShape,{depth:2.3,bevelEnabled:false});
truckGeo.translate(0,0,-1.15);
var glassGeo=new THREE.ExtrudeGeometry((function(){var s=new THREE.Shape();s.moveTo(2.42,1.1);s.lineTo(0.42,1.98);s.lineTo(0.42,1.1);s.closePath();return s;})(),{depth:2.34,bevelEnabled:false});
glassGeo.translate(0,0,-1.17);
var glassMat=new THREE.MeshLambertMaterial({color:0x0a0f16});
var wheelGeo=new THREE.CylinderGeometry(1.06,1.06,0.62,18);wheelGeo.rotateX(Math.PI/2);
var wheelMat=new THREE.MeshLambertMaterial({color:0x101215});
var hubGeo=new THREE.CylinderGeometry(0.5,0.5,0.66,6);hubGeo.rotateX(Math.PI/2);
var hubMat=new THREE.MeshLambertMaterial({color:0x9aa3ad});
var RIVALS=[
{name:'BEDLAM', color:0xff5a3c, pers:'brawler', max:18},
{name:'CHROME DOME', color:0xf2f5f8, pers:'bully', max:19},
{name:'PANEL WRECK', color:0xffc531, pers:'scrapper',max:17},
{name:'OVERHANG', color:0x8f6bff, pers:'showboat',max:18}
];
var trucks=[];
function buildTruck(def,isPlayer,ang){
var grp=new THREE.Group();
var mat=new THREE.MeshStandardMaterial({color:isPlayer?0xc9ced6:def.color,metalness:0.85,roughness:0.35,emissive:0x000000});
var body=new THREE.Mesh(truckGeo,mat);body.castShadow=true;grp.add(body);
var glass=new THREE.Mesh(glassGeo,glassMat);grp.add(glass);
var lb=new THREE.Mesh(new THREE.BoxGeometry(0.1,0.12,2.1),new THREE.MeshBasicMaterial({color:0xeaf6ff}));
lb.position.set(2.72,1.0,0);grp.add(lb);
var tl=new THREE.Mesh(new THREE.BoxGeometry(0.1,0.14,2.1),new THREE.MeshBasicMaterial({color:0xff2d1e}));
tl.position.set(-2.72,1.18,0);grp.add(tl);
if(isPlayer){
var stripe=new THREE.Mesh(new THREE.BoxGeometry(4.6,0.16,0.06),new THREE.MeshBasicMaterial({color:0x41d9ff}));
stripe.position.set(0,0.95,1.19);grp.add(stripe);
var stripe2=stripe.clone();stripe2.position.z=-1.19;grp.add(stripe2);
}
var wheels=[];
[[1.8,1.32],[1.8,-1.32],[-1.8,1.32],[-1.8,-1.32]].forEach(function(p){
var w=new THREE.Mesh(wheelGeo,wheelMat);w.position.set(p[0],1.06,p[1]);
var hub=new THREE.Mesh(hubGeo,hubMat);w.add(hub);
grp.add(w);wheels.push(w);
});
scene.add(grp);
var t={name:isPlayer?'YOU':def.name,isPlayer:isPlayer,grp:grp,mat:mat,body:body,wheels:wheels,
x:Math.cos(ang)*24,z:Math.sin(ang)*24,heading:ang+Math.PI,vx:0,vz:0,
max:isPlayer?21:def.max,accel:isPlayer?26:20,
integrity:100,wrecked:false,wreckTilt:0,stun:0,flash:0,spinVel:0,
think:rand(0,0.5),wp:null,chargeT:rand(3,6),retreat:0,retX:0,retZ:0,pairCd:{},scrapCd:0,pers:def.pers};
trucks.push(t);
return t;
}
buildTruck(null,true,Math.PI/2);
for(i=0;i<4;i++)buildTruck(RIVALS[i],false,i*Math.PI/2);
var player=trucks[0];
/* ---------- particles ---------- */
function makePool(n,size,blend,op){
var geo=new THREE.BufferGeometry();
var pos=new Float32Array(n*3),col=new Float32Array(n*3);
for(var p=0;p<n;p++)pos[p*3+1]=-999;
geo.setAttribute('position',new THREE.BufferAttribute(pos,3).setUsage(THREE.DynamicDrawUsage));
geo.setAttribute('color',new THREE.BufferAttribute(col,3));
var pts=new THREE.Points(geo,new THREE.PointsMaterial({size:size,vertexColors:true,transparent:true,opacity:op,blending:blend,depthWrite:false}));
pts.frustumCulled=false;scene.add(pts);
return {geo:geo,pos:pos,col:col,vel:new Float32Array(n*3),life:new Float32Array(n),max:new Float32Array(n),n:n,cur:0};
}
var sparks=makePool(240,0.45,THREE.AdditiveBlending,1);
var smoke=makePool(140,1.15,THREE.NormalBlending,0.3);
function spawnPool(P,x,y,z,count,spd,up,life,cr,cg,cb,grav){
for(var k=0;k<count;k++){
var i=P.cur;P.cur=(P.cur+1)%P.n;
P.pos[i*3]=x;P.pos[i*3+1]=y;P.pos[i*3+2]=z;
var a=rand(0,TAU),r=rand(0.2,1)*spd;
P.vel[i*3]=Math.cos(a)*r;P.vel[i*3+1]=rand(0.3,1)*up;P.vel[i*3+2]=Math.sin(a)*r;
P.life[i]=P.max[i]=rand(0.6,1)*life;
P.col[i*3]=cr;P.col[i*3+1]=cg;P.col[i*3+2]=cb;
P.vel[i*3+1]+=grav||0;
}
}
function updatePool(P,dt,grav){
for(var i=0;i<P.n;i++){
if(P.life[i]<=0)continue;
P.life[i]-=dt;
if(P.life[i]<=0){P.pos[i*3+1]=-999;continue;}
P.vel[i*3+1]-=grav*dt;
P.pos[i*3]+=P.vel[i*3]*dt;P.pos[i*3+1]+=P.vel[i*3+1]*dt;P.pos[i*3+2]+=P.vel[i*3+2]*dt;
var f=P.life[i]/P.max[i];
var base=i*3;
if(P===sparks){P.col[base]*=f;P.col[base+1]*=f;P.col[base+2]*=f;}
}
P.geo.attributes.position.needsUpdate=true;
P.geo.attributes.color.needsUpdate=true;
}
/* ---------- audio (all procedural) ---------- */
var AU={ctx:null,ok:false};
function initAudio(){
if(AU.ctx)return;
try{
var C=window.AudioContext||window.webkitAudioContext;if(!C)return;
AU.ctx=new C();
var master=AU.ctx.createGain();master.gain.value=0.5;master.connect(AU.ctx.destination);AU.master=master;
var nb=AU.ctx.createBuffer(1,AU.ctx.sampleRate,AU.ctx.sampleRate),nd=nb.getChannelData(0);
for(var a2=0;a2<nd.length;a2++)nd[a2]=Math.random()*2-1;
AU.noise=nb;
var crowdSrc=AU.ctx.createBufferSource();crowdSrc.buffer=nb;crowdSrc.loop=true;
var bp=AU.ctx.createBiquadFilter();bp.type='bandpass';bp.frequency.value=850;bp.Q.value=0.6;
AU.crowdGain=AU.ctx.createGain();AU.crowdGain.gain.value=0.045;
crowdSrc.connect(bp);bp.connect(AU.crowdGain);AU.crowdGain.connect(master);crowdSrc.start();
AU.osc=AU.ctx.createOscillator();AU.osc.type='sawtooth';AU.osc.frequency.value=50;
AU.sub=AU.ctx.createOscillator();AU.sub.type='triangle';AU.sub.frequency.value=25;
AU.engGain=AU.ctx.createGain();AU.engGain.gain.value=0;
var lp=AU.ctx.createBiquadFilter();lp.type='lowpass';lp.frequency.value=900;
AU.osc.connect(lp);AU.sub.connect(lp);lp.connect(AU.engGain);AU.engGain.connect(master);
AU.osc.start();AU.sub.start();
AU.ok=true;
}catch(e){AU.ok=false;}
}
function audioTick(dt){
if(!AU.ok)return;
if(AU.ctx.state==='suspended'){AU.ctx.resume();}
var sp=Math.sqrt(player.vx*player.vx+player.vz*player.vz);
AU.osc.frequency.value=45+sp*7+(S.photo?0:Math.sin(S.simTime*30)*1.5);
AU.sub.frequency.value=22+sp*3.5;
AU.engGain.gain.value=(S.phase==='play'&&!S.photo)?0.028+sp*0.0016:0;
var cheer=0.045+S.cheer*0.14;
AU.crowdGain.gain.value+=(cheer-AU.crowdGain.gain.value)*Math.min(1,dt*3);
}
function thud(p){
if(!AU.ok)return;
try{
var t=AU.ctx.currentTime;
var o=AU.ctx.createOscillator(),g=AU.ctx.createGain();
o.type='sine';o.frequency.setValueAtTime(90,t);o.frequency.exponentialRampToValueAtTime(38,t+0.18);
g.gain.setValueAtTime(Math.min(0.5,p*0.05),t);g.gain.exponentialRampToValueAtTime(0.001,t+0.22);
o.connect(g);g.connect(AU.master);o.start(t);o.stop(t+0.25);
var s=AU.ctx.createBufferSource();s.buffer=AU.noise;
var hp=AU.ctx.createBiquadFilter();hp.type='highpass';hp.frequency.value=1800;
var g2=AU.ctx.createGain();g2.gain.setValueAtTime(Math.min(0.3,p*0.02),t);g2.gain.exponentialRampToValueAtTime(0.001,t+0.08);
s.connect(hp);hp.connect(g2);g2.connect(AU.master);s.start(t);s.stop(t+0.1);
}catch(e){}
}
function cheerSfx(){
if(!AU.ok)return;
try{
var t=AU.ctx.currentTime,s=AU.ctx.createBufferSource();s.buffer=AU.noise;
var bp=AU.ctx.createBiquadFilter();bp.type='bandpass';bp.frequency.setValueAtTime(900,t);bp.frequency.linearRampToValueAtTime(1600,t+0.4);bp.Q.value=0.8;
var g=AU.ctx.createGain();g.gain.setValueAtTime(0.001,t);g.gain.linearRampToValueAtTime(0.22,t+0.15);g.gain.exponentialRampToValueAtTime(0.001,t+1.1);
s.connect(bp);bp.connect(g);g.connect(AU.master);s.start(t);s.stop(t+1.2);
}catch(e){}
}
/* ---------- HUD / fx helpers ---------- */
var scoreEl=$('score'),clockEl=$('clock'),toastsEl=$('toasts');
function toast(txt){
var d=document.createElement('div');d.className='toast disp';d.textContent=txt;
toastsEl.appendChild(d);
setTimeout(function(){d.remove();},1600);
while(toastsEl.children.length>4)toastsEl.firstChild.remove();
}
function addScore(n){
S.score+=n;
scoreEl.textContent=S.score;
scoreEl.classList.remove('pop');void scoreEl.offsetWidth;scoreEl.classList.add('pop');
}
function vignette(){var v=$('vignette');v.classList.remove('hit');void v.offsetWidth;v.classList.add('hit');}
function cheer(amt){S.cheer=Math.min(1,S.cheer+amt);S.cheerT=Math.max(S.cheerT,1.2);}
/* ---------- combat ---------- */
function slam(att,def,imp){
def.flash=0.35;
def.retreat=1.4;def.retX=att.x;def.retZ=att.z;
def.spinVel+=rand(-3,3)*Math.min(1,imp/12);
cheer(0.25);thud(imp);
if(def.isPlayer){
S.shake=Math.min(1.6,S.shake+imp*0.05);
player.stun=0.45;vignette();
toast(def.name+' SLAMMED YOU!');
return;
}
if(att.isPlayer){
addScore(150);
def.integrity-=imp*4;
toast('+150 SLAM — '+def.name);
if(def.integrity<=0&&!def.wrecked)wreck(def);
}
}
function wreck(t){
t.wrecked=true;t.wreckTilt=0;t.vx*=0.2;t.vz*=0.2;
addScore(600);cheer(0.6);cheerSfx();
toast('+600 '+t.name+' WRECKED!');
spawnPool(sparks,t.x,2,t.z,26,9,7,0.7,1,0.75,0.3,22);
}
/* ---------- AI ---------- */
function pickWP(t){
var base=Math.atan2(t.z,t.x),a=base+rand(-1.2,1.2),r=rand(12,33);
return {x:Math.cos(a)*r,z:Math.sin(a)*r};
}
function nearestTruck(t){
var best=null,bd=1e9;
for(var i=0;i<trucks.length;i++){
var o=trucks[i];if(o===t||o.wrecked)continue;
var d=(o.x-t.x)*(o.x-t.x)+(o.z-t.z)*(o.z-t.z);
if(d<bd){bd=d;best=o;}
}
return best;
}
function nearestScrap(t){
var best=null,bd=1e9,i,d;
for(i=0;i<TIRES.length;i++){var o=TIRES[i];d=(o.x-t.x)*(o.x-t.x)+(o.z-t.z)*(o.z-t.z);if(d<bd){bd=d;best=o;}}
for(i=0;i<BARRELS.length;i++){var b=BARRELS[i];d=(b.x-t.x)*(b.x-t.x)+(b.z-t.z)*(b.z-t.z);if(d<bd){bd=d;best=b;}}
return best;
}
function aiThink(t){
if(t.retreat>0)return;
switch(t.pers){
case 'brawler':t.tg=nearestTruck(t);break;
case 'bully':t.tg=player;break;
case 'scrapper':
if(Math.random()<0.3)t.tg=nearestTruck(t);
else{var s=nearestScrap(t);t.tg=s?{x:s.x,z:s.z}:nearestTruck(t);}
break;
case 'showboat':
t.chargeT-=0.5;
if(t.chargeT<=0){t.tg=nearestTruck(t);if(Math.random()<0.5)t.chargeT=6;}
else if(!t.wp||Math.random()<0.25)t.wp=pickWP(t);
break;
}
}
function aiDrive(t,dt){
t.think-=dt;t.retreat-=dt;t.scrapCd-=dt;
if(t.think<=0){aiThink(t);t.think=0.4+Math.random()*0.4;}
var tx,tz;
if(t.retreat>0){tx=t.x+(t.x-t.retX);tz=t.z+(t.z-t.retZ);}
else if(t.tg){tx=t.tg.x;t.tg.z!==undefined&&(tz=t.tg.z);}
if(tx===undefined&&t.wp){tx=t.wp.x;tz=t.wp.z;if((tx-t.x)*(tx-t.x)+(tz-t.z)*(tz-t.z)<25)t.wp=null;}
if(tx===undefined){t.wp=pickWP(t);tx=t.wp.x;tz=t.wp.z;}
var dx=tx-t.x,dz=tz-t.z,dist=Math.sqrt(dx*dx+dz*dz)||1;
var ux=dx/dist,uz=dz/dist;
for(var i=0;i<trucks.length;i++){
var o=trucks[i];if(o===t)continue;
var ox=t.x-o.x,oz=t.z-o.z,od=Math.sqrt(ox*ox+oz*oz);
var sep=(o===t.tg)?3.5:8;
if(od<sep&&od>0.01){var w=(sep-od)/sep*1.6;ux+=ox/od*w;uz+=oz/od*w;}
}
var cd=Math.sqrt(t.x*t.x+t.z*t.z);
if(cd>34){ux-=t.x/cd*1.5;uz-=t.z/cd*1.5;}
var desired=Math.atan2(uz,ux);
var steer=clamp(angDiff(t.heading,desired)*2.5,-1,1);
var throttle=1;
if(Math.abs(angDiff(t.heading,desired))>2.2)throttle=0.3;
driveTruck(t,throttle,steer,false,dt);
}
/* ---------- driving & physics ---------- */
function driveTruck(t,throttle,steer,hb,dt){
if(t.wrecked){t.vx*=Math.exp(-2*dt);t.vz*=Math.exp(-2*dt);t.heading+=t.spinVel*dt;t.spinVel*=Math.exp(-2*dt);return;}
var sp0=Math.sqrt(t.vx*t.vx+t.vz*t.vz);
var turn=steer*2.4*clamp(sp0/6,0,1)*(t.vx*Math.cos(t.heading)+t.vz*Math.sin(t.heading)<-0.5?-1:1);
t.heading+=turn*dt+t.spinVel*dt;
t.spinVel*=Math.exp(-2.5*dt);
var fx=Math.cos(t.heading),fz=Math.sin(t.heading);
var stunK=t.isPlayer&&t.stun>0?0.35:1;
t.vx+=fx*throttle*t.accel*stunK*dt;
t.vz+=fz*throttle*t.accel*stunK*dt;
var f=t.vx*fx+t.vz*fz,lat=t.vx*-fz+t.vz*fx;
lat*=Math.exp(-(hb?2.0:7.0)*dt);
f*=Math.exp(-0.25*dt);
var mx=throttle<0?t.max*0.45:t.max;
if(f>mx)f+=(mx-f)*Math.min(1,dt*3);
if(f<-t.max*0.45)f+=(-t.max*0.45-f)*Math.min(1,dt*3);
t.vx=fx*f+(-fz)*lat;t.vz=fz*f+fx*lat;
}
function integrate(t,dt){
t.x+=t.vx*dt;t.z+=t.vz*dt;
var cd=Math.sqrt(t.x*t.x+t.z*t.z);
if(cd>R_WALL){
var nx=t.x/cd,nz=t.z/cd;
t.x=nx*R_WALL;t.z=nz*R_WALL;
var vn=t.vx*nx+t.vz*nz;
if(vn>0){t.vx-=1.6*vn*nx;t.vz-=1.6*vn*nz;if(vn>5){spawnPool(sparks,t.x,1.5,t.z,8,6,5,0.5,1,0.8,0.4,22);thud(vn*0.6);}}
}
var gh=groundH(t.x,t.z);
t.grp.position.set(t.x,gh,t.z);
var e=0.9,h1=groundH(t.x+e,t.z),h2=groundH(t.x-e,t.z),h3=groundH(t.x,t.z+e),h4=groundH(t.x,t.z-e);
t.grp.rotation.set(Math.atan2(h3-h4,2*e),t.heading,Math.atan2(h2-h1,2*e),'YXZ');
if(t.wrecked){t.wreckTilt=Math.min(0.55,t.wreckTilt+dt*1.2);t.grp.rotateZ(t.wreckTilt);t.grp.position.y-=t.wreckTilt*0.5;}
var sp=Math.sqrt(t.vx*t.vx+t.vz*t.vz);
for(var w=0;w<4;w++)t.wheels[w].rotation.x+=sp*dt/1.06;
if(t.flash>0){t.flash-=dt;t.mat.emissive.setRGB(t.flash*3,t.flash*2.2,t.flash*1.2);}
else t.mat.emissive.setRGB(0,0,0);
if(t.wrecked&&Math.random()<dt*7)spawnPool(smoke,t.x-fxOf(t)*1.5,gh+2,t.z,1,1.2,2.2,1.6,0.16,0.16,0.18,-2);
}
function fxOf(t){return Math.cos(t.heading);}
function collideTrucks(dt){
for(var i=0;i<trucks.length;i++)for(var j=i+1;j<trucks.length;j++){
var a=trucks[i],b=trucks[j];
var dx=b.x-a.x,dz=b.z-a.z,d=Math.sqrt(dx*dx+dz*dz),min=TRUCK_R*2;
if(d>=min||d<0.001)continue;
var nx=dx/d,nz=dz/d,push=(min-d)/2;
a.x-=nx*push;a.z-=nz*push;b.x+=nx*push;b.z+=nz*push;
var rv=(b.vx-a.vx)*nx+(b.vz-a.vz)*nz;
if(rv<0){
var imp=-rv,jm=-(1+0.55)*rv/2;
a.vx-=nx*jm;a.vz-=nz*jm;b.vx+=nx*jm;b.vz+=nz*jm;
var key=i+'_'+j;a.pairCd[key]=(a.pairCd[key]||0)-dt;b.pairCd[key]=a.pairCd[key];
if(imp>5&&a.pairCd[key]<=0){
a.pairCd[key]=0.8;b.pairCd[key]=0.8;
var mx=(a.x+b.x)/2,mz=(a.z+b.z)/2;
spawnPool(sparks,mx,2,mz,Math.min(20,imp*1.5|0),8,6,0.6,1,0.8,0.35,22);
slam(rv>0?a:b,rv>0?b:a,imp);
if(!a.isPlayer&&!b.isPlayer&&imp>8)cheer(0.1);
}
}
}
}
function collideStatics(t){
for(var i=0;i<WRECKS.length;i++){
var w=WRECKS[i],dx=t.x-w.x,dz=t.z-w.z,d=Math.sqrt(dx*dx+dz*dz),min=TRUCK_R+w.r;
if(d<min&&d>0.001){
var nx=dx/d,nz=dz/d;
t.x=w.x+nx*min;t.z=w.z+nz*min;
var vn=t.vx*nx+t.vz*nz;
if(vn<0){t.vx-=1.7*vn*nx;t.vz-=1.7*vn*nz;if(-vn>6){spawnPool(sparks,(t.x+w.x)/2,1.5,(t.z+w.z)/2,10,7,5,0.5,1,0.75,0.3,22);thud(-vn*0.7);w.rock=0.4;}}
}
}
}
function hitDynamics(t,dt){
var i,o,dx,dz,d,nx,nz;
for(i=0;i<TIRES.length;i++){
o=TIRES[i];o.cd-=dt;
dx=o.x-t.x,dz=o.z-t.z,d=Math.sqrt(dx*dx+dz*dz);
if(d<TRUCK_R+0.9&&d>0.001){
nx=dx/d;nz=dz/d;
var sp=Math.sqrt(t.vx*t.vx+t.vz*t.vz);
o.vx=t.vx*0.8+nx*(4+sp*0.9);o.vz=t.vz*0.8+nz*(4+sp*0.9);o.vy=rand(2,5);
o.spin=rand(-10,10);
if(o.cd<=0&&t.isPlayer&&sp>5){o.cd=2;addScore(25);toast('+25 SHRED');cheer(0.05);}
}
}
for(i=0;i<BARRELS.length;i++){
o=BARRELS[i];o.cd-=dt;
dx=o.x-t.x,dz=o.z-t.z;d=Math.sqrt(dx*dx+dz*dz);
if(d<TRUCK_R+0.6&&d>0.001){
nx=dx/d;nz=dz/d;
var sp2=Math.sqrt(t.vx*t.vx+t.vz*t.vz);
o.vx=t.vx*0.9+nx*(3+sp2);o.vz=t.vz*0.9+nz*(3+sp2);o.vy=rand(3,6);
if(o.cd<=0&&t.isPlayer&&sp2>5){o.cd=2;addScore(15);toast('+15 CLANG');}
}
}
}
function stepDynamics(arr,rad,dt){
for(var i=0;i<arr.length;i++){
var o=arr[i];
o.vy-=22*dt;
o.x+=o.vx*dt;o.z+=o.vz*dt;o.y+=o.vy*dt;
var rest=arr===TIRES?0.3:0.62;
if(o.y<(arr===TIRES?0.3:0.65)){
o.y=arr===TIRES?0.3:0.65;
if(o.vy<0)o.vy=-o.vy*rest;
if(Math.abs(o.vy)<1)o.vy=0;
o.vx*=Math.exp(-3*dt);o.vz*=Math.exp(-3*dt);
}
var cd=Math.sqrt(o.x*o.x+o.z*o.z);
if(cd>R_WALL-0.5){var nx=o.x/cd,nz=o.z/cd;o.x=nx*(R_WALL-0.5);o.z=nz*(R_WALL-0.5);var vn=o.vx*nx+o.vz*nz;if(vn>0){o.vx-=1.6*vn*nx;o.vz-=1.6*vn*nz;}}
o.mesh.position.set(o.x,o.y,o.z);
if(arr===TIRES){o.mesh.rotation.z+=o.spin*dt;o.spin*=Math.exp(-1.5*dt);o.mesh.rotation.y+=o.vx*dt*0.1;}
else o.mesh.rotation.x+=o.vx*dt*0.8;
}
}
/* ---------- camera ---------- */
var camPos=new THREE.Vector3(0,8,-35),lookCur=new THREE.Vector3(),lookInit=false,photoAng=0;
function updateCamera(dt,real){
if(S.photo){
photoAng+=real*0.12;
var r=34,y=13;
camPos.set(player.x+Math.cos(photoAng)*r,y,player.z+Math.sin(photoAng)*r);
camera.position.copy(camPos);
camera.lookAt(player.x,2,player.z);
return;
}
var fx=Math.cos(player.heading),fz=Math.sin(player.heading);
var sp=Math.sqrt(player.vx*player.vx+player.vz*player.vz);
var back=9+sp*0.18;
var gh=groundH(player.x,player.z);
var tx=player.x-fx*back,tz=player.z-fz*back,ty=gh+5.2+sp*0.04;
var cd=Math.sqrt(tx*tx+tz*tz);
if(cd>43){tx*=43/cd;tz*=43/cd;}
var k=1-Math.exp(-5*dt);
camPos.x+=(tx-camPos.x)*k;camPos.y+=(ty-camPos.y)*k;camPos.z+=(tz-camPos.z)*k;
S.shake*=Math.exp(-4*dt);
camera.position.set(camPos.x+rand(-1,1)*S.shake*0.3,camPos.y+rand(-1,1)*S.shake*0.2,camPos.z+rand(-1,1)*S.shake*0.3);
var lt=new THREE.Vector3(player.x+fx*4,gh+1.6,player.z+fz*4);
if(!lookInit){lookCur.copy(lt);lookInit=true;}
lookCur.lerp(lt,1-Math.exp(-8*dt));
camera.lookAt(lookCur);
camera.fov=62+Math.min(10,sp*0.35);camera.updateProjectionMatrix();
}
/* ---------- game flow ---------- */
function resetGame(){
S.score=DEMO?5000:0;
S.timeLeft=DEMO?20:MATCH;
S.phase=DEMO?'play':'count';S.countT=3.2;S.lastNum='';
S.photo=false;S.shake=0;S.cheer=0;S.cheerT=0;S.simTime=0;
$('photoCard').classList.add('hidden');$('endCard').classList.add('hidden');
scoreEl.textContent=S.score;
trucks.forEach(function(t,i){
var ang=i===0?Math.PI/2:(i-1)*Math.PI/2;
t.x=Math.cos(ang)*24;t.z=Math.sin(ang)*24;t.heading=ang+Math.PI;
t.vx=t.vz=0;t.integrity=100;t.wrecked=false;t.wreckTilt=0;t.stun=0;t.flash=0;t.spinVel=0;
t.retreat=0;t.tg=null;t.wp=null;t.pairCd={};
t.mat.color.setHex(t.isPlayer?0xc9ced6:RIVALS[i-1].color);
t.mat.emissive.setRGB(0,0,0);
});
TIRES.forEach(function(o,i){
var a=rand(0,TAU),r=rand(8,30);
o.x=Math.cos(a)*r;o.z=Math.sin(a)*r;o.y=0.3;o.vx=o.vy=o.vz=0;o.spin=0;o.cd=0;
});
BARRELS.forEach(function(o){
var a=rand(0,TAU),r=rand(9,28);
o.x=Math.cos(a)*r;o.z=Math.sin(a)*r;o.y=0.65;o.vx=o.vy=o.vz=0;o.cd=0;
});
for(var p=0;p<sparks.n;p++){sparks.life[p]=0;sparks.pos[p*3+1]=-999;}
for(p=0;p<smoke.n;p++){smoke.life[p]=0;smoke.pos[p*3+1]=-999;}
sparks.geo.attributes.position.needsUpdate=true;
smoke.geo.attributes.position.needsUpdate=true;
lookInit=false;photoAng=0;
}
function endMatch(){
S.phase='over';
var s=S.score,rank=s>=12000?'STAINLESS LEGEND':s>=7000?'APEX PREDATOR':s>=3500?'CONTENDER':s>=1200?'SCRAPY':'SCRAP METAL';
$('endScore').textContent=s;
$('endRank').textContent=rank;
$('endCard').classList.remove('hidden');
cheerSfx();
}
function togglePhoto(){
S.photo=!S.photo;
if(S.photo){
photoAng=Math.atan2(camPos.z-player.z,camPos.x-player.x);
$('photoArena').textContent=ARENA;
$('photoScore').textContent='SCORE '+S.score;
$('photoCard').classList.remove('hidden');
}else $('photoCard').classList.add('hidden');
}
/* ---------- input ---------- */
var KEYMAP={ArrowUp:'u',KeyW:'u',ArrowDown:'d',KeyS:'d',ArrowLeft:'l',KeyA:'l',ArrowRight:'r',KeyD:'r',Space:'hb'};
addEventListener('keydown',function(e){
if(KEYMAP[e.code]!==undefined){KEY[KEYMAP[e.code]]=1;e.preventDefault();initAudio();}
if(e.code==='KeyP'&&!e.repeat&&S.phase!=='over')togglePhoto();
if(e.code==='KeyM'&&!e.repeat){S.muted=!S.muted;if(AU.master)AU.master.gain.value=S.muted?0:0.5;toast(S.muted?'SOUND OFF':'SOUND ON');}
if(e.code==='KeyR'&&!e.repeat)resetGame();
});
addEventListener('keyup',function(e){if(KEYMAP[e.code]!==undefined){KEY[KEYMAP[e.code]]=0;e.preventDefault();}});
addEventListener('blur',function(){KEY.u=KEY.d=KEY.l=KEY.r=KEY.hb=0;});
addEventListener('resize',function(){camera.aspect=innerWidth/innerHeight;camera.updateProjectionMatrix();renderer.setSize(innerWidth,innerHeight);});
/* ---------- main loop ---------- */
var last=performance.now();
function frame(now){
requestAnimationFrame(frame);
var real=Math.min(0.05,(now-last)/1000);last=now;
if(!S.photo){
if(S.phase==='count'){
S.countT-=real;
var n=S.countT>2?'3':S.countT>1?'2':S.countT>0?'1':'GO!';
if(n!==S.lastNum){S.lastNum=n;var c=$('count');c.textContent=n;c.classList.remove('show');void c.offsetWidth;c.classList.add('show');}
if(S.countT<-0.6)S.phase='play';
}
if(S.phase==='play'){
var dt=real;
S.simTime+=dt;uTime.value=S.simTime;
S.timeLeft-=dt;
if(S.timeLeft<=0){S.timeLeft=0;endMatch();}
player.stun-=dt;
var th=(KEY.u?1:0)-(KEY.d?1:0),st=(KEY.r?1:0)-(KEY.l?1:0);
driveTruck(player,th,st,KEY.hb===1,dt);
for(var i=1;i<trucks.length;i++){
var t=trucks[i];
if(!t.wrecked)aiDrive(t,dt);else driveTruck(t,0,0,false,dt);
}
collideTrucks(dt);
for(i=0;i<trucks.length;i++){collideStatics(trucks[i]);hitDynamics(trucks[i],dt);integrate(trucks[i],dt);}
stepDynamics(TIRES,0.85,dt);
stepDynamics(BARRELS,0.5,dt);
WRECKS.forEach(function(w){if(w.rock>0){w.rock-=dt;w.mesh.rotation.z=Math.sin(w.rock*30)*0.05*w.rock;}});
updatePool(sparks,dt,22);
updatePool(smoke,dt,-2);
S.cheerT-=dt;if(S.cheerT<=0)S.cheer=Math.max(0,S.cheer-real*0.4);
audioTick(dt);
}
}
clockEl.textContent=fmt(S.timeLeft);
clockEl.classList.toggle('low',S.timeLeft<=10&&S.phase==='play');
updateCamera(S.photo?0:real,real);
renderer.render(scene,camera);
}
resetGame();
requestAnimationFrame(frame);
})();
</script>
</body>
</html>
buildTruck(null,true,…) plus a loop over a four-entry RIVALS array. Clumping is broken four ways: each rival runs a different goal function (brawler hunts the nearest truck, bully hunts only you, scrapper hunts tires/barrels, showboat patrols waypoints and charges on a timer), every rival steers a separation force away from non-target trucks inside 8 m, spawns are pinned to four quadrants with waypoint picks biased to the rival's own sector, and any truck that eats a slam gets a 1.4 s retreat timer driving it straight away from its attacker — so packs dissolve the moment they form.S.timeLeft = 20; S.score = 5000; (and it's re-applied by resetGame() on R). That is the same state object the countdown, the clock HUD, the match-end check and the score renderer all read — there is no overlay, which is why #demo also skips the 3-2-1 count so none of your 20 seconds is spent watching numerals.P flips S.photo, which gates the entire simulation block — truck integration, AI, collisions, particles, the match clock, and the crowd's uTime shader uniform — so the frozen frame is truly frozen (check the crowd mid-wave). The rAF loop keeps running only to render that static scene, drift the camera slowly around the player for framing, and show the arena-name/score card; pressing P again resumes from the exact same state.j = -(1+e)·v_rel·n / 2). The scalar closing speed is the "impact": above 5 it scores, flashes the victim's emissive, spawns sparks, and subtracts impact×4 from a rival's 100-point integrity — at zero the rival enters a persistent wreck state (tilted, darkened, smoking, drivable only as an obstacle). Wrecked sedans are static circles that reflect velocity with restitution 0.7; tires and barrels are light dynamic circles that inherit the truck's velocity plus a normal-direction kick and a vertical hop, then decay under friction and gravity.