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.SILICON DOME: CYBERTRUCK MONSTER RALLY is a neon-lit, top-down stadium demolition derby where five armored Cybertrucks battle through hills, wreckage, barriers, and a roaring crowd.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1">
<meta name="color-scheme" content="dark">
<title>Silicon Dome — Cybertruck Monster Rally</title>
<style>
* { box-sizing: border-box; }
html, body {
width: 100%;
height: 100%;
margin: 0;
overflow: hidden;
background: #05070b;
}
body {
color: #eef7ff;
font-family: Arial, Helvetica, sans-serif;
user-select: none;
-webkit-user-select: none;
}
[hidden] { display: none !important; }
#game {
position: fixed;
inset: 0;
display: block;
width: 100%;
height: 100%;
outline: none;
touch-action: none;
}
#hud {
position: fixed;
z-index: 10;
top: 16px;
left: 50%;
width: min(1180px, calc(100% - 28px));
transform: translateX(-50%);
pointer-events: none;
}
.hud-shell {
min-height: 72px;
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
gap: 24px;
padding: 12px 20px;
border: 1px solid rgba(135, 226, 255, .28);
border-top-color: rgba(255, 255, 255, .14);
background:
linear-gradient(100deg, rgba(13, 22, 30, .96), rgba(8, 14, 20, .91)),
repeating-linear-gradient(135deg, transparent 0 14px, rgba(255,255,255,.025) 14px 16px);
box-shadow: 0 16px 50px rgba(0, 0, 0, .42), inset 0 1px rgba(255,255,255,.05);
clip-path: polygon(0 0, calc(100% - 17px) 0, 100% 17px, 100% 100%, 17px 100%, 0 calc(100% - 17px));
backdrop-filter: blur(10px);
}
.brand { min-width: 0; }
.kicker {
margin-bottom: 3px;
color: #6eeaff;
font-size: 10px;
font-weight: 900;
letter-spacing: .24em;
text-transform: uppercase;
}
.arena-name {
overflow: hidden;
color: #f5fbff;
font-family: Impact, "Arial Black", Arial, sans-serif;
font-size: clamp(21px, 2.2vw, 31px);
line-height: 1;
letter-spacing: .055em;
text-overflow: ellipsis;
white-space: nowrap;
text-shadow: 0 0 18px rgba(78, 220, 255, .18);
}
.metrics {
display: flex;
align-items: stretch;
gap: 18px;
}
.metric {
min-width: 105px;
text-align: center;
}
.metric + .metric {
padding-left: 18px;
border-left: 1px solid rgba(255,255,255,.13);
}
.metric span {
display: block;
margin-bottom: 3px;
color: #8da3b2;
font-size: 9px;
font-weight: 900;
letter-spacing: .2em;
}
.metric strong {
display: block;
color: #fff;
font-family: Impact, "Arial Black", Arial, sans-serif;
font-size: 25px;
font-variant-numeric: tabular-nums;
letter-spacing: .045em;
line-height: 1;
}
#clock.warning {
color: #ff6565;
text-shadow: 0 0 14px rgba(255, 70, 70, .48);
}
.controls {
justify-self: end;
color: #a8bbc8;
font-size: 10px;
font-weight: 800;
line-height: 1.65;
letter-spacing: .11em;
text-align: right;
}
.controls b { color: #6eeaff; }
.photo-overlay {
position: fixed;
z-index: 30;
inset: 0;
display: grid;
place-items: center;
padding: 24px;
background: rgba(2, 6, 10, .53);
backdrop-filter: blur(4px);
}
.photo-card {
width: min(620px, 92vw);
padding: 42px 48px 36px;
border: 1px solid rgba(110, 234, 255, .55);
background:
linear-gradient(145deg, rgba(13, 25, 34, .97), rgba(5, 10, 15, .97)),
repeating-linear-gradient(120deg, transparent 0 18px, rgba(255,255,255,.03) 18px 20px);
box-shadow: 0 30px 100px #000, inset 0 0 50px rgba(43, 212, 255, .07);
clip-path: polygon(0 0, calc(100% - 28px) 0, 100% 28px, 100% 100%, 28px 100%, 0 calc(100% - 28px));
text-align: center;
}
.photo-label {
color: #6eeaff;
font-size: 11px;
font-weight: 900;
letter-spacing: .35em;
}
.photo-card h1 {
margin: 13px 0 26px;
font-family: Impact, "Arial Black", Arial, sans-serif;
font-size: clamp(43px, 8vw, 76px);
letter-spacing: .035em;
line-height: .95;
}
.photo-score {
display: inline-block;
min-width: 220px;
padding: 14px 28px;
border-top: 1px solid rgba(255,255,255,.12);
border-bottom: 1px solid rgba(255,255,255,.12);
}
.photo-score span {
display: block;
color: #91a7b5;
font-size: 10px;
font-weight: 900;
letter-spacing: .25em;
}
.photo-score strong {
display: block;
margin-top: 4px;
color: #fff;
font-family: Impact, "Arial Black", Arial, sans-serif;
font-size: 38px;
font-variant-numeric: tabular-nums;
}
.photo-hint {
margin: 25px 0 0;
color: #9fb1bd;
font-size: 10px;
font-weight: 800;
letter-spacing: .19em;
}
.game-over {
position: fixed;
z-index: 20;
inset: 0;
display: grid;
place-items: center;
background: rgba(3, 7, 11, .68);
backdrop-filter: blur(5px);
}
.finish-card {
min-width: min(480px, 90vw);
padding: 34px 42px;
border: 1px solid rgba(255, 185, 74, .45);
background: rgba(9, 15, 21, .95);
box-shadow: 0 25px 80px #000;
text-align: center;
}
.finish-card h2 {
margin: 0 0 8px;
font-family: Impact, "Arial Black", Arial, sans-serif;
font-size: 46px;
letter-spacing: .04em;
}
.finish-card p { color: #a8bbc8; }
#finalScore {
color: #6eeaff;
font-size: 28px;
font-weight: 900;
}
@media (max-width: 760px) {
#hud { top: 9px; width: calc(100% - 18px); }
.hud-shell { grid-template-columns: 1fr auto; gap: 10px; padding: 10px 12px; }
.metrics { justify-self: end; gap: 10px; }
.metric { min-width: 74px; }
.metric + .metric { padding-left: 10px; }
.metric strong { font-size: 21px; }
.controls { display: none; }
.photo-card { padding: 32px 24px 28px; }
}
</style>
</head>
<body>
<canvas id="game" tabindex="0" aria-label="Silicon Dome Cybertruck monster truck rally"></canvas>
<div id="hud">
<div class="hud-shell">
<div class="brand">
<div class="kicker">Cybertruck Monster Rally</div>
<div class="arena-name">SILICON DOME</div>
</div>
<div class="metrics">
<div class="metric">
<span>SCORE</span>
<strong id="score">0</strong>
</div>
<div class="metric">
<span>CLOCK</span>
<strong id="clock">01:30</strong>
</div>
</div>
<div class="controls">
<b>WASD / ARROWS</b> DRIVE<br>
SPACE BRAKE · <b>P</b> PHOTO
</div>
</div>
</div>
<div id="photoOverlay" class="photo-overlay" role="dialog" aria-modal="true" hidden>
<div class="photo-card">
<div class="photo-label">PHOTO MODE</div>
<h1>SILICON DOME</h1>
<div class="photo-score">
<span>SCORE</span>
<strong id="photoScore">0</strong>
</div>
<p class="photo-hint">PRESS P TO RETURN TO THE RALLY</p>
</div>
</div>
<div id="gameOver" class="game-over" hidden>
<div class="finish-card">
<h2>TIME</h2>
<p>FINAL SCORE</p>
<div id="finalScore">0</div>
<p>PRESS ENTER TO RACE AGAIN</p>
</div>
</div>
<script>
(() => {
"use strict";
const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d", { alpha: false });
const scoreEl = document.getElementById("score");
const clockEl = document.getElementById("clock");
const photoOverlay = document.getElementById("photoOverlay");
const photoScoreEl = document.getElementById("photoScore");
const gameOver = document.getElementById("gameOver");
const finalScore = document.getElementById("finalScore");
const WORLD_W = 1400;
const WORLD_H = 900;
const CX = 700;
const CY = 450;
const FLOOR_RX = 488;
const FLOOR_RY = 234;
const TAU = Math.PI * 2;
const FIXED_DT = 1 / 60;
const STATIC_SCALE = 1.5;
let demoMode = window.location.hash === "#demo";
const state = {
score: demoMode ? 5000 : 0,
timeLeft: demoMode ? 20 : 90,
elapsed: 0,
photo: false,
over: false,
shake: 0,
shakeX: 0,
shakeY: 0
};
const keys = new Set();
const ramCooldowns = new Map();
const particles = [];
const popups = [];
const driveCodes = new Set([
"ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight",
"KeyW", "KeyA", "KeyS", "KeyD", "Space"
]);
let trucks = [];
let accumulator = 0;
let lastFrame = performance.now();
let cssWidth = 1;
let cssHeight = 1;
let dpr = 1;
let viewScale = 1;
let viewX = 0;
let viewY = 0;
let screenVignette = null;
const obstacles = [
{ id: "hill-a", type: "hill", x: 395, y: 365, rx: 55, ry: 34 },
{ id: "hill-b", type: "hill", x: 1010, y: 510, rx: 59, ry: 37 },
{ id: "hill-c", type: "hill", x: 680, y: 610, rx: 50, ry: 30 },
{ id: "hill-d", type: "hill", x: 790, y: 300, rx: 45, ry: 28 },
{ id: "tire-a", type: "tires", x: 590, y: 300, r: 18 },
{ id: "tire-b", type: "tires", x: 850, y: 590, r: 18 },
{ id: "tire-c", type: "tires", x: 325, y: 490, r: 18 },
{ id: "tire-d", type: "tires", x: 1035, y: 405, r: 18 },
{ id: "crate-a", type: "crate", x: 575, y: 550, w: 46, h: 32, angle: .18 },
{ id: "crate-b", type: "crate", x: 885, y: 405, w: 46, h: 32, angle: -.34 },
{ id: "crate-c", type: "crate", x: 350, y: 420, w: 46, h: 32, angle: .52 },
{ id: "crate-d", type: "crate", x: 1010, y: 610, w: 46, h: 32, angle: -.12 },
{ id: "barrier-a", type: "barrier", x: 620, y: 275, w: 70, h: 20, angle: .12 },
{ id: "barrier-b", type: "barrier", x: 800, y: 625, w: 70, h: 20, angle: -.38 },
{ id: "barrier-c", type: "barrier", x: 280, y: 560, w: 70, h: 20, angle: .72 },
{ id: "ramp-a", type: "ramp", x: 920, y: 285, w: 62, h: 38, angle: .34 },
{ id: "ramp-b", type: "ramp", x: 470, y: 600, w: 62, h: 38, angle: -.42 }
];
const ROAMER_ROUTE = [
{ x: 325, y: 350 },
{ x: 390, y: 600 },
{ x: 620, y: 640 },
{ x: 555, y: 330 }
];
function mulberry32(seed) {
return function () {
let t = seed += 0x6D2B79F5;
t = Math.imul(t ^ t >>> 15, t | 1);
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
return ((t ^ t >>> 14) >>> 0) / 4294967296;
};
}
function clamp(value, min, max) {
return Math.max(min, Math.min(max, value));
}
function angleDelta(a, b) {
return Math.atan2(Math.sin(a - b), Math.cos(a - b));
}
function normalizeAngle(a) {
return Math.atan2(Math.sin(a), Math.cos(a));
}
function roundedRect(g, x, y, w, h, r) {
r = Math.min(r, w / 2, h / 2);
g.beginPath();
g.moveTo(x + r, y);
g.lineTo(x + w - r, y);
g.quadraticCurveTo(x + w, y, x + w, y + r);
g.lineTo(x + w, y + h - r);
g.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
g.lineTo(x + r, y + h);
g.quadraticCurveTo(x, y + h, x, y + h - r);
g.lineTo(x, y + r);
g.quadraticCurveTo(x, y, x + r, y);
g.closePath();
}
function buildStadiumLayer() {
const layer = document.createElement("canvas");
layer.width = Math.round(WORLD_W * STATIC_SCALE);
layer.height = Math.round(WORLD_H * STATIC_SCALE);
const g = layer.getContext("2d");
g.scale(STATIC_SCALE, STATIC_SCALE);
const background = g.createRadialGradient(CX, CY, 50, CX, CY, 720);
background.addColorStop(0, "#1c2a33");
background.addColorStop(.52, "#111922");
background.addColorStop(1, "#05080d");
g.fillStyle = background;
g.fillRect(0, 0, WORLD_W, WORLD_H);
g.save();
g.fillStyle = "rgba(0,0,0,.55)";
g.beginPath();
g.ellipse(CX + 7, CY + 13, 631, 350, 0, 0, TAU);
g.fill();
g.restore();
const bowl = g.createLinearGradient(0, 95, 0, 805);
bowl.addColorStop(0, "#33414c");
bowl.addColorStop(.45, "#18232d");
bowl.addColorStop(1, "#26333c");
g.fillStyle = bowl;
g.beginPath();
g.ellipse(CX, CY, 619, 339, 0, 0, TAU);
g.fill();
g.strokeStyle = "#52616b";
g.lineWidth = 4;
g.beginPath();
g.ellipse(CX, CY, 607, 328, 0, 0, TAU);
g.stroke();
g.fillStyle = "#202c36";
g.beginPath();
g.ellipse(CX, CY, 602, 323, 0, 0, TAU);
g.fill();
const rowRadii = [
{ rx: 585, ry: 309 },
{ rx: 558, ry: 292 },
{ rx: 531, ry: 275 },
{ rx: 504, ry: 257 }
];
rowRadii.forEach((row, index) => {
g.strokeStyle = index % 2 ? "#35434e" : "#3c4b56";
g.lineWidth = 3;
g.beginPath();
g.ellipse(CX, CY, row.rx, row.ry, 0, 0, TAU);
g.stroke();
});
for (let i = 0; i < 28; i++) {
const a = i / 28 * TAU;
const c = Math.cos(a);
const s = Math.sin(a);
g.strokeStyle = "rgba(6,10,14,.72)";
g.lineWidth = 4;
g.beginPath();
g.moveTo(CX + c * 496, CY + s * 242);
g.lineTo(CX + c * 606, CY + s * 326);
g.stroke();
}
const rng = mulberry32(84521);
const shirtColors = [
"#f15b69", "#49c9f2", "#ffd166", "#a978ff",
"#61e294", "#ff914d", "#eef4f7", "#5e7cff",
"#ff76c8", "#8de1d1"
];
const skinColors = ["#f3c6a5", "#c98e68", "#8e5a43", "#dba77d"];
rowRadii.forEach((row, rowIndex) => {
const count = 300;
for (let i = 0; i < count; i++) {
const a = i / count * TAU + rowIndex * .031 + (rng() - .5) * .024;
const x = CX + Math.cos(a) * row.rx;
const y = CY + Math.sin(a) * row.ry;
const shirt = shirtColors[(rng() * shirtColors.length) | 0];
const skin = skinColors[(rng() * skinColors.length) | 0];
g.fillStyle = "rgba(0,0,0,.42)";
g.fillRect(x - 1.4, y + .6, 3.2, 4.1);
g.fillStyle = shirt;
g.fillRect(x - 1.25, y - .7, 2.5, 3.8);
g.fillStyle = skin;
g.beginPath();
g.arc(x, y - 1.55, 1.12, 0, TAU);
g.fill();
if (i % 61 === 0) {
g.strokeStyle = "#dce8ed";
g.lineWidth = .8;
g.beginPath();
g.moveTo(x, y - 1);
g.lineTo(x, y - 10);
g.stroke();
g.fillStyle = i % 122 === 0 ? "#5ee8ff" : "#ffbf4c";
g.beginPath();
g.moveTo(x, y - 10);
g.lineTo(x + 5, y - 8.4);
g.lineTo(x, y - 6.7);
g.closePath();
g.fill();
}
}
});
g.fillStyle = "#9da7a8";
g.beginPath();
g.ellipse(CX, CY, 500, 245, 0, 0, TAU);
g.fill();
g.fillStyle = "#273039";
g.beginPath();
g.ellipse(CX, CY, 495, 240, 0, 0, TAU);
g.fill();
const dirt = g.createRadialGradient(625, 360, 20, CX, CY, 570);
dirt.addColorStop(0, "#9b6d43");
dirt.addColorStop(.48, "#805535");
dirt.addColorStop(1, "#4d3024");
g.fillStyle = dirt;
g.beginPath();
g.ellipse(CX, CY, FLOOR_RX, FLOOR_RY, 0, 0, TAU);
g.fill();
g.save();
g.beginPath();
g.ellipse(CX, CY, FLOOR_RX, FLOOR_RY, 0, 0, TAU);
g.clip();
for (let i = 0; i < 2100; i++) {
const x = rng() * WORLD_W;
const y = rng() * WORLD_H;
g.fillStyle = rng() > .5 ? "rgba(255,220,170,.055)" : "rgba(28,13,8,.09)";
const size = .5 + rng() * 1.8;
g.fillRect(x, y, size, size);
}
for (let i = 0; i < 66; i++) {
const x = 230 + rng() * 940;
const y = 245 + rng() * 410;
const rx = 25 + rng() * 90;
const ry = 8 + rng() * 30;
const rotation = rng() * TAU;
g.strokeStyle = `rgba(28,16,12,${.08 + rng() * .13})`;
g.lineWidth = 2 + rng() * 5;
g.beginPath();
g.ellipse(x, y, rx, ry, rotation, 0, TAU * (.25 + rng() * .7));
g.stroke();
}
g.save();
g.translate(CX, CY);
g.globalAlpha = .13;
g.strokeStyle = "#f6ead7";
g.lineWidth = 5;
g.beginPath();
g.arc(0, 0, 76, 0, TAU);
g.stroke();
g.fillStyle = "#f6ead7";
g.font = "900 54px Arial";
g.textAlign = "center";
g.fillText("SD", 0, 18);
g.beginPath();
g.moveTo(47, -58);
g.lineTo(5, -4);
g.lineTo(29, -4);
g.lineTo(8, 58);
g.lineTo(52, -9);
g.lineTo(27, -9);
g.closePath();
g.fill();
g.restore();
g.restore();
g.strokeStyle = "rgba(85,230,255,.56)";
g.lineWidth = 3;
g.setLineDash([18, 11]);
g.lineDashOffset = 4;
g.beginPath();
g.ellipse(CX, CY, 493, 239, 0, 0, TAU);
g.stroke();
g.setLineDash([]);
const lamps = [
{ x: 150, y: 145, a: -.3 },
{ x: 1250, y: 145, a: .3 },
{ x: 150, y: 755, a: .3 },
{ x: 1250, y: 755, a: -.3 }
];
lamps.forEach(light => {
const glow = g.createRadialGradient(light.x, light.y, 2, light.x, light.y, 115);
glow.addColorStop(0, "rgba(195,235,255,.22)");
glow.addColorStop(1, "rgba(195,235,255,0)");
g.fillStyle = glow;
g.fillRect(light.x - 115, light.y - 115, 230, 230);
g.save();
g.translate(light.x, light.y);
g.rotate(light.a);
g.fillStyle = "#111820";
roundedRect(g, -39, -9, 78, 18, 4);
g.fill();
for (let i = 0; i < 6; i++) {
g.fillStyle = "#dff7ff";
g.beginPath();
g.arc(-29 + i * 11.6, 0, 3.6, 0, TAU);
g.fill();
}
g.restore();
});
return layer;
}
const stadiumLayer = buildStadiumLayer();
function resize() {
cssWidth = window.innerWidth;
cssHeight = window.innerHeight;
dpr = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = Math.round(cssWidth * dpr);
canvas.height = Math.round(cssHeight * dpr);
viewScale = Math.min(cssWidth / WORLD_W, cssHeight / WORLD_H);
viewX = (cssWidth - WORLD_W * viewScale) / 2;
viewY = (cssHeight - WORLD_H * viewScale) / 2;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
const radius = Math.max(cssWidth, cssHeight) * .72;
screenVignette = ctx.createRadialGradient(
cssWidth / 2, cssHeight / 2, Math.min(cssWidth, cssHeight) * .24,
cssWidth / 2, cssHeight / 2, radius
);
screenVignette.addColorStop(0, "rgba(0,0,0,0)");
screenVignette.addColorStop(1, "rgba(0,0,0,.52)");
}
function createTrucks() {
const definitions = [
{ id: 0, player: true, name: "YOU", role: "PLAYER", color: "#dce5e8", accent: "#43eaff", x: 700, y: 450, heading: 0, vx: 0, vy: 0 },
{ id: 1, player: false, name: "RIVAL 01", role: "HUNTER", color: "#ef4654", accent: "#ffb14a", x: 510, y: 325, heading: 2.78, vx: 18, vy: 0 },
{ id: 2, player: false, name: "RIVAL 02", role: "BRAWLER", color: "#f18a32", accent: "#ffe16a", x: 930, y: 345, heading: 3.65, vx: 0, vy: 14 },
{ id: 3, player: false, name: "RIVAL 03", role: "ROAMER", color: "#397de1", accent: "#5ff4ff", x: 410, y: 575, heading: 1.22, vx: -14, vy: 0 },
{ id: 4, player: false, name: "RIVAL 04", role: "INTERCEPT", color: "#9b57d3", accent: "#ff5cda", x: 940, y: 600, heading: 4.36, vx: 0, vy: -12 }
];
return definitions.map(def => ({
...def,
hw: 31,
hh: 17,
speed: 0,
flash: 0,
wreckUntil: 0,
dustTimer: 0,
smokeTimer: 0,
targetTimer: 0,
target: null,
routeIndex: 0,
routeTarget: ROAMER_ROUTE[0],
obstacleCooldowns: {},
lastWallHit: -10
}));
}
function resetGame() {
state.score = demoMode ? 5000 : 0;
state.timeLeft = demoMode ? 20 : 90;
state.elapsed = 0;
state.photo = false;
state.over = false;
state.shake = 0;
state.shakeX = 0;
state.shakeY = 0;
trucks = createTrucks();
particles.length = 0;
popups.length = 0;
ramCooldowns.clear();
keys.clear();
accumulator = 0;
lastFrame = performance.now();
photoOverlay.hidden = true;
gameOver.hidden = true;
updateHud();
}
function formatScore(score) {
return Math.max(0, Math.floor(score)).toLocaleString("en-US");
}
function formatClock(seconds) {
const total = Math.ceil(Math.max(0, seconds));
const minutes = Math.floor(total / 60);
const remainder = total % 60;
return String(minutes).padStart(2, "0") + ":" + String(remainder).padStart(2, "0");
}
function updateHud() {
scoreEl.textContent = formatScore(state.score);
clockEl.textContent = formatClock(state.timeLeft);
clockEl.classList.toggle("warning", state.timeLeft <= 10 && !state.over);
photoScoreEl.textContent = formatScore(state.score);
}
function togglePhoto() {
state.photo = !state.photo;
photoOverlay.hidden = !state.photo;
photoOverlay.setAttribute("aria-hidden", String(!state.photo));
keys.clear();
accumulator = 0;
if (state.photo) photoScoreEl.textContent = formatScore(state.score);
}
function endGame() {
if (state.over) return;
state.over = true;
state.timeLeft = 0;
keys.clear();
finalScore.textContent = formatScore(state.score);
gameOver.hidden = false;
}
function isWrecked(truck) {
return state.elapsed < truck.wreckUntil;
}
function truckOBB(truck) {
const fX = Math.cos(truck.heading);
const fY = Math.sin(truck.heading);
return {
x: truck.x,
y: truck.y,
fX,
fY,
rX: -fY,
rY: fX,
hw: truck.hw,
hh: truck.hh
};
}
function rectOBB(rect) {
const fX = Math.cos(rect.angle || 0);
const fY = Math.sin(rect.angle || 0);
return {
x: rect.x,
y: rect.y,
fX,
fY,
rX: -fY,
rY: fX,
hw: rect.w / 2,
hh: rect.h / 2
};
}
function obbVsObb(a, b) {
const deltaX = b.x - a.x;
const deltaY = b.y - a.y;
const axes = [
[a.fX, a.fY], [a.rX, a.rY],
[b.fX, b.fY], [b.rX, b.rY]
];
let minimum = Infinity;
let normalX = 0;
let normalY = 0;
for (const axis of axes) {
const axisX = axis[0];
const axisY = axis[1];
const distance = Math.abs(deltaX * axisX + deltaY * axisY);
const radiusA = a.hw * Math.abs(a.fX * axisX + a.fY * axisY)
+ a.hh * Math.abs(a.rX * axisX + a.rY * axisY);
const radiusB = b.hw * Math.abs(b.fX * axisX + b.fY * axisY)
+ b.hh * Math.abs(b.rX * axisX + b.rY * axisY);
const overlap = radiusA + radiusB - distance;
if (overlap <= 0) return null;
if (overlap < minimum) {
minimum = overlap;
const sign = deltaX * axisX + deltaY * axisY >= 0 ? 1 : -1;
normalX = axisX * sign;
normalY = axisY * sign;
}
}
return { nx: normalX, ny: normalY, overlap: minimum };
}
function integrateTruck(truck, acceleration, steering, dt, drag, grip, turnRate) {
const oldFX = Math.cos(truck.heading);
const oldFY = Math.sin(truck.heading);
const oldRX = -oldFY;
const oldRY = oldFX;
let forward = truck.vx * oldFX + truck.vy * oldFY;
let lateral = truck.vx * oldRX + truck.vy * oldRY;
forward += acceleration * dt;
const dragAmount = acceleration === 0 ? drag : .10;
forward *= Math.exp(-dragAmount * dt);
lateral *= Math.exp(-grip * dt);
forward = clamp(forward, -115, 285);
lateral = clamp(lateral, -135, 135);
const turnAuthority = Math.max(.20, Math.min(Math.abs(forward) / 90, 1));
const reverse = forward < -4 ? -1 : 1;
truck.heading = normalizeAngle(
truck.heading + steering * turnRate * dt * turnAuthority * reverse
);
const fX = Math.cos(truck.heading);
const fY = Math.sin(truck.heading);
truck.vx = fX * forward - fY * lateral;
truck.vy = fY * forward + fX * lateral;
truck.x += truck.vx * dt;
truck.y += truck.vy * dt;
}
function drivePlayer(truck, dt) {
const forward = truck.vx * Math.cos(truck.heading) + truck.vy * Math.sin(truck.heading);
const up = keys.has("ArrowUp") || keys.has("KeyW");
const down = keys.has("ArrowDown") || keys.has("KeyS");
const left = keys.has("ArrowLeft") || keys.has("KeyA");
const right = keys.has("ArrowRight") || keys.has("KeyD");
const handbrake = keys.has("Space");
let acceleration = 0;
if (up) {
acceleration = 245;
} else if (down) {
acceleration = forward > 22 ? -360 : -175;
}
const steering = (right ? 1 : 0) - (left ? 1 : 0);
integrateTruck(
truck,
acceleration,
steering,
dt,
handbrake ? 2.5 : .68,
handbrake ? 1.15 : 6.4,
handbrake ? 2.8 : 2.12
);
}
function chooseAITarget(truck) {
const player = trucks[0];
if (truck.role === "HUNTER") {
return { target: player, intended: player };
}
if (truck.role === "BRAWLER") {
let nearest = null;
let nearestDistance = Infinity;
for (const other of trucks) {
if (other === truck || other.player) continue;
const distance = Math.hypot(other.x - truck.x, other.y - truck.y);
if (distance < nearestDistance) {
nearest = other;
nearestDistance = distance;
}
}
return { target: nearest || player, intended: nearest };
}
if (truck.role === "ROAMER") {
if (!truck.routeTarget || Math.hypot(
truck.routeTarget.x - truck.x,
truck.routeTarget.y - truck.y
) < 58) {
truck.routeIndex = (truck.routeIndex + 1) % ROAMER_ROUTE.length;
truck.routeTarget = ROAMER_ROUTE[truck.routeIndex];
}
return { target: truck.routeTarget, intended: null };
}
const predictionTime = .55;
const rightX = -Math.sin(player.heading);
const rightY = Math.cos(player.heading);
const side = truck.id % 2 === 0 ? -64 : 64;
return {
target: {
x: player.x + player.vx * predictionTime + rightX * side,
y: player.y + player.vy * predictionTime + rightY * side
},
intended: player
};
}
function driveAI(truck, dt) {
truck.targetTimer -= dt;
if (truck.targetTimer <= 0) {
const selection = chooseAITarget(truck);
truck.target = selection.target;
truck.intended = selection.intended;
truck.targetTimer = .22 + Math.random() * .18;
}
if (!truck.target) return;
let dx = truck.target.x - truck.x;
let dy = truck.target.y - truck.y;
for (const other of trucks) {
if (other === truck || other === truck.intended) continue;
const distance = Math.hypot(other.x - truck.x, other.y - truck.y);
if (distance < 120) {
const divisor = Math.max(distance, 1);
const force = (120 - distance) * 1.35;
dx += (other.x - truck.x) / divisor * force;
dy += (other.y - truck.y) / divisor * force;
}
}
for (const obstacle of obstacles) {
const distance = Math.hypot(obstacle.x - truck.x, obstacle.y - truck.y);
const clearance = obstacle.type === "hill"
? Math.min(obstacle.rx, obstacle.ry)
: obstacle.type === "tires"
? obstacle.r
: Math.max(obstacle.w, obstacle.h) * .55;
const avoidDistance = clearance + 58;
if (distance < avoidDistance) {
const divisor = Math.max(distance, 1);
const force = (avoidDistance - distance) * 1.5;
dx += (obstacle.x - truck.x) / divisor * force;
dy += (obstacle.y - truck.y) / divisor * force;
}
}
const desiredAngle = Math.atan2(dy, dx);
const difference = angleDelta(desiredAngle, truck.heading);
const currentForward = truck.vx * Math.cos(truck.heading) + truck.vy * Math.sin(truck.heading);
const targetDistance = Math.hypot(dx, dy);
let desiredSpeed = truck.role === "BRAWLER" ? 225 : truck.role === "ROAMER" ? 150 : 205;
if (truck.role === "ROAMER" && targetDistance < 85) desiredSpeed = 82;
let acceleration = (desiredSpeed - currentForward) * 1.75;
if (Math.abs(difference) > 1.15 && currentForward > 30) {
acceleration = Math.min(acceleration, -250);
}
integrateTruck(
truck,
acceleration,
clamp(difference * 1.55, -1, 1),
dt,
.28,
5.7,
2.2
);
}
function spawnBurst(x, y, count, color) {
for (let i = 0; i < count; i++) {
const angle = Math.random() * TAU;
const speed = 70 + Math.random() * 230;
const life = .28 + Math.random() * .42;
particles.push({
kind: "spark",
x, y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
life,
maxLife: life,
size: 1.3 + Math.random() * 2.2,
color
});
}
}
function spawnDust(x, y, count = 1) {
for (let i = 0; i < count; i++) {
const life = .55 + Math.random() * .6;
particles.push({
kind: "dust",
x: x + (Math.random() - .5) * 8,
y: y + (Math.random() - .5) * 8,
vx: (Math.random() - .5) * 34,
vy: (Math.random() - .5) * 34,
life,
maxLife: life,
size: 4 + Math.random() * 7,
color: "#c89a68"
});
}
}
function spawnSmoke(x, y) {
const life = .9 + Math.random() * .5;
particles.push({
kind: "smoke",
x, y,
vx: (Math.random() - .5) * 16,
vy: -18 - Math.random() * 18,
life,
maxLife: life,
size: 5 + Math.random() * 4,
color: "#3c4146"
});
}
function addPopup(text, x, y, color) {
popups.push({ text, x, y, color, life: 1.25, maxLife: 1.25 });
}
function registerRam(a, b, impact, x, y) {
const key = a.id < b.id ? a.id + ":" + b.id : b.id + ":" + a.id;
const last = ramCooldowns.has(key) ? ramCooldowns.get(key) : -Infinity;
if (state.elapsed - last < 1.15) return;
ramCooldowns.set(key, state.elapsed);
const playerInvolved = a.player || b.player;
const gain = playerInvolved ? 250 + Math.round(impact * 1.25) : 75;
state.score += gain;
addPopup(
playerInvolved ? "RAM +" + gain : "BRAWL +" + gain,
x,
y - 14,
playerInvolved ? "#65efff" : "#ffd166"
);
for (const truck of [a, b]) {
truck.wreckUntil = Math.max(
truck.wreckUntil,
state.elapsed + (truck.player ? .28 : 1.05)
);
truck.flash = .45;
truck.vx *= .82;
truck.vy *= .82;
}
spawnBurst(x, y, 20, "#fff0a1");
spawnDust(x, y, 7);
state.shake = Math.max(state.shake, Math.min(16, impact * .09));
}
function handleObstacleCollision(truck, obstacle) {
let impact = 0;
let contactX = obstacle.x;
let contactY = obstacle.y;
if (obstacle.type === "tires") {
const fX = Math.cos(truck.heading);
const fY = Math.sin(truck.heading);
const rX = -fY;
const rY = fX;
const relX = obstacle.x - truck.x;
const relY = obstacle.y - truck.y;
const localX = relX * fX + relY * fY;
const localY = relX * rX + relY * rY;
const closestX = clamp(localX, -truck.hw, truck.hw);
const closestY = clamp(localY, -truck.hh, truck.hh);
let normalX = closestX - localX;
let normalY = closestY - localY;
let distance = Math.hypot(normalX, normalY);
let penetration;
if (distance > .001) {
normalX /= distance;
normalY /= distance;
penetration = obstacle.r - distance;
} else {
const edgeX = truck.hw - Math.abs(localX);
const edgeY = truck.hh - Math.abs(localY);
if (edgeX < edgeY) {
const sign = -(Math.sign(localX) || 1);
normalX = fX * sign;
normalY = fY * sign;
penetration = obstacle.r + edgeX;
} else {
const sign = -(Math.sign(localY) || 1);
normalX = rX * sign;
normalY = rY * sign;
penetration = obstacle.r + edgeY;
}
}
if (penetration > 0) {
truck.x += normalX * (penetration + .2);
truck.y += normalY * (penetration + .2);
const normalVelocity = truck.vx * normalX + truck.vy * normalY;
if (normalVelocity < 0) {
impact = -normalVelocity;
truck.vx -= normalX * normalVelocity * 1.35;
truck.vy -= normalY * normalVelocity * 1.35;
}
truck.vx *= .985;
truck.vy *= .985;
}
} else if (obstacle.type === "hill") {
const rawX = (truck.x - obstacle.x) / (obstacle.rx * obstacle.rx);
const rawY = (truck.y - obstacle.y) / (obstacle.ry * obstacle.ry);
const q = Math.hypot(rawX, rawY);
if (q < 1) {
let normalX = rawX;
let normalY = rawY;
let magnitude = Math.hypot(normalX, normalY);
if (magnitude < .0001) {
normalX = 1;
normalY = 0;
magnitude = 1;
}
normalX /= magnitude;
normalY /= magnitude;
const depth = (1 - q) * Math.min(obstacle.rx, obstacle.ry);
truck.x += normalX * (depth + 1);
truck.y += normalY * (depth + 1);
const normalVelocity = truck.vx * normalX + truck.vy * normalY;
if (normalVelocity < 0) {
impact = -normalVelocity;
truck.vx -= normalX * normalVelocity * 1.18;
truck.vy -= normalY * normalVelocity * 1.18;
}
truck.vx *= .94;
truck.vy *= .94;
contactX = truck.x - normalX * obstacle.rx;
contactY = truck.y - normalY * obstacle.ry;
}
} else {
const hit = obbVsObb(rectOBB(obstacle), truckOBB(truck));
if (hit) {
truck.x += hit.nx * (hit.overlap + .2);
truck.y += hit.ny * (hit.overlap + .2);
const normalVelocity = truck.vx * hit.nx + truck.vy * hit.ny;
if (normalVelocity < 0) {
impact = -normalVelocity;
truck.vx -= hit.nx * normalVelocity * 1.35;
truck.vy -= hit.ny * normalVelocity * 1.35;
}
truck.vx *= .985;
truck.vy *= .985;
contactX = (truck.x + obstacle.x) / 2;
contactY = (truck.y + obstacle.y) / 2;
}
}
if (impact > 42) {
const last = truck.obstacleCooldowns[obstacle.id] ?? -Infinity;
if (state.elapsed - last > .24) {
truck.obstacleCooldowns[obstacle.id] = state.elapsed;
spawnBurst(contactX, contactY, 7, obstacle.type === "hill" ? "#c99b68" : "#ffd166");
spawnDust(contactX, contactY, 3);
}
}
}
function handleTruckCollisions() {
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 hit = obbVsObb(truckOBB(a), truckOBB(b));
if (!hit) continue;
a.x -= hit.nx * hit.overlap * .5;
a.y -= hit.ny * hit.overlap * .5;
b.x += hit.nx * hit.overlap * .5;
b.y += hit.ny * hit.overlap * .5;
const relativeX = b.vx - a.vx;
const relativeY = b.vy - a.vy;
const normalVelocity = relativeX * hit.nx + relativeY * hit.ny;
let impact = 0;
if (normalVelocity < 0) {
impact = -normalVelocity;
const impulse = -(1 + .55) * normalVelocity / 2;
a.vx -= hit.nx * impulse;
a.vy -= hit.ny * impulse;
b.vx += hit.nx * impulse;
b.vy += hit.ny * impulse;
const spin = clamp(
(relativeX * hit.ny - relativeY * hit.nx) * .0012,
-.22,
.22
);
a.heading = normalizeAngle(a.heading - spin);
b.heading = normalizeAngle(b.heading + spin);
}
const contactX = (a.x + b.x) / 2;
const contactY = (a.y + b.y) / 2;
if (impact > 20) {
spawnBurst(contactX, contactY, Math.min(12, 4 + impact / 20), "#ffd166");
spawnDust(contactX, contactY, 3);
}
if (impact > 65) {
registerRam(a, b, impact, contactX, contactY);
}
}
}
}
function constrainToArena(truck) {
const radiusX = FLOOR_RX - 31;
const radiusY = FLOOR_RY - 29;
const normalizedX = (truck.x - CX) / radiusX;
const normalizedY = (truck.y - CY) / radiusY;
const q = Math.hypot(normalizedX, normalizedY);
if (q <= 1) return;
let normalX = normalizedX / (radiusX * radiusX);
let normalY = normalizedY / (radiusY * radiusY);
const magnitude = Math.hypot(normalX, normalY) || 1;
normalX /= magnitude;
normalY /= magnitude;
truck.x = CX + normalX * radiusX;
truck.y = CY + normalY * radiusY;
const outward = truck.vx * normalX + truck.vy * normalY;
if (outward > 0) {
truck.vx -= normalX * outward * 1.35;
truck.vy -= normalY * outward * 1.35;
truck.vx *= .78;
truck.vy *= .78;
if (outward > 45 && state.elapsed - truck.lastWallHit > .25) {
truck.lastWallHit = state.elapsed;
spawnBurst(truck.x, truck.y, 7, "#ff9f43");
}
}
}
function emitTruckEffects(truck, dt) {
const speed = Math.hypot(truck.vx, truck.vy);
truck.dustTimer -= dt;
if (speed > 45 && truck.dustTimer <= 0) {
truck.dustTimer = .045 + Math.random() * .035;
const fX = Math.cos(truck.heading);
const fY = Math.sin(truck.heading);
const rX = -fY;
const rY = fX;
const side = Math.random() > .5 ? 18 : -18;
const x = truck.x - fX * 23 + rX * side;
const y = truck.y - fY * 23 + rY * side;
spawnDust(x, y, 1);
}
if (isWrecked(truck)) {
truck.smokeTimer -= dt;
if (truck.smokeTimer <= 0) {
truck.smokeTimer = .12;
const fX = Math.cos(truck.heading);
const fY = Math.sin(truck.heading);
spawnSmoke(truck.x + fX * 20, truck.y + fY * 20);
}
}
}
function updateParticles(dt) {
for (let i = particles.length - 1; i >= 0; i--) {
const particle = particles[i];
particle.life -= dt;
particle.x += particle.vx * dt;
particle.y += particle.vy * dt;
if (particle.kind === "smoke") {
particle.vx *= Math.exp(-.8 * dt);
particle.vy *= Math.exp(-.55 * dt);
particle.size += 9 * dt;
} else if (particle.kind === "dust") {
particle.vx *= Math.exp(-1.6 * dt);
particle.vy *= Math.exp(-1.6 * dt);
particle.size += 2 * dt;
} else {
particle.vx *= Math.exp(-1.2 * dt);
particle.vy *= Math.exp(-1.2 * dt);
}
if (particle.life <= 0) particles.splice(i, 1);
}
if (particles.length > 360) particles.splice(0, particles.length - 360);
}
function updatePopups(dt) {
for (let i = popups.length - 1; i >= 0; i--) {
const popup = popups[i];
popup.life -= dt;
popup.y -= 25 * dt;
if (popup.life <= 0) popups.splice(i, 1);
}
}
function updateGame(dt) {
state.timeLeft = Math.max(0, state.timeLeft - dt);
state.elapsed += dt;
if (state.timeLeft <= 0) {
endGame();
return;
}
if (state.shake > 0) {
state.shake = Math.max(0, state.shake - 26 * dt);
state.shakeX = (Math.random() - .5) * state.shake * 2;
state.shakeY = (Math.random() - .5) * state.shake * 2;
} else {
state.shakeX = 0;
state.shakeY = 0;
}
for (const truck of trucks) {
truck.flash = Math.max(0, truck.flash - dt);
if (isWrecked(truck)) {
truck.vx *= Math.exp(-3.2 * dt);
truck.vy *= Math.exp(-3.2 * dt);
} else if (truck.player) {
drivePlayer(truck, dt);
} else {
driveAI(truck, dt);
}
}
for (const truck of trucks) {
for (const obstacle of obstacles) {
handleObstacleCollision(truck, obstacle);
}
}
handleTruckCollisions();
for (const truck of trucks) {
constrainToArena(truck);
emitTruckEffects(truck, dt);
}
updateParticles(dt);
updatePopups(dt);
}
function drawObstacle(obstacle) {
ctx.save();
ctx.translate(obstacle.x, obstacle.y);
if (obstacle.type === "hill") {
ctx.fillStyle = "rgba(0,0,0,.34)";
ctx.beginPath();
ctx.ellipse(5, 8, obstacle.rx, obstacle.ry, 0, 0, TAU);
ctx.fill();
const mound = ctx.createRadialGradient(
-obstacle.rx * .28, -obstacle.ry * .35, 2,
0, 0, obstacle.rx
);
mound.addColorStop(0, "#b68755");
mound.addColorStop(.55, "#815333");
mound.addColorStop(1, "#493020");
ctx.fillStyle = mound;
ctx.beginPath();
ctx.ellipse(0, 0, obstacle.rx, obstacle.ry, 0, 0, TAU);
ctx.fill();
ctx.strokeStyle = "rgba(236,190,126,.25)";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.ellipse(-5, -4, obstacle.rx * .67, obstacle.ry * .57, -.12, 0, TAU);
ctx.stroke();
} else if (obstacle.type === "tires") {
ctx.fillStyle = "rgba(0,0,0,.38)";
ctx.beginPath();
ctx.ellipse(4, 6, obstacle.r + 4, obstacle.r + 2, 0, 0, TAU);
ctx.fill();
ctx.fillStyle = "#101317";
ctx.beginPath();
ctx.arc(0, 0, obstacle.r, 0, TAU);
ctx.fill();
ctx.strokeStyle = "#3c4248";
ctx.lineWidth = 4;
ctx.beginPath();
ctx.arc(0, 0, obstacle.r - 4, 0, TAU);
ctx.stroke();
ctx.fillStyle = "#272c31";
ctx.beginPath();
ctx.arc(0, 0, obstacle.r * .58, 0, TAU);
ctx.fill();
ctx.fillStyle = "#0a0c0e";
ctx.beginPath();
ctx.arc(0, 0, obstacle.r * .28, 0, TAU);
ctx.fill();
ctx.strokeStyle = "rgba(255,255,255,.16)";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(-2, -2, obstacle.r - 6, 3.5, 5.4);
ctx.stroke();
} else {
ctx.rotate(obstacle.angle || 0);
if (obstacle.type === "crate") {
ctx.fillStyle = "rgba(0,0,0,.42)";
roundedRect(ctx, -obstacle.w / 2 + 4, -obstacle.h / 2 + 7, obstacle.w, obstacle.h, 2);
ctx.fill();
ctx.fillStyle = "#634322";
ctx.fillRect(-obstacle.w / 2 + 2, -obstacle.h / 2 + 4, obstacle.w, obstacle.h);
ctx.fillStyle = "#9b6c35";
ctx.fillRect(-obstacle.w / 2, -obstacle.h / 2, obstacle.w, obstacle.h);
ctx.strokeStyle = "#4a301b";
ctx.lineWidth = 3;
ctx.strokeRect(-obstacle.w / 2, -obstacle.h / 2, obstacle.w, obstacle.h);
ctx.beginPath();
ctx.moveTo(-obstacle.w / 2 + 4, -obstacle.h / 2 + 4);
ctx.lineTo(obstacle.w / 2 - 4, obstacle.h / 2 - 4);
ctx.moveTo(obstacle.w / 2 - 4, -obstacle.h / 2 + 4);
ctx.lineTo(-obstacle.w / 2 + 4, obstacle.h / 2 - 4);
ctx.stroke();
} else if (obstacle.type === "barrier") {
ctx.fillStyle = "rgba(0,0,0,.4)";
roundedRect(ctx, -obstacle.w / 2 + 4, -obstacle.h / 2 + 7, obstacle.w, obstacle.h, 3);
ctx.fill();
ctx.fillStyle = "#b4b8b6";
ctx.beginPath();
ctx.moveTo(-obstacle.w / 2, -obstacle.h / 2);
ctx.lineTo(obstacle.w / 2 - 8, -obstacle.h / 2);
ctx.lineTo(obstacle.w / 2, -obstacle.h / 2 + 5);
ctx.lineTo(obstacle.w / 2, obstacle.h / 2 - 5);
ctx.lineTo(obstacle.w / 2 - 8, obstacle.h / 2);
ctx.lineTo(-obstacle.w / 2, obstacle.h / 2);
ctx.closePath();
ctx.fill();
ctx.fillStyle = "#e09a31";
for (let x = -obstacle.w / 2 + 9; x < obstacle.w / 2 - 8; x += 17) {
ctx.fillRect(x, -4, 8, 8);
}
} else if (obstacle.type === "ramp") {
ctx.fillStyle = "rgba(0,0,0,.42)";
ctx.beginPath();
ctx.moveTo(-obstacle.w / 2 + 4, -obstacle.h / 2 + 8);
ctx.lineTo(obstacle.w / 2 + 4, -obstacle.h / 2 + 8);
ctx.lineTo(obstacle.w / 2 + 4, obstacle.h / 2 + 8);
ctx.lineTo(-obstacle.w / 2 + 4, obstacle.h / 2 + 8);
ctx.closePath();
ctx.fill();
ctx.fillStyle = "#35434b";
ctx.beginPath();
ctx.moveTo(-obstacle.w / 2, obstacle.h / 2);
ctx.lineTo(-obstacle.w / 2 + 13, -obstacle.h / 2);
ctx.lineTo(obstacle.w / 2 - 4, -obstacle.h / 2);
ctx.lineTo(obstacle.w / 2, obstacle.h / 2);
ctx.closePath();
ctx.fill();
ctx.strokeStyle = "#111a20";
ctx.lineWidth = 3;
ctx.stroke();
ctx.strokeStyle = "#f4c84b";
ctx.lineWidth = 5;
for (let x = -15; x < 22; x += 14) {
ctx.beginPath();
ctx.moveTo(x, 8);
ctx.lineTo(x + 9, -7);
ctx.lineTo(x + 18, 8);
ctx.stroke();
}
}
}
ctx.restore();
}
function drawTruck(truck) {
ctx.save();
ctx.fillStyle = "rgba(0,0,0,.38)";
ctx.beginPath();
ctx.ellipse(truck.x + 4, truck.y + 8, 38, 23, 0, 0, TAU);
ctx.fill();
ctx.translate(truck.x, truck.y);
ctx.rotate(truck.heading);
for (const wheelX of [-23, 23]) {
for (const wheelY of [-19, 19]) {
ctx.fillStyle = "#080a0d";
roundedRect(ctx, wheelX - 9, wheelY - 6, 18, 12, 4);
ctx.fill();
ctx.strokeStyle = "#3a4046";
ctx.lineWidth = 1.5;
ctx.stroke();
ctx.strokeStyle = "rgba(255,255,255,.13)";
ctx.lineWidth = 1;
for (let tread = -5; tread <= 5; tread += 4) {
ctx.beginPath();
ctx.moveTo(wheelX + tread, wheelY - 5);
ctx.lineTo(wheelX + tread, wheelY + 5);
ctx.stroke();
}
ctx.fillStyle = "#7c858b";
ctx.beginPath();
ctx.arc(wheelX, wheelY, 2.5, 0, TAU);
ctx.fill();
}
}
const bodyGradient = ctx.createLinearGradient(0, -18, 0, 18);
bodyGradient.addColorStop(0, "#f5f7f7");
bodyGradient.addColorStop(.28, truck.color);
bodyGradient.addColorStop(.72, truck.color);
bodyGradient.addColorStop(1, "#303840");
ctx.beginPath();
ctx.moveTo(-34, -11);
ctx.lineTo(-25, -17);
ctx.lineTo(18, -17);
ctx.lineTo(35, -7);
ctx.lineTo(35, 7);
ctx.lineTo(18, 17);
ctx.lineTo(-25, 17);
ctx.lineTo(-34, 11);
ctx.lineTo(-28, 0);
ctx.closePath();
ctx.fillStyle = bodyGradient;
ctx.fill();
ctx.strokeStyle = "rgba(235,248,255,.72)";
ctx.lineWidth = 1.5;
ctx.stroke();
ctx.globalAlpha = .78;
ctx.fillStyle = truck.accent;
ctx.fillRect(-18, -15, 35, 3);
ctx.fillRect(-18, 12, 35, 3);
ctx.globalAlpha = 1;
ctx.beginPath();
ctx.moveTo(-12, -12);
ctx.lineTo(13, -12);
ctx.lineTo(21, -6);
ctx.lineTo(21, 6);
ctx.lineTo(13, 12);
ctx.lineTo(-12, 12);
ctx.lineTo(-7, 0);
ctx.closePath();
ctx.fillStyle = "#17222c";
ctx.fill();
ctx.strokeStyle = "rgba(190,235,255,.45)";
ctx.lineWidth = 1;
ctx.stroke();
ctx.beginPath();
ctx.moveTo(-4, -9);
ctx.lineTo(11, -9);
ctx.lineTo(15, -4);
ctx.lineTo(-3, -4);
ctx.closePath();
ctx.fillStyle = "rgba(154,220,242,.48)";
ctx.fill();
ctx.fillStyle = "#7b858d";
ctx.beginPath();
ctx.moveTo(22, -14);
ctx.lineTo(32, -7);
ctx.lineTo(32, 7);
ctx.lineTo(22, 14);
ctx.closePath();
ctx.fill();
ctx.fillStyle = "#eaffff";
ctx.fillRect(30, -7, 5, 4);
ctx.fillRect(30, 3, 5, 4);
ctx.fillStyle = "#ff4455";
ctx.fillRect(-34, -8, 4, 5);
ctx.fillRect(-34, 3, 4, 5);
ctx.strokeStyle = truck.accent;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(-10, 0);
ctx.lineTo(9, 0);
ctx.moveTo(0, -7);
ctx.lineTo(0, 7);
ctx.stroke();
if (truck.player) {
ctx.fillStyle = "#43eaff";
ctx.beginPath();
ctx.arc(-1, 0, 3, 0, TAU);
ctx.fill();
ctx.strokeStyle = "#d9fbff";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(-7, 0);
ctx.lineTo(-15, -8);
ctx.moveTo(-7, 0);
ctx.lineTo(-15, 8);
ctx.stroke();
} else {
ctx.fillStyle = "rgba(255,255,255,.82)";
ctx.font = "900 9px Arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(String(truck.id), 3, 0);
}
if (isWrecked(truck)) {
ctx.strokeStyle = "#ffb24b";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(-6, -15);
ctx.lineTo(1, -7);
ctx.lineTo(-3, -2);
ctx.lineTo(7, 7);
ctx.moveTo(1, -7);
ctx.lineTo(9, -11);
ctx.stroke();
}
if (truck.flash > 0) {
ctx.globalAlpha = clamp(truck.flash / .45, 0, 1) * .48;
ctx.fillStyle = "#ffffff";
ctx.beginPath();
ctx.moveTo(-34, -11);
ctx.lineTo(-25, -17);
ctx.lineTo(18, -17);
ctx.lineTo(35, -7);
ctx.lineTo(35, 7);
ctx.lineTo(18, 17);
ctx.lineTo(-25, 17);
ctx.lineTo(-34, 11);
ctx.lineTo(-28, 0);
ctx.closePath();
ctx.fill();
ctx.globalAlpha = 1;
}
ctx.restore();
const wrecked = isWrecked(truck);
const label = wrecked ? "WRECKED" : truck.player ? "YOU" : "RIVAL " + String(truck.id).padStart(2, "0");
ctx.font = "900 10px Arial";
const labelWidth = ctx.measureText(label).width + 14;
const labelY = truck.y - 31;
ctx.fillStyle = wrecked ? "rgba(255,132,45,.95)" : truck.player ? "rgba(20,116,137,.94)" : "rgba(10,16,22,.86)";
roundedRect(ctx, truck.x - labelWidth / 2, labelY - 10, labelWidth, 18, 5);
ctx.fill();
ctx.strokeStyle = wrecked ? "#ffd166" : truck.accent;
ctx.lineWidth = 1;
ctx.stroke();
ctx.fillStyle = wrecked ? "#fff4c4" : "#f4fbff";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(label, truck.x, labelY - .5);
if (truck.player) {
ctx.fillStyle = "#52efff";
ctx.beginPath();
ctx.moveTo(truck.x, truck.y - 47);
ctx.lineTo(truck.x - 5, truck.y - 56);
ctx.lineTo(truck.x + 5, truck.y - 56);
ctx.closePath();
ctx.fill();
}
}
function drawParticles(sparksOnly) {
ctx.save();
for (const particle of particles) {
const isSpark = particle.kind === "spark";
if (isSpark !== sparksOnly) continue;
const alpha = clamp(particle.life / particle.maxLife, 0, 1);
if (isSpark) {
ctx.globalAlpha = alpha;
ctx.strokeStyle = particle.color;
ctx.lineWidth = particle.size;
ctx.beginPath();
ctx.moveTo(particle.x, particle.y);
ctx.lineTo(particle.x - particle.vx * .025, particle.y - particle.vy * .025);
ctx.stroke();
} else {
ctx.globalAlpha = alpha * (particle.kind === "smoke" ? .28 : .34);
ctx.fillStyle = particle.color;
ctx.beginPath();
ctx.arc(particle.x, particle.y, particle.size, 0, TAU);
ctx.fill();
}
}
ctx.globalAlpha = 1;
ctx.restore();
}
function drawPopups() {
ctx.save();
ctx.textAlign = "center";
ctx.textBaseline = "middle";
for (const popup of popups) {
const alpha = clamp(popup.life / popup.maxLife, 0, 1);
ctx.globalAlpha = alpha;
ctx.font = "900 16px Arial";
ctx.lineWidth = 4;
ctx.strokeStyle = "rgba(0,0,0,.82)";
ctx.strokeText(popup.text, popup.x, popup.y);
ctx.fillStyle = popup.color;
ctx.fillText(popup.text, popup.x, popup.y);
}
ctx.globalAlpha = 1;
ctx.restore();
}
function render() {
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.fillStyle = "#05070b";
ctx.fillRect(0, 0, cssWidth, cssHeight);
ctx.save();
ctx.translate(viewX, viewY);
ctx.scale(viewScale, viewScale);
ctx.translate(state.shakeX, state.shakeY);
ctx.drawImage(stadiumLayer, 0, 0, WORLD_W, WORLD_H);
for (const obstacle of obstacles) drawObstacle(obstacle);
drawParticles(false);
const orderedTrucks = trucks.slice().sort((a, b) => a.y - b.y);
for (const truck of orderedTrucks) drawTruck(truck);
drawParticles(true);
drawPopups();
ctx.restore();
if (screenVignette) {
ctx.fillStyle = screenVignette;
ctx.fillRect(0, 0, cssWidth, cssHeight);
}
}
function frame(now) {
const delta = Math.min(.10, Math.max(0, (now - lastFrame) / 1000));
lastFrame = now;
if (!state.photo && !state.over) {
accumulator += delta;
let steps = 0;
while (accumulator >= FIXED_DT && steps < 6 && !state.over) {
updateGame(FIXED_DT);
accumulator -= FIXED_DT;
steps++;
}
if (steps === 6) accumulator = 0;
} else {
accumulator = 0;
}
render();
updateHud();
requestAnimationFrame(frame);
}
window.addEventListener("resize", resize);
window.addEventListener("keydown", event => {
if (driveCodes.has(event.code)) event.preventDefault();
if (event.code === "KeyP") {
event.preventDefault();
if (!event.repeat) togglePhoto();
return;
}
if (event.code === "Enter" && state.over) {
event.preventDefault();
resetGame();
return;
}
if (!state.photo && !state.over) keys.add(event.code);
});
window.addEventListener("keyup", event => {
keys.delete(event.code);
});
window.addEventListener("blur", () => keys.clear());
window.addEventListener("hashchange", () => {
demoMode = window.location.hash === "#demo";
resetGame();
});
canvas.addEventListener("pointerdown", () => canvas.focus({ preventScroll: true }));
resize();
resetGame();
requestAnimationFrame(frame);
})();
</script>
</body>
</html>
location.hash === "#demo" sets the live state to score: 5000 and timeLeft: 20; the same timer, scoring, AI, collisions, and HUD use that state, with no cosmetic substitute overlay.P stops the fixed-step physics and game-state update, including AI, timer, particles, and collisions; the render loop continues only to repaint the same frozen frame and photo card until P is pressed again.