27:18Gemini 4 Hydrogen: Cheaper Tokens, More Expensive Answers?
Nov 13, 2026 · YouTube
18:40ERNIE 5.5 Flash Preview vs GLM 5.3 Flash: Baidu's new model is very impressive!
Nov 11, 2026 · YouTube
36:22Fable 5.2 robbing me blind, plus: Nex AGI's N2.6 vs GLM 5.3 Flash
Nov 11, 2026 · YouTube
14:03Gemini 4 Carbon: Can it handle my challenges? (First look with one-shot coding tests)
Nov 10, 2026 · YouTube
47:51Ranking major U.S. Open-Weights Models. (Where does Ripple 65B fit in?)
Nov 6, 2026 · YouTube
10:57"Crouton Alpha": the latest super-efficient coding model by Meta?
Nov 3, 2026 · YouTube
38:58Testing Particle-1: a post-trained DeepSeek V4.1 Flash specializing in coding and efficiency
Oct 28, 2026 · YouTube
22:14GPT-6.1 Terra Pro vs Terra: How Much Better Is Pro?
Oct 27, 2026 · YouTube
29:55Kimi K3.1 Pro Looked Incredible. Then I Kept Testing.
Oct 26, 2026 · YouTube
31:28Is this company reselling GPT Sol 6.1 at a loss (for non-agentic/text-only)?🤔 Is this sustainable?
Oct 20, 2026 · YouTube
17:58Nemotron 3.5 Super VL vs Opus 5.5: Testing NVIDIA’s new coding juggernaut
Oct 15, 2026 · YouTube
16:04Testing a *massive* new open-weights model 🤯: Muse Spark Open (vs. Nemotron, Opus 5.5, and more)
Oct 15, 2026 · YouTube
16:43Gemini 4 Argon: Great 🐈 Animation, but a broken escalator. (Coding challenge vs Opus 5.5, Sol 6.1)
Oct 13, 2026 · YouTube
16:37Meta's new "Muse Wonder" AI is impressive... but can it compete with Anthropic's models?
Oct 12, 2026 · YouTube
11:12WOW... Haiku 5.5 is very impressive. RIP OpenAI?
Oct 7, 2026 · YouTube
22:47Mistral Large 4 Is Cheap. But Is It Good Enough to Compete?
Oct 6, 2026 · YouTube
22:02Is Pareto 26.10 Preview Just Routing to GPT Sol? I Tested It Against Sol 6.1 Pro
Oct 1, 2026 · YouTube
11:22I Tested Sonnet 5.5 Against Opus 5.5. Is It Actually Cheaper?
Sep 28, 2026 · YouTube
30:00Did Elon Musk's team "train the gay away"... and make Grok 4.7 worse?
Sep 26, 2026 · YouTube
10:58So... how much better is Claude Opus 5.5 vs. GPT-6 Sol, really?
Sep 23, 2026 · YouTube
9:38Claude Opus 5.5... the BEST new model for coding!! 🤩
Sep 22, 2026 · YouTube
29:00Grok 4.7: improved, but GLM 5.3 FlashX beat it anyway (benchmark against Gemini, Sol, etc.)
Sep 22, 2026 · YouTube
16:54Stealth Union Alpha: Is this another cheap GPT model? (better than Luna, cheaper than Terra?)
Sep 16, 2026 · YouTube
16:45DeepSeek V4.1 Flash Is Shockingly Good for the Price
Sep 10, 2026 · YouTube
17:14How much better is GPT-6 Astra over Claude Opus 5? Real Website and Coding Tests
Sep 5, 2026 · YouTube
29:13Implementing a multiplayer game with GPT-6 Astra (+ Gemini 3.8, Opus 5, Sol Pro)
Sep 5, 2026 · YouTube
19:42Meta Muse Spark 1.3 Review: A Huge Leap Forward
Sep 2, 2026 · YouTube
17:31Testing Gemini 3.8 Flash on Real Coding Challenges (vs. other models)
Sep 2, 2026 · YouTube
19:45Fable 5.1 writes an animated novel using Ren'Py... did it do better than Sol Pro 5.6?
Sep 1, 2026 · YouTube
14:09Testing IBM Granite-4.2-8B! Can it name 100 women?
Aug 31, 2026 · YouTube
17:37Tencent Hy4 Preview Tested: Is It Really Better Than Kimi K3 and GLM 5.3?
Aug 28, 2026 · YouTube
23:58Qwen 3.8 Flash Tested: UI, Coding, Physics & more - compared to Opus 5, Sol Pro, Grok
Aug 27, 2026 · YouTube
You are a game developer who ships browser demos that have to work on the first click, in front of an audience, on someone else's laptop.
Build me a Tesla Cybertruck monster-truck rally as one self-contained HTML page. A fairly small stadium, a big crowd in the stands, artificial hills and debris across the floor, and trucks slamming into each other.
Then make it match the spec below exactly. The spec is the challenge; a great-looking rally that ignores it is a miss.
.html page. No build step. Saved to disk and double-clicked, it runs.localStorage, sessionStorage, cookies or indexedDB. It runs in a sandboxed frame where touching them throws.Both are checkable in ten seconds, which is why they are here.
#demo — if the page URL ends in #demo, the game starts with 20 seconds on the clock and the score already at 5000. This must run through the real game state, not a cosmetic overlay. Everything else behaves normally.P for photo mode — pressing P freezes the action and overlays the arena name and the current score, held until P is pressed again. The simulation stops; it does not keep playing behind the card.One sentence: the name of your rally and what it looks like.
Exactly one fenced html block containing the whole page and nothing else.
Exactly four bullets.
#demo sets, and why it is not an overlay.P is pressed — the render loop, the physics step, or both.THE GALVANIC BOWL — a floodlit night coliseum of dirt mounds, dead cars and tire stacks where five team-painted Cybertruck monster trucks spend two minutes slamming each other into scrap in front of ~3,000 bobbing fans.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>THE GALVANIC BOWL — Cybertruck Monster Rally</title>
<style>
:root{
--ink:#f2efe6; --dim:#9a958a; --accent:#ff5a1f;
--panel:rgba(12,13,16,.8); --line:rgba(242,239,230,.16);
--mono:ui-monospace,"Cascadia Mono",Consolas,Menlo,monospace;
}
*{margin:0;padding:0;box-sizing:border-box}
html,body{height:100%;overflow:hidden}
body{background:#07080c;color:var(--ink);cursor:default;user-select:none;
font-family:"Haettenschweiler","Arial Narrow Bold",Impact,"Franklin Gothic Medium",sans-serif}
canvas{display:block}
.strip{position:fixed;z-index:6;pointer-events:none;background:var(--panel);
border:1px solid var(--line);transform:skewX(-8deg)}
.strip .in{display:block;transform:skewX(8deg)}
#arenaTag{top:20px;left:26px;padding:10px 24px 12px;border-left:4px solid var(--accent)}
#arenaTag h1{font-size:29px;font-weight:400;letter-spacing:2.5px;line-height:1}
#arenaTag span{display:block;margin-top:5px;font-size:10px;letter-spacing:3.5px;color:var(--dim)}
#stats{position:fixed;top:20px;right:26px;z-index:6;display:flex;gap:10px;pointer-events:none}
.stat{background:var(--panel);border:1px solid var(--line);transform:skewX(-8deg);
padding:8px 20px 10px;min-width:132px}
.stat .in{transform:skewX(8deg);text-align:right}
.stat label{display:block;font-size:10px;letter-spacing:3.5px;color:var(--dim)}
.stat b{font-family:var(--mono);font-size:25px;font-weight:700;letter-spacing:1px}
#clock.low{color:var(--accent);animation:blink .5s steps(2) infinite}
@keyframes blink{50%{opacity:.4}}
#controls{position:fixed;bottom:18px;left:50%;transform:translateX(-50%) skewX(-8deg);
z-index:6;pointer-events:none;background:var(--panel);border:1px solid var(--line);
padding:8px 22px;font-size:12px;letter-spacing:2.5px;color:#c9c4b6;white-space:nowrap}
#controls .k{color:var(--accent)}
#popups{position:fixed;inset:0;z-index:7;pointer-events:none;overflow:hidden}
.pop{position:absolute;transform:translate(-50%,-50%);font-size:23px;letter-spacing:2px;
color:var(--accent);white-space:nowrap;
text-shadow:0 2px 0 rgba(0,0,0,.9),0 0 8px rgba(0,0,0,.6);
animation:rise .95s ease-out forwards}
.pop.big{font-size:34px}
@keyframes rise{0%{opacity:0}12%{opacity:1}100%{opacity:0;transform:translate(-50%,-170%)}}
#flash{position:fixed;inset:0;z-index:5;pointer-events:none;opacity:0;
box-shadow:inset 0 0 140px 50px rgba(255,46,20,.5)}
#photoCard{position:fixed;left:50%;bottom:10%;transform:translateX(-50%) skewX(-8deg);
z-index:8;display:none;background:var(--panel);border:1px solid var(--line);
border-left:4px solid var(--accent);padding:16px 46px 18px;text-align:center}
#photoCard.show{display:block}
#photoCard .tag{font-size:11px;letter-spacing:4px;color:var(--accent)}
#photoCard h2{font-size:42px;font-weight:400;letter-spacing:3px;line-height:1.1;margin:4px 0 6px}
#photoCard .score{font-family:var(--mono);font-size:19px;letter-spacing:2px}
#photoCard .hint{margin-top:8px;font-size:10px;letter-spacing:3px;color:var(--dim)}
#endCard{position:fixed;inset:0;z-index:9;display:none;align-items:center;justify-content:center;
background:rgba(4,5,7,.55)}
#endCard.show{display:flex}
#endCard .panel{background:var(--panel);border:1px solid var(--line);border-top:4px solid var(--accent);
transform:skewX(-6deg);padding:26px 64px 30px;text-align:center}
#endCard .pin{transform:skewX(6deg)}
#endCard .tag{font-size:11px;letter-spacing:4px;color:var(--dim)}
#endCard h2{font-size:56px;font-weight:400;letter-spacing:4px;line-height:1.1;margin:6px 0 8px}
#endCard .score{font-family:var(--mono);font-size:22px;letter-spacing:2px}
#endCard .score b{color:var(--accent)}
#endCard .hint{margin-top:10px;font-size:11px;letter-spacing:3px;color:var(--dim)}
#intro{position:fixed;inset:0;z-index:4;display:flex;align-items:center;justify-content:center;
pointer-events:none;text-align:center;animation:intro 2.8s ease forwards}
#intro h2{font-size:64px;font-weight:400;letter-spacing:6px;text-shadow:0 4px 0 rgba(0,0,0,.7)}
#intro span{display:block;margin-top:8px;font-size:13px;letter-spacing:5px;opacity:.85}
#intro .rule{width:220px;height:3px;background:var(--accent);margin:16px auto 0}
@keyframes intro{0%{opacity:0}14%{opacity:1}68%{opacity:1}100%{opacity:0}}
</style>
</head>
<body>
<div id="intro"><div>
<h2>THE GALVANIC BOWL</h2>
<span>CYBERTRUCK MONSTER RALLY · FIVE TRUCKS · NO MERCY</span>
<div class="rule"></div>
</div></div>
<div id="hud">
<div class="strip" id="arenaTag"><div class="in">
<h1>THE GALVANIC BOWL</h1>
<span>CYBERTRUCK MONSTER RALLY · NIGHT SESSION</span>
</div></div>
<div id="stats">
<div class="stat"><div class="in"><label>SCORE</label><b id="score">0</b></div></div>
<div class="stat"><div class="in"><label>CLOCK</label><b id="clock">2:00</b></div></div>
</div>
</div>
<div id="controls">
<span class="k">WASD / ARROWS</span> DRIVE · <span class="k">SPACE</span> HANDBRAKE
· <span class="k">P</span> PHOTO MODE · <span class="k">M</span> SOUND
</div>
<div id="popups"></div>
<div id="flash"></div>
<div id="photoCard">
<div class="tag">PHOTO MODE · ACTION FROZEN</div>
<h2>THE GALVANIC BOWL</h2>
<div class="score">SCORE <b id="photoScore">0</b></div>
<div class="hint">PRESS P TO RESUME</div>
</div>
<div id="endCard"><div class="panel"><div class="pin">
<div class="tag">FULL TIME · THE GALVANIC BOWL</div>
<h2>TIME'S UP</h2>
<div class="score">FINAL SCORE <b id="endScore">0</b></div>
<div class="hint">PRESS R TO RUN IT BACK</div>
</div></div></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/0.158.0/three.min.js"></script>
<script>
'use strict';
/* ================= helpers & mode flags ================= */
const DEMO = location.href.endsWith('#demo'); // #demo → real state: 20s clock, 5000 score
const TAU = Math.PI * 2;
const clamp = (v, a, b) => v < a ? a : v > b ? b : v;
const rand = (a, b) => a + Math.random() * (b - a);
const wrapA = a => { a %= TAU; if (a > Math.PI) a -= TAU; if (a < -Math.PI) a += TAU; return a; };
/* ================= renderer / scene ================= */
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.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.15;
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x07080c);
scene.fog = new THREE.Fog(0x07080c, 120, 260);
const camera = new THREE.PerspectiveCamera(62, innerWidth / innerHeight, 0.1, 600);
addEventListener('resize', () => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
if (photoMode) renderer.render(scene, camera); // restore the frozen frame after a resize
});
/* tiny procedural environment cube so the stainless steel has something to reflect */
(function makeEnv(){
const face = (top, bot) => {
const c = document.createElement('canvas'); c.width = c.height = 32;
const g = c.getContext('2d');
const gr = g.createLinearGradient(0, 0, 0, 32);
gr.addColorStop(0, top); gr.addColorStop(1, bot);
g.fillStyle = gr; g.fillRect(0, 0, 32, 32);
return c;
};
const sky = '#232833', hor = '#4a4438', gnd = '#15120e';
const tex = new THREE.CubeTexture([
face(sky, hor), face(sky, hor), face('#3d4452', sky),
face(gnd, gnd), face(sky, hor), face(sky, hor)
]);
tex.colorSpace = THREE.SRGBColorSpace;
tex.needsUpdate = true;
scene.environment = tex;
})();
/* lights */
scene.add(new THREE.HemisphereLight(0x9db2cc, 0x40342a, 0.55));
const key = new THREE.DirectionalLight(0xfff1da, 2.5);
key.position.set(45, 70, 25);
key.castShadow = true;
key.shadow.mapSize.set(2048, 2048);
Object.assign(key.shadow.camera, { left: -70, right: 70, top: 70, bottom: -70, far: 200 });
key.shadow.camera.updateProjectionMatrix();
key.shadow.bias = -0.0006;
key.shadow.normalBias = 0.03;
scene.add(key);
[[49.5, 49.5], [-49.5, -49.5]].forEach(([x, z]) => {
const s = new THREE.SpotLight(0xffe6c4, 1.5, 0, 0.62, 0.6, 0);
s.position.set(x, 26, z);
s.target.position.set(0, 0, 0);
scene.add(s, s.target);
});
/* ================= the stadium ================= */
function floorTexture(){
const c = document.createElement('canvas'); c.width = c.height = 1024;
const g = c.getContext('2d');
g.fillStyle = '#5c4630'; g.fillRect(0, 0, 1024, 1024);
for (let i = 0; i < 1400; i++){
g.fillStyle = Math.random() < 0.5 ? 'rgba(38,26,15,0.10)' : 'rgba(140,110,74,0.08)';
g.beginPath(); g.arc(rand(0, 1024), rand(0, 1024), rand(3, 26), 0, TAU); g.fill();
}
g.lineCap = 'round';
for (let i = 0; i < 46; i++){ // old tire arcs
g.strokeStyle = 'rgba(30,20,12,' + rand(0.08, 0.22).toFixed(2) + ')';
g.lineWidth = rand(4, 10);
const a0 = rand(0, TAU);
g.beginPath(); g.arc(rand(0, 1024), rand(0, 1024), rand(60, 480), a0, a0 + rand(0.4, 2.2)); g.stroke();
}
g.strokeStyle = 'rgba(240,236,224,0.16)';
g.lineWidth = 10; g.beginPath(); g.arc(512, 512, 330, 0, TAU); g.stroke();
g.lineWidth = 4; g.beginPath(); g.arc(512, 512, 240, 0, TAU); g.stroke();
g.fillStyle = 'rgba(240,236,224,0.15)';
g.textAlign = 'center';
g.font = '700 92px Impact, "Arial Narrow", sans-serif';
g.fillText('GALVANIC', 512, 492);
g.fillText('BOWL', 512, 588);
const t = new THREE.CanvasTexture(c);
t.colorSpace = THREE.SRGBColorSpace;
t.anisotropy = renderer.capabilities.getMaxAnisotropy();
return t;
}
function dirtTexture(){
const c = document.createElement('canvas'); c.width = c.height = 256;
const g = c.getContext('2d');
g.fillStyle = '#5c4630'; g.fillRect(0, 0, 256, 256);
for (let i = 0; i < 260; i++){
g.fillStyle = Math.random() < 0.5 ? 'rgba(38,26,15,0.12)' : 'rgba(140,110,74,0.10)';
g.beginPath(); g.arc(rand(0, 256), rand(0, 256), rand(2, 10), 0, TAU); g.fill();
}
const t = new THREE.CanvasTexture(c);
t.colorSpace = THREE.SRGBColorSpace;
t.wrapS = t.wrapT = THREE.RepeatWrapping;
t.repeat.set(2, 2);
return t;
}
const floor = new THREE.Mesh(
new THREE.CircleGeometry(55, 80).rotateX(-Math.PI / 2),
new THREE.MeshStandardMaterial({ map: floorTexture(), roughness: 1, metalness: 0, envMapIntensity: 0.25 })
);
floor.receiveShadow = true;
scene.add(floor);
const apron = new THREE.Mesh(
new THREE.RingGeometry(55, 62, 80).rotateX(-Math.PI / 2),
new THREE.MeshStandardMaterial({ color: 0x4e5054, roughness: 0.95, envMapIntensity: 0.2 })
);
apron.position.y = 0.02; apron.receiveShadow = true; scene.add(apron);
const wall = new THREE.Mesh(
new THREE.CylinderGeometry(55.3, 55.3, 3.4, 80, 1, true),
new THREE.MeshStandardMaterial({ color: 0x63656a, roughness: 0.9, side: THREE.DoubleSide, envMapIntensity: 0.2 })
);
wall.position.y = 1.7; wall.receiveShadow = true; scene.add(wall);
/* sponsor boards on the wall */
const SPONSORS = ['VOLT COLA', 'SCRAPCO STEEL', 'MEGAWATT TIRES', 'RUSTBADGE TOOLS', 'NITRO MILK', 'BOWL-MART'];
function boardTexture(txt, i){
const c = document.createElement('canvas'); c.width = 512; c.height = 96;
const g = c.getContext('2d');
g.fillStyle = '#0e0f13'; g.fillRect(0, 0, 512, 96);
g.strokeStyle = 'rgba(244,241,232,0.25)'; g.lineWidth = 4; g.strokeRect(6, 6, 500, 84);
g.fillStyle = i % 2 ? '#ff5a1f' : '#e8e4d8';
g.font = '700 52px Impact, "Arial Narrow", sans-serif';
g.textAlign = 'center'; g.textBaseline = 'middle';
g.fillText(txt, 256, 52);
const t = new THREE.CanvasTexture(c);
t.colorSpace = THREE.SRGBColorSpace;
return t;
}
for (let i = 0; i < 10; i++){
const a = i / 10 * TAU + 0.31;
const bd = new THREE.Mesh(
new THREE.PlaneGeometry(13, 2.4),
new THREE.MeshBasicMaterial({ map: boardTexture(SPONSORS[i % SPONSORS.length], i) })
);
bd.position.set(Math.sin(a) * 54.4, 1.75, Math.cos(a) * 54.4);
bd.rotation.y = a + Math.PI;
scene.add(bd);
}
/* tiered stands, all the way round */
const deck = new THREE.Mesh(
new THREE.CylinderGeometry(82, 60, 14, 80, 1, true),
new THREE.MeshStandardMaterial({ color: 0x43454b, roughness: 0.95, side: THREE.DoubleSide, envMapIntensity: 0.15 })
);
deck.position.y = 10; scene.add(deck);
const facade = new THREE.Mesh(
new THREE.CylinderGeometry(84, 84, 24, 80, 1, true),
new THREE.MeshStandardMaterial({ color: 0x1d1f24, roughness: 0.9, side: THREE.DoubleSide })
);
facade.position.y = 12; scene.add(facade);
const roof = new THREE.Mesh(
new THREE.RingGeometry(60, 84, 80).rotateX(-Math.PI / 2),
new THREE.MeshStandardMaterial({ color: 0x14151a, roughness: 0.9, side: THREE.DoubleSide })
);
roof.position.y = 23; scene.add(roof);
const stripGeo = new THREE.PlaneGeometry(2.4, 1.1);
const stripMat = new THREE.MeshBasicMaterial({ color: 0xffe9c8 });
for (let i = 0; i < 28; i++){
const a = i / 28 * TAU;
const m = new THREE.Mesh(stripGeo, stripMat);
m.position.set(Math.sin(a) * 61.5, 22.2, Math.cos(a) * 61.5);
m.rotation.y = a + Math.PI;
scene.add(m);
}
const poleGeo = new THREE.CylinderGeometry(0.5, 0.8, 27, 8);
const poleMat = new THREE.MeshStandardMaterial({ color: 0x2a2c31, roughness: 0.8 });
const headGeo = new THREE.BoxGeometry(7, 3.2, 0.8);
const headMat = new THREE.MeshBasicMaterial({ color: 0xfff3d6 });
[Math.PI / 4, 3 * Math.PI / 4, 5 * Math.PI / 4, 7 * Math.PI / 4].forEach(a => {
const tower = new THREE.Group();
const pole = new THREE.Mesh(poleGeo, poleMat); pole.position.y = 13.5;
const head = new THREE.Mesh(headGeo, headMat); head.position.y = 27.5;
tower.add(pole, head);
tower.position.set(Math.sin(a) * 70, 0, Math.cos(a) * 70);
tower.rotation.y = a;
scene.add(tower);
});
/* ================= the crowd (instanced, GPU-animated) ================= */
const crowdUniforms = { uTime: { value: 0 }, uExcite: { value: 0 } };
function crowdMat(){
const m = new THREE.MeshLambertMaterial({ color: 0xffffff });
m.onBeforeCompile = sh => {
sh.uniforms.uTime = crowdUniforms.uTime;
sh.uniforms.uExcite = crowdUniforms.uExcite;
sh.vertexShader = 'uniform float uTime;\nuniform float uExcite;\nattribute float aPhase;\n' +
sh.vertexShader.replace('#include <begin_vertex>',
`#include <begin_vertex>
float cPh = aPhase;
float cSp = 2.2 + mod(cPh, 2.3);
float cB = 0.5 + 0.5 * sin(uTime * cSp + cPh * 6.2831);
transformed.y += cB * (0.06 + uExcite * 0.5);`);
};
return m;
}
const CROWD_MAX = 3600;
const cBodyGeo = new THREE.BoxGeometry(0.55, 0.95, 0.4); cBodyGeo.translate(0, 0.475, 0);
const cHeadGeo = new THREE.SphereGeometry(0.17, 6, 5); cHeadGeo.translate(0, 1.16, 0);
const phases = new Float32Array(CROWD_MAX);
for (let i = 0; i < CROWD_MAX; i++) phases[i] = Math.random() * 10;
cBodyGeo.setAttribute('aPhase', new THREE.InstancedBufferAttribute(phases, 1));
cHeadGeo.setAttribute('aPhase', new THREE.InstancedBufferAttribute(phases, 1));
const crowdBodies = new THREE.InstancedMesh(cBodyGeo, crowdMat(), CROWD_MAX);
const crowdHeads = new THREE.InstancedMesh(cHeadGeo, crowdMat(), CROWD_MAX);
crowdBodies.frustumCulled = crowdHeads.frustumCulled = false;
const SHIRTS = ['#c9c2b2','#8a4a3a','#3d4a56','#5a6b4a','#7a6a8a','#d96b2f','#b3aa90','#54423a','#2f3a4a','#a33b2f','#e0d8c4','#435a52'];
const SKINS = ['#e8b48c','#c98a5e','#8a5a3a','#6b4226','#f0c8a0'];
const dummy = new THREE.Object3D();
const col = new THREE.Color();
let ci = 0;
for (let k = 0; k < 8; k++){
const r = 61.5 + k * 2.7;
const y = 3 + (r - 60) * (14 / 22); // standing on the sloped deck
const n = Math.floor(TAU * r / 1.05);
for (let j = 0; j < n && ci < CROWD_MAX; j++){
if (Math.random() < 0.12) continue; // empty seats
const a = j / n * TAU + rand(-0.004, 0.004);
dummy.position.set(Math.sin(a) * r + rand(-0.3, 0.3), y, Math.cos(a) * r + rand(-0.3, 0.3));
dummy.rotation.set(0, rand(-0.4, 0.4), 0);
const s = rand(0.85, 1.15);
dummy.scale.set(s, s * rand(0.92, 1.1), s);
dummy.updateMatrix();
crowdBodies.setMatrixAt(ci, dummy.matrix);
crowdHeads.setMatrixAt(ci, dummy.matrix);
crowdBodies.setColorAt(ci, col.set(SHIRTS[(Math.random() * SHIRTS.length) | 0]));
crowdHeads.setColorAt(ci, col.set(SKINS[(Math.random() * SKINS.length) | 0]));
ci++;
}
}
crowdBodies.count = ci; crowdHeads.count = ci;
crowdBodies.instanceColor.needsUpdate = true;
crowdHeads.instanceColor.needsUpdate = true;
scene.add(crowdBodies, crowdHeads);
/* ================= trucks ================= */
const TEAM = [0xff5a1f, 0x00c2b0, 0xffc400, 0x7ad04f, 0xe0457a];
const AI_TYPES = ['hunter', 'berserker', 'jumper', 'bully'];
function buildTruckMesh(accent){
const group = new THREE.Group();
const tilt = new THREE.Group();
group.add(tilt);
const bodyMat = new THREE.MeshStandardMaterial({ color: 0xd6d8da, metalness: 0.85, roughness: 0.3, envMapIntensity: 0.7 });
const accMat = new THREE.MeshStandardMaterial({ color: accent, metalness: 0.4, roughness: 0.45, envMapIntensity: 0.4 });
const darkMat = new THREE.MeshStandardMaterial({ color: 0x141519, metalness: 0.1, roughness: 0.85 });
// extruded Cybertruck side profile → the faceted wedge
const sh = new THREE.Shape();
sh.moveTo(-2.9, 0.55); sh.lineTo(2.75, 0.55); sh.lineTo(2.95, 0.95);
sh.lineTo(0.75, 1.78); sh.lineTo(-1.35, 1.62); sh.lineTo(-2.95, 1.02);
sh.closePath();
const bodyGeo = new THREE.ExtrudeGeometry(sh, { depth: 2.3, bevelEnabled: false });
bodyGeo.translate(0, 0, -1.15);
bodyGeo.rotateY(-Math.PI / 2); // nose now points +Z
const body = new THREE.Mesh(bodyGeo, bodyMat);
body.position.y = 0.8; body.castShadow = true;
tilt.add(body);
const stripeGeo = new THREE.BoxGeometry(0.07, 0.3, 4.6);
[1.19, -1.19].forEach(sx => {
const st = new THREE.Mesh(stripeGeo, accMat);
st.position.set(sx, 1.72, -0.05);
tilt.add(st);
});
const lamp = new THREE.Mesh(new THREE.BoxGeometry(1.7, 0.15, 0.1), new THREE.MeshBasicMaterial({ color: 0xfff6da }));
lamp.position.set(0, 1.62, 2.9); tilt.add(lamp);
const tail = new THREE.Mesh(new THREE.BoxGeometry(2.1, 0.13, 0.08), new THREE.MeshBasicMaterial({ color: 0xff2213 }));
tail.position.set(0, 1.8, -2.97); tilt.add(tail);
const skid = new THREE.Mesh(new THREE.BoxGeometry(2.0, 0.55, 4.5), darkMat);
skid.position.set(0, 1.08, 0); skid.castShadow = true; tilt.add(skid);
const wg = new THREE.CylinderGeometry(0.95, 0.95, 0.72, 18); wg.rotateZ(Math.PI / 2);
const hg = new THREE.CylinderGeometry(0.42, 0.42, 0.78, 10); hg.rotateZ(Math.PI / 2);
const spg = new THREE.BoxGeometry(0.82, 1.5, 0.16);
const wheels = [];
[[1.34, 1.95], [-1.34, 1.95], [1.34, -1.95], [-1.34, -1.95]].forEach(([wx, wz]) => {
const w = new THREE.Group();
const tire = new THREE.Mesh(wg, darkMat); tire.castShadow = true;
w.add(tire, new THREE.Mesh(hg, accMat), new THREE.Mesh(spg, darkMat));
w.position.set(wx, 0.95, wz);
tilt.add(w);
wheels.push(w);
});
return { group, tilt, bodyMat, accMat, wheels, accent };
}
const trucks = [];
for (let i = 0; i < 5; i++){
const mesh = buildTruckMesh(TEAM[i]);
scene.add(mesh.group);
if (i !== 0){ // pennant marker over rivals
const mk = new THREE.Mesh(new THREE.OctahedronGeometry(0.32), new THREE.MeshBasicMaterial({ color: TEAM[i] }));
mk.position.y = 4.3;
mesh.group.add(mk);
mesh.marker = mk;
}
trucks.push({
i, isPlayer: i === 0, mesh,
x: 0, z: 0, y: 0, vy: 0, prevGh: 0, h: 0, vx: 0, vz: 0,
throttle: 0, steer: 0, brake: false,
hp: 100, wrecked: false, wreckT: 0, invuln: 0, flash: 0,
spinVis: 0, grounded: true, airT: 0, smokeT: 0,
ai: i === 0 ? null : {
type: AI_TYPES[i - 1], retarget: rand(0.2, 1.5), tx: 0, tz: 0, target: null,
phase: rand(0, 9), top: rand(0.84, 1.0), aggr: rand(0.6, 1), stuck: 0, rev: 0
}
});
}
const player = trucks[0];
/* ================= obstacles ================= */
const hills = [], dyns = [], barriers = [];
const TR = 2.4, BOUND = 52;
const dirtTex = dirtTexture();
const hillMat = new THREE.MeshStandardMaterial({ map: dirtTex, roughness: 1, envMapIntensity: 0.2 });
function makeHillMesh(r, h){
const g = new THREE.PlaneGeometry(2 * r, 2 * r, 22, 22);
const p = g.attributes.position;
for (let i = 0; i < p.count; i++){
const d = Math.hypot(p.getX(i), p.getY(i)) / r;
p.setZ(i, d < 1 ? h * Math.pow(1 - d * d, 2) : 0);
}
g.computeVertexNormals();
g.rotateX(-Math.PI / 2);
const m = new THREE.Mesh(g, hillMat);
m.castShadow = true; m.receiveShadow = true;
return m;
}
[[10, 2.3], [7.5, 1.9], [6.5, 1.7], [8, 2.0], [6, 1.5], [7, 1.8]].forEach(([r, h]) => {
const mesh = makeHillMesh(r, h);
mesh.position.y = 0.02;
scene.add(mesh);
hills.push({ x: 0, z: 0, r, h, mesh });
});
function terrainHeight(x, z){
let y = 0;
for (const hl of hills){
const d = Math.hypot(x - hl.x, z - hl.z) / hl.r;
if (d < 1) y += hl.h * Math.pow(1 - d * d, 2);
}
return y;
}
const CAR_COLORS = [0x7a4a3a, 0x4a5a6b, 0x6b6b5a, 0x8a7a4a, 0x54453a, 0x4a5a4a];
function makeCarMesh(){
const g = new THREE.Group();
const mat = new THREE.MeshStandardMaterial({
color: CAR_COLORS[(Math.random() * CAR_COLORS.length) | 0], metalness: 0.5, roughness: 0.6, envMapIntensity: 0.4
});
const b = new THREE.Mesh(new THREE.BoxGeometry(3.4, 0.8, 1.7), mat);
b.position.y = 0.55; b.castShadow = true;
const cab = new THREE.Mesh(new THREE.BoxGeometry(1.9, 0.65, 1.5), mat);
cab.position.set(-0.25, 1.2, 0); cab.castShadow = true;
const wm = new THREE.MeshStandardMaterial({ color: 0x1a1a1c, roughness: 0.9 });
const wg = new THREE.CylinderGeometry(0.34, 0.34, 0.3, 10); wg.rotateZ(Math.PI / 2);
[[1.1, 0.85], [-1.1, 0.85], [1.1, -0.85], [-1.1, -0.85]].forEach(([wx, wz]) => {
const w = new THREE.Mesh(wg, wm); w.position.set(wx, 0.3, wz); g.add(w);
});
g.add(b, cab);
g.rotation.z = rand(-0.05, 0.05);
return g;
}
const tireGeo = new THREE.TorusGeometry(0.55, 0.26, 8, 14).rotateX(Math.PI / 2);
const tireMat = new THREE.MeshStandardMaterial({ color: 0x1c1d20, roughness: 0.9, envMapIntensity: 0.2 });
function makeStackMesh(){
const g = new THREE.Group();
const n = 3 + ((Math.random() * 2) | 0);
for (let i = 0; i < n; i++){
const t = new THREE.Mesh(tireGeo, tireMat);
t.position.set(rand(-0.12, 0.12), 0.28 + i * 0.52, rand(-0.12, 0.12));
t.rotation.y = rand(0, TAU);
t.castShadow = true;
g.add(t);
}
return g;
}
function makeBarrierMesh(){
const g = new THREE.Group();
const b = new THREE.Mesh(new THREE.BoxGeometry(3.1, 0.9, 0.85),
new THREE.MeshStandardMaterial({ color: 0x9a978c, roughness: 0.9, envMapIntensity: 0.25 }));
b.position.y = 0.45; b.castShadow = true; b.receiveShadow = true;
const s = new THREE.Mesh(new THREE.BoxGeometry(3.1, 0.18, 0.87),
new THREE.MeshStandardMaterial({ color: 0xd96b2f, roughness: 0.7 }));
s.position.y = 0.86;
g.add(b, s);
return g;
}
for (let i = 0; i < 8; i++){
const m = makeCarMesh(); scene.add(m);
dyns.push({ kind: 'car', mesh: m, x: 0, z: 0, vx: 0, vz: 0, r: 1.9, m: 7, alive: true, respawn: 0 });
}
for (let i = 0; i < 10; i++){
const m = makeStackMesh(); scene.add(m);
dyns.push({ kind: 'stack', mesh: m, x: 0, z: 0, vx: 0, vz: 0, r: 1.05, m: 1.2, alive: true, respawn: 0 });
}
for (let i = 0; i < 12; i++){
const m = makeBarrierMesh(); scene.add(m);
barriers.push({ mesh: m, x: 0, z: 0, rot: 0, c: 1, s: 0, hx: 1.55, hz: 0.45 });
}
/* flying tires released when a stack is smashed */
const flyTires = [];
for (let i = 0; i < 24; i++){
const m = new THREE.Mesh(tireGeo, tireMat);
m.visible = false; m.castShadow = true;
scene.add(m);
flyTires.push({ mesh: m, active: false, x: 0, y: 0, z: 0, vx: 0, vy: 0, vz: 0, rx: 0, rz: 0, life: 0 });
}
function burstTires(x, z, n){
let spawned = 0;
for (const f of flyTires){
if (f.active) continue;
f.active = true; f.mesh.visible = true;
f.x = x + rand(-0.4, 0.4); f.y = 0.8 + spawned * 0.5; f.z = z + rand(-0.4, 0.4);
const a = rand(0, TAU), sp = rand(4, 9);
f.vx = Math.sin(a) * sp; f.vz = Math.cos(a) * sp; f.vy = rand(6, 12);
f.rx = rand(-9, 9); f.rz = rand(-9, 9);
f.life = rand(2.2, 3.0);
if (++spawned >= n) break;
}
}
function stepFlyTires(dt){
for (const f of flyTires){
if (!f.active) continue;
f.vy -= 26 * dt;
f.x += f.vx * dt; f.y += f.vy * dt; f.z += f.vz * dt;
const gh = terrainHeight(f.x, f.z) + 0.28;
if (f.y < gh && f.vy < 0){ f.y = gh; f.vy *= -0.38; f.vx *= 0.7; f.vz *= 0.7; }
f.life -= dt;
f.mesh.rotation.x += f.rx * dt; f.mesh.rotation.z += f.rz * dt;
f.mesh.position.set(f.x, f.y, f.z);
const s = clamp(f.life * 1.5, 0, 1);
f.mesh.scale.set(s, s, s);
if (f.life <= 0){ f.active = false; f.mesh.visible = false; }
}
}
/* spawn ring + obstacle layout */
const SPAWN = [];
for (let i = 0; i < 5; i++){
const a = i / 5 * TAU + 0.3;
SPAWN.push([Math.sin(a) * 34, Math.cos(a) * 34]);
}
function layoutObstacles(){
const placed = [];
const clear = (x, z, r) => {
if (Math.hypot(x, z) > 47) return false;
if (terrainHeight(x, z) > 0.25) return false;
for (const p of placed) if (Math.hypot(x - p.x, z - p.z) < r + p.r) return false;
for (const s of SPAWN) if (Math.hypot(x - s[0], z - s[1]) < r + 7) return false;
return true;
};
const spot = r => {
for (let t = 0; t < 60; t++){
const a = rand(0, TAU), d = rand(8, 46);
const x = Math.sin(a) * d, z = Math.cos(a) * d;
if (clear(x, z, r)) return [x, z];
}
return null;
};
// center mound is a fixed feature, the rest scatter
hills[0].x = 0; hills[0].z = 0;
hills[0].mesh.position.set(0, 0.02, 0);
placed.push({ x: 0, z: 0, r: hills[0].r });
for (let k = 1; k < hills.length; k++){
const hl = hills[k];
for (let t = 0; t < 60; t++){
const a = rand(0, TAU), d = rand(10, 40);
const x = Math.sin(a) * d, z = Math.cos(a) * d;
let ok = Math.hypot(x, z) < 46;
if (ok) for (const p of placed) if (Math.hypot(x - p.x, z - p.z) < hl.r + p.r + 2) ok = false;
if (ok) for (const s of SPAWN) if (Math.hypot(x - s[0], z - s[1]) < hl.r + 8) ok = false;
if (ok){
hl.x = x; hl.z = z;
hl.mesh.position.set(x, 0.02, z);
placed.push({ x, z, r: hl.r });
break;
}
}
}
for (const d of dyns){
const p = spot(d.r + 0.5);
if (p){
d.x = p[0]; d.z = p[1]; d.vx = d.vz = 0;
d.mesh.position.set(d.x, 0, d.z);
d.mesh.rotation.y = rand(0, TAU);
d.alive = true; d.mesh.visible = true; d.respawn = 0;
placed.push({ x: d.x, z: d.z, r: d.r });
}
}
let bi = 0;
for (let row = 0; row < 4; row++){
for (let t = 0; t < 50; t++){
const a = rand(0, TAU), d = rand(12, 42);
const ax = Math.sin(a) * d, az = Math.cos(a) * d;
const ra = rand(0, TAU);
const ux = Math.cos(ra), uz = -Math.sin(ra); // row direction
const pts = [-3.5, 0, 3.5].map(o => [ax + ux * o, az + uz * o]);
if (pts.every(p => clear(p[0], p[1], 2.2))){
for (const p of pts){
const b = barriers[bi++];
b.x = p[0]; b.z = p[1]; b.rot = ra;
b.c = Math.cos(ra); b.s = Math.sin(ra);
b.mesh.position.set(b.x, 0, b.z);
b.mesh.rotation.y = ra;
placed.push({ x: b.x, z: b.z, r: 2.2 });
}
break;
}
}
}
while (bi < barriers.length){
const p = spot(2.4);
if (!p) break;
const b = barriers[bi++];
b.x = p[0]; b.z = p[1]; b.rot = rand(0, TAU);
b.c = Math.cos(b.rot); b.s = Math.sin(b.rot);
b.mesh.position.set(b.x, 0, b.z);
b.mesh.rotation.y = b.rot;
placed.push({ x: b.x, z: b.z, r: 2.2 });
}
}
/* circle (truck/debris) vs static OBB (barrier) */
function circleVsOBB(px, pz, r, b){
const dx = px - b.x, dz = pz - b.z;
const lx = b.c * dx - b.s * dz;
const lz = b.s * dx + b.c * dz;
const cx = clamp(lx, -b.hx, b.hx), cz = clamp(lz, -b.hz, b.hz);
const ddx = lx - cx, ddz = lz - cz;
const d2 = ddx * ddx + ddz * ddz;
if (d2 > r * r) return null;
let lnx, lnz, depth;
if (d2 > 1e-9){
const d = Math.sqrt(d2);
lnx = ddx / d; lnz = ddz / d; depth = r - d;
} else {
const penX = b.hx - Math.abs(lx), penZ = b.hz - Math.abs(lz);
if (penX < penZ){ lnx = lx > 0 ? 1 : -1; lnz = 0; depth = penX + r; }
else { lnx = 0; lnz = lz > 0 ? 1 : -1; depth = penZ + r; }
}
return { nx: lnx * b.c + lnz * b.s, nz: -lnx * b.s + lnz * b.c, depth };
}
/* ================= particle pools ================= */
class ShardPool {
constructor(color, additive, n){
const geo = new THREE.TetrahedronGeometry(0.17);
const mat = additive
? new THREE.MeshBasicMaterial({ color, blending: THREE.AdditiveBlending, depthWrite: false, transparent: true })
: new THREE.MeshLambertMaterial({ color });
this.mesh = new THREE.InstancedMesh(geo, mat, n);
this.mesh.frustumCulled = false;
scene.add(this.mesh);
this.n = n; this.grav = 26;
this.p = Array.from({ length: n }, () => ({ life: 0, max: 1, x: 0, y: 0, z: 0, vx: 0, vy: 0, vz: 0, s: 1 }));
this.dummy = new THREE.Object3D();
}
spawn(x, y, z, vx, vy, vz, life, s){
const e = this.p.find(e => e.life <= 0) || this.p[0];
e.life = e.max = life; e.x = x; e.y = y; e.z = z; e.vx = vx; e.vy = vy; e.vz = vz; e.s = s;
}
step(dt){
for (let i = 0; i < this.n; i++){
const e = this.p[i];
let k = 0;
if (e.life > 0){
e.life -= dt;
e.vy -= this.grav * dt;
e.x += e.vx * dt; e.y += e.vy * dt; e.z += e.vz * dt;
if (e.y < terrainHeight(e.x, e.z) + 0.05 && e.vy < 0) e.life = 0;
else k = clamp(e.life / e.max * 1.8, 0, 1) * e.s;
}
this.dummy.position.set(e.x, e.y, e.z);
this.dummy.rotation.set(e.life * 9, e.life * 7, 0);
this.dummy.scale.set(k, k, k);
this.dummy.updateMatrix();
this.mesh.setMatrixAt(i, this.dummy.matrix);
}
this.mesh.instanceMatrix.needsUpdate = true;
}
}
const sparks = new ShardPool(0xffc266, true, 140);
const dirt = new ShardPool(0x6b4a30, false, 140);
function sparkBurst(x, y, z, n, pow){
for (let i = 0; i < n; i++){
const a = rand(0, TAU), sp = rand(3, 4 + pow);
sparks.spawn(x, y, z, Math.sin(a) * sp, rand(2, 3 + pow * 0.7), Math.cos(a) * sp, rand(0.3, 0.7), rand(0.6, 1.4));
}
}
function dirtBurst(x, y, z, n, pow){
for (let i = 0; i < n; i++){
const a = rand(0, TAU), sp = rand(2, 3 + pow);
dirt.spawn(x, y + 0.2, z, Math.sin(a) * sp, rand(3, 4 + pow), Math.cos(a) * sp, rand(0.4, 0.9), rand(0.8, 1.8));
}
}
const smokes = [];
const smokeGeo = new THREE.SphereGeometry(0.55, 7, 6);
for (let i = 0; i < 36; i++){
const m = new THREE.Mesh(smokeGeo, new THREE.MeshLambertMaterial({ color: 0x8a8a88, transparent: true, opacity: 0, depthWrite: false }));
m.visible = false;
scene.add(m);
smokes.push({ mesh: m, life: 0, max: 1 });
}
function puff(x, y, z, big){
const s = smokes.find(s => s.life <= 0) || smokes[0];
s.life = s.max = big ? 1.6 : 1.1;
s.mesh.position.set(x + rand(-0.3, 0.3), y, z + rand(-0.3, 0.3));
s.mesh.scale.setScalar(big ? 1.2 : 0.8);
s.mesh.visible = true;
}
function stepSmoke(dt){
for (const s of smokes){
if (s.life <= ){ continue; }
}
}
/* (real implementation — the stub above is replaced) */
function stepSmoke(dt){
for (const s of smokes){
if (s.life <= 0){ if (s.mesh.visible) s.mesh.visible = false; continue; }
s.life -= dt;
s.mesh.position.y += 2.4 * dt;
s.mesh.scale.multiplyScalar(1 + 1.4 * dt);
s.mesh.material.opacity = 0.34 * clamp(s.life / s.max, 0, 1);
if (s.life <= 0) s.mesh.visible = false;
}
}
/* ================= sound (all synthesized, Web Audio) ================= */
let AC = null, master = null, noiseBuf = null, engine = null, crowdG = null, muted = false;
function initAudio(){
if (AC) return;
try { AC = new (window.AudioContext || window.webkitAudioContext)(); } catch (e) { return; }
master = AC.createGain(); master.gain.value = muted ? 0 : 0.7; master.connect(AC.destination);
noiseBuf = AC.createBuffer(1, AC.sampleRate, AC.sampleRate);
const nd = noiseBuf.getChannelData(0);
for (let i = 0; i < nd.length; i++) nd[i] = Math.random() * 2 - 1;
const o1 = AC.createOscillator(); o1.type = 'sawtooth'; o1.frequency.value = 55;
const o2 = AC.createOscillator(); o2.type = 'square'; o2.frequency.value = 28;
const f = AC.createBiquadFilter(); f.type = 'lowpass'; f.frequency.value = 320; f.Q.value = 1.4;
const g = AC.createGain(); g.gain.value = 0;
o1.connect(f); o2.connect(f); f.connect(g); g.connect(master);
o1.start(); o2.start();
engine = { o1, o2, f, g };
const len = AC.sampleRate * 2, buf = AC.createBuffer(1, len, AC.sampleRate), d = buf.getChannelData(0);
let lastN = 0;
for (let i = 0; i < len; i++){ const w = Math.random() * 2 - 1; lastN = (lastN + 0.03 * w) / 1.03; d[i] = lastN * 4; }
const src = AC.createBufferSource(); src.buffer = buf; src.loop = true;
const bp = AC.createBiquadFilter(); bp.type = 'bandpass'; bp.frequency.value = 650; bp.Q.value = 0.35;
crowdG = AC.createGain(); crowdG.gain.value = 0.05;
src.connect(bp); bp.connect(crowdG); crowdG.connect(master);
src.start();
}
function sfxThud(v){
if (!AC || muted) return;
v = clamp(v, 0.1, 1);
const t = AC.currentTime;
const o = AC.createOscillator(); o.type = 'sine';
o.frequency.setValueAtTime(130, t); o.frequency.exponentialRampToValueAtTime(36, t + 0.2);
const g = AC.createGain(); g.gain.setValueAtTime(0.55 * v, t); g.gain.exponentialRampToValueAtTime(0.001, t + 0.24);
o.connect(g); g.connect(master); o.start(t); o.stop(t + 0.26);
const n = AC.createBufferSource(); n.buffer = noiseBuf;
const nf = AC.createBiquadFilter(); nf.type = 'lowpass'; nf.frequency.value = 800;
const ng = AC.createGain(); ng.gain.setValueAtTime(0.3 * v, t); ng.gain.exponentialRampToValueAtTime(0.001, t + 0.12);
n.connect(nf); nf.connect(ng); ng.connect(master); n.start(t); n.stop(t + 0.14);
}
function sfxClang(v){
if (!AC || muted) return;
v = clamp(v, 0.1, 1);
const t = AC.currentTime;
[218, 331].forEach(fr => {
const o = AC.createOscillator(); o.type = 'square'; o.frequency.value = fr;
const f = AC.createBiquadFilter(); f.type = 'bandpass'; f.frequency.value = 500; f.Q.value = 2;
const g = AC.createGain(); g.gain.setValueAtTime(0.12 * v, t); g.gain.exponentialRampToValueAtTime(0.001, t + 0.22);
o.connect(f); f.connect(g); g.connect(master); o.start(t); o.stop(t + 0.24);
});
}
function sfxSmash(){
if (!AC || muted) return;
const t = AC.currentTime;
const n = AC.createBufferSource(); n.buffer = noiseBuf;
const f = AC.createBiquadFilter(); f.type = 'highpass'; f.frequency.value = 1200;
const g = AC.createGain(); g.gain.setValueAtTime(0.3, t); g.gain.exponentialRampToValueAtTime(0.001, t + 0.3);
n.connect(f); f.connect(g); g.connect(master); n.start(t); n.stop(t + 0.32);
}
function sfxHorn(){
if (!AC || muted) return;
const t = AC.currentTime;
[233, 293].forEach(fr => {
const o = AC.createOscillator(); o.type = 'sawtooth'; o.frequency.value = fr;
const g = AC.createGain();
g.gain.setValueAtTime(0.0001, t);
g.gain.linearRampToValueAtTime(0.14, t + 0.05);
g.gain.setValueAtTime(0.14, t + 0.9);
g.gain.exponentialRampToValueAtTime(0.001, t + 1.3);
o.connect(g); g.connect(master); o.start(t); o.stop(t + 1.35);
});
}
/* ================= game state & HUD ================= */
let score = 0, timeLeft = 120, ended = false, photoMode = false, excite = 0, camShake = 0;
const hudScore = document.getElementById('score');
const hudClock = document.getElementById('clock');
const popupsEl = document.getElementById('popups');
const flashEl = document.getElementById('flash');
const photoCard = document.getElementById('photoCard');
const photoScore = document.getElementById('photoScore');
const endCard = document.getElementById('endCard');
const endScore = document.getElementById('endScore');
function fmtClock(t){
t = Math.max(0, t);
return Math.floor(t / 60) + ':' + String(Math.floor(t % 60)).padStart(2, '0');
}
const projV = new THREE.Vector3();
function popup(txt, wx, wy, wz, big){
projV.set(wx, wy, wz).project(camera);
if (projV.z > 1) return;
const el = document.createElement('div');
el.className = 'pop' + (big ? ' big' : '');
el.textContent = txt;
el.style.left = ((projV.x * 0.5 + 0.5) * innerWidth) + 'px';
el.style.top = ((-projV.y * 0.5 + 0.5) * innerHeight) + 'px';
popupsEl.appendChild(el);
setTimeout(() => el.remove(), 950);
}
function addScore(pts, label, x, z, big){
score += pts;
hudScore.textContent = score.toLocaleString('en-US');
popup('+' + pts + ' ' + label, x, 3.5, z, big);
}
function crowdExcite(v){ excite = Math.max(excite, v); }
function playerFlash(){ flashEl.animate([{ opacity: 0.85 }, { opacity: 0 }], { duration: 520, easing: 'ease-out' }); }
function togglePhoto(){
photoMode = !photoMode;
if (photoMode){
photoScore.textContent = score.toLocaleString('en-US');
photoCard.classList.add('show');
if (engine) engine.g.gain.value = 0;
if (crowdG) crowdG.gain.value = 0;
} else {
photoCard.classList.remove('show');
}
}
/* ================= wrecks & respawn ================= */
function wreckTruck(t, byPlayer){
t.wrecked = true;
t.wreckT = t.isPlayer ? 3.5 : 4.5;
t.hp = 0;
t.spinVis = rand(-1, 1) * 4;
t.vx *= 0.4; t.vz *= 0.4;
t.mesh.bodyMat.color.setHex(0x26262a);
t.mesh.bodyMat.metalness = 0.15; t.mesh.bodyMat.roughness = 0.9;
t.mesh.accMat.color.setHex(0x3a3a3c);
puff(t.x, t.y + 2, t.z, true); puff(t.x, t.y + 2, t.z, true);
sfxThud(0.9);
crowdExcite(1);
if (byPlayer && !t.isPlayer) addScore(500, 'RIVAL WRECKED', t.x, t.z, true);
if (t.isPlayer){ playerFlash(); popup('WRECKED!', t.x, 4, t.z, true); }
}
function respawnTruck(t){
let best = SPAWN[0], bd = -1;
for (const s of SPAWN){
let md = 1e9;
for (const o of trucks){ if (o === t) continue; md = Math.min(md, Math.hypot(s[0] - o.x, s[1] - o.z)); }
if (md > bd){ bd = md; best = s; }
}
t.x = best[0]; t.z = best[1];
t.y = terrainHeight(t.x, t.z); t.prevGh = t.y;
t.vx = t.vz = t.vy = 0;
t.h = Math.atan2(-t.x, -t.z);
t.hp = 100; t.wrecked = false; t.invuln = 2; t.flash = 0;
t.spinVis = 0; t.airT = 0; t.grounded = true; t.smokeT = 0;
t.mesh.bodyMat.color.setHex(0xd6d8da);
t.mesh.bodyMat.metalness = 0.85; t.mesh.bodyMat.roughness = 0.3;
t.mesh.accMat.color.setHex(t.mesh.accent);
}
/* ================= collision events ================= */
function applyHit(victim, rammer, impact, hx, hz){
if (victim.wrecked || rammer.wrecked || victim.invuln > 0) return;
victim.hp -= Math.min(55, impact * 1.7);
rammer.hp -= Math.min(30, impact * 0.5);
victim.flash = 1; rammer.flash = 0.5;
victim.spinVis += rand(-1, 1) * impact * 0.14;
sparkBurst(hx, 1.7, hz, Math.min(14, (4 + impact * 0.6) | 0), impact * 0.35);
sfxThud(impact / 26);
const pInv = victim.isPlayer || rammer.isPlayer;
camShake = Math.max(camShake, impact * (pInv ? 0.05 : 0.015));
crowdExcite(clamp(impact / 22, 0.2, 1));
if (rammer.isPlayer && !victim.isPlayer)
addScore(Math.round(Math.min(impact, 26) * 9), 'SLAM', hx, hz, impact > 16);
if (victim.isPlayer) playerFlash();
if (victim.hp <= 0) wreckTruck(victim, rammer.isPlayer);
else if (rammer.hp <= 0) wreckTruck(rammer, victim.isPlayer);
}
function smashStack(d, byPlayer){
d.alive = false; d.mesh.visible = false;
d.respawn = rand(8, 13);
burstTires(d.x, d.z, 4);
sfxSmash();
crowdExcite(0.3);
if (byPlayer) addScore(25, 'STACK', d.x, d.z);
}
/* ================= physics ================= */
const keys = { up: false, down: false, left: false, right: false, brake: false };
function stepTruck(t, dt){
if (t.invuln > 0) t.invuln -= dt;
if (t.flash > 0) t.flash = Math.max(0, t.flash - 5 * dt);
const gh0 = terrainHeight(t.x, t.z);
const airborne = t.y > gh0 + 0.1;
if (t.wrecked){
t.throttle = 0; t.steer = 0; t.brake = false;
t.wreckT -= dt;
t.smokeT -= dt;
if (t.smokeT <= 0){
t.smokeT = 0.13;
puff(t.x + Math.sin(t.h) * 1.6, t.y + 2.2, t.z + Math.cos(t.h) * 1.6, false);
}
if (t.wreckT <= 0) respawnTruck(t);
} else if (t.isPlayer){
t.throttle = (keys.up ? 1 : 0) + (keys.down ? -0.62 : 0);
t.steer = (keys.left ? 1 : 0) + (keys.right ? -1 : 0);
t.brake = keys.brake;
} else {
aiSteer(t, dt);
}
if (!t.wrecked){
if (!airborne){
const fx = Math.sin(t.h), fz = Math.cos(t.h);
let vf = t.vx * fx + t.vz * fz;
let latX = t.vx - fx * vf, latZ = t.vz - fz * vf;
const gk = Math.max(0, 1 - (t.brake ? 1.7 : 7.5) * dt); // handbrake = drift
latX *= gk; latZ *= gk;
vf += t.throttle * 30 * dt;
vf = clamp(vf, -9, 26 * (t.isPlayer ? 1 : t.ai.top));
vf *= Math.max(0, 1 - (t.throttle !== 0 ? 0.1 : 0.85) * dt);
if (t.brake) vf *= Math.max(0, 1 - 2.4 * dt);
t.h += t.steer * 2.6 * clamp(vf / 7, -1, 1) * (t.brake ? 1.35 : 1) * dt;
const nfx = Math.sin(t.h), nfz = Math.cos(t.h);
t.vx = nfx * vf + latX;
t.vz = nfz * vf + latZ;
if (t.throttle > 0 && Math.hypot(t.vx, t.vz) < 14 && Math.random() < 0.45)
dirtBurst(t.x - nfx * 2.6, t.y, t.z - nfz * 2.6, 1, 2.2); // roost
} else {
t.h += t.steer * 0.7 * dt;
}
} else {
const f = Math.max(0, 1 - 2.2 * dt);
t.vx *= f; t.vz *= f;
}
/* vertical: gravity + terrain field, slope rate → ballistic at crests */
t.vy -= 30 * dt;
t.y += t.vy * dt;
const gh = terrainHeight(t.x, t.z);
if (t.y <= gh){
const wasAir = !t.grounded;
if (wasAir){
const impact = -t.vy;
if (impact > 5){
dirtBurst(t.x, gh, t.z, Math.min(16, (4 + impact) | 0), impact * 0.4);
sfxThud(impact / 34);
if (t.isPlayer) camShake = Math.max(camShake, impact * 0.03);
}
if (t.airT > 0.5 && !t.wrecked){
crowdExcite(0.5);
if (t.isPlayer) addScore(Math.round(t.airT * 180), 'BIG AIR', t.x, t.z, t.airT > 0.8);
}
}
t.y = gh; t.grounded = true; t.airT = 0;
t.vy = clamp((gh - t.prevGh) / dt, -20, 15);
} else if (t.grounded && t.y - gh < 0.13 && t.vy <= 0.5){
t.y = gh; // anti-chatter on gentle downslopes
t.vy = clamp((gh - t.prevGh) / dt, -20, 15);
} else {
t.grounded = false;
t.airT += dt;
}
t.prevGh = gh;
t.x += t.vx * dt;
t.z += t.vz * dt;
const dc = Math.hypot(t.x, t.z);
if (dc > BOUND){
const nx = t.x / dc, nz = t.z / dc;
t.x = nx * BOUND; t.z = nz * BOUND;
const vn = t.vx * nx + t.vz * nz;
if (vn > 0){
t.vx -= nx * vn * 1.5; t.vz -= nz * vn * 1.5;
if (vn > 6){
sfxThud(vn / 30);
sparkBurst(t.x, 1.4, t.z, 5, 2);
if (t.isPlayer){ camShake = Math.max(camShake, vn * 0.03); playerFlash(); }
}
}
}
t.spinVis *= Math.max(0, 1 - 2.5 * dt);
/* slope alignment + body lean */
const fwx = Math.sin(t.h), fwz = Math.cos(t.h);
const rwx = -Math.cos(t.h), rwz = Math.sin(t.h);
const e = 1.4;
const tp = -Math.atan2(terrainHeight(t.x + fwx * e, t.z + fwz * e) - terrainHeight(t.x - fwx * e, t.z - fwz * e), 2 * e);
const tr = -Math.atan2(terrainHeight(t.x + rwx * e, t.z + rwz * e) - terrainHeight(t.x - rwx * e, t.z - rwz * e), 2 * e)
+ (t.grounded ? t.steer * 0.1 * clamp(Math.hypot(t.vx, t.vz) / 22, 0, 1) : 0);
const tilt = t.mesh.tilt;
tilt.rotation.x += (tp - tilt.rotation.x) * Math.min(1, 8 * dt);
tilt.rotation.z += (tr - tilt.rotation.z) * Math.min(1, 8 * dt);
t.mesh.group.position.set(t.x, t.y, t.z);
t.mesh.group.rotation.y = t.h + t.spinVis;
const vf2 = t.vx * fwx + t.vz * fwz;
for (const w of t.mesh.wheels) w.rotation.x += vf2 / 0.95 * dt;
if (t.mesh.marker) t.mesh.marker.rotation.y += 2.4 * dt;
const bm = t.mesh.bodyMat;
bm.emissive.setHex(t.mesh.accent);
bm.emissiveIntensity = t.flash * 0.8 + (t.invuln > 0 ? 0.22 + 0.18 * Math.sin(performance.now() * 0.02) : 0);
}
function collideTrucks(){
for (let i = 0; i < trucks.length; i++){
for (let j = i + 1; j < trucks.length; j++){
const a = trucks[i], b = trucks[j];
const dx = b.x - a.x, dz = b.z - a.z;
const R = TR * 2;
const d2 = dx * dx + dz * dz;
if (d2 > R * R || d2 < 1e-9) continue;
const d = Math.sqrt(d2), nx = dx / d, nz = dz / d;
const overlap = R - d;
a.x -= nx * overlap * 0.5; a.z -= nz * overlap * 0.5;
b.x += nx * overlap * 0.5; b.z += nz * overlap * 0.5;
const rvn = (b.vx - a.vx) * nx + (b.vz - a.vz) * nz;
if (rvn >= 0) continue;
const van = a.vx * nx + a.vz * nz;
const vbn = b.vx * nx + b.vz * nz;
const impact = -rvn;
const jimp = -1.35 * rvn * 5; // equal masses 10
a.vx -= nx * jimp * 0.1; a.vz -= nz * jimp * 0.1;
b.vx += nx * jimp * 0.1; b.vz += nz * jimp * 0.1;
if (impact > 4){
const rammer = van > -vbn ? a : b;
const victim = rammer === a ? b : a;
applyHit(victim, rammer, impact, (a.x + b.x) * 0.5, (a.z + b.z) * 0.5);
}
}
}
}
function collideTruckDyn(t, d){
const dx = d.x - t.x, dz = d.z - t.z;
const R = TR + d.r;
const d2 = dx * dx + dz * dz;
if (d2 > R * R || d2 < 1e-9) return;
const dist = Math.sqrt(d2), nx = dx / dist, nz = dz / dist;
const overlap = R - dist, tot = 10 + d.m;
t.x -= nx * overlap * (d.m / tot); t.z -= nz * overlap * (d.m / tot);
d.x += nx * overlap * (10 / tot); d.z += nz * overlap * (10 / tot);
const rvn = (d.vx - t.vx) * nx + (d.vz - t.vz) * nz;
if (rvn >= 0) return;
const impact = -rvn;
const jimp = -1.3 * rvn / (0.1 + 1 / d.m);
t.vx -= nx * jimp * 0.1; t.vz -= nz * jimp * 0.1;
d.vx += nx * jimp / d.m; d.vz += nz * jimp / d.m;
if (impact > 3){
sparkBurst(d.x - nx * d.r, 1.2, d.z - nz * d.r, Math.min(10, (3 + impact * 0.5) | 0), impact * 0.25);
sfxThud(impact / 30);
if (t.isPlayer) camShake = Math.max(camShake, impact * 0.02);
if (d.kind === 'stack' && impact > 6.5) smashStack(d, t.isPlayer && !t.wrecked);
else if (d.kind === 'car' && impact > 6 && t.isPlayer && !t.wrecked) addScore(40, 'CAR SLAM', d.x, d.z);
}
}
function truckBarriers(t){
for (const b of barriers){
const hit = circleVsOBB(t.x, t.z, TR, b);
if (!hit) continue;
t.x += hit.nx * hit.depth; t.z += hit.nz * hit.depth;
const vn = t.vx * hit.nx + t.vz * hit.nz;
if (vn < 0){
const impact = -vn;
t.vx -= hit.nx * vn * 1.5; t.vz -= hit.nz * vn * 1.5;
if (impact > 5){
sfxClang(impact / 20);
sparkBurst(t.x + hit.nx * TR, 1.2, t.z + hit.nz * TR, 6, impact * 0.2);
if (t.isPlayer){ camShake = Math.max(camShake, impact * 0.03); playerFlash(); }
if (!t.wrecked && t.invuln <= 0) t.hp -= impact * 0.35;
if (t.hp <= 0) wreckTruck(t, false);
}
}
}
}
function stepDyns(dt){
for (const d of dyns){
if (d.kind === 'stack' && !d.alive){
d.respawn -= dt;
if (d.respawn <= 0){
for (let t = 0; t < 50; t++){
const a = rand(0, TAU), dd = rand(8, 46);
const x = Math.sin(a) * dd, z = Math.cos(a) * dd;
if (Math.hypot(x, z) < 48 && terrainHeight(x, z) < 0.3){
let ok = true;
for (const o of dyns) if (o !== d && o.alive && Math.hypot(x - o.x, z - o.z) < 4) ok = false;
for (const s of SPAWN) if (Math.hypot(x - s[0], z - s[1]) < 7) ok = false;
if (ok){ d.x = x; d.z = z; break; }
}
}
d.alive = true; d.vx = d.vz = 0;
d.mesh.visible = true;
d.mesh.position.set(d.x, 0, d.z);
}
continue;
}
d.x += d.vx * dt; d.z += d.vz * dt;
const f = Math.max(0, 1 - 2.8 * dt);
d.vx *= f; d.vz *= f;
const dd = Math.hypot(d.x, d.z);
if (dd > 51){ d.x *= 51 / dd; d.z *= 51 / dd; d.vx = d.vz = 0; }
d.mesh.position.set(d.x, 0, d.z);
}
for (let i = 0; i < dyns.length; i++){
for (let j = i + 1; j < dyns.length; j++){
const a = dyns[i], b = dyns[j];
if (!a.alive || !b.alive) continue;
const dx = b.x - a.x, dz = b.z - a.z, R = a.r + b.r;
const d2 = dx * dx + dz * dz;
if (d2 > R * R || d2 < 1e-9) continue;
const dist = Math.sqrt(d2), push = (R - dist) / 2, nx = dx / dist, nz = dz / dist;
a.x -= nx * push; a.z -= nz * push;
b.x += nx * push; b.z += nz * push;
a.mesh.position.set(a.x, 0, a.z);
b.mesh.position.set(b.x, 0, b.z);
}
}
for (const d of dyns){
if (!d.alive) continue;
for (const b of barriers){
const hit = circleVsOBB(d.x, d.z, d.r, b);
if (hit){
d.x += hit.nx * hit.depth; d.z += hit.nz * hit.depth;
const vn = d.vx * hit.nx + d.vz * hit.nz;
if (vn < 0){ d.vx -= hit.nx * vn * 1.4; d.vz -= hit.nz * vn * 1.4; }
d.mesh.position.set(d.x, 0, d.z);
}
}
}
}
/* ================= rival AI ================= */
function pickTarget(t){
const a = t.ai;
a.retarget = rand(3, 6.5);
a.target = null;
const setPoint = () => { a.tx = rand(-32, 32); a.tz = rand(-32, 32); };
const others = trucks.filter(o => o !== t && !o.wrecked);
const roll = Math.random();
if (t.hp < 35 && roll < 0.6 && others.length){ // flee when hurt
let th = others[0], td = 1e9;
for (const o of others){ const d = Math.hypot(o.x - t.x, o.z - t.z); if (d < td){ td = d; th = o; } }
const fx = t.x - th.x, fz = t.z - th.z, fd = Math.hypot(fx, fz) || 1;
a.tx = clamp(t.x + fx / fd * 24, -42, 42);
a.tz = clamp(t.z + fz / fd * 24, -42, 42);
return;
}
if (a.type === 'hunter'){
if (!player.wrecked && roll < 0.75) a.target = player;
} else if (a.type === 'berserker'){
if (others.length) a.target = others.reduce((m, o) =>
Math.hypot(o.x - t.x, o.z - t.z) < Math.hypot(m.x - t.x, m.z - t.z) ? o : m);
} else if (a.type === 'jumper'){
if (roll < 0.45 && hills.length){
const hl = hills[(Math.random() * hills.length) | 0];
a.tx = hl.x + rand(-0.4, 0.4) * hl.r;
a.tz = hl.z + rand(-0.4, 0.4) * hl.r;
return;
}
if (roll < 0.8 && others.length) a.target = others[(Math.random() * others.length) | 0];
} else {
const weak = others.filter(o => o.hp < 85);
if (weak.length && roll < 0.7) a.target = weak.reduce((m, o) => o.hp < m.hp ? o : m);
else if (others.length && roll < 0.9) a.target = others[(Math.random() * others.length) | 0];
}
if (!a.target) setPoint();
}
function aiSteer(t, dt){
const a = t.ai;
a.retarget -= dt;
if (a.retarget <= 0 || (a.target && a.target.wrecked)) pickTarget(t);
let dx, dz;
if (a.target && !a.target.wrecked){
dx = a.target.x + a.target.vx * 0.3 - t.x;
dz = a.target.z + a.target.vz * 0.3 - t.z;
} else {
dx = a.tx - t.x; dz = a.tz - t.z;
if (dx * dx + dz * dz < 25){
pickTarget(t);
if (a.target && !a.target.wrecked){ dx = a.target.x - t.x; dz = a.target.z - t.z; }
else { dx = a.tx - t.x; dz = a.tz - t.z; }
}
}
/* separation: never bunch up */
for (const o of trucks){
if (o === t) continue;
const sx = t.x - o.x, sz = t.z - o.z;
const d2 = sx * sx + sz * sz;
if (d2 < 81 && d2 > 0.01){
const d = Math.sqrt(d2), w = (9 - d) * 0.55;
dx += sx / d * w; dz += sz / d * w;
}
}
const wv = Math.sin(performance.now() * 0.0006 + a.phase * 7) * 0.5;
const wx = -dz, wz = dx;
dx += wx * wv * 0.3; dz += wz * wv * 0.3;
if (Math.hypot(t.x, t.z) > 44){ dx -= t.x * 0.9; dz -= t.z * 0.9; }
const ad = Math.atan2(dx, dz);
const diff = wrapA(ad - t.h);
t.steer = clamp(diff * 2.4, -1, 1);
t.throttle = Math.abs(diff) > 1.7 ? 0.4 : 0.55 + 0.45 * a.aggr;
if (a.target && !a.target.wrecked){
const tdx = a.target.x - t.x, tdz = a.target.z - t.z;
if (tdx * tdx + tdz * tdz < 110) t.throttle = 1;
}
t.brake = false;
/* avoidance probe */
const speed = Math.hypot(t.vx, t.vz);
const pl = 6 + speed * 0.25;
const fx = Math.sin(t.h), fz = Math.cos(t.h);
const px = t.x + fx * pl, pz = t.z + fz * pl;
let away = 0;
const avoid = (ox, oz, r) => {
const ax = ox - px, az = oz - pz;
const rr = r + 2.6;
if (ax * ax + az * az < rr * rr){
away += (fx * az - fz * ax < 0 ? -1 : 1) * 0.9;
t.throttle *= 0.8;
}
};
for (const d of dyns) if (d.alive) avoid(d.x, d.z, d.r);
for (const b of barriers) avoid(b.x, b.z, 2.3);
if (away) t.steer = clamp(t.steer + away, -1, 1);
/* unstuck */
if (speed < 1.2) a.stuck += dt; else a.stuck = 0;
if (a.stuck > 1.5){ a.rev = 0.9; a.stuck = 0; }
if (a.rev > 0){
a.rev -= dt;
t.throttle = -0.7;
t.steer = -(t.steer || 1);
}
}
/* ================= input ================= */
const KEYMAP = { w: 'up', arrowup: 'up', s: 'down', arrowdown: 'down',
a: 'left', arrowleft: 'left', d: 'right', arrowright: 'right', ' ': 'brake' };
addEventListener('keydown', e => {
initAudio();
if (AC && AC.state === 'suspended') AC.resume();
const k = e.key.toLowerCase();
if (k === 'p'){ if (!ended) togglePhoto(); e.preventDefault(); return; }
if (k === 'm'){ muted = !muted; if (master) master.gain.value = muted ? 0 : 0.7; return; }
if (k === 'r' && ended){ resetMatch(); return; }
const m = KEYMAP[k];
if (m){ keys[m] = true; e.preventDefault(); }
});
addEventListener('keyup', e => {
const m = KEYMAP[e.key.toLowerCase()];
if (m) keys[m] = false;
});
addEventListener('pointerdown', () => {
initAudio();
if (AC && AC.state === 'suspended') AC.resume();
});
/* ================= camera ================= */
const lookTarget = new THREE.Vector3();
const camTmp = new THREE.Vector3();
let endAngle = rand(0, TAU);
function updateCamera(dt){
if (ended){
endAngle += dt * 0.12;
camera.position.set(Math.sin(endAngle) * 42, 17, Math.cos(endAngle) * 42);
camera.lookAt(0, 2, 0);
return;
}
const t = player;
const fx = Math.sin(t.h), fz = Math.cos(t.h);
const speed = Math.hypot(t.vx, t.vz);
camTmp.set(t.x - fx * (10.5 + speed * 0.08), t.y + 5.4 + speed * 0.03, t.z - fz * (10.5 + speed * 0.08));
camera.position.lerp(camTmp, 1 - Math.pow(0.0015, dt));
const cd = Math.hypot(camera.position.x, camera.position.z);
if (cd > 54.2){ const s = 54.2 / cd; camera.position.x *= s; camera.position.z *= s; }
const minY = terrainHeight(camera.position.x, camera.position.z) + 2.2;
if (camera.position.y < minY) camera.position.y = minY;
camTmp.set(t.x + fx * 5, t.y + 1.6, t.z + fz * 5);
lookTarget.lerp(camTmp, 1 - Math.pow(0.001, dt));
camShake = Math.max(0, camShake - camShake * 6 * dt - 0.02 * dt);
if (camShake > 0.001){
camera.position.x += rand(-1, 1) * camShake;
camera.position.y += rand(-1, 1) * camShake * 0.6;
camera.position.z += rand(-1, 1) * camShake;
}
camera.lookAt(lookTarget);
const fovT = 60 + speed * 0.45;
camera.fov += (fovT - camera.fov) * Math.min(1, 4 * dt);
camera.updateProjectionMatrix();
}
/* ================= match flow ================= */
function resetMatch(){
score = DEMO ? 5000 : 0; // #demo sets the REAL score the HUD reads
timeLeft = DEMO ? 20 : 120; // #demo sets the REAL clock the tick decrements
ended = false; photoMode = false;
endCard.classList.remove('show');
photoCard.classList.remove('show');
hudScore.textContent = score.toLocaleString('en-US');
layoutObstacles();
trucks.forEach((t, i) => {
const s = SPAWN[i];
t.x = s[0]; t.z = s[1];
t.y = terrainHeight(t.x, t.z); t.prevGh = t.y;
t.h = Math.atan2(-t.x, -t.z);
t.vx = t.vz = t.vy = 0;
t.hp = 100; t.wrecked = false; t.wreckT = 0; t.invuln = 1.2; t.flash = 0;
t.spinVis = 0; t.airT = 0; t.grounded = true; t.smokeT = 0;
t.throttle = t.steer = 0; t.brake = false;
t.mesh.bodyMat.color.setHex(0xd6d8da);
t.mesh.bodyMat.metalness = 0.85; t.mesh.bodyMat.roughness = 0.3;
t.mesh.accMat.color.setHex(t.mesh.accent);
if (t.ai){ t.ai.retarget = rand(0.2, 1.5); t.ai.target = null; t.ai.tx = 0; t.ai.tz = 0; t.ai.stuck = 0; t.ai.rev = 0; }
});
for (const f of flyTires){ f.active = false; f.mesh.visible = false; }
for (const s of smokes){ s.life = 0; s.mesh.visible = false; }
popupsEl.innerHTML = '';
excite = 0; camShake = 0;
const p = player;
camera.position.set(p.x - Math.sin(p.h) * 12, p.y + 6, p.z - Math.cos(p.h) * 12);
lookTarget.set(p.x, p.y + 1.5, p.z);
}
function endMatch(){
ended = true;
endScore.textContent = score.toLocaleString('en-US');
endCard.classList.add('show');
sfxHorn();
crowdExcite(1);
}
/* ================= audio per-frame ================= */
function updateAudio(dt){
if (!AC || !engine) return;
const sp = Math.hypot(player.vx, player.vz);
const tg = muted ? 0 : 0.03 + sp * 0.0022 + (player.throttle > 0 ? 0.012 : 0);
engine.g.gain.value += (tg - engine.g.gain.value) * Math.min(1, 8 * dt);
engine.o1.frequency.value = 50 + sp * 3.4;
engine.o2.frequency.value = 25 + sp * 1.7;
engine.f.frequency.value = 260 + sp * 46;
if (crowdG) crowdG.gain.value = muted ? 0 : 0.045 + excite * 0.11;
}
/* ================= main loop ================= */
let last = performance.now();
function frame(now){
requestAnimationFrame(frame);
const dt = Math.min(0.033, (now - last) / 1000) || 0.016;
last = now;
if (photoMode) return; // PHOTO MODE: physics step AND render are both skipped
if (!ended){
for (const t of trucks) stepTruck(t, dt);
collideTrucks();
for (const t of trucks) for (const d of dyns) if (d.alive) collideTruckDyn(t, d);
for (const t of trucks) truckBarriers(t);
stepDyns(dt);
stepFlyTires(dt);
sparks.step(dt); dirt.step(dt); stepSmoke(dt);
timeLeft -= dt;
if (timeLeft <= 0){ timeLeft = 0; endMatch(); }
crowdUniforms.uTime.value += dt;
excite = Math.max(0, excite - excite * 1.1 * dt - 0.01 * dt);
crowdUniforms.uExcite.value = excite;
} else {
crowdUniforms.uTime.value += dt;
sparks.step(dt); dirt.step(dt); stepSmoke(dt);
excite = Math.max(0, excite - excite * 0.8 * dt);
crowdUniforms.uExcite.value = excite;
}
updateCamera(dt);
updateAudio(dt);
hudClock.textContent = fmtClock(timeLeft);
hudClock.classList.toggle('low', timeLeft <= 10 && !ended);
renderer.render(scene, camera);
}
resetMatch();
requestAnimationFrame(frame);
</script>
</body>
</html>
Five trucks: each rival gets a role — hunter (chases you), berserker (chases the nearest truck), jumper (treats the mounds as targets), bully (hunts the weakest truck, and everyone flees when their own HP drops) — on independent 3–6.5 s retarget timers, plus a boids-style separation push inside 9 units, per-truck top-speed/turn-rate variance, a sinusoidal wander on the steering aim, and a reverse-out routine when stuck. They converge on the pack from different angles instead of forming a wall.
Demo mode: #demo is read once (location.href.endsWith('#demo')) and applied inside resetMatch() — score = DEMO ? 5000 : 0; timeLeft = DEMO ? 20 : 120; — the same two variables the HUD renders, the tick decrements, and the full-time check reads. It's the live match state starting pre-loaded, not a painted overlay: the clock genuinely runs out 20 seconds later and the real end card fires.
Photo mode: P flips a flag and the RAF callback returns right after timestamping — so both the physics step (trucks, AI, clock, particles, crowd-wave time) and renderer.render() are skipped. The last WebGL frame stays untouched on the canvas, the DOM card draws on top, and the next P resumes on the very following frame (a resize during the freeze re-renders the frozen state once so the frame isn't lost).
Collisions: everything is circles in the XZ plane — trucks are r = 2.4, mass-10 bodies; debris are circles (cars mass 7, tire stacks 1.2); barriers and the bowl wall are static, handled by a closest-point test against an oriented box. Penetration is split by mass ratio, then a normal-direction impulse with restitution 0.3–0.5 is applied from the relative normal velocity, and that impact speed is the single number driving damage, score, sparks, camera shake and the crowd surge. The vertical axis is separate: gravity plus a terrain-height field for the mounds, with the slope rate handed off to ballistic velocity at crests so trucks launch off the ramps.