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.Stainless Steel Stampede at the GigaDome drops five jacked-up, angular brushed-steel monster Cybertrucks into a floodlit dirt bowl packed with bouncing spectators, dirt jumps, concrete jersey barriers, and crushable junked sedans.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cybertruck Monster Rally — The GigaDome</title>
<style>
* { box-sizing: border-box; user-select: none; margin: 0; padding: 0; }
body { overflow: hidden; background: #0b0d12; font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif; color: #f0f4f8; }
canvas { display: block; width: 100vw; height: 100vh; }
#hud {
position: fixed; top: 0; left: 0; right: 0;
display: flex; justify-content: space-between; align-items: flex-start;
padding: 18px 26px; pointer-events: none; z-index: 10;
background: linear-gradient(to bottom, rgba(8,10,15,0.82) 0%, rgba(8,10,15,0) 100%);
}
.hud-block { display: flex; flex-direction: column; gap: 4px; }
.hud-label { font-size: 11px; letter-spacing: 2px; text-transform: uppercase; color: #8fa3b8; font-weight: 700; }
#arena-name { font-size: 22px; font-weight: 900; letter-spacing: 1.5px; text-transform: uppercase; color: #00e5ff; text-shadow: 0 0 12px rgba(0,229,255,0.45); }
#controls-hint { font-size: 12px; color: #cbd5e1; background: rgba(15,23,42,0.75); border: 1px solid rgba(148,163,184,0.3); padding: 5px 10px; border-radius: 4px; margin-top: 6px; display: inline-block; letter-spacing: 0.5px; }
.hud-stats { display: flex; gap: 32px; text-align: right; }
.stat-val { font-size: 32px; font-weight: 900; font-variant-numeric: tabular-nums; line-height: 1; color: #ffffff; text-shadow: 0 2px 10px rgba(0,0,0,0.7); }
#clock-val.low { color: #ff3b5c; animation: pulse 0.6s infinite alternate; }
@keyframes pulse { from { opacity: 1; } to { opacity: 0.5; } }
#hit-banner {
position: fixed; top: 92px; left: 50%; transform: translateX(-50%);
font-size: 18px; font-weight: 900; letter-spacing: 2px; text-transform: uppercase;
color: #ffe600; text-shadow: 0 2px 12px rgba(0,0,0,0.9);
opacity: 0; transition: opacity 0.15s ease-out; pointer-events: none; z-index: 11;
}
#photo-overlay {
position: fixed; inset: 0; display: none;
flex-direction: column; justify-content: space-between;
padding: 40px 52px; pointer-events: none; z-index: 30;
border: 14px solid rgba(255,255,255,0.9);
box-shadow: inset 0 0 90px rgba(0,0,0,0.75);
}
#photo-overlay.active { display: flex; }
.photo-badge {
align-self: flex-start; background: #ff2a5f; color: #fff;
font-weight: 900; font-size: 13px; letter-spacing: 2.5px; padding: 6px 14px; text-transform: uppercase;
}
.photo-card {
background: rgba(10, 14, 22, 0.88); border-left: 6px solid #00e5ff;
padding: 22px 30px; max-width: 560px; backdrop-filter: blur(4px);
}
.photo-arena { font-size: 30px; font-weight: 900; letter-spacing: 2px; color: #00e5ff; text-transform: uppercase; }
.photo-score { font-size: 46px; font-weight: 900; color: #fff; margin-top: 4px; font-variant-numeric: tabular-nums; }
.photo-sub { font-size: 13px; color: #94a3b8; letter-spacing: 1.5px; text-transform: uppercase; margin-top: 8px; }
#gameover-overlay {
position: fixed; inset: 0; display: none;
align-items: center; justify-content: center;
background: rgba(6, 9, 15, 0.82); z-index: 25;
}
#gameover-overlay.active { display: flex; }
.gameover-box {
background: #111827; border: 2px solid #00e5ff; padding: 36px 48px;
text-align: center; box-shadow: 0 20px 60px rgba(0,0,0,0.85);
}
.gameover-title { font-size: 14px; letter-spacing: 3px; color: #00e5ff; text-transform: uppercase; font-weight: 800; }
.gameover-main { font-size: 38px; font-weight: 900; margin: 8px 0 18px; letter-spacing: 1px; }
.gameover-score { font-size: 24px; color: #fde047; font-weight: 800; margin-bottom: 24px; }
#restart-btn {
pointer-events: auto; cursor: pointer; background: #00e5ff; color: #061018;
border: none; font-weight: 900; font-size: 15px; letter-spacing: 1.5px;
padding: 12px 28px; text-transform: uppercase; border-radius: 2px;
}
#restart-btn:hover { background: #67f0ff; }
</style>
</head>
<body>
<div id="hud">
<div class="hud-block">
<div class="hud-label">Monster Rally Venue</div>
<div id="arena-name">THUNDER VOLT GIGADOME</div>
<div id="controls-hint">Drive: <b>WASD</b> or <b>Arrow Keys</b> | Boost/Handbrake: <b>Space</b> | Photo Mode: <b>P</b></div>
</div>
<div class="hud-stats">
<div class="hud-block">
<div class="hud-label">Score</div>
<div class="stat-val" id="score-val">0</div>
</div>
<div class="hud-block">
<div class="hud-label">Clock</div>
<div class="stat-val" id="clock-val">01:30</div>
</div>
</div>
</div>
<div id="hit-banner">SMASH +250!</div>
<div id="photo-overlay">
<div class="photo-badge">PHOTO MODE — SIMULATION PAUSED</div>
<div class="photo-card">
<div class="photo-arena" id="photo-arena-text">THUNDER VOLT GIGADOME</div>
<div class="photo-score" id="photo-score-text">SCORE: 0</div>
<div class="photo-sub">Press P to resume rally</div>
</div>
</div>
<div id="gameover-overlay">
<div class="gameover-box">
<div class="gameover-title">Thunder Volt GigaDome</div>
<div class="gameover-main">TIME EXPIRED</div>
<div class="gameover-score" id="final-score-text">FINAL SCORE: 0</div>
<button id="restart-btn">Run Rally Again</button>
</div>
</div>
<script src="https://unpkg.com/three@0.160.0/build/three.min.js"></script>
<script>
(() => {
'use strict';
const ARENA_NAME = 'THUNDER VOLT GIGADOME';
const ARENA_RX = 64; // Elliptical dirt floor X semi-axis
const ARENA_RZ = 48; // Elliptical dirt floor Z semi-axis
// --- Web Audio Synthesizer (pure procedural, zero external assets) ---
let audioCtx = null;
let engineOsc = null;
let engineGain = null;
function ensureAudio() {
if (audioCtx) {
if (audioCtx.state === 'suspended') audioCtx.resume();
return;
}
const AC = window.AudioContext || window.webkitAudioContext;
if (!AC) return;
audioCtx = new AC();
engineOsc = audioCtx.createOscillator();
const subOsc = audioCtx.createOscillator();
engineGain = audioCtx.createGain();
const filter = audioCtx.createBiquadFilter();
engineOsc.type = 'sawtooth';
subOsc.type = 'triangle';
engineOsc.frequency.value = 48;
subOsc.frequency.value = 24;
filter.type = 'lowpass';
filter.frequency.value = 210;
engineGain.gain.value = 0.04;
engineOsc.connect(filter);
subOsc.connect(filter);
filter.connect(engineGain);
engineGain.connect(audioCtx.destination);
engineOsc.start();
subOsc.start();
engineOsc._sub = subOsc;
}
function playCrashSound(intensity, wreck) {
if (!audioCtx || audioCtx.state !== 'running') return;
const now = audioCtx.currentTime;
const dur = wreck ? 0.48 : 0.24;
const bufSize = Math.floor(audioCtx.sampleRate * dur);
const buf = audioCtx.createBuffer(1, bufSize, audioCtx.sampleRate);
const data = buf.getChannelData(0);
for (let i = 0; i < bufSize; i++) {
data[i] = (Math.random() * 2 - 1) * Math.exp(-i / (bufSize * 0.28));
}
const noise = audioCtx.createBufferSource();
noise.buffer = buf;
const filter = audioCtx.createBiquadFilter();
filter.type = 'bandpass';
filter.frequency.setValueAtTime(wreck ? 320 : 520, now);
filter.Q.value = 1.4;
const gain = audioCtx.createGain();
const vol = Math.min(0.35, 0.08 + intensity * 0.012);
gain.gain.setValueAtTime(vol, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + dur);
noise.connect(filter);
filter.connect(gain);
gain.connect(audioCtx.destination);
noise.start(now);
if (wreck) {
const clang = audioCtx.createOscillator();
const cg = audioCtx.createGain();
clang.type = 'square';
clang.frequency.setValueAtTime(190, now);
clang.frequency.exponentialRampToValueAtTime(58, now + 0.38);
cg.gain.setValueAtTime(0.18, now);
cg.gain.exponentialRampToValueAtTime(0.001, now + 0.4);
clang.connect(cg);
cg.connect(audioCtx.destination);
clang.start(now);
clang.stop(now + 0.41);
}
}
// --- Game State ---
const state = {
arenaName: ARENA_NAME,
score: 0,
timeLeft: 90,
photoMode: false,
gameOver: false,
elapsed: 0,
bannerTimer: 0
};
function applyInitialModeState() {
const isDemo = window.location.hash.toLowerCase() === '#demo';
state.score = isDemo ? 5000 : 0;
state.timeLeft = isDemo ? 20 : 90;
state.photoMode = false;
state.gameOver = false;
document.getElementById('photo-overlay').classList.remove('active');
document.getElementById('gameover-overlay').classList.remove('active');
}
// --- Procedural Canvas Textures ---
function makeDirtTexture() {
const c = document.createElement('canvas');
c.width = 512; c.height = 512;
const ctx = c.getContext('2d');
ctx.fillStyle = '#5b432c';
ctx.fillRect(0, 0, 512, 512);
for (let i = 0; i < 9000; i++) {
const x = Math.random() * 512;
const y = Math.random() * 512;
const r = 1 + Math.random() * 3;
const shade = Math.random();
ctx.fillStyle = shade > 0.6 ? 'rgba(115,88,58,0.35)' : (shade > 0.25 ? 'rgba(58,40,24,0.4)' : 'rgba(140,112,80,0.25)');
ctx.fillRect(x, y, r, r);
}
// Tire track arcs
ctx.strokeStyle = 'rgba(40,28,16,0.28)';
ctx.lineWidth = 10;
for (let i = 0; i < 6; i++) {
ctx.beginPath();
ctx.ellipse(256, 256, 90 + i * 26, 70 + i * 20, i * 0.4, 0, Math.PI * 2);
ctx.stroke();
}
const tex = new THREE.CanvasTexture(c);
tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
tex.repeat.set(6, 6);
return tex;
}
function makeSteelTexture() {
const c = document.createElement('canvas');
c.width = 256; c.height = 256;
const ctx = c.getContext('2d');
const grad = ctx.createLinearGradient(0, 0, 256, 256);
grad.addColorStop(0, '#cfd6dd');
grad.addColorStop(0.5, '#9aa5b1');
grad.addColorStop(1, '#dce2e8');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, 256, 256);
for (let i = 0; i < 1200; i++) {
ctx.fillStyle = i % 2 === 0 ? 'rgba(255,255,255,0.08)' : 'rgba(30,40,50,0.08)';
ctx.fillRect(0, Math.random() * 256, 256, 1);
}
return new THREE.CanvasTexture(c);
}
function makeHazardTexture() {
const c = document.createElement('canvas');
c.width = 128; c.height = 128;
const ctx = c.getContext('2d');
ctx.fillStyle = '#9ca3af';
ctx.fillRect(0, 0, 128, 128);
ctx.fillStyle = '#eab308';
ctx.fillRect(0, 0, 128, 34);
ctx.fillStyle = '#111827';
for (let x = -40; x < 160; x += 28) {
ctx.beginPath();
ctx.moveTo(x, 0); ctx.lineTo(x + 14, 0);
ctx.lineTo(x + 32, 34); ctx.lineTo(x + 18, 34);
ctx.closePath();
ctx.fill();
}
return new THREE.CanvasTexture(c);
}
// --- Three.js Scene Setup ---
const renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: 'high-performance' });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0b101b);
scene.fog = new THREE.FogExp2(0x0e1525, 0.0042);
const camera = new THREE.PerspectiveCamera(58, window.innerWidth / window.innerHeight, 0.5, 420);
camera.position.set(0, 16, 34);
// Stadium lighting
const hemiLight = new THREE.HemisphereLight(0xaad4ff, 0x4d3824, 0.85);
scene.add(hemiLight);
const sunLight = new THREE.DirectionalLight(0xfff6e5, 1.55);
sunLight.position.set(42, 78, 36);
sunLight.castShadow = true;
sunLight.shadow.mapSize.width = 2048;
sunLight.shadow.mapSize.height = 2048;
sunLight.shadow.camera.near = 10;
sunLight.shadow.camera.far = 210;
const d = 82;
sunLight.shadow.camera.left = -d;
sunLight.shadow.camera.right = d;
sunLight.shadow.camera.top = d;
sunLight.shadow.camera.bottom = -d;
scene.add(sunLight);
const rimLight = new THREE.DirectionalLight(0x38bdf8, 0.65);
rimLight.position.set(-55, 45, -45);
scene.add(rimLight);
// --- Terrain Height Function (Artificial hills & central tabletop jump) ---
const hills = [
{ x: 0, z: 0, rx: 12, rz: 10, h: 3.4, flat: 0.45 }, // Central tabletop jump
{ x: -28, z: -16, rx: 11, rz: 9, h: 2.7, flat: 0.15 }, // NW dirt mound
{ x: 28, z: 16, rx: 11, rz: 9, h: 2.7, flat: 0.15 }, // SE dirt mound
{ x: -24, z: 20, rx: 9, rz: 8, h: 2.2, flat: 0.1 }, // SW kicker
{ x: 25, z: -19, rx: 9, rz: 8, h: 2.2, flat: 0.1 } // NE kicker
];
function getGroundHeight(x, z) {
let h = 0;
for (let i = 0; i < hills.length; i++) {
const m = hills[i];
const dx = (x - m.x) / m.rx;
const dz = (z - m.z) / m.rz;
const distSq = dx * dx + dz * dz;
if (distSq < 1.0) {
let falloff = Math.cos(Math.sqrt(distSq) * Math.PI * 0.5);
falloff = Math.min(1, falloff / (1 - m.flat * 0.55));
h = Math.max(h, m.h * falloff * falloff);
}
}
return h;
}
function getGroundNormal(x, z) {
const eps = 0.6;
const hL = getGroundHeight(x - eps, z);
const hR = getGroundHeight(x + eps, z);
const hD = getGroundHeight(x, z - eps);
const hU = getGroundHeight(x, z + eps);
const n = new THREE.Vector3(hL - hR, 2 * eps, hD - hU);
return n.normalize();
}
// --- Build Stadium Floor, Stands, Towers & Full-Ring Crowd ---
const dirtTex = makeDirtTexture();
const hazardTex = makeHazardTexture();
const steelTex = makeSteelTexture();
// Sculpted dirt floor mesh
const floorGeo = new THREE.PlaneGeometry(156, 126, 96, 78);
floorGeo.rotateX(-Math.PI / 2);
const fPos = floorGeo.attributes.position;
for (let i = 0; i < fPos.count; i++) {
const vx = fPos.getX(i);
const vz = fPos.getZ(i);
fPos.setY(i, getGroundHeight(vx, vz));
}
floorGeo.computeVertexNormals();
const floorMat = new THREE.MeshStandardMaterial({ map: dirtTex, roughness: 0.92, metalness: 0.05 });
const floorMesh = new THREE.Mesh(floorGeo, floorMat);
floorMesh.receiveShadow = true;
scene.add(floorMesh);
// Elliptical Stadium Bowl & Bleachers
const stadiumGroup = new THREE.Group();
scene.add(stadiumGroup);
function buildEllipticalRing(innerRx, innerRz, outerRx, outerRz, yBottom, yTop, segs, mat) {
const geo = new THREE.BufferGeometry();
const positions = [];
const indices = [];
for (let i = 0; i <= segs; i++) {
const a = (i / segs) * Math.PI * 2;
const ca = Math.cos(a), sa = Math.sin(a);
// 0: inner bottom, 1: inner top, 2: outer top, 3: outer bottom
positions.push(
ca * innerRx, yBottom, sa * innerRz,
ca * innerRx, yTop, sa * innerRz,
ca * outerRx, yTop, sa * outerRz,
ca * outerRx, yBottom, sa * outerRz
);
if (i < segs) {
const b = i * 4;
// Inner wall
indices.push(b, b + 4, b + 5, b, b + 5, b + 1);
// Top tier surface
indices.push(b + 1, b + 5, b + 6, b + 1, b + 6, b + 2);
// Outer wall
indices.push(b + 2, b + 6, b + 7, b + 2, b + 7, b + 3);
}
}
geo.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
geo.setIndex(indices);
geo.computeVertexNormals();
const m = new THREE.Mesh(geo, mat);
m.receiveShadow = true;
return m;
}
const wallMat = new THREE.MeshStandardMaterial({ map: hazardTex, roughness: 0.75 });
const concreteMat = new THREE.MeshStandardMaterial({ color: 0x262f3d, roughness: 0.88 });
const fasciaMat = new THREE.MeshStandardMaterial({ color: 0x0f172a, emissive: 0x003b4d, emissiveIntensity: 0.5 });
// Perimeter retaining wall right at arena boundary
stadiumGroup.add(buildEllipticalRing(ARENA_RX + 2.5, ARENA_RZ + 2.5, ARENA_RX + 5.2, ARENA_RZ + 5.2, -0.5, 3.3, 84, wallMat));
stadiumGroup.add(buildEllipticalRing(ARENA_RX + 5.2, ARENA_RZ + 5.2, ARENA_RX + 7.4, ARENA_RZ + 7.4, 0, 4.6, 84, fasciaMat));
// Stepped Bleacher Tiers all the way around
const TIER_COUNT = 8;
for (let t = 0; t < TIER_COUNT; t++) {
const rInX = ARENA_RX + 7.4 + t * 2.8;
const rInZ = ARENA_RZ + 7.4 + t * 2.8;
const y0 = 3.2 + t * 1.9;
const y1 = y0 + 1.95;
stadiumGroup.add(buildEllipticalRing(rInX, rInZ, rInX + 3.0, rInZ + 3.0, y0 - 1.5, y1, 84, concreteMat));
}
// Upper rim parapet
const topRx = ARENA_RX + 7.4 + TIER_COUNT * 2.8;
const topRz = ARENA_RZ + 7.4 + TIER_COUNT * 2.8;
stadiumGroup.add(buildEllipticalRing(topRx, topRz, topRx + 2.5, topRz + 2.5, 16, 22.5, 84, concreteMat));
// Four Floodlight Towers
const towerMat = new THREE.MeshStandardMaterial({ color: 0x1e293b, metalness: 0.6, roughness: 0.4 });
const bulbMat = new THREE.MeshBasicMaterial({ color: 0xf8fafc });
for (let i = 0; i < 4; i++) {
const ang = (i + 0.5) * (Math.PI / 2);
const tx = Math.cos(ang) * (topRx + 1.2);
const tz = Math.sin(ang) * (topRz + 1.2);
const pole = new THREE.Mesh(new THREE.CylinderGeometry(0.7, 1.1, 26, 8), towerMat);
pole.position.set(tx, 31, tz);
stadiumGroup.add(pole);
const bank = new THREE.Mesh(new THREE.BoxGeometry(8, 3.6, 1.4), bulbMat);
bank.position.set(tx, 44, tz);
bank.lookAt(0, 2, 0);
stadiumGroup.add(bank);
}
// --- Instanced Animated Crowd (All the way round 360 degrees) ---
const crowdSpecs = [];
const palette = [
0xef4444, 0x3b82f6, 0xfacc15, 0x22c55e, 0xa855f7,
0xf97316, 0x06b6d4, 0xec4899, 0xf1f5f9, 0x84cc16
];
const skinTones = [0xf1c27d, 0xe0ac69, 0xc68642, 0x8d5524, 0xffdbac];
for (let t = 0; t < TIER_COUNT; t++) {
const rx = ARENA_RX + 8.8 + t * 2.8;
const rz = ARENA_RZ + 8.8 + t * 2.8;
const baseY = 5.15 + t * 1.9;
const seats = 136 + t * 12;
for (let s = 0; s < seats; s++) {
// Leave tiny aisle notches every 22 seats so bleachers read like real stadium sections
if (s % 22 === 0) continue;
const a = (s / seats) * Math.PI * 2 + (t % 2) * 0.02;
crowdSpecs.push({
x: Math.cos(a) * rx,
y: baseY,
z: Math.sin(a) * rz,
rotY: Math.atan2(-Math.cos(a) * rx, -Math.sin(a) * rz) + Math.PI,
phase: Math.random() * Math.PI * 2,
speed: 3.5 + Math.random() * 4.5,
amp: 0.12 + Math.random() * 0.24,
shirt: palette[(Math.random() * palette.length) | 0],
skin: skinTones[(Math.random() * skinTones.length) | 0]
});
}
}
const crowdCount = crowdSpecs.length;
const torsoGeo = new THREE.BoxGeometry(0.78, 0.95, 0.46);
const headGeo = new THREE.BoxGeometry(0.46, 0.46, 0.46);
const crowdTorsoMat = new THREE.MeshLambertMaterial({ color: 0xffffff });
const crowdHeadMat = new THREE.MeshLambertMaterial({ color: 0xffffff });
const crowdTorsos = new THREE.InstancedMesh(torsoGeo, crowdTorsoMat, crowdCount);
const crowdHeads = new THREE.InstancedMesh(headGeo, crowdHeadMat, crowdCount);
crowdTorsos.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
crowdHeads.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
const dummyObj = new THREE.Object3D();
const tmpColor = new THREE.Color();
for (let i = 0; i < crowdCount; i++) {
const c = crowdSpecs[i];
tmpColor.setHex(c.shirt);
crowdTorsos.setColorAt(i, tmpColor);
tmpColor.setHex(c.skin);
crowdHeads.setColorAt(i, tmpColor);
}
crowdTorsos.instanceColor.needsUpdate = true;
crowdHeads.instanceColor.needsUpdate = true;
scene.add(crowdTorsos);
scene.add(crowdHeads);
function updateCrowd(time) {
for (let i = 0; i < crowdCount; i++) {
const c = crowdSpecs[i];
const hop = Math.max(0, Math.sin(time * c.speed + c.phase)) * c.amp;
dummyObj.position.set(c.x, c.y + 0.48 + hop, c.z);
dummyObj.rotation.set(0, c.rotY, 0);
dummyObj.updateMatrix();
crowdTorsos.setMatrixAt(i, dummyObj.matrix);
dummyObj.position.set(c.x, c.y + 1.2 + hop * 1.12, c.z);
dummyObj.updateMatrix();
crowdHeads.setMatrixAt(i, dummyObj.matrix);
}
crowdTorsos.instanceMatrix.needsUpdate = true;
crowdHeads.instanceMatrix.needsUpdate = true;
}
updateCrowd(0);
// --- Three Distinct Floor Obstacle Types ---
// Type 1: Crushable Junked Sedans (deform/flatten when hit, award points)
// Type 2: Concrete Jersey Barriers (heavy movable/blocking dividers, sparks & knockback)
// Type 3: Giant Monster Tire Stacks / Explosive Steel Drums (scatter & bounce)
const obstacles = [];
const obstacleGroup = new THREE.Group();
scene.add(obstacleGroup);
function createCrushCar(x, z, rotY, colorHex) {
const g = new THREE.Group();
const bodyMat = new THREE.MeshStandardMaterial({ color: colorHex, roughness: 0.45, metalness: 0.35 });
const glassMat = new THREE.MeshStandardMaterial({ color: 0x1e293b, roughness: 0.2 });
const wheelMat = new THREE.MeshStandardMaterial({ color: 0x111111, roughness: 0.9 });
const lower = new THREE.Mesh(new THREE.BoxGeometry(2.3, 0.75, 4.5), bodyMat);
lower.position.y = 0.62;
lower.castShadow = true; lower.receiveShadow = true;
g.add(lower);
const cabin = new THREE.Mesh(new THREE.BoxGeometry(1.95, 0.72, 2.3), bodyMat);
cabin.position.set(0, 1.32, -0.15);
cabin.castShadow = true;
g.add(cabin);
const wind = new THREE.Mesh(new THREE.BoxGeometry(2.0, 0.55, 2.36), glassMat);
wind.position.set(0, 1.3, -0.15);
g.add(wind);
for (const sx of [-1.08, 1.08]) {
for (const sz of [-1.35, 1.35]) {
const w = new THREE.Mesh(new THREE.CylinderGeometry(0.38, 0.38, 0.3, 12), wheelMat);
w.rotation.z = Math.PI / 2;
w.position.set(sx, 0.38, sz);
g.add(w);
}
}
const y = getGroundHeight(x, z);
g.position.set(x, y, z);
g.rotation.y = rotY;
obstacleGroup.add(g);
obstacles.push({
type: 'car',
mesh: g,
cabin,
wind,
x, z, y,
vx: 0, vz: 0,
radius: 2.35,
crushed: false,
crushScale: 1.0,
hitCooldown: 0
});
}
function createJerseyBarrier(x, z, rotY) {
const g = new THREE.Group();
const bMat = new THREE.MeshStandardMaterial({ map: hazardTex, roughness: 0.85 });
const base = new THREE.Mesh(new THREE.BoxGeometry(4.8, 0.65, 1.35), bMat);
base.position.y = 0.32;
base.castShadow = true; base.receiveShadow = true;
const top = new THREE.Mesh(new THREE.BoxGeometry(4.75, 0.85, 0.72), bMat);
top.position.y = 1.02;
top.castShadow = true;
g.add(base, top);
const y = getGroundHeight(x, z);
g.position.set(x, y, z);
g.rotation.y = rotY;
obstacleGroup.add(g);
obstacles.push({
type: 'barrier',
mesh: g,
x, z, y,
vx: 0, vz: 0,
radius: 2.3,
hitCooldown: 0
});
}
function createBarrelCluster(x, z, colorHex) {
const g = new THREE.Group();
const drumMat = new THREE.MeshStandardMaterial({ color: colorHex, roughness: 0.4, metalness: 0.45 });
const ringMat = new THREE.MeshStandardMaterial({ color: 0xfacc15, roughness: 0.5 });
const drum = new THREE.Mesh(new THREE.CylinderGeometry(0.78, 0.78, 1.75, 14), drumMat);
drum.position.y = 0.88;
drum.castShadow = true; drum.receiveShadow = true;
const stripe = new THREE.Mesh(new THREE.CylinderGeometry(0.81, 0.81, 0.34, 14), ringMat);
stripe.position.y = 0.92;
g.add(drum, stripe);
const y = getGroundHeight(x, z);
g.position.set(x, y, z);
obstacleGroup.add(g);
obstacles.push({
type: 'barrel',
mesh: g,
x, z, y,
vy: 0,
vx: 0, vz: 0,
tiltX: 0, tiltZ: 0,
radius: 1.05,
hitCooldown: 0
});
}
function buildArenaObstacles() {
while (obstacleGroup.children.length) obstacleGroup.remove(obstacleGroup.children[0]);
obstacles.length = 0;
// 1. Crushable Junked Sedans (8 placed in rows & near jumps)
const carColors = [0xdc2626, 0x2563eb, 0x16a34a, 0xd97706, 0x7c3aed, 0x0891b2];
const carSpots = [
[-13, 0, 0], [-16.2, 0, 0],
[13, 0, Math.PI], [16.2, 0, Math.PI],
[0, -24, Math.PI / 2], [0, 24, -Math.PI / 2],
[-36, 4, 0.4], [36, -4, -0.4]
];
carSpots.forEach((s, idx) => createCrushCar(s[0], s[1], s[2], carColors[idx % carColors.length]));
// 2. Concrete Jersey Barriers (6 heavy chicanes)
const barrierSpots = [
[-20, -29, 0.35], [20, -29, -0.35],
[-20, 29, -0.35], [20, 29, 0.35],
[-44, -12, Math.PI / 2 + 0.2], [44, 12, Math.PI / 2 + 0.2]
];
barrierSpots.forEach(s => createJerseyBarrier(s[0], s[1], s[2]));
// 3. Heavy Steel Rally Drums (12 knockable debris barrels)
const barrelSpots = [
[-8, 13], [-6.2, 14.3], [8, -13], [6.2, -14.3],
[-33, -25], [-31, -26.2], [33, 25], [31, 26.2],
[10, 15], [-10, -15], [40, -18], [-40, 18]
];
barrelSpots.forEach((s, i) => createBarrelCluster(s[0], s[1], i % 2 === 0 ? 0xf97316 : 0xe11d48));
}
// --- Particle System (Sparks, Smoke, Dirt Clods) ---
const MAX_PARTICLES = 180;
const particles = [];
const pGeo = new THREE.BoxGeometry(0.32, 0.32, 0.32);
for (let i = 0; i < MAX_PARTICLES; i++) {
const mat = new THREE.MeshBasicMaterial({ color: 0xffaa00 });
const mesh = new THREE.Mesh(pGeo, mat);
mesh.visible = false;
scene.add(mesh);
particles.push({ mesh, life: 0, maxLife: 1, vx: 0, vy: 0, vz: 0, gravity: 18, scale: 1 });
}
let pCursor = 0;
function spawnParticles(x, y, z, count, colorHex, speed, gravity = 22, scale = 1) {
for (let i = 0; i < count; i++) {
const p = particles[pCursor];
pCursor = (pCursor + 1) % MAX_PARTICLES;
p.mesh.visible = true;
p.mesh.position.set(x + (Math.random() - 0.5) * 1.2, y + Math.random() * 0.8, z + (Math.random() - 0.5) * 1.2);
p.mesh.material.color.setHex(colorHex);
const a = Math.random() * Math.PI * 2;
const spd = (0.35 + Math.random() * 0.85) * speed;
p.vx = Math.cos(a) * spd;
p.vy = (0.4 + Math.random() * 0.9) * speed * 0.75;
p.vz = Math.sin(a) * spd;
p.gravity = gravity;
p.maxLife = 0.35 + Math.random() * 0.45;
p.life = p.maxLife;
p.scale = scale;
p.mesh.scale.setScalar(scale);
}
}
function updateParticles(dt) {
for (let i = 0; i < MAX_PARTICLES; i++) {
const p = particles[i];
if (p.life <= 0) continue;
p.life -= dt;
if (p.life <= 0) {
p.mesh.visible = false;
continue;
}
p.vy -= p.gravity * dt;
p.mesh.position.x += p.vx * dt;
p.mesh.position.y += p.vy * dt;
p.mesh.position.z += p.vz * dt;
if (p.mesh.position.y < 0.15) {
p.mesh.position.y = 0.15;
p.vy *= -0.35;
}
p.mesh.rotation.x += 6 * dt;
p.mesh.rotation.y += 5 * dt;
const s = p.scale * (p.life / p.maxLife);
p.mesh.scale.setScalar(Math.max(0.05, s));
}
}
// --- Monster Cybertruck Mesh Builder ---
function createCybertruckMesh(accentHex, isPlayer) {
const root = new THREE.Group();
const tiltNode = new THREE.Group();
root.add(tiltNode);
// Suspension struts + Monster Frame
const frameMat = new THREE.MeshStandardMaterial({ color: 0x181c24, roughness: 0.7, metalness: 0.5 });
const shockMat = new THREE.MeshStandardMaterial({ color: accentHex, emissive: accentHex, emissiveIntensity: 0.25, roughness: 0.3 });
const chassisBox = new THREE.Mesh(new THREE.BoxGeometry(2.1, 0.55, 4.2), frameMat);
chassisBox.position.y = 1.35;
chassisBox.castShadow = true;
tiltNode.add(chassisBox);
// 4 Visible Coilover Shocks
for (const sx of [-1.05, 1.05]) {
for (const sz of [-1.55, 1.55]) {
const strut = new THREE.Mesh(new THREE.CylinderGeometry(0.14, 0.14, 1.05, 8), shockMat);
strut.position.set(sx, 1.25, sz);
strut.rotation.z = sx > 0 ? 0.22 : -0.22;
tiltNode.add(strut);
}
}
// Iconic Angular Wedge Stainless Steel Exoskeleton (Extruded Side Profile)
// Truck faces +Z: z = -2.65 (tailgate) to z = +2.65 (front nose)
const shape = new THREE.Shape();
shape.moveTo(-2.65, 0.15); // Rear bumper bottom
shape.lineTo(2.55, 0.15); // Lower rocker to front skid
shape.lineTo(2.72, 0.78); // Front nose fascia
shape.lineTo(0.35, 1.72); // Sharp triangular Cybertruck roofline peak!
shape.lineTo(-2.65, 1.12); // Sloping tonneau / vault bed line
shape.closePath();
const extrudeGeo = new THREE.ExtrudeGeometry(shape, {
depth: 2.36,
bevelEnabled: true,
bevelThickness: 0.06,
bevelSize: 0.05,
bevelSegments: 1
});
// Rotate so profile X->Z (length) and Extrude Z->X (width), centered
extrudeGeo.rotateY(Math.PI / 2);
extrudeGeo.translate(-1.18, 1.42, 0);
const bodyMat = new THREE.MeshStandardMaterial({
map: steelTex,
color: isPlayer ? 0xeef4fa : 0xcbd5e1,
metalness: 0.84,
roughness: 0.24
});
const bodyMesh = new THREE.Mesh(extrudeGeo, bodyMat);
bodyMesh.castShadow = true;
bodyMesh.receiveShadow = true;
tiltNode.add(bodyMesh);
// Tinted Armored Glass Canopy Inset
const glassShape = new THREE.Shape();
glassShape.moveTo(-0.95, 1.16);
glassShape.lineTo(1.75, 0.92);
glassShape.lineTo(0.32, 1.63);
glassShape.closePath();
const glassGeo = new THREE.ExtrudeGeometry(glassShape, { depth: 2.44, bevelEnabled: false });
glassGeo.rotateY(Math.PI / 2);
glassGeo.translate(-1.22, 1.42, 0);
const glassMat = new THREE.MeshStandardMaterial({ color: 0x0b1320, roughness: 0.12, metalness: 0.9 });
tiltNode.add(new THREE.Mesh(glassGeo, glassMat));
// Signature Full-Width Front LED Lightbar (+Z) & Rear Red Lightbar (-Z)
const frontLed = new THREE.Mesh(
new THREE.BoxGeometry(2.38, 0.12, 0.14),
new THREE.MeshBasicMaterial({ color: 0xe0faff })
);
frontLed.position.set(0, 2.18, 2.68);
tiltNode.add(frontLed);
const rearLed = new THREE.Mesh(
new THREE.BoxGeometry(2.38, 0.13, 0.14),
new THREE.MeshBasicMaterial({ color: 0xff1e38 })
);
rearLed.position.set(0, 2.48, -2.66);
tiltNode.add(rearLed);
// Accent Underglow / Rally Number Strip so player & rivals are distinct at a glance
const accentBar = new THREE.Mesh(
new THREE.BoxGeometry(2.48, 0.14, 4.4),
new THREE.MeshBasicMaterial({ color: accentHex })
);
accentBar.position.set(0, 1.52, -0.05);
tiltNode.add(accentBar);
if (isPlayer) {
// Player roof beacon arrow / marker
const cone = new THREE.Mesh(
new THREE.ConeGeometry(0.38, 0.65, 4),
new THREE.MeshBasicMaterial({ color: 0x00e5ff })
);
cone.rotation.x = Math.PI;
cone.position.set(0, 4.15, 0.25);
tiltNode.add(cone);
root.userData.beacon = cone;
}
// 4 Oversized Monster-Truck Tires with Deep Treads + Colored Beadlock Rims
const tireMat = new THREE.MeshStandardMaterial({ color: 0x17191d, roughness: 0.88 });
const treadMat = new THREE.MeshStandardMaterial({ color: 0x242830, roughness: 0.92 });
const rimMat = new THREE.MeshStandardMaterial({ color: accentHex, metalness: 0.6, roughness: 0.3 });
const wheels = [];
const wheelPositions = [
{ x: -1.58, y: 1.08, z: 1.72, front: true },
{ x: 1.58, y: 1.08, z: 1.72, front: true },
{ x: -1.58, y: 1.08, z: -1.72, front: false },
{ x: 1.58, y: 1.08, z: -1.72, front: false }
];
wheelPositions.forEach(wp => {
const steerPivot = new THREE.Group();
steerPivot.position.set(wp.x, wp.y, wp.z);
const spinGroup = new THREE.Group();
const tire = new THREE.Mesh(new THREE.CylinderGeometry(1.08, 1.08, 0.98, 18), tireMat);
tire.rotation.z = Math.PI / 2;
tire.castShadow = true;
spinGroup.add(tire);
// 8 chunky paddle treads around tire
for (let k = 0; k < 8; k++) {
const ang = (k / 8) * Math.PI * 2;
const lug = new THREE.Mesh(new THREE.BoxGeometry(1.02, 0.14, 0.22), treadMat);
lug.position.set(0, Math.cos(ang) * 1.07, Math.sin(ang) * 1.07);
lug.rotation.x = -ang;
spinGroup.add(lug);
}
const rim = new THREE.Mesh(new THREE.CylinderGeometry(0.56, 0.56, 1.03, 10), rimMat);
rim.rotation.z = Math.PI / 2;
spinGroup.add(rim);
steerPivot.add(spinGroup);
root.add(steerPivot);
wheels.push({ steerPivot, spinGroup, front: wp.front });
});
return { root, tiltNode, bodyMat, wheels };
}
// --- Exactly Five Trucks (1 Player + 4 AI Rivals) ---
const trucks = [];
const TRUCK_CONFIGS = [
{ name: 'CYBER-01 (YOU)', isPlayer: true, accent: 0x00e5ff, startX: 0, startZ: 32, heading: Math.PI, role: 'player' },
{ name: 'GIGA-CRUSHER', isPlayer: false, accent: 0xff2a5f, startX: -32, startZ: -18, heading: 0.7, role: 'hunter' },
{ name: 'VOLT-REAPER', isPlayer: false, accent: 0xfacc15, startX: 32, startZ: -18, heading: -0.7, role: 'roamerCW' },
{ name: 'STEEL-RHINO', isPlayer: false, accent: 0xa855f7, startX: -34, startZ: 16, heading: 2.1, role: 'jumper' },
{ name: 'PLASMA-BEAST', isPlayer: false, accent: 0x22c55e, startX: 34, startZ: 16, heading: -2.1, role: 'roamerCCW' }
];
function pickWaypoint(role, idx) {
if (role === 'jumper') {
const m = hills[(Math.random() * hills.length) | 0];
return { x: m.x + (Math.random() - 0.5) * 6, z: m.z + (Math.random() - 0.5) * 6 };
}
const a = Math.random() * Math.PI * 2;
const rad = 0.45 + Math.random() * 0.42;
return { x: Math.cos(a) * ARENA_RX * rad, z: Math.sin(a) * ARENA_RZ * rad };
}
TRUCK_CONFIGS.forEach((cfg, idx) => {
const built = createCybertruckMesh(cfg.accent, cfg.isPlayer);
scene.add(built.root);
trucks.push({
id: idx,
name: cfg.name,
isPlayer: cfg.isPlayer,
accent: cfg.accent,
role: cfg.role,
mesh: built.root,
tiltNode: built.tiltNode,
bodyMat: built.bodyMat,
wheels: built.wheels,
x: cfg.startX,
y: getGroundHeight(cfg.startX, cfg.startZ),
z: cfg.startZ,
vx: 0,
vy: 0,
vz: 0,
heading: cfg.heading,
yawVel: 0,
steerAngle: 0,
wheelSpin: 0,
radius: 2.75,
mass: cfg.isPlayer ? 1.15 : 1.0,
damage: 0,
wreckTimer: 0,
spinOutVel: 0,
hitFlash: 0,
airborneTime: 0,
waypoint: pickWaypoint(cfg.role, idx),
wpTimer: 2 + idx * 0.9,
orbitAngle: idx * 1.5
});
});
function resetTrucks() {
trucks.forEach((t, idx) => {
const cfg = TRUCK_CONFIGS[idx];
t.x = cfg.startX;
t.z = cfg.startZ;
t.y = getGroundHeight(t.x, t.z);
t.vx = 0; t.vy = 0; t.vz = 0;
t.heading = cfg.heading;
t.yawVel = 0;
t.steerAngle = 0;
t.damage = 0;
t.wreckTimer = 0;
t.spinOutVel = 0;
t.hitFlash = 0;
t.airborneTime = 0;
t.waypoint = pickWaypoint(t.role, idx);
t.wpTimer = 3.0;
t.tiltNode.rotation.set(0, 0, 0);
t.mesh.position.set(t.x, t.y, t.z);
t.mesh.rotation.set(0, t.heading, 0);
});
}
// --- Input Handling ---
const keys = {};
const bannerEl = document.getElementById('hit-banner');
const scoreEl = document.getElementById('score-val');
const clockEl = document.getElementById('clock-val');
const photoOverlay = document.getElementById('photo-overlay');
const photoArenaText = document.getElementById('photo-arena-text');
const photoScoreText = document.getElementById('photo-score-text');
const gameoverOverlay = document.getElementById('gameover-overlay');
const finalScoreText = document.getElementById('final-score-text');
function showBanner(msg, color = '#ffe600') {
bannerEl.textContent = msg;
bannerEl.style.color = color;
bannerEl.style.opacity = '1';
state.bannerTimer = 1.25;
}
function addScore(pts, label, color) {
if (state.gameOver) return;
state.score += pts;
if (label) showBanner(`${label} +${pts}`, color);
}
function togglePhotoMode() {
if (state.gameOver) return;
state.photoMode = !state.photoMode;
if (state.photoMode) {
photoArenaText.textContent = state.arenaName;
photoScoreText.textContent = `SCORE: ${state.score.toLocaleString()}`;
photoOverlay.classList.add('active');
if (engineGain && audioCtx) engineGain.gain.setValueAtTime(0.0001, audioCtx.currentTime);
// Render current frozen frame cleanly once
renderer.render(scene, camera);
} else {
photoOverlay.classList.remove('active');
lastFrameTime = performance.now();
}
}
window.addEventListener('keydown', (e) => {
ensureAudio();
const code = e.code;
if (code === 'KeyP' && !e.repeat) {
togglePhotoMode();
return;
}
if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Space'].includes(code)) {
e.preventDefault();
}
keys[code] = true;
});
window.addEventListener('keyup', (e) => {
keys[e.code] = false;
});
window.addEventListener('hashchange', () => {
applyInitialModeState();
buildArenaObstacles();
resetTrucks();
syncHUD();
});
document.getElementById('restart-btn').addEventListener('click', () => {
ensureAudio();
applyInitialModeState();
buildArenaObstacles();
resetTrucks();
syncHUD();
lastFrameTime = performance.now();
});
// --- AI Steering & Anti-Clumping Controller ---
function computeAIControls(t, dt) {
if (t.wreckTimer > 0) return { throttle: 0, steer: 0, boost: false };
t.wpTimer -= dt;
const player = trucks[0];
// Role-based target selection so all 4 rivals spread across the bowl
let tx = t.waypoint.x;
let tz = t.waypoint.z;
if (t.role === 'hunter') {
// Leads ahead of player slightly, then peels off after contact via wpTimer
if (t.wpTimer > 0) {
tx = player.x + Math.sin(player.heading) * 6;
tz = player.z + Math.cos(player.heading) * 6;
}
if (t.wpTimer < -3.2) t.wpTimer = 5.5;
} else if (t.role === 'roamerCW' || t.role === 'roamerCCW') {
const dir = t.role === 'roamerCW' ? 1 : -1;
t.orbitAngle += dir * dt * 0.55;
tx = Math.cos(t.orbitAngle) * (ARENA_RX * 0.62);
tz = Math.sin(t.orbitAngle) * (ARENA_RZ * 0.62);
// Opportunistic charge if player crosses near front cone
const dpx = player.x - t.x, dpz = player.z - t.z;
if (dpx * dpx + dpz * dpz < 420) {
tx = player.x; tz = player.z;
}
} else {
// 'jumper' hits mounds and crushable cars
const dxw = tx - t.x, dzw = tz - t.z;
if (dxw * dxw + dzw * dzw < 85 || t.wpTimer <= 0) {
t.waypoint = pickWaypoint('jumper', t.id);
t.wpTimer = 4.5 + Math.random() * 2.5;
}
}
// Repulsion vector from other AI trucks so they never clump into one moving wall
let sepX = 0, sepZ = 0;
for (let j = 1; j < trucks.length; j++) {
if (j === t.id) continue;
const other = trucks[j];
const rx = t.x - other.x;
const rz = t.z - other.z;
const distSq = rx * rx + rz * rz;
const minSep = 15.0;
if (distSq > 0.01 && distSq < minSep * minSep) {
const dist = Math.sqrt(distSq);
const push = ((minSep - dist) / minSep) * 24.0;
sepX += (rx / dist) * push;
sepZ += (rz / dist) * push;
}
}
// Soft wall avoidance near arena ellipse rim
const normEx = t.x / (ARENA_RX - 7);
const normEz = t.z / (ARENA_RZ - 7);
if (normEx * normEx + normEz * normEz > 0.85) {
sepX -= t.x * 0.45;
sepZ -= t.z * 0.45;
}
const goalX = tx + sepX - t.x;
const goalZ = tz + sepZ - t.z;
const desiredHeading = Math.atan2(goalX, goalZ);
let diff = desiredHeading - t.heading;
while (diff > Math.PI) diff -= Math.PI * 2;
while (diff < -Math.PI) diff += Math.PI * 2;
const steer = Math.max(-1, Math.min(1, diff * 1.85));
const throttle = Math.abs(diff) > 2.1 ? 0.55 : 0.96;
return { throttle, steer, boost: Math.abs(diff) < 0.25 && Math.random() < 0.15 };
}
// --- Physics & Collision Step ---
function stepSimulation(dt) {
state.elapsed += dt;
// Countdown rally clock
if (!state.gameOver) {
state.timeLeft = Math.max(0, state.timeLeft - dt);
if (state.timeLeft <= 0) {
state.gameOver = true;
finalScoreText.textContent = `FINAL SCORE: ${state.score.toLocaleString()}`;
gameoverOverlay.classList.add('active');
if (engineGain && audioCtx) engineGain.gain.setValueAtTime(0.0001, audioCtx.currentTime);
}
}
if (state.bannerTimer > 0) {
state.bannerTimer -= dt;
if (state.bannerTimer <= 0) bannerEl.style.opacity = '0';
}
// 1. Update all 5 Monster Cybertrucks
for (let i = 0; i < trucks.length; i++) {
const t = trucks[i];
let throttle = 0, steer = 0, boost = false;
if (t.wreckTimer > 0) {
t.wreckTimer -= dt;
t.heading += t.spinOutVel * dt;
t.spinOutVel *= Math.pow(0.25, dt);
if (Math.random() < 0.45) {
spawnParticles(t.x, t.y + 2.0, t.z, 1, 0x334155, 4, -3, 1.35);
}
if (t.wreckTimer <= 0) {
t.damage = 0;
}
} else if (!state.gameOver) {
if (t.isPlayer) {
const up = keys['KeyW'] || keys['ArrowUp'];
const down = keys['KeyS'] || keys['ArrowDown'];
const left = keys['KeyA'] || keys['ArrowLeft'];
const right = keys['KeyD'] || keys['ArrowRight'];
throttle = (up ? 1 : 0) - (down ? 0.72 : 0);
steer = (left ? 1 : 0) - (right ? 1 : 0);
boost = !!keys['Space'];
} else {
const ai = computeAIControls(t, dt);
throttle = ai.throttle;
steer = ai.steer;
boost = ai.boost;
}
}
const fwdX = Math.sin(t.heading);
const fwdZ = Math.cos(t.heading);
const rightX = Math.cos(t.heading);
const rightZ = -Math.sin(t.heading);
const groundY = getGroundHeight(t.x, t.z);
const onGround = t.y <= groundY + 0.18;
const forwardSpeed = t.vx * fwdX + t.vz * fwdZ;
const lateralSpeed = t.vx * rightX + t.vz * rightZ;
// Steer responsiveness
t.steerAngle += (steer * 0.52 - t.steerAngle) * Math.min(1, dt * 10);
if (onGround && Math.abs(forwardSpeed) > 0.6) {
const revSign = forwardSpeed >= -0.2 ? 1 : -1;
const turnFactor = Math.min(1, Math.abs(forwardSpeed) / 9);
const driftBoost = boost && t.isPlayer ? 1.35 : 1.0;
t.heading += steer * revSign * 2.35 * turnFactor * driftBoost * dt;
}
// Drive acceleration & lateral tire grip
if (onGround) {
const accelPower = (boost ? 48 : 35) * (t.isPlayer ? 1.08 : 0.96);
t.vx += fwdX * throttle * accelPower * dt;
t.vz += fwdZ * throttle * accelPower * dt;
// Kill sideways slip unless handbrake/boost sliding
const grip = boost ? 2.6 : 6.8;
const slipCancel = lateralSpeed * Math.min(1, grip * dt);
t.vx -= rightX * slipCancel;
t.vz -= rightZ * slipCancel;
// Rolling drag
const drag = throttle === 0 ? 1.85 : 0.58;
t.vx *= Math.max(0, 1 - drag * dt);
t.vz *= Math.max(0, 1 - drag * dt);
// Dust rooster-tail at high speed
const spdSq = t.vx * t.vx + t.vz * t.vz;
if (spdSq > 160 && Math.random() < 0.4) {
spawnParticles(t.x - fwdX * 2.2, t.y + 0.3, t.z - fwdZ * 2.2, 1, 0x7c5c38, 4.5, 10, 0.9);
}
// Big air landing bonus for player!
if (t.isPlayer && t.airborneTime > 0.65) {
addScore(150, 'MONSTER AIR', '#38bdf8');
spawnParticles(t.x, t.y + 0.3, t.z, 10, 0x8b6942, 9, 18, 1.1);
}
t.airborneTime = 0;
} else {
t.airborneTime += dt;
t.vx *= Math.max(0, 1 - 0.12 * dt);
t.vz *= Math.max(0, 1 - 0.12 * dt);
}
// Cap top speed
const maxSpd = boost ? 36 : 29;
const curSpd = Math.hypot(t.vx, t.vz);
if (curSpd > maxSpd) {
t.vx = (t.vx / curSpd) * maxSpd;
t.vz = (t.vz / curSpd) * maxSpd;
}
// Gravity & ramp launch
t.vy -= 25.0 * dt;
const prevGround = getGroundHeight(t.x, t.z);
t.x += t.vx * dt;
t.z += t.vz * dt;
const nextGround = getGroundHeight(t.x, t.z);
// Launch upward along ramp slope if climbing fast
if (onGround && dt > 0) {
const slopeVy = (nextGround - prevGround) / dt;
if (slopeVy > t.vy) t.vy = slopeVy;
}
t.y += t.vy * dt;
if (t.y < nextGround) {
t.y = nextGround;
if (t.vy < -9) {
t.vy = -t.vy * 0.22; // Monster suspension bounce
} else {
t.vy = 0;
}
}
// Elliptical Stadium Retaining Wall Collision
const nx = t.x / (ARENA_RX - t.radius);
const nz = t.z / (ARENA_RZ - t.radius);
const ellip = nx * nx + nz * nz;
if (ellip > 1.0) {
const inv = 1 / Math.sqrt(ellip);
t.x *= inv;
t.z *= inv;
// Normal pointing inward
let wnx = -nx, wnz = -nz;
const wLen = Math.hypot(wnx, wnz) || 1;
wnx /= wLen; wnz /= wLen;
const vn = t.vx * wnx + t.vz * wnz;
if (vn < 0) {
t.vx -= 1.55 * vn * wnx;
t.vz -= 1.55 * vn * wnz;
if (Math.abs(vn) > 10) {
spawnParticles(t.x - wnx * 2, t.y + 1.4, t.z - wnz * 2, 6, 0xfacc15, 9);
if (t.isPlayer) playCrashSound(Math.abs(vn), false);
}
}
}
// Update wheel spin, steering & body pitch/roll
t.wheelSpin += forwardSpeed * dt * 0.95;
for (let w = 0; w < t.wheels.length; w++) {
const wh = t.wheels[w];
wh.spinGroup.rotation.x = t.wheelSpin;
if (wh.front) wh.steerPivot.rotation.y = t.steerAngle;
}
// Align chassis tilt smoothly to terrain slope + suspension roll
const gNorm = getGroundNormal(t.x, t.z);
const localPitch = -(gNorm.x * fwdX + gNorm.z * fwdZ) * 0.65 - (t.vy * 0.018);
const localRoll = (gNorm.x * rightX + gNorm.z * rightZ) * 0.65 + (lateralSpeed * 0.018);
t.tiltNode.rotation.x += (localPitch - t.tiltNode.rotation.x) * Math.min(1, dt * 9);
t.tiltNode.rotation.z += (localRoll - t.tiltNode.rotation.z) * Math.min(1, dt * 9);
if (t.hitFlash > 0) {
t.hitFlash = Math.max(0, t.hitFlash - dt * 3.5);
t.bodyMat.emissive.setHex(t.wreckTimer > 0 ? 0xff2a00 : 0xffffff);
t.bodyMat.emissiveIntensity = t.hitFlash * 0.75;
} else if (t.wreckTimer > 0) {
t.bodyMat.emissive.setHex(0x991b1b);
t.bodyMat.emissiveIntensity = 0.35 + Math.sin(state.elapsed * 18) * 0.2;
} else {
t.bodyMat.emissiveIntensity = 0;
}
if (t.isPlayer && t.mesh.userData.beacon) {
t.mesh.userData.beacon.position.y = 4.15 + Math.sin(state.elapsed * 6) * 0.18;
t.mesh.userData.beacon.rotation.y += dt * 3;
}
t.mesh.position.set(t.x, t.y, t.z);
t.mesh.rotation.y = t.heading;
}
// 2. Truck-vs-Truck Collisions (Impulse, Damage, Wreck Spin-Out & Rally Score)
for (let i = 0; i < trucks.length; i++) {
for (let j = i + 1; j < trucks.length; j++) {
const a = trucks[i];
const b = trucks[j];
const dx = b.x - a.x;
const dz = b.z - a.z;
const distSq = dx * dx + dz * dz;
const minDist = a.radius + b.radius;
if (distSq < minDist * minDist && Math.abs(a.y - b.y) < 3.2) {
const dist = Math.sqrt(distSq) || 0.01;
const nx = dx / dist;
const nz = dz / dist;
// Positional separation
const overlap = minDist - dist;
const totalMass = a.mass + b.mass;
a.x -= nx * overlap * (b.mass / totalMass);
a.z -= nz * overlap * (b.mass / totalMass);
b.x += nx * overlap * (a.mass / totalMass);
b.z += nz * overlap * (a.mass / totalMass);
// Relative velocity along impact normal
const rvx = b.vx - a.vx;
const rvz = b.vz - a.vz;
const relNormalVel = rvx * nx + rvz * nz;
if (relNormalVel < 0) {
const restitution = 0.72;
const jImpulse = -(1 + restitution) * relNormalVel / (1 / a.mass + 1 / b.mass);
a.vx -= (jImpulse / a.mass) * nx;
a.vz -= (jImpulse / a.mass) * nz;
b.vx += (jImpulse / b.mass) * nx;
b.vz += (jImpulse / b.mass) * nz;
const impactSpeed = -relNormalVel;
if (impactSpeed > 4.5) {
const cx = (a.x + b.x) * 0.5;
const cz = (a.z + b.z) * 0.5;
const cy = Math.max(a.y, b.y) + 1.6;
spawnParticles(cx, cy, cz, Math.min(22, (impactSpeed * 0.9) | 0), 0xffb703, impactSpeed * 0.65);
a.hitFlash = 1;
b.hitFlash = 1;
a.vy = Math.max(a.vy, Math.min(7.5, impactSpeed * 0.22));
b.vy = Math.max(b.vy, Math.min(7.5, impactSpeed * 0.22));
// Determine who rammed whom via forward alignment with collision normal
const aAlign = Math.sin(a.heading) * nx + Math.cos(a.heading) * nz;
const bAlign = -(Math.sin(b.heading) * nx + Math.cos(b.heading) * nz);
if (a.isPlayer || b.isPlayer) {
const playerTruck = a.isPlayer ? a : b;
const rival = a.isPlayer ? b : a;
const playerAlign = a.isPlayer ? aAlign : bAlign;
const playerSpeed = Math.hypot(playerTruck.vx, playerTruck.vz);
let wreckedNow = false;
if (playerAlign > 0.15 || impactSpeed > 11) {
rival.damage += impactSpeed * (playerAlign > 0.35 ? 3.4 : 2.1);
const pts = Math.round(150 + impactSpeed * 12);
if (rival.damage >= 95 && rival.wreckTimer <= 0) {
rival.wreckTimer = 3.2;
rival.spinOutVel = (Math.random() > 0.5 ? 1 : -1) * 11.5;
rival.vy = 8.5;
rival.vx += nx * (a.isPlayer ? 12 : -12);
rival.vz += nz * (a.isPlayer ? 12 : -12);
wreckedNow = true;
spawnParticles(rival.x, rival.y + 2, rival.z, 28, 0xff4d00, 15);
addScore(600, `WRECKED ${rival.name}!`, '#ff3b5c');
} else {
addScore(pts, `SLAMMED ${rival.name}`, '#ffe600');
}
} else if (playerSpeed > 3) {
addScore(75, 'TRADING PAINT', '#fde047');
}
playCrashSound(impactSpeed, wreckedNow);
} else if (impactSpeed > 15) {
// Rival-on-rival bash can also spin one out visibly
b.spinOutVel = 7.5;
b.wreckTimer = 1.4;
}
}
}
}
}
}
// 3. Truck-vs-Obstacle Collisions (Cars crush, Barriers jolt, Drums launch)
for (let o = 0; o < obstacles.length; o++) {
const obs = obstacles[o];
if (obs.hitCooldown > 0) obs.hitCooldown -= dt;
// Move dynamic barrels / nudged obstacles
if (Math.abs(obs.vx) > 0.05 || Math.abs(obs.vz) > 0.05 || (obs.type === 'barrel' && obs.y > getGroundHeight(obs.x, obs.z))) {
obs.x += obs.vx * dt;
obs.z += obs.vz * dt;
obs.vx *= Math.max(0, 1 - 2.6 * dt);
obs.vz *= Math.max(0, 1 - 2.6 * dt);
// Keep obstacles inside arena
const enx = obs.x / (ARENA_RX - 3);
const enz = obs.z / (ARENA_RZ - 3);
const el = enx * enx + enz * enz;
if (el > 1) {
const inv = 1 / Math.sqrt(el);
obs.x *= inv; obs.z *= inv;
obs.vx *= -0.5; obs.vz *= -0.5;
}
const gy = getGroundHeight(obs.x, obs.z);
if (obs.type === 'barrel') {
obs.vy = (obs.vy || 0) - 24 * dt;
obs.y += obs.vy * dt;
if (obs.y < gy) { obs.y = gy; obs.vy = Math.abs(obs.vy) > 4 ? -obs.vy * 0.35 : 0; }
obs.mesh.rotation.x += obs.vz * dt * 0.5;
obs.mesh.rotation.z -= obs.vx * dt * 0.5;
} else {
obs.y = gy;
}
obs.mesh.position.set(obs.x, obs.y, obs.z);
}
for (let i = 0; i < trucks.length; i++) {
const t = trucks[i];
const dx = obs.x - t.x;
const dz = obs.z - t.z;
const distSq = dx * dx + dz * dz;
const minDist = t.radius + obs.radius;
if (distSq < minDist * minDist && Math.abs(t.y - obs.y) < 2.8) {
const dist = Math.sqrt(distSq) || 0.01;
const nx = dx / dist;
const nz = dz / dist;
const tSpd = Math.hypot(t.vx, t.vz);
if (obs.type === 'car') {
// Monster truck climbs & crushes the junked sedan!
t.vy = Math.max(t.vy, Math.min(7.5, 2.8 + tSpd * 0.16));
t.y = Math.max(t.y, obs.y + 0.65);
t.vx *= 0.95;
t.vz *= 0.95;
if (!obs.crushed) {
obs.crushed = true;
obs.cabin.scale.y = 0.28;
obs.cabin.position.y = 0.88;
obs.wind.scale.y = 0.28;
obs.wind.position.y = 0.88;
obs.mesh.scale.set(1.12, 0.54, 1.05);
spawnParticles(obs.x, obs.y + 1.1, obs.z, 16, 0x94a3b8, 10);
if (t.isPlayer) {
addScore(200, 'CAR CRUSHED', '#4ade80');
playCrashSound(12, false);
}
}
} else if (obs.type === 'barrier') {
// Heavy concrete jersey barrier pushes truck back & slides slightly
const overlap = minDist - dist;
t.x -= nx * overlap * 0.78;
t.z -= nz * overlap * 0.78;
obs.x += nx * overlap * 0.22;
obs.z += nz * overlap * 0.22;
obs.vx += nx * tSpd * 0.28;
obs.vz += nz * tSpd * 0.28;
obs.mesh.position.set(obs.x, getGroundHeight(obs.x, obs.z), obs.z);
const vn = t.vx * nx + t.vz * nz;
if (vn > 0) {
t.vx -= 1.35 * vn * nx;
t.vz -= 1.35 * vn * nz;
}
if (obs.hitCooldown <= 0 && tSpd > 7) {
obs.hitCooldown = 0.45;
spawnParticles(obs.x, obs.y + 1.0, obs.z, 10, 0xfde047, 9);
if (t.isPlayer) {
addScore(50, 'BARRIER BASH', '#cbd5e1');
playCrashSound(tSpd * 0.7, false);
}
}
} else if (obs.type === 'barrel') {
// Steel rally drum launches across the dirt floor!
obs.vx = nx * Math.max(14, tSpd * 1.35);
obs.vz = nz * Math.max(14, tSpd * 1.35);
obs.vy = 6.5 + Math.random() * 4.5;
t.vx *= 0.96;
t.vz *= 0.96;
if (obs.hitCooldown <= 0) {
obs.hitCooldown = 0.6;
spawnParticles(obs.x, obs.y + 1.1, obs.z, 12, 0xf97316, 11);
if (t.isPlayer) {
addScore(100, 'DRUM SMASH', '#fb923c');
playCrashSound(9, false);
}
}
}
}
}
}
// 4. Particles, Crowd Animation, Engine Sound & Follow Camera
updateParticles(dt);
updateCrowd(state.elapsed);
const player = trucks[0];
const pSpd = Math.hypot(player.vx, player.vz);
if (engineOsc && audioCtx && audioCtx.state === 'running' && !state.gameOver) {
const now = audioCtx.currentTime;
const freq = 44 + pSpd * 3.1 + (keys['Space'] ? 18 : 0);
engineOsc.frequency.setTargetAtTime(freq, now, 0.05);
engineOsc._sub.frequency.setTargetAtTime(freq * 0.5, now, 0.05);
engineGain.gain.setTargetAtTime(0.035 + Math.min(0.05, pSpd * 0.0018), now, 0.06);
}
// Smooth Chase Camera
const camDist = 17.5 + Math.min(4.5, pSpd * 0.12);
const camHeight = 8.4 + Math.min(2.2, Math.max(0, player.y * 0.35));
const targetCamX = player.x - Math.sin(player.heading) * camDist;
const targetCamZ = player.z - Math.cos(player.heading) * camDist;
const targetCamY = Math.max(getGroundHeight(targetCamX, targetCamZ) + 3.5, player.y + camHeight);
camera.position.x += (targetCamX - camera.position.x) * Math.min(1, dt * 6.5);
camera.position.z += (targetCamZ - camera.position.z) * Math.min(1, dt * 6.5);
camera.position.y += (targetCamY - camera.position.y) * Math.min(1, dt * 5.5);
camera.lookAt(
player.x + Math.sin(player.heading) * 5.5,
player.y + 2.3,
player.z + Math.cos(player.heading) * 5.5
);
}
function syncHUD() {
document.getElementById('arena-name').textContent = state.arenaName;
scoreEl.textContent = state.score.toLocaleString();
const totalSec = Math.ceil(state.timeLeft);
const mins = String(Math.floor(totalSec / 60)).padStart(2, '0');
const secs = String(totalSec % 60).padStart(2, '0');
clockEl.textContent = `${mins}:${secs}`;
if (totalSec <= 10 && !state.gameOver) {
clockEl.classList.add('low');
} else {
clockEl.classList.remove('low');
}
}
//Resize support
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
if (state.photoMode) renderer.render(scene, camera);
});
// --- Boot & Main Loop ---
applyInitialModeState();
buildArenaObstacles();
resetTrucks();
syncHUD();
let lastFrameTime = performance.now();
function loop(now) {
requestAnimationFrame(loop);
if (state.photoMode) {
lastFrameTime = now;
return;
}
const dt = Math.min(0.05, Math.max(0.001, (now - lastFrameTime) / 1000));
lastFrameTime = now;
stepSimulation(dt);
syncHUD();
renderer.render(scene, camera);
}
requestAnimationFrame(loop);
})();
</script>
</body>
</html>
hunter, clockwise perimeter roamerCW, counter-clockwise roamerCCW, and hill-seeking jumper) combined with an inverse-distance separation vector summed across all rival pairs within 15 meters, steering them apart before they can stack into a single wall.applyInitialModeState() checks window.location.hash.toLowerCase() === '#demo' and writes directly to state.score = isDemo ? 5000 : 0 and state.timeLeft = isDemo ? 20 : 90, which the live physics scoring and countdown timer mutate and the HUD reads every frame.P halts both the physics/simulation step (stepSimulation(dt)) and per-frame rendering inside loop() via an early if (state.photoMode) return; guard after rendering the frozen frame once (only re-rendering that static scene if the browser window is resized).x, z, radius plus a vertical height overlap check) with terrain height sampled analytically from smooth cosine-squared mounds. Truck-to-truck impacts resolve positional overlap weighted by inverse mass, compute relative velocity along the collision normal $\mathbf{n}$, and apply a restitution impulse $j = -(1 + e)(\mathbf{v}_{rel} \cdot \mathbf{n}) / (m_a^{-1} + m_b^{-1})$; normal impact speed weighted by the attacker's heading dot product accumulates rival damage until crossing a threshold triggers a timed spin-out wreck state, vertical pop, and rally score awards.