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.Here's a self-contained HTML page for your Tesla Cybertruck monster-truck rally. It loads only three.js from a pinned CDN, generates all art and audio at runtime, and runs on a single click.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no">
<title>IRONPIT MEGADROME — Cybertruck Monster Rally</title>
<style>
html,body{margin:0;padding:0;height:100%;overflow:hidden;background:#05070d;touch-action:none;
font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,"Courier New",monospace;
color:#eaf0f6;-webkit-font-smoothing:antialiased;user-select:none;}
canvas{display:block;}
#hud{position:fixed;inset:0;pointer-events:none;z-index:10;}
.panel{position:absolute;background:rgba(8,12,20,.55);border:1px solid rgba(255,255,255,.14);
padding:7px 13px;border-radius:4px;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);}
#arenaBox{top:14px;left:14px;}
#arenaBox .name{font-size:19px;letter-spacing:.15em;font-weight:700;color:#ffd08a;
text-shadow:0 0 14px rgba(255,150,40,.55);white-space:nowrap;}
#arenaBox .sub{font-size:10px;letter-spacing:.22em;color:#93a7bd;margin-top:3px;}
#scoreBox{top:14px;left:50%;transform:translateX(-50%);text-align:center;min-width:150px;}
#scoreBox .lbl{font-size:9px;letter-spacing:.32em;color:#93a7bd;}
#scoreBox .val{font-size:29px;font-weight:700;letter-spacing:.05em;color:#fff;
font-variant-numeric:tabular-nums;text-shadow:0 0 16px rgba(110,175,255,.5);transition:color .12s;}
#scoreBox.flash .val{color:#ffe57a;text-shadow:0 0 22px rgba(255,190,60,.9);}
#clockBox{top:14px;right:14px;text-align:right;min-width:96px;}
#clockBox .lbl{font-size:9px;letter-spacing:.32em;color:#93a7bd;}
#clockBox .val{font-size:29px;font-weight:700;font-variant-numeric:tabular-nums;color:#eaf0f6;}
#clockBox.low .val{color:#ff5544;text-shadow:0 0 18px rgba(255,60,40,.8);}
#hint{position:absolute;left:50%;bottom:18px;transform:translateX(-50%);font-size:11px;
letter-spacing:.16em;color:#9fb2c8;background:rgba(8,12,20,.5);border:1px solid rgba(255,255,255,.1);
padding:7px 16px;border-radius:20px;transition:opacity 1.2s;white-space:nowrap;}
#hint.gone{opacity:0;}
#toasts{position:absolute;left:50%;top:96px;transform:translateX(-50%);text-align:center;}
.toast{font-size:15px;font-weight:700;letter-spacing:.14em;color:#ffe08a;margin:4px 0;
text-shadow:0 0 14px rgba(255,150,40,.8);opacity:1;transition:opacity .45s;}
.toast.out{opacity:0;}
#card{position:fixed;inset:0;display:none;align-items:center;justify-content:center;z-index:20;
background:radial-gradient(ellipse at center,rgba(4,7,14,.35),rgba(2,4,9,.82));}
#card.on{display:flex;}
#cardInner{text-align:center;padding:34px 54px;border:1px solid rgba(255,255,255,.18);
border-radius:6px;background:rgba(6,10,18,.66);backdrop-filter:blur(6px);
box-shadow:0 0 60px rgba(0,0,0,.7);}
#cardTitle{font-size:34px;font-weight:700;letter-spacing:.2em;color:#ffd08a;
text-shadow:0 0 26px rgba(255,150,40,.6);}
#cardSub{font-size:13px;letter-spacing:.44em;color:#9fb2c8;margin-top:12px;}
#cardMsg{font-size:26px;font-weight:700;letter-spacing:.12em;color:#fff;margin-top:22px;
font-variant-numeric:tabular-nums;}
#cardHint{font-size:11px;letter-spacing:.22em;color:#7f93aa;margin-top:22px;}
</style>
</head>
<body>
<div id="hud">
<div class="panel" id="arenaBox">
<div class="name" id="arenaName">IRONPIT MEGADROME</div>
<div class="sub">CYBERTRUCK MONSTER RALLY</div>
</div>
<div class="panel" id="scoreBox">
<div class="lbl">SCORE</div>
<div class="val" id="scoreVal">000000</div>
</div>
<div class="panel" id="clockBox">
<div class="lbl">TIME</div>
<div class="val" id="clockVal">60.0</div>
</div>
<div id="hint">W A S D / ARROW KEYS — DRIVE · SPACE — HANDBRAKE · P — PHOTO MODE · R — RESTART</div>
<div id="toasts"></div>
</div>
<div id="card"><div id="cardInner">
<div id="cardTitle"></div>
<div id="cardSub"></div>
<div id="cardMsg"></div>
<div id="cardHint"></div>
</div></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
(function(){
'use strict';
/* ================================================================
CONSTANTS
================================================================ */
var ARENA_NAME = 'IRONPIT MEGADROME';
var PLAY_R = 52.5; // inner wall radius
var TRUCK_R = 2.35;
var ROUND_TIME = 60;
var DEMO_TIME = 20;
var DEMO_SCORE = 5000;
var TAU = Math.PI * 2;
var ENGINE_ACC = 38, MAX_FWD = 26, MAX_REV = 11, DRAG = 1.5, GRIP = 7.5, TURN = 2.35;
var GRAV = 34;
var clamp = function(v,a,b){ return v<a?a:(v>b?b:v); };
var rand = function(a,b){ return a + Math.random()*(b-a); };
var fmtScore = function(n){ return String(Math.max(0,Math.floor(n))).padStart ? String(Math.max(0,Math.floor(n))).padStart(6,'0') : ('000000'+Math.max(0,Math.floor(n))).slice(-6); };
var DEMO = (location.hash || '').toLowerCase().endsWith('demo');
/* ================================================================
DOM
================================================================ */
var elArenaName = document.getElementById('arenaName');
var elScoreVal = document.getElementById('scoreVal');
var elClockVal = document.getElementById('clockVal');
var elScoreBox = document.getElementById('scoreBox');
var elClockBox = document.getElementById('clockBox');
var elHint = document.getElementById('hint');
var elToasts = document.getElementById('toasts');
var elCard = document.getElementById('card');
var elCardTitle = document.getElementById('cardTitle');
var elCardSub = document.getElementById('cardSub');
var elCardMsg = document.getElementById('cardMsg');
var elCardHint = document.getElementById('cardHint');
elArenaName.textContent = ARENA_NAME;
var scoreFlashT = null;
function flashScore(){
elScoreBox.classList.add('flash');
clearTimeout(scoreFlashT);
scoreFlashT = setTimeout(function(){ elScoreBox.classList.remove('flash'); }, 160);
}
function toast(msg){
var el = document.createElement('div');
el.className = 'toast';
el.textContent = msg;
elToasts.appendChild(el);
while (elToasts.children.length > 3) elToasts.removeChild(elToasts.firstChild);
setTimeout(function(){ el.classList.add('out'); setTimeout(function(){ el.remove(); }, 500); }, 850);
}
function showCard(title, sub, msg, hint){
elCardTitle.textContent = title;
elCardSub.textContent = sub;
elCardMsg.textContent = msg;
elCardHint.textContent = hint || '';
elCard.classList.add('on');
}
function hideCard(){ elCard.classList.remove('on'); }
setTimeout(function(){ elHint.classList.add('gone'); }, 12000);
/* ================================================================
RENDERER / SCENE / CAMERA
================================================================ */
var renderer = new THREE.WebGLRenderer({ antialias:true, powerPreference:'high-performance' });
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 1.75));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
if (THREE.sRGBEncoding !== undefined) renderer.outputEncoding = THREE.sRGBEncoding;
document.body.appendChild(renderer.domElement);
var scene = new THREE.Scene();
scene.fog = new THREE.Fog(0x2b3350, 130, 330);
var camera = new THREE.PerspectiveCamera(58, window.innerWidth/window.innerHeight, 0.4, 700);
camera.position.set(0, 14, -46);
camera.lookAt(0, 3, 0);
/* ---- lights ---- */
var hemi = new THREE.HemisphereLight(0x9db6ff, 0x453423, 0.85);
scene.add(hemi);
var sun = new THREE.DirectionalLight(0xffd7a8, 1.15);
sun.position.set(60, 105, 45);
sun.castShadow = true;
sun.shadow.mapSize.width = 2048;
sun.shadow.mapSize.height = 2048;
sun.shadow.camera.left = -90;
sun.shadow.camera.right = 90;
sun.shadow.camera.top = 90;
sun.shadow.camera.bottom = -90;
sun.shadow.camera.near = 10;
sun.shadow.camera.far = 300;
sun.shadow.bias = -0.0009;
scene.add(sun);
scene.add(sun.target);
var fill = new THREE.DirectionalLight(0x6f8dff, 0.28);
fill.position.set(-70, 40, -60);
scene.add(fill);
/* ---- sky dome ---- */
(function(){
var c = document.createElement('canvas'); c.width = 8; c.height = 256;
var g = c.getContext('2d');
var grd = g.createLinearGradient(0,0,0,256);
grd.addColorStop(0.00,'#080f22');
grd.addColorStop(0.34,'#1f3768');
grd.addColorStop(0.55,'#5b5f92');
grd.addColorStop(0.70,'#c07a63');
grd.addColorStop(0.82,'#7a4f4b');
grd.addColorStop(1.00,'#241c22');
g.fillStyle = grd; g.fillRect(0,0,8,256);
var tex = new THREE.CanvasTexture(c);
var mat = new THREE.MeshBasicMaterial({ map:tex, side:THREE.BackSide, fog:false, depthWrite:false });
var sky = new THREE.Mesh(new THREE.SphereGeometry(340, 24, 16), mat);
scene.add(sky);
})();
/* ================================================================
TERRAIN
================================================================ */
var HILLS = [];
function addHill(x,z,r,h){ HILLS.push({ x:x, z:z, r:r, h:h, r2:r*r }); }
addHill(-20,-16, 9.5, 2.6);
addHill( 18,-26, 8.5, 2.1);
addHill( 31, 9, 10.0, 3.0);
addHill(-33, 14, 9.0, 2.3);
addHill( -4, 31, 8.0, 1.9);
addHill( 12, 14, 6.5, 1.5);
addHill(-31,-31, 7.5, 2.7);
addHill( 34,-14, 7.5, 1.7);
addHill( -3,-36, 9.0, 2.2);
addHill( 24, 30, 7.5, 2.0);
addHill( 2, -6, 5.5, 1.1);
function terrainH(x,z){
var h = 0;
for (var i=0;i<HILLS.length;i++){
var hill = HILLS[i];
var dx = x - hill.x, dz = z - hill.z;
var d2 = dx*dx + dz*dz;
if (d2 < hill.r2){
var t = 1 - Math.sqrt(d2)/hill.r;
h += hill.h * t*t*(3 - 2*t);
}
}
return h;
}
var _n = new THREE.Vector3();
function terrainNormal(x,z,out){
var e = 0.7;
var hL = terrainH(x-e,z), hR = terrainH(x+e,z);
var hD = terrainH(x,z-e), hU = terrainH(x,z+e);
out.set(hL-hR, 2*e, hD-hU).normalize();
return out;
}
/* ---- dirt texture ---- */
function makeDirtTexture(){
var s = 512;
var c = document.createElement('canvas'); c.width = c.height = s;
var g = c.getContext('2d');
g.fillStyle = '#5a4635'; g.fillRect(0,0,s,s);
for (var i=0;i<2800;i++){
var r = 3 + Math.random()*26;
var light = Math.random() < 0.5;
g.fillStyle = light ? 'rgba(104,84,60,0.15)' : 'rgba(42,32,24,0.16)';
g.beginPath(); g.arc(Math.random()*s, Math.random()*s, r, 0, 6.3); g.fill();
}
for (var j=0;j<9000;j++){
var a = Math.random()*0.20;
g.fillStyle = Math.random() < 0.5 ? 'rgba(220,196,158,'+a+')' : 'rgba(0,0,0,'+a+')';
g.fillRect(Math.random()*s, Math.random()*s, 2, 2);
}
var t = new THREE.CanvasTexture(c);
t.wrapS = t.wrapT = THREE.RepeatWrapping;
t.repeat.set(6,6);
t.anisotropy = Math.min(4, renderer.capabilities.getMaxAnisotropy ? renderer.capabilities.getMaxAnisotropy() : 1);
return t;
}
var groundGeo = new THREE.PlaneGeometry(150,150,130,130);
groundGeo.rotateX(-Math.PI/2);
(function(){
var p = groundGeo.attributes.position;
for (var i=0;i<p.count;i++){
p.setY(i, terrainH(p.getX(i), p.getZ(i)));
}
groundGeo.computeVertexNormals();
})();
var ground = new THREE.Mesh(groundGeo, new THREE.MeshStandardMaterial({
map: makeDirtTexture(), roughness: 1.0, metalness: 0.0, color: 0xbbbbbb
}));
ground.receiveShadow = true;
scene.add(ground);
/* ================================================================
STADIUM : ring wall, tiered stands, crowd
================================================================ */
var WALL_R = PLAY_R;
(function(){
var wallMat = new THREE.MeshStandardMaterial({ color:0x6a7079, roughness:0.9, metalness:0.05, side:THREE.DoubleSide });
var wall = new THREE.Mesh(new THREE.CylinderGeometry(WALL_R, WALL_R, 2.3, 72, 1, true), wallMat);
wall.position.y = 1.15;
scene.add(wall);
var capMat = new THREE.MeshStandardMaterial({ color:0xff8a2b, roughness:0.6, metalness:0.2, side:THREE.DoubleSide,
emissive:0x3a1400, emissiveIntensity:1 });
var cap = new THREE.Mesh(new THREE.CylinderGeometry(WALL_R+0.08, WALL_R+0.08, 0.42, 72, 1, true), capMat);
cap.position.y = 2.42;
scene.add(cap);
})();
var R0 = 55, DR = 2.8, TIER_H = 1.9, TIERS = 8;
var crowdSpots = [];
var standMat = new THREE.MeshStandardMaterial({ color:0x4b5058, roughness:0.97, metalness:0.0 });
(function(){
var rIn = WALL_R;
for (var i=0;i<TIERS;i++){
var rOut = R0 + i*DR;
var topY = (i+1)*TIER_H;
var ring = new THREE.Mesh(new THREE.RingGeometry(rIn, rOut, 72, 1), standMat);
ring.rotation.x = -Math.PI/2;
ring.position.y = topY;
scene.add(ring);
var riser = new THREE.Mesh(new THREE.CylinderGeometry(rOut, rOut, TIER_H, 72, 1, true), standMat);
riser.position.y = topY - TIER_H/2;
scene.add(riser);
var rows = [
{ off: 0.80, spacing: 1.95 },
{ off: 1.85, spacing: 2.55 }
];
for (var rI=0; rI<rows.length; rI++){
var rr = rIn + rows[rI].off;
if (rr > rOut - 0.25) continue;
var n = Math.max(10, Math.floor(TAU*rr / rows[rI].spacing));
for (var k=0;k<n;k++){
var ang = (k/n)*TAU + rand(-0.14,0.14);
crowdSpots.push({
x: Math.cos(ang)*rr + rand(-0.2,0.2),
z: Math.sin(ang)*rr + rand(-0.2,0.2),
y: topY,
a: ang + Math.PI + rand(-0.3,0.3),
p: Math.random()*TAU,
sy: rand(0.85,1.18),
sxz: rand(0.85,1.12)
});
}
}
rIn = rOut;
}
/* outer shell */
var shell = new THREE.Mesh(
new THREE.CylinderGeometry(rIn+0.4, rIn+0.4, TIERS*TIER_H + 1.2, 72, 1, true),
new THREE.MeshStandardMaterial({ color:0x2e333b, roughness:1, metalness:0, side:THREE.DoubleSide }));
shell.position.y = (TIERS*TIER_H)/2;
scene.add(shell);
/* floodlight towers */
var poleMat = new THREE.MeshStandardMaterial({ color:0x35393f, roughness:0.8, metalness:0.4 });
var lampMat = new THREE.MeshBasicMaterial({ color:0xfff2cc });
for (var t=0;t<4;t++){
var a = t*Math.PI/2 + Math.PI/4;
var g = new THREE.Group();
g.position.set(Math.cos(a)*(rIn+6), 0, Math.sin(a)*(rIn+6));
var pole = new THREE.Mesh(new THREE.CylinderGeometry(0.5,0.8,26,7), poleMat);
pole.position.y = 13;
g.add(pole);
var head = new THREE.Mesh(new THREE.BoxGeometry(7,2.6,1.2), poleMat);
head.position.y = 26;
g.add(head);
for (var q=0;q<4;q++){
var lamp = new THREE.Mesh(new THREE.BoxGeometry(1.4,1.9,0.25), lampMat);
lamp.position.set(-2.4 + q*1.6, 26, 0.75);
g.add(lamp);
}
scene.add(g);
}
})();
/* ---- crowd instancing ---- */
var crowdGeo = new THREE.BoxGeometry(0.52, 1.05, 0.4);
crowdGeo.translate(0, 0.525, 0);
var crowdMat = new THREE.MeshLambertMaterial({});
var crowdCount = crowdSpots.length;
var crowd = new THREE.InstancedMesh(crowdGeo, crowdMat, crowdCount);
crowd.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
crowd.frustumCulled = false;
var cBase = new Float32Array(crowdCount*3);
var cAng = new Float32Array(crowdCount);
var cPhase = new Float32Array(crowdCount);
var cSy = new Float32Array(crowdCount);
var cSxz = new Float32Array(crowdCount);
(function(){
var dummy = new THREE.Object3D();
var col = new THREE.Color();
var palette = [0xff5a3c, 0xffcf4d, 0x3fa4ff, 0xf2f5f8, 0x2fd68a, 0xe04ad0, 0x9b6bff,
0xd8dee6, 0x2b2f38, 0xff8a2b, 0x5ee0ff, 0xc9d1da];
for (var i=0;i<crowdCount;i++){
var s = crowdSpots[i];
cBase[i*3] = s.x;
cBase[i*3+1] = s.y;
cBase[i*3+2] = s.z;
cAng[i] = s.a;
cPhase[i] = s.p;
cSy[i] = s.sy;
cSxz[i] = s.sxz;
dummy.position.set(s.x, s.y, s.z);
dummy.rotation.set(0, s.a, 0);
dummy.scale.set(s.sxz, s.sy, s.sxz);
dummy.updateMatrix();
crowd.setMatrixAt(i, dummy.matrix);
col.setHex(palette[(Math.random()*palette.length)|0]);
crowd.setColorAt(i, col);
}
crowd.instanceMatrix.needsUpdate = true;
if (crowd.instanceColor) crowd.instanceColor.needsUpdate = true;
scene.add(crowd);
})();
var crowdCursor = 0;
var _cd = new THREE.Object3D();
function updateCrowd(dt){
var slice = 900;
var waveAmp = 0.62 * Math.max(0, Math.sin(state.t*0.42));
waveAmp *= waveAmp;
for (var k=0;k<slice;k++){
var i = (crowdCursor + k) % crowdCount;
var ang = cAng[i];
var theta = Math.atan2(cBase[i*3+2], cBase[i*3]);
var wave = waveAmp * Math.max(0, Math.sin(theta*2 - state.t*3.4)) * 1.05;
var bob = Math.sin(state.t*2.3 + cPhase[i]) * 0.11;
_cd.position.set(cBase[i*3], cBase[i*3+1] + bob + wave, cBase[i*3+2]);
_cd.rotation.set(0, ang, 0);
_cd.scale.set(cSxz[i], cSy[i], cSxz[i]);
_cd.updateMatrix();
crowd.setMatrixAt(i, _cd.matrix);
}
crowdCursor = (crowdCursor + slice) % crowdCount;
crowd.instanceMatrix.needsUpdate = true;
}
/* ================================================================
PARTICLES (custom shader points)
================================================================ */
var FOCAL = 1000;
function updateFocal(){
var pr = renderer.getPixelRatio();
FOCAL = (window.innerHeight * pr * 0.5) / Math.tan(camera.fov * 0.5 * Math.PI/180);
smoke.mat.uniforms.uScale.value = FOCAL;
sparks.mat.uniforms.uScale.value = FOCAL;
}
function makeParticles(count, additive, alpha){
var posA = new Float32Array(count*3);
var colA = new Float32Array(count*3);
var lifeA = new Float32Array(count);
var sizeA = new Float32Array(count);
var vel = new Float32Array(count*3);
var maxL = new Float32Array(count);
var grav = new Float32Array(count);
var dragA = new Float32Array(count);
var cursor = 0;
for (var i=0;i<count;i++){ posA[i*3+1] = -9999; colA[i*3]=colA[i*3+1]=colA[i*3+2]=1; }
var geo = new THREE.BufferGeometry();
var aPos = new THREE.BufferAttribute(posA,3); aPos.setUsage(THREE.DynamicDrawUsage);
var aCol = new THREE.BufferAttribute(colA,3); aCol.setUsage(THREE.DynamicDrawUsage);
var aLife = new THREE.BufferAttribute(lifeA,1); aLife.setUsage(THREE.DynamicDrawUsage);
var aSize = new THREE.BufferAttribute(sizeA,1); aSize.setUsage(THREE.DynamicDrawUsage);
geo.setAttribute('position', aPos);
geo.setAttribute('aColor', aCol);
geo.setAttribute('aLife', aLife);
geo.setAttribute('aSize', aSize);
var mat = new THREE.ShaderMaterial({
uniforms: { uScale: { value: FOCAL }, uAlpha: { value: alpha } },
vertexShader: [
'attribute vec3 aColor; attribute float aLife; attribute float aSize;',
'uniform float uScale;',
'varying vec3 vCol; varying float vLife;',
'void main(){',
' vCol = aColor; vLife = aLife;',
' vec4 mv = modelViewMatrix * vec4(position,1.0);',
' float s = aSize * uScale / max(0.001, -mv.z);',
' gl_PointSize = clamp(s, 1.0, 240.0);',
' gl_Position = projectionMatrix * mv;',
'}'
].join('\n'),
fragmentShader: [
'varying vec3 vCol; varying float vLife;',
'uniform float uAlpha;',
'void main(){',
' vec2 d = gl_PointCoord - 0.5;',
' float a = smoothstep(0.5, 0.06, length(d));',
' gl_FragColor = vec4(vCol, a * vLife * uAlpha);',
'}'
].join('\n'),
transparent: true,
depthWrite: false,
blending: additive ? THREE.AdditiveBlending : THREE.NormalBlending
});
var points = new THREE.Points(geo, mat);
points.frustumCulled = false;
scene.add(points);
return {
mat: mat, geo: geo, points: points,
emit: function(x,y,z, vx,vy,vz, life, size, col, gravity, drag){
var i = cursor; cursor = (cursor+1) % count;
var i3 = i*3;
posA[i3]=x; posA[i3+1]=y; posA[i3+2]=z;
vel[i3]=vx; vel[i3+1]=vy; vel[i3+2]=vz;
colA[i3]=col[0]; colA[i3+1]=col[1]; colA[i3+2]=col[2];
lifeA[i]=1; maxL[i]=life; sizeA[i]=size;
grav[i]=gravity; dragA[i]=drag;
/* store real life in a side array via lifeLeft */
lifeLeft[i]=life;
},
update: function(dt){
for (var i=0;i<count;i++){
if (lifeLeft[i] <= 0) continue;
lifeLeft[i] -= dt;
var i3 = i*3;
if (lifeLeft[i] <= 0){
lifeA[i] = 0;
posA[i3+1] = -9999;
continue;
}
lifeA[i] = lifeLeft[i] / maxL[i];
vel[i3+1] += grav[i]*dt;
var dmp = Math.exp(-dragA[i]*dt);
vel[i3] *= dmp; vel[i3+1] *= dmp; vel[i3+2] *= dmp;
posA[i3] += vel[i3]*dt;
posA[i3+1] += vel[i3+1]*dt;
posA[i3+2] += vel[i3+2]*dt;
}
aPos.needsUpdate = true;
aCol.needsUpdate = true;
aLife.needsUpdate = true;
aSize.needsUpdate = true;
}
};
function noop(){}
}
/* the emitter needs a lifeLeft array; build it via closure trick */
function makeEmitter(count, additive, alpha){
var P = makeParticles(count, additive, alpha);
/* inject lifeLeft via a wrapper: rebuild with a shared array is messy,
so we attach it here by monkey-patching the update/emit pair. */
return P;
}
/* Because the factory above references `lifeLeft`, define it globally per instance: */
var lifeLeftStore = [];
function Particles(count, additive, alpha){
var posA = new Float32Array(count*3);
var colA = new Float32Array(count*3);
var lifeA = new Float32Array(count);
var sizeA = new Float32Array(count);
var vel = new Float32Array(count*3);
var maxL = new Float32Array(count);
var grav = new Float32Array(count);
var dragA = new Float32Array(count);
var lifeLeft = new Float32Array(count);
var cursor = 0;
for (var i=0;i<count;i++){ posA[i*3+1] = -9999; colA[i*3]=colA[i*3+1]=colA[i*3+2]=1; }
var geo = new THREE.BufferGeometry();
var aPos = new THREE.BufferAttribute(posA,3); aPos.setUsage(THREE.DynamicDrawUsage);
var aCol = new THREE.BufferAttribute(colA,3); aCol.setUsage(THREE.DynamicDrawUsage);
var aLife = new THREE.BufferAttribute(lifeA,1); aLife.setUsage(THREE.DynamicDrawUsage);
var aSize = new THREE.BufferAttribute(sizeA,1); aSize.setUsage(THREE.DynamicDrawUsage);
geo.setAttribute('position', aPos);
geo.setAttribute('aColor', aCol);
geo.setAttribute('aLife', aLife);
geo.setAttribute('aSize', aSize);
var mat = new THREE.ShaderMaterial({
uniforms: { uScale: { value: FOCAL }, uAlpha: { value: alpha } },
vertexShader: [
'attribute vec3 aColor; attribute float aLife; attribute float aSize;',
'uniform float uScale;',
'varying vec3 vCol; varying float vLife;',
'void main(){',
' vCol = aColor; vLife = aLife;',
' vec4 mv = modelViewMatrix * vec4(position,1.0);',
' float s = aSize * uScale / max(0.001, -mv.z);',
' gl_PointSize = clamp(s, 1.0, 240.0);',
' gl_Position = projectionMatrix * mv;',
'}'
].join('\n'),
fragmentShader: [
'varying vec3 vCol; varying float vLife;',
'uniform float uAlpha;',
'void main(){',
' vec2 d = gl_PointCoord - 0.5;',
' float a = smoothstep(0.5, 0.06, length(d));',
' gl_FragColor = vec4(vCol, a * vLife * uAlpha);',
'}'
].join('\n'),
transparent: true, depthWrite: false,
blending: additive ? THREE.AdditiveBlending : THREE.NormalBlending
});
var points = new THREE.Points(geo, mat);
points.frustumCulled = false;
scene.add(points);
this.mat = mat;
this.emit = function(x,y,z, vx,vy,vz, life, size, col, gravity, drag){
var i = cursor; cursor = (cursor+1)%count;
var i3 = i*3;
posA[i3]=x; posA[i3+1]=y; posA[i3+2]=z;
vel[i3]=vx; vel[i3+1]=vy; vel[i3+2]=vz;
colA[i3]=col[0]; colA[i3+1]=col[1]; colA[i3+2]=col[2];
lifeA[i]=1; maxL[i]=life; lifeLeft[i]=life; sizeA[i]=size;
grav[i]=gravity; dragA[i]=drag;
};
this.update = function(dt){
for (var i=0;i<count;i++){
if (lifeLeft[i] <= 0) continue;
lifeLeft[i] -= dt;
var i3 = i*3;
if (lifeLeft[i] <= 0){ lifeA[i] = 0; posA[i3+1] = -9999; continue; }
lifeA[i] = lifeLeft[i]/maxL[i];
vel[i3+1] += grav[i]*dt;
var dmp = Math.exp(-dragA[i]*dt);
vel[i3] *= dmp; vel[i3+1] *= dmp; vel[i3+2] *= dmp;
posA[i3] += vel[i3]*dt;
posA[i3+1] += vel[i3+1]*dt;
posA[i3+2] += vel[i3+2]*dt;
}
aPos.needsUpdate = true; aCol.needsUpdate = true;
aLife.needsUpdate = true; aSize.needsUpdate = true;
};
}
var smoke = new Particles(300, false, 0.42);
var sparks = new Particles(340, true, 0.95);
updateFocal();
function smokePuff(x,y,z,scale,dark){
var col = dark ? [0.16,0.15,0.15] : [0.42,0.40,0.37];
smoke.emit(x,y,z, rand(-1.2,1.2), rand(0.8,2.6), rand(-1.2,1.2),
rand(0.9,1.8), rand(0.7,1.5)*scale, col, 1.6, 1.4);
}
function sparkBurst(x,y,z,n,strength){
for (var i=0;i<n;i++){
var a = Math.random()*TAU, e = rand(0.1,1.0);
var sp = rand(6,20) * strength;
sparks.emit(x,y,z,
Math.cos(a)*sp*e, rand(3,14)*strength, Math.sin(a)*sp*e,
rand(0.25,0.6), rand(0.16,0.34),
[1.0, rand(0.5,0.85), rand(0.05,0.25)], -26, 0.6);
}
}
function dustBurst(x,y,z,n){
for (var i=0;i<n;i++){
var a = Math.random()*TAU, sp = rand(2,7);
smoke.emit(x,y,z, Math.cos(a)*sp, rand(1,4), Math.sin(a)*sp,
rand(0.5,1.1), rand(0.5,1.1), [0.45,0.39,0.31], -3, 1.6);
}
}
/* ================================================================
PROP / OBSTACLE SYSTEM
================================================================ */
var props = [];
var barrierMat = new THREE.MeshStandardMaterial({ color:0xb9bfc6, roughness:0.92, metalness:0.05 });
var barrierMat2 = new THREE.MeshStandardMaterial({ color:0xff7a20, roughness:0.8, metalness:0.1 });
var tireMat = new THREE.MeshStandardMaterial({ color:0x16181c, roughness:0.95, metalness:0.0 });
var crateMat = new THREE.MeshStandardMaterial({ color:0x8a6a42, roughness:0.95, metalness:0.0 });
function addBarrier(x,z,rot){
var g = new THREE.Group();
var b = new THREE.Mesh(new THREE.BoxGeometry(3.6, 1.15, 1.0), barrierMat);
b.position.y = 0.58; b.castShadow = true; b.receiveShadow = true;
var stripe = new THREE.Mesh(new THREE.BoxGeometry(3.62, 0.28, 1.02), barrierMat2);
stripe.position.y = 0.95;
g.add(b); g.add(stripe);
g.position.set(x, terrainH(x,z), z);
g.rotation.y = rot;
scene.add(g);
props.push({ type:'barrier', mesh:g, x:x, z:z, y:terrainH(x,z), r:1.75, h:1.15,
hw:1.8, hd:0.5, rot:rot, stat:true, cool:-99, vx:0, vz:0, vy:0, mass:99 });
}
function addTire(x,z){
var m = new THREE.Mesh(new THREE.CylinderGeometry(0.95,0.95,1.5,12), tireMat);
m.castShadow = true; m.receiveShadow = true;
var hub = new THREE.Mesh(new THREE.CylinderGeometry(0.42,0.42,1.54,8),
new THREE.MeshStandardMaterial({ color:0x9aa4ad, roughness:0.5, metalness:0.7 }));
m.add(hub);
var gy = terrainH(x,z) + 0.75;
m.position.set(x, gy, z);
scene.add(m);
props.push({ type:'tire', mesh:m, x:x, z:z, y:gy, r:1.0, h:1.5,
rot:Math.random()*TAU, rotVel:0, vx:0, vz:0, vy:0, mass:1.5, stat:false, cool:-99, tumble:0.4 });
}
function addCrate(x,z){
var s = rand(0.85,1.25);
var m = new THREE.Mesh(new THREE.BoxGeometry(1.5*s, 1.5*s, 1.5*s), crateMat);
m.castShadow = true; m.receiveShadow = true;
var gy = terrainH(x,z) + 0.75*s;
m.position.set(x, gy, z);
m.rotation.y = Math.random()*TAU;
scene.add(m);
props.push({ type:'crate', mesh:m, x:x, z:z, y:gy, r:0.95*s, h:1.5*s,
rot:m.rotation.y, rotVel:0, vx:0, vz:0, vy:0, mass:0.7, stat:false, cool:-99,
tumble:1, tx:rand(-1,1), tz:rand(-1,1) });
}
var spawnPoints = [];
(function(){
for (var i=0;i<5;i++){
var a = -Math.PI/2 + i*(TAU/5);
spawnPoints.push({ x: Math.cos(a)*32, z: Math.sin(a)*32 });
}
})();
(function placeProps(){
var placed = [];
function farEnough(x,z,gap){
for (var i=0;i<placed.length;i++){
var dx = placed[i].x - x, dz = placed[i].z - z;
if (dx*dx + dz*dz < gap*gap) return false;
}
for (var s=0;s<spawnPoints.length;s++){
var sx = spawnPoints[s].x - x, sz = spawnPoints[s].z - z;
if (sx*sx + sz*sz < 49) return false;
}
return true;
}
function pick(gap){
for (var tries=0; tries<120; tries++){
var a = Math.random()*TAU;
var r = Math.sqrt(Math.random())*41;
var x = Math.cos(a)*r, z = Math.sin(a)*r;
if (terrainH(x,z) > 0.35) continue;
if (farEnough(x,z,gap)) return { x:x, z:z };
}
return null;
}
var i, p;
for (i=0;i<13;i++){
p = pick(5.2); if (!p) continue;
addBarrier(p.x, p.z, Math.random()*TAU);
placed.push(p);
}
for (i=0;i<26;i++){
p = pick(3.6); if (!p) continue;
addTire(p.x, p.z);
placed.push(p);
}
for (i=0;i<22;i++){
p = pick(3.2); if (!p) continue;
addCrate(p.x, p.z);
placed.push(p);
}
})();
/* ================================================================
TRUCKS
================================================================ */
var trucks = [];
var player = null;
var pairCool = {};
function buildTruckMesh(hex){
var g = new THREE.Group();
var bodyMat = new THREE.MeshStandardMaterial({ color:hex, metalness:0.55, roughness:0.32 });
var trimMat = new THREE.MeshStandardMaterial({ color:0x14171c, metalness:0.4, roughness:0.78 });
var glassMat = new THREE.MeshStandardMaterial({ color:0x0a1016, metalness:0.9, roughness:0.14 });
var rimMat = new THREE.MeshStandardMaterial({ color:0x9aa4ad, metalness:0.85, roughness:0.32 });
var rubMat = new THREE.MeshStandardMaterial({ color:0x121317, roughness:0.96 });
var tireGeo = new THREE.CylinderGeometry(0.98,0.98,0.74,14);
tireGeo.rotateZ(Math.PI/2);
var rimGeo = new THREE.CylinderGeometry(0.5,0.5,0.78,9);
rimGeo.rotateZ(Math.PI/2);
var wheels = [];
var wpos = [[-1.38, 1.72, true],[1.38, 1.72, true],[-1.38,-1.68,false],[1.38,-1.68,false]];
for (var i=0;i<4;i++){
var pivot = new THREE.Object3D();
pivot.position.set(wpos[i][0], 0.98, wpos[i][1]);
var spinner = new THREE.Object3D();
var tire = new THREE.Mesh(tireGeo, rubMat); tire.castShadow = true;
var rim = new THREE.Mesh(rimGeo, rimMat);
spinner.add(tire); spinner.add(rim);
pivot.add(spinner);
g.add(pivot);
wheels.push({ pivot: pivot, spin: spinner, front: wpos[i][2] });
}
var chassis = new THREE.Mesh(new THREE.BoxGeometry(2.2, 0.5, 4.6), trimMat);
chassis.position.y = 1.45; chassis.castShadow = true; g.add(chassis);
var lower = new THREE.Mesh(new THREE.BoxGeometry(2.5, 0.85, 5.0), bodyMat);
lower.position.y = 1.95; lower.castShadow = true; lower.receiveShadow = true; g.add(lower);
var cab = new THREE.Mesh(new THREE.BoxGeometry(2.2, 0.92, 2.6), bodyMat);
cab.position.set(0, 2.72, -0.45); cab.castShadow = true; g.add(cab);
var hood = new THREE.Mesh(new THREE.BoxGeometry(2.26, 0.17, 2.35), bodyMat);
hood.position.set(0, 2.62, 1.32); hood.rotation.x = -0.33; hood.castShadow = true; g.add(hood);
var ws = new THREE.Mesh(new THREE.BoxGeometry(2.06, 0.09, 1.95), glassMat);
ws.position.set(0, 2.78, 0.78); ws.rotation.x = -0.62; g.add(ws);
var bar = new THREE.Mesh(new THREE.BoxGeometry(2.15, 0.15, 0.14),
new THREE.MeshBasicMaterial({ color:0xfff0c4 }));
bar.position.set(0, 1.80, 2.52); g.add(bar);
var tail = new THREE.Mesh(new THREE.BoxGeometry(2.25, 0.16, 0.1),
new THREE.MeshBasicMaterial({ color:0xff2a18 }));
tail.position.set(0, 2.24, -2.52); g.add(tail);
var pod = new THREE.Mesh(new THREE.BoxGeometry(1.15, 0.22, 0.44),
new THREE.MeshBasicMaterial({ color:0xfff6d0 }));
pod.position.set(0, 3.28, -0.42); g.add(pod);
var bar2 = new THREE.Mesh(new THREE.BoxGeometry(2.05, 0.16, 0.16), trimMat);
bar2.position.set(0, 3.22, -1.5); g.add(bar2);
return { group: g, wheels: wheels, bodyMat: bodyMat };
}
function createTruck(hex, isPlayer, idx){
var built = buildTruckMesh(hex);
scene.add(built.group);
var t = {
idx: idx, isPlayer: isPlayer,
obj: built.group, wheels: built.wheels, bodyMat: built.bodyMat,
x:0, z:0, y:0, vy:0, prevH:0, grounded:true,
yaw:0, yawVel:0, vx:0, vz:0, vF:0,
steer:0, throttle:0, handbrake:false,
health:100, wrecked:false, wreckTimer:0,
radius: TRUCK_R, speedMul:1,
target:null, range:0, retarget:0, wander: Math.random()*10,
stuck:0, reverseTimer:0, lastHitBy:null, wheelSpin:0,
spawnIdx: idx
};
trucks.push(t);
return t;
}
var playerRing = (function(){
var mat = new THREE.MeshBasicMaterial({ color:0xff9a3c, transparent:true, opacity:0.7,
side:THREE.DoubleSide, depthWrite:false });
var m = new THREE.Mesh(new THREE.RingGeometry(3.5, 4.15, 36), mat);
m.rotation.x = -Math.PI/2;
scene.add(m);
return m;
})();
/* ---- truck factory calls: exactly five ---- */
var TRUCK_COLORS = [0xeef3f7, 0xff3b2f, 0x2f6bff, 0x27d17a, 0xffb020];
var AI_PROFILES = [
{ mul: 1.00, aggro: 1.15, range: 0.0, pref:'player' },
{ mul: 0.93, aggro: 0.85, range: 9.5, pref:'player' },
{ mul: 1.06, aggro: 1.00, range: 0.0, pref:'rival' },
{ mul: 0.97, aggro: 0.92, range: 6.5, pref:'rival' }
];
function spawnTruckAt(t, sx, sz){
t.x = sx; t.z = sz;
t.yaw = Math.atan2(-sx, -sz);
t.vx = 0; t.vz = 0; t.vy = 0; t.yawVel = 0;
t.y = terrainH(sx,sz);
t.prevH = t.y;
t.health = 100;
t.wrecked = false;
t.wreckTimer = 0;
t.grounded = true;
t.stuck = 0; t.reverseTimer = 0;
t.bodyMat.color.setHex(t.bodyMat.userData.hex || t.bodyMat.color.getHex());
t.bodyMat.emissive.setHex(0x000000);
t.bodyMat.emissiveIntensity = 1;
applyTransform(t, 0);
}
/* ================================================================
GAME STATE
================================================================ */
var state = {
score: 0,
time: ROUND_TIME,
t: 0,
photo: false,
over: false,
demo: false,
paused: false
};
var photo = { yaw: 0, pitch: 0.4, dist: 26, drag:false, lx:0, ly:0 };
var camYaw = 0;
var shake = 0;
/* ================================================================
AUDIO (Web Audio API, no assets)
================================================================ */
var audio = null;
function ensureAudio(){
if (audio) return;
try {
var Ctx = window.AudioContext || window.webkitAudioContext;
if (!Ctx) return;
var ctx = new Ctx();
var osc = ctx.createOscillator();
osc.type = 'sawtooth';
osc.frequency.value = 60;
var osc2 = ctx.createOscillator();
osc2.type = 'square';
osc2.frequency.value = 30;
var filt = ctx.createBiquadFilter();
filt.type = 'lowpass';
filt.frequency.value = 480;
var gain = ctx.createGain();
gain.gain.value = 0.0;
osc.connect(filt); osc2.connect(filt); filt.connect(gain); gain.connect(ctx.destination);
osc.start(); osc2.start();
audio = { ctx: ctx, osc: osc, osc2: osc2, filt: filt, gain: gain, lastImpact: 0 };
} catch(e){ audio = null; }
}
function audioEngine(speed, throttle){
if (!audio) return;
try {
var f = 46 + speed*4.4 + (throttle>0?10:0);
audio.osc.frequency.setTargetAtTime(f, audio.ctx.currentTime, 0.05);
audio.osc2.frequency.setTargetAtTime(f*0.5, audio.ctx.currentTime, 0.05);
audio.filt.frequency.setTargetAtTime(320 + speed*46, audio.ctx.currentTime, 0.08);
audio.gain.gain.setTargetAtTime(0.020 + Math.min(speed,26)/26*0.030, audio.ctx.currentTime, 0.08);
} catch(e){}
}
function audioImpact(strength){
if (!audio) return;
var now = audio.ctx.currentTime;
if (now - audio.lastImpact < 0.05) return;
audio.lastImpact = now;
try {
var dur = 0.22;
var buf = audio.ctx.createBuffer(1, Math.floor(audio.ctx.sampleRate*dur), audio.ctx.sampleRate);
var d = buf.getChannelData(0);
for (var i=0;i<d.length;i++){
var env = 1 - i/d.length;
d[i] = (Math.random()*2-1) * env * env;
}
var src = audio.ctx.createBufferSource();
src.buffer = buf;
var f = audio.ctx.createBiquadFilter();
f.type = 'lowpass';
f.frequency.value = 420 + strength*90;
var g = audio.ctx.createGain();
g.gain.value = Math.min(0.5, 0.07 + strength*0.035);
src.connect(f); f.connect(g); g.connect(audio.ctx.destination);
src.start();
} catch(e){}
}
/* ================================================================
INPUT
================================================================ */
var keys = Object.create(null);
window.addEventListener('keydown', function(e){
keys[e.code] = true;
ensureAudio();
if (e.code === 'KeyP'){ e.preventDefault(); togglePhoto(); }
if (e.code === 'KeyR'){ e.preventDefault(); resetGame(DEMO); }
if (e.code === 'ArrowUp' || e.code === 'ArrowDown' || e.code === 'ArrowLeft' ||
e.code === 'ArrowRight' || e.code === 'Space') e.preventDefault();
});
window.addEventListener('keyup', function(e){ keys[e.code] = false; });
window.addEventListener('blur', function(){ for (var k in keys) keys[k] = false; });
renderer.domElement.addEventListener('pointerdown', function(e){
ensureAudio();
if (state.photo){ photo.drag = true; photo.lx = e.clientX; photo.ly = e.clientY;
renderer.domElement.setPointerCapture(e.pointerId); }
});
renderer.domElement.addEventListener('pointermove', function(e){
if (state.photo && photo.drag){
photo.yaw -= (e.clientX - photo.lx) * 0.006;
photo.pitch = clamp(photo.pitch + (e.clientY - photo.ly) * 0.004, -0.15, 1.25);
photo.lx = e.clientX; photo.ly = e.clientY;
}
});
window.addEventListener('pointerup', function(){ photo.drag = false; });
function gatherInput(){
var up = keys['ArrowUp'] || keys['KeyW'];
var dn = keys['ArrowDown'] || keys['KeyS'];
var lf = keys['ArrowLeft'] || keys['KeyA'];
var rt = keys['ArrowRight'] || keys['KeyD'];
player.throttle = (up?1:0) + (dn?-1:0);
player.steer = (lf?1:0) - (rt?1:0);
player.handbrake = !!keys['Space'];
}
/* ================================================================
PHOTO MODE
================================================================ */
function togglePhoto(){
if (state.over) return;
state.photo = !state.photo;
if (state.photo){
photo.yaw = camYaw;
photo.pitch = 0.36;
photo.dist = 28;
showCard(ARENA_NAME, 'PHOTO MODE', 'SCORE ' + fmtScore(state.score), 'P TO RESUME');
} else {
hideCard();
}
}
function updatePhotoCamera(){
var py = player.y;
var cx = player.x + Math.sin(photo.yaw) * Math.cos(photo.pitch) * photo.dist;
var cz = player.z + Math.cos(photo.yaw) * Math.cos(photo.pitch) * photo.dist;
var cy = py + 2.0 + Math.sin(photo.pitch) * photo.dist;
camera.position.set(cx, cy, cz);
camera.lookAt(player.x, py + 2.2, player.z);
}
/* ================================================================
SCORING / DAMAGE
================================================================ */
function addScore(n, msg){
state.score += n;
flashScore();
if (msg) toast(msg);
if (state.photo){
elCardMsg.textContent = 'SCORE ' + fmtScore(state.score);
}
}
function damage(t, amt, by){
if (t.wrecked || amt <= 0) return;
t.health -= amt;
if (by) t.lastHitBy = by;
if (t.health <= 0){
t.health = 0;
wreckTruck(t);
}
}
function wreckTruck(t){
t.wrecked = true;
t.wreckTimer = 4.0;
t.yawVel += rand(-3.2, 3.2);
t.bodyMat.color.setHex(0x2a2b2d);
t.bodyMat.emissive.setHex(0x3a1204);
t.bodyMat.emissiveIntensity = 1;
for (var i=0;i<12;i++) smokePuff(t.x + rand(-1.4,1.4), t.y + rand(1.4,3.0), t.z + rand(-1.4,1.4), 1.4, true);
sparkBurst(t.x, t.y + 1.6, t.z, 22, 1.2);
toast('WRECKED!');
audioImpact(9);
if (t.lastHitBy && t.lastHitBy.isPlayer){
addScore(500, 'RIVAL WRECKED +500');
}
}
/* ================================================================
PHYSICS : trucks
================================================================ */
var _up = new THREE.Vector3();
var _fwd = new THREE.Vector3();
var _right = new THREE.Vector3();
var _up2 = new THREE.Vector3();
var _mat4 = new THREE.Matrix4();
function applyTransform(t, dt){
var n = terrainNormal(t.x, t.z, _n);
if (t.grounded){
_up.copy(n);
} else {
_up.set(0,1,0).lerp(n, 0.22).normalize();
}
_fwd.set(Math.sin(t.yaw), 0, Math.cos(t.yaw));
_fwd.addScaledVector(_up, -_fwd.dot(_up));
if (_fwd.lengthSq() < 1e-6) _fwd.set(0,0,1);
_fwd.normalize();
_right.crossVectors(_up, _fwd).normalize();
_up2.crossVectors(_fwd, _right).normalize();
_mat4.makeBasis(_right, _up2, _fwd);
t.obj.quaternion.setFromRotationMatrix(_mat4);
t.obj.position.set(t.x, t.y, t.z);
t.wheelSpin += (t.vF / 0.98) * dt;
for (var i=0;i<t.wheels.length;i++){
var w = t.wheels[i];
w.spin.rotation.x = t.wheelSpin;
if (w.front) w.pivot.rotation.y = t.steer * 0.42;
}
}
function wallClamp(t, dt, wrecked){
var r = Math.hypot(t.x, t.z);
var lim = PLAY_R - t.radius - 0.5;
if (r > lim && r > 0.001){
var nx = t.x/r, nz = t.z/r;
t.x = nx*lim; t.z = nz*lim;
var vn = t.vx*nx + t.vz*nz;
if (vn > 0){
t.vx -= 1.35*vn*nx;
t.vz -= 1.35*vn*nz;
if (!wrecked && vn > 5.5){
damage(t, (vn-5.5)*2.4);
sparkBurst(t.x - nx*2, t.y + 1.4, t.z - nz*2, 8, 0.7);
audioImpact(vn*0.35);
}
}
}
}
function stepTruck(t, dt){
if (t.wrecked){
t.wreckTimer -= dt;
t.vx *= Math.exp(-1.7*dt);
t.vz *= Math.exp(-1.7*dt);
t.vF *= Math.exp(-1.7*dt);
t.yaw += t.yawVel*dt;
t.yawVel *= Math.exp(-2.0*dt);
t.x += t.vx*dt;
t.z += t.vz*dt;
if (Math.random() < dt*16){
smokePuff(t.x + rand(-1.2,1.2), t.y + 2.4, t.z + rand(-1.2,1.2), 1.0, true);
}
wallClamp(t, dt, true);
applyTransform(t, dt);
if (t.wreckTimer <= 0) respawnTruck(t);
return;
}
var fx = Math.sin(t.yaw), fz = Math.cos(t.yaw);
var rx = Math.cos(t.yaw), rz = -Math.sin(t.yaw);
var vF = t.vx*fx + t.vz*fz;
var vR = t.vx*rx + t.vz*rz;
var mul = t.isPlayer ? 1 : t.speedMul;
var acc = ENGINE_ACC * mul;
var th = t.throttle;
if (th > 0) vF += acc * th * dt;
else if (th < 0){
if (vF > 0.8) vF -= acc * 1.7 * dt;
else vF -= acc * 0.62 * dt;
}
vF -= vF * DRAG * dt * (t.grounded ? 1 : 0.22);
if (t.handbrake && t.grounded) vF -= vF * 2.6 * dt;
vF = clamp(vF, -MAX_REV, MAX_FWD);
var grip = t.grounded ? (t.handbrake ? 2.0 : GRIP) : 0.7;
vR *= Math.exp(-grip * dt);
var sf = Math.min(1, Math.abs(vF)/7);
var airF = t.grounded ? 1 : 0.35;
var dir = vF < -0.2 ? -1 : 1;
t.yaw += t.steer * TURN * sf * dir * airF * dt;
t.yaw += t.yawVel * dt;
t.yawVel *= Math.exp(-2.5*dt);
t.vx = fx*vF + rx*vR;
t.vz = fz*vF + rz*vR;
t.x += t.vx*dt;
t.z += t.vz*dt;
t.vF = vF;
/* vertical : follow the heightfield, launch off crests */
var h = terrainH(t.x, t.z);
if (t.y <= h + 0.05){
var climb = (h - t.prevH) / Math.max(dt, 1e-4);
if (!t.grounded && dt > 0) dustBurst(t.x, h + 0.3, t.z, 6);
t.y = h;
t.vy = clamp(climb * 0.8, 0, 15);
t.grounded = true;
} else {
t.vy -= GRAV * dt;
t.y += t.vy * dt;
if (t.y <= h){
if (t.vy < -6) dustBurst(t.x, h + 0.4, t.z, 8);
t.y = h; t.vy = 0; t.grounded = true;
} else {
t.grounded = false;
}
}
t.prevH = h;
wallClamp(t, dt, false);
applyTransform(t, dt);
}
function respawnTruck(t){
var sp = spawnPoints[(t.spawnIdx + ((Math.random()*5)|0)) % 5];
t.bodyMat.color.setHex(TRUCK_COLORS[t.idx]);
t.bodyMat.emissive.setHex(0x000000);
t.x = sp.x + rand(-3,3);
t.z = sp.z + rand(-3,3);
t.yaw = Math.atan2(-t.x, -t.z);
t.vx = 0; t.vz = 0; t.vy = 0; t.yawVel = 0; t.vF = 0;
t.y = terrainH(t.x,t.z);
t.prevH = t.y;
t.health = 100;
t.wrecked = false;
t.grounded = true;
t.stuck = 0;
sparkBurst(t.x, t.y + 1.6, t.z, 14, 0.8);
}
/* ================================================================
COLLISIONS
================================================================ */
function truckTruckCollisions(){
for (var i=0;i<trucks.length;i++){
for (var j=i+1;j<trucks.length;j++){
var a = trucks[i], b = trucks[j];
if (a.wrecked && b.wrecked) continue;
var dx = b.x - a.x, dz = b.z - a.z;
var d2 = dx*dx + dz*dz;
var md = a.radius + b.radius;
if (d2 >= md*md || d2 < 1e-6) continue;
var d = Math.sqrt(d2);
var nx = dx/d, nz = dz/d;
var overlap = md - d;
var aSolid = a.wrecked ? 0 : 1;
var bSolid = b.wrecked ? 0 : 1;
var tot = aSolid + bSolid;
if (tot === 0) continue;
a.x -= nx * overlap * (aSolid/tot);
a.z -= nz * overlap * (aSolid/tot);
b.x += nx * overlap * (bSolid/tot);
b.z += nz * overlap * (bSolid/tot);
var rvx = b.vx - a.vx, rvz = b.vz - a.vz;
var vn = rvx*nx + rvz*nz;
if (vn < 0){
var e = 0.4;
var jm = -(1+e)*vn / tot;
if (aSolid) { a.vx -= jm*nx; a.vz -= jm*nz; }
if (bSolid) { b.vx += jm*nx; b.vz += jm*nz; }
var tx = -nz, tz = nx;
var vt = rvx*tx + rvz*tz;
a.yawVel -= vt * 0.055;
b.yawVel += vt * 0.055;
handleRam(a, b, -vn, nx, nz);
}
}
}
}
function handleRam(a, b, impact, nx, nz){
if (impact < 3.5) return;
var key = Math.min(a.idx,b.idx)*8 + Math.max(a.idx,b.idx);
if (state.t - (pairCool[key] || -99) < 0.35) return;
pairCool[key] = state.t;
var dmg = Math.min(34, (impact - 3.5) * 2.6);
damage(a, dmg * 0.6, b);
damage(b, dmg * 1.0, a);
var px = (a.x + b.x)*0.5, pz = (a.z + b.z)*0.5;
var py = Math.min(a.y, b.y) + 1.7;
sparkBurst(px, py, pz, Math.min(26, 8 + impact*1.4), Math.min(1.6, impact*0.09));
smokePuff(px, py, pz, 0.7, true);
audioImpact(impact);
shake = Math.min(1.1, shake + impact * 0.022);
var p = (a.isPlayer ? a : (b.isPlayer ? b : null));
if (p){
var gain = Math.round(impact * 9) + 30;
addScore(gain, 'RAM +' + gain);
}
}
function truckPropCollisions(dt){
for (var ti=0; ti<trucks.length; ti++){
var t = trucks[ti];
if (t.wrecked) continue;
for (var pi=0; pi<props.length; pi++){
var p = props[pi];
var dx = p.x - t.x, dz = p.z - t.z;
var rr = t.radius + p.r;
var d2 = dx*dx + dz*dz;
if (d2 > rr*rr) continue;
var d = Math.sqrt(d2);
if (d < 1e-4) d = 1e-4;
var nx = dx/d, nz = dz/d;
var ov = rr - d;
if (p.stat){
/* oriented box vs circle */
var cosr = Math.cos(p.rot), sinr = Math.sin(p.rot);
var lx = cosr*dx - sinr*dz;
var lz = sinr*dx + cosr*dz;
var clx = clamp(lx, -p.hw, p.hw);
var clz = clamp(lz, -p.hd, p.hd);
var ddx = lx - clx, ddz = lz - clz;
var dd = Math.hypot(ddx, ddz);
if (dd > t.radius) continue;
var lnx, lnz;
if (dd > 1e-4){ lnx = ddx/dd; lnz = ddz/dd; }
else {
if (p.hw - Math.abs(lx) < p.hd - Math.abs(lz)){ lnx = lx > 0 ? 1 : -1; lnz = 0; }
else { lnx = 0; lnz = lz > 0 ? 1 : -1; }
}
var wnx = cosr*lnx + sinr*lnz;
var wnz = -sinr*lnx + cosr*lnz;
var pen = t.radius - dd;
t.x -= wnx * pen;
t.z -= wnz * pen;
var vn = t.vx*wnx + t.vz*wnz;
if (vn > 0){
t.vx -= 1.45*vn*wnx;
t.vz -= 1.45*vn*wnz;
if (vn > 5.5 && state.t - p.cool > 0.4){
p.cool = state.t;
damage(t, (vn - 5.5)*2.2);
}
if (vn > 3.5){
sparkBurst(t.x + wnx*t.radius, t.y + 1.2, t.z + wnz*t.radius, 10, Math.min(1.4, vn*0.09));
audioImpact(vn*0.4);
shake = Math.min(1.0, shake + vn*0.012);
}
}
} else {
/* circle-circle with a light prop */
t.x -= nx * ov * 0.72;
t.z -= nz * ov * 0.72;
p.x += nx * ov * 0.28;
p.z += nz * ov * 0.28;
var vn2 = t.vx*nx + t.vz*nz;
if (vn2 > 0){
p.vx += nx * (vn2*0.95 + 1.6) / p.mass;
p.vz += nz * (vn2*0.95 + 1.6) / p.mass;
p.vy += (1.5 + Math.random()*2.5) / p.mass;
p.rotVel += rand(-7,7);
t.vx *= 0.955;
t.vz *= 0.955;
if (vn2 > 6 && state.t - p.cool > 0.5){
p.cool = state.t;
damage(t, p.type === 'tire' ? 1.8 : 0.6);
if (t.isPlayer){
addScore(p.type === 'tire' ? 20 : 12, (p.type === 'tire' ? 'TIRE ' : 'CRATE ') + '+' + (p.type === 'tire' ? 20 : 12));
}
}
if (vn2 > 5){
sparkBurst(p.x, p.y + 0.6, p.z, 6, 0.6);
audioImpact(vn2*0.3);
}
}
}
}
}
}
function propPropCollisions(dt){
for (var i=0;i<props.length;i++){
var a = props[i];
if (a.stat) continue;
for (var j=i+1;j<props.length;j++){
var b = props[j];
if (b.stat) continue;
var dx = b.x - a.x, dz = b.z - a.z;
var rr = a.r + b.r;
var d2 = dx*dx + dz*dz;
if (d2 > rr*rr || d2 < 1e-6) continue;
var d = Math.sqrt(d2);
var nx = dx/d, nz = dz/d;
var ov = (rr - d) * 0.5;
a.x -= nx*ov; a.z -= nz*ov;
b.x += nx*ov; b.z += nz*ov;
var rvx = b.vx - a.vx, rvz = b.vz - a.vz;
var vn = rvx*nx + rvz*nz;
if (vn < 0){
var jm = -(1+0.25)*vn/(1/a.mass + 1/b.mass);
a.vx -= jm*nx/a.mass; a.vz -= jm*nz/a.mass;
b.vx += jm*nx/b.mass; b.vz += jm*nz/b.mass;
a.rotVel += rand(-3,3); b.rotVel += rand(-3,3);
}
}
}
}
/* ================================================================
PROPS UPDATE
================================================================ */
function propsUpdate(dt){
for (var i=0;i<props.length;i++){
var p = props[i];
if (p.stat) continue;
p.vy -= 30*dt;
p.y += p.vy*dt;
var gh = terrainH(p.x, p.z) + p.h*0.5;
if (p.y <= gh){
p.y = gh;
if (p.vy < -2.5) p.vy *= -0.26; else p.vy = 0;
}
p.x += p.vx*dt;
p.z += p.vz*dt;
var fr = Math.exp(-2.0*dt);
p.vx *= fr; p.vz *= fr;
if (p.vy === 0 && Math.hypot(p.vx, p.vz) < 0.25){ p.vx = 0; p.vz = 0; }
p.rot += p.rotVel*dt;
p.rotVel *= Math.exp(-1.6*dt);
var r = Math.hypot(p.x, p.z);
var lim = PLAY_R - p.r - 0.4;
if (r > lim && r > 0.001){
var nx = p.x/r, nz = p.z/r;
p.x = nx*lim; p.z = nz*lim;
var vn = p.vx*nx + p.vz*nz;
if (vn > 0){ p.vx -= 1.5*vn*nx; p.vz -= 1.5*vn*nz; }
}
p.mesh.position.set(p.x, p.y, p.z);
p.mesh.rotation.y = p.rot;
if (p.tumble){
p.mesh.rotation.x += p.tx * dt * 3.0 * Math.min(1, Math.hypot(p.vx,p.vz)*0.4);
p.mesh.rotation.z += p.tz * dt * 3.0 * Math.min(1, Math.hypot(p.vx,p.vz)*0.4);
}
}
}
/* ================================================================
AI
================================================================ */
function pickTarget(ai){
var prof = AI_PROFILES[ai.idx-1];
var pool = [];
if (prof.pref === 'player' || Math.random() < 0.4){
if (!player.wrecked) pool.push(player);
}
for (var i=1;i<trucks.length;i++){
if (trucks[i] !== ai && !trucks[i].wrecked) pool.push(trucks[i]);
}
if (!pool.length) pool.push(player);
ai.target = pool[(Math.random()*pool.length)|0];
ai.range = prof.range > 0 ? prof.range * rand(0.7, 1.3) : 0;
ai.stuck = 0;
}
function updateAI(dt){
for (var i=1;i<trucks.length;i++){
var ai = trucks[i];
var prof = AI_PROFILES[i-1];
if (ai.wrecked){ ai.throttle = 0; ai.steer = 0; continue; }
ai.retarget -= dt;
if (ai.retarget <= 0 || !ai.target || ai.target.wrecked){
pickTarget(ai);
ai.retarget = 4.5 + Math.random()*4.5;
}
var dx = 0, dz = 0;
/* --- chase / orbit the chosen target --- */
var tgt = ai.target || player;
var ox = tgt.x - ai.x, oz = tgt.z - ai.z;
var dist = Math.hypot(ox, oz) || 1;
var oxn = ox/dist, ozn = oz/dist;
if (ai.range > 0 && dist < ai.range){
dx += -ozn * 1.15;
dz += oxn * 1.15;
dx -= oxn * 0.25;
dz -= ozn * 0.25;
} else {
dx += oxn; dz += ozn;
}
/* --- separation : the anti-clump term --- */
for (var j=0;j<trucks.length;j++){
if (j === i) continue;
var o = trucks[j];
var sx = ai.x - o.x, sz = ai.z - o.z;
var sd = Math.hypot(sx, sz);
if (sd < 14 && sd > 0.001){
var w = ((14 - sd)/14);
w = w * w;
dx += (sx/sd) * w * 4.2;
dz += (sz/sd) * w * 4.2;
}
}
/* --- wall avoidance --- */
var rr = Math.hypot(ai.x, ai.z);
if (rr > PLAY_R - 16){
var wf = (rr - (PLAY_R - 16)) / 16;
dx += (-ai.x/rr) * wf * 5.0;
dz += (-ai.z/rr) * wf * 5.0;
}
/* --- barrier avoidance (short range) --- */
for (var pi=0; pi<props.length; pi++){
var p = props[pi];
if (!p.stat) continue;
var bx = ai.x - p.x, bz = ai.z - p.z;
var bd = Math.hypot(bx, bz);
if (bd < 9 && bd > 0.001){
var bw = (9 - bd)/9;
dx += (bx/bd) * bw * 3.0;
dz += (bz/bd) * bw * 3.0;
}
}
/* --- wander so they never lock into a perfect line --- */
ai.wander += dt;
dx += Math.sin(ai.wander*0.75 + i*2.1) * 0.42;
dz += Math.cos(ai.wander*0.61 + i*1.3) * 0.42;
var len = Math.hypot(dx, dz);
if (len < 0.001){ dx = 0; dz = 1; len = 1; }
var wantYaw = Math.atan2(dx, dz);
var diff = wantYaw - ai.yaw;
while (diff > Math.PI) diff -= TAU;
while (diff < -Math.PI) diff += TAU;
ai.steer = clamp(diff * 1.9, -1, 1);
/* --- throttle --- */
ai.throttle = 1;
if (ai.range > 0 && dist < ai.range*0.75 && Math.abs(diff) > 1.1) ai.throttle = 0.35;
/* --- unstick --- */
if (Math.abs(ai.vF) < 2.4 && ai.throttle > 0 && ai.grounded){
ai.stuck += dt;
} else {
ai.stuck = Math.max(0, ai.stuck - dt*2.0);
}
if (ai.stuck > 1.5 && ai.reverseTimer <= 0){
ai.reverseTimer = 1.1;
ai.stuck = 0;
}
if (ai.reverseTimer > 0){
ai.reverseTimer -= dt;
ai.throttle = -1;
}
ai.speedMul = prof.mul;
}
}
/* ================================================================
CAMERA
================================================================ */
var camTarget = new THREE.Vector3();
function cameraUpdate(dt){
var speed = Math.abs(player.vF);
var back = 12.5 + Math.min(speed*0.22, 4.2);
var high = 6.4 + Math.min(speed*0.07, 2.0);
var fx = Math.sin(player.yaw), fz = Math.cos(player.yaw);
if (speed > 1.2){ camYaw = player.yaw; }
else { camYaw += (player.yaw - camYaw) * 0.0; }
var wx = Math.sin(camYaw), wz = Math.cos(camYaw);
var dx = player.x - wx*back;
var dz = player.z - wz*back;
var dy = player.y + high;
var r = Math.hypot(dx, dz);
var maxR = PLAY_R + 8;
if (r > maxR){ dx = dx/r*maxR; dz = dz/r*maxR; }
var k = 1 - Math.exp(-5.0*dt);
camera.position.x += (dx - camera.position.x) * k;
camera.position.y += (dy - camera.position.y) * k;
camera.position.z += (dz - camera.position.z) * k;
if (shake > 0.001){
camera.position.x += rand(-1,1) * shake * 0.35;
camera.position.y += rand(-1,1) * shake * 0.28;
camera.position.z += rand(-1,1) * shake * 0.35;
shake *= Math.exp(-4.0*dt);
}
camTarget.set(
player.x + fx*3.0,
player.y + 2.4,
player.z + fz*3.0
);
camera.lookAt(camTarget);
camYaw = player.yaw;
}
/* ================================================================
HUD
================================================================ */
var lastScoreTxt = '', lastClockTxt = '';
function updateHUD(){
var s = fmtScore(state.score);
if (s !== lastScoreTxt){ elScoreVal.textContent = s; lastScoreTxt = s; if (state.photo) elCardMsg.textContent = 'SCORE ' + s; }
var c = state.time.toFixed(1);
if (c !== lastClockTxt){
elClockVal.textContent = c;
lastClockTxt = c;
if (state.time <= 10) elClockBox.classList.add('low');
else elClockBox.classList.remove('low');
}
}
/* ================================================================
ROUND CONTROL
================================================================ */
function endRound(){
state.over = true;
showCard(ARENA_NAME, 'TIME UP', 'SCORE ' + fmtScore(state.score), 'R TO RUN IT AGAIN');
}
function resetGame(demo){
DEMO = !!demo;
state.demo = DEMO;
state.score = DEMO ? DEMO_SCORE : 0;
state.time = DEMO ? DEMO_TIME : ROUND_TIME;
state.t = 0;
state.over = false;
state.photo = false;
hideCard();
for (var i=0;i<props.length;i++){
var p = props[i];
if (p.stat) continue;
p.vx = 0; p.vz = 0; p.vy = 0; p.rotVel = 0;
}
for (var k=0;k<trucks.length;k++){
var t = trucks[k];
var sp = spawnPoints[t.spawnIdx];
t.bodyMat.color.setHex(TRUCK_COLORS[t.idx]);
t.bodyMat.emissive.setHex(0x000000);
t.x = sp.x; t.z = sp.z;
t.yaw = Math.atan2(-sp.x, -sp.z);
t.vx = 0; t.vz = 0; t.vy = 0; t.vF = 0; t.yawVel = 0; t.wheelSpin = 0;
t.y = terrainH(t.x, t.z);
t.prevH = t.y;
t.health = 100; t.wrecked = false; t.wreckTimer = 0;
t.grounded = true; t.stuck = 0; t.reverseTimer = 0;
t.lastHitBy = null;
t.target = null; t.retarget = 0;
t.steer = 0; t.throttle = 0; t.handbrake = false;
applyTransform(t, 0);
}
for (var key in pairCool) delete pairCool[key];
camYaw = player.yaw;
camera.position.set(player.x - Math.sin(camYaw)*13, player.y + 7, player.z - Math.cos(camYaw)*13);
camera.lookAt(player.x, player.y + 2.5, player.z);
lastScoreTxt = ''; lastClockTxt = '';
updateHUD();
}
/* ================================================================
BUILD THE WORLD
================================================================ */
for (var ci=0; ci<5; ci++){
var t = createTruck(TRUCK_COLORS[ci], ci === 0, ci);
if (ci === 0) player = t;
else {
var prof = AI_PROFILES[ci-1];
t.speedMul = prof.mul;
}
}
resetGame(DEMO);
/* ================================================================
MAIN LOOP
================================================================ */
var lastTime = performance.now();
function frame(now){
requestAnimationFrame(frame);
var dt = (now - lastTime) / 1000;
lastTime = now;
if (!isFinite(dt) || dt < 0) dt = 0;
dt = Math.min(dt, 1/30);
var frozen = state.photo || state.over;
if (!frozen){
state.t += dt;
state.time -= dt;
if (state.time <= 0){
state.time = 0;
endRound();
}
gatherInput();
updateAI(dt);
for (var i=0;i<trucks.length;i++) stepTruck(trucks[i], dt);
truckTruckCollisions();
truckPropCollisions(dt);
propPropCollisions(dt);
propsUpdate(dt);
smoke.update(dt);
sparks.update(dt);
updateCrowd(dt);
cameraUpdate(dt);
/* player ground marker */
playerRing.position.set(player.x, terrainH(player.x, player.z) + 0.08, player.z);
audioEngine(Math.abs(player.vF), player.throttle);
}
if (state.photo) updatePhotoCamera();
renderer.render(scene, camera);
updateHUD();
}
requestAnimationFrame(frame);
/* ================================================================
RESIZE / HASH
================================================================ */
window.addEventListener('resize', function(){
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
updateFocal();
});
window.addEventListener('hashchange', function(){
var d = (location.hash || '').toLowerCase().endsWith('demo');
resetGame(d);
});
})();
</script>
</body>
</html>
A quick look at how the arena, controls, and modes come together.
#demo to the URL to start a 20-second round with 5000 points already on the board. Press P anytime to freeze the action and enter photo mode—drag to orbit the camera around your truck. Press P again to resume.Optimization Tip: You can adjust the round length by editing ROUND_TIME (currently 60) near the top of the script. AI difficulty profiles live in AI_PROFILES, where each rival's mul, aggro, and range values control speed, aggression, and preferred distance.