Canyon Run
Minimax m3 · basic
◈Preview unavailable — try playing the result
Browser check: Preview unavailable or timed out. This is a technical check, not a quality review.
Reviews & generation details
Reviews
Human review · Jul 17, 2026 · rubric 1
Historical path-based review. Association with these exact bytes is unverified.
The reviewer marked this result as broken. It remains in the archive.
- Visual design
- 4 / 5
- UX / feel
- 1 / 5
- Works
- 1 / 5
- Prompt spirit
- 4 / 5
- Delight
- 4 / 5
Gorgeous handling of lighting, decent to good models, but broken
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 38,068 bytes · SHA-256 a2208d942b22
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Canyon Racer</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #050010; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; color: #fff; }
canvas { display: block; }
#hud { position: fixed; inset: 0; pointer-events: none; z-index: 10; }
#speed-container { position: absolute; top: 28px; left: 50%; transform: translateX(-50%); width: 260px; text-align: center; }
#speed-label { font-size: 9px; letter-spacing: 6px; opacity: 0.5; margin-bottom: 8px; }
#speed-bar { width: 100%; height: 2px; background: rgba(255,255,255,0.12); position: relative; overflow: hidden; }
#speed-fill { position: absolute; top: 0; left: 0; height: 100%; background: linear-gradient(90deg, #ffaa44, #ff3366); width: 30%; box-shadow: 0 0 8px #ff8866; transition: width 0.15s; }
#score { position: absolute; top: 70px; left: 50%; transform: translateX(-50%); font-size: 30px; font-weight: 100; letter-spacing: 8px; color: #fff; text-shadow: 0 0 12px rgba(255,200,120,0.4); }
#multiplier { position: absolute; top: 50%; right: 60px; transform: translateY(-50%); font-size: 64px; font-weight: 100; color: #ffaa44; text-shadow: 0 0 24px rgba(255,170,68,0.5); letter-spacing: -2px; }
#multiplier-label { position: absolute; top: 50%; right: 60px; transform: translate(0, 70px); font-size: 10px; letter-spacing: 4px; opacity: 0.45; }
#multiplier.pulse { animation: pulse 0.4s; }
@keyframes pulse { 0% { transform: translateY(-50%) scale(1); } 30% { transform: translateY(-50%) scale(1.6); color: #fff; text-shadow: 0 0 40px #fff; } 100% { transform: translateY(-50%) scale(1); color: #ffaa44; } }
#energy-container { position: absolute; bottom: 56px; left: 50%; transform: translateX(-50%); width: 320px; text-align: center; }
#energy-label { font-size: 9px; letter-spacing: 6px; opacity: 0.5; margin-bottom: 8px; }
#energy-bar { width: 100%; height: 4px; background: rgba(255,255,255,0.08); }
#energy-fill { height: 100%; background: linear-gradient(90deg, #44ddff, #ffee66); width: 100%; box-shadow: 0 0 12px #ffee66; transition: width 0.2s, box-shadow 0.2s, background 0.2s; }
#turbo-container { position: absolute; bottom: 28px; left: 50%; transform: translateX(-50%); width: 320px; text-align: center; }
#turbo-label { font-size: 9px; letter-spacing: 6px; opacity: 0.5; margin-bottom: 8px; }
#turbo-bar { width: 100%; height: 2px; background: rgba(255,255,255,0.08); }
#turbo-fill { height: 100%; background: linear-gradient(90deg, #ff5522, #ffaa44); width: 0%; box-shadow: 0 0 8px #ff5522; transition: width 0.1s, box-shadow 0.2s; }
#turbo-fill.ready { box-shadow: 0 0 20px #ffaa44; }
.overlay { position: absolute; inset: 0; background: linear-gradient(135deg, rgba(15,5,40,0.92), rgba(50,15,30,0.88)); display: flex; flex-direction: column; justify-content: center; align-items: center; pointer-events: auto; text-align: center; z-index: 20; }
.overlay h1 { font-size: 64px; font-weight: 100; color: #ffaa44; letter-spacing: 14px; margin-bottom: 14px; text-shadow: 0 0 30px rgba(255,170,68,0.5); }
.overlay h2 { font-size: 12px; font-weight: 300; letter-spacing: 8px; color: rgba(255,255,255,0.6); margin-bottom: 40px; }
.overlay .stats { display: flex; gap: 70px; margin: 30px 0 10px; }
.overlay .stat { text-align: center; }
.overlay .stat .value { font-size: 38px; font-weight: 100; color: #ffeeaa; }
.overlay .stat .label { font-size: 9px; letter-spacing: 5px; opacity: 0.5; margin-top: 8px; }
.overlay .keys { margin-top: 24px; font-size: 11px; letter-spacing: 3px; opacity: 0.55; line-height: 2; }
.btn { margin-top: 40px; padding: 14px 40px; background: transparent; border: 1px solid #ffaa44; color: #ffaa44; font-size: 13px; letter-spacing: 8px; cursor: pointer; transition: all 0.2s; font-family: inherit; }
.btn:hover { background: #ffaa44; color: #1a0a2a; box-shadow: 0 0 30px rgba(255,170,68,0.4); }
#warning { position: absolute; top: 40%; left: 50%; transform: translate(-50%, -50%); font-size: 22px; font-weight: 200; letter-spacing: 10px; color: #ff4466; opacity: 0; transition: opacity 0.3s; text-shadow: 0 0 30px #ff4466; }
#speed-strip { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 1px; height: 100%; background: linear-gradient(180deg, transparent, rgba(255,255,255,0.05), transparent); pointer-events: none; opacity: 0.5; }
</style>
</head>
<body>
<canvas id="game"></canvas>
<div id="hud">
<div id="speed-container">
<div id="speed-label">VELOCITY</div>
<div id="speed-bar"><div id="speed-fill"></div></div>
</div>
<div id="score">00000</div>
<div id="multiplier">1.0×</div>
<div id="multiplier-label">MULTIPLIER</div>
<div id="energy-container">
<div id="energy-label">ENERGY</div>
<div id="energy-bar"><div id="energy-fill"></div></div>
</div>
<div id="turbo-container">
<div id="turbo-label">TURBO</div>
<div id="turbo-bar"><div id="turbo-fill"></div></div>
</div>
<div id="warning">ENERGY CRITICAL</div>
</div>
<div id="start" class="overlay">
<h1>CANYON RACER</h1>
<h2>PROCEDURAL HIGH-SPEED RUN</h2>
<div class="keys">
A / D BANK<br>
W / S PITCH<br>
SPACE TURBO BOOST<br>
R RESTART
</div>
<button class="btn" id="start-btn">LAUNCH</button>
</div>
<div id="gameover" class="overlay" style="display: none;">
<h1>RUN OVER</h1>
<div class="stats">
<div class="stat"><div class="value" id="final-score">0</div><div class="label">SCORE</div></div>
<div class="stat"><div class="value" id="final-speed">0</div><div class="label">MAX SPEED</div></div>
<div class="stat"><div class="value" id="final-mult">0</div><div class="label">MAX MULT</div></div>
</div>
<button class="btn" id="restart-btn">RESTART</button>
</div>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.160.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
import { OutputPass } from 'three/addons/postprocessing/OutputPass.js';
// === Constants ===
const CHUNK_LENGTH = 80;
const CHUNKS_AHEAD = 5;
const CHUNKS_BEHIND = 2;
const PLAYER_RADIUS = 0.85;
// === Seeded RNG ===
function mulberry32(seed) {
let s = seed >>> 0;
return function() {
s = (s + 0x6D2B79F5) >>> 0;
let t = s;
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
// === Game State ===
const state = {
playing: false,
score: 0,
speed: 55,
maxSpeed: 0,
multiplier: 1,
maxMultiplier: 1,
energy: 1,
turbo: 0,
difficulty: 0,
shake: 0,
nearMissCooldown: 0,
spawnTrailDist: 0,
spawnDebrisDist: 0,
frameTime: 0,
};
// === Input ===
const keys = {};
window.addEventListener('keydown', e => {
const k = e.key.toLowerCase();
keys[k] = true;
if (k === 'r') { if (state.playing) restart(); else if (document.getElementById('gameover').style.display === 'flex') restart(); }
if (k === ' ') e.preventDefault();
});
window.addEventListener('keyup', e => { keys[e.key.toLowerCase()] = false; });
// === Three.js Setup ===
const canvas = document.getElementById('game');
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, powerPreference: 'high-performance' });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.15;
renderer.outputColorSpace = THREE.SRGBColorSpace;
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0xff5522, 0.0055);
const camera = new THREE.PerspectiveCamera(72, window.innerWidth / window.innerHeight, 0.1, 800);
camera.position.set(0, 6, -12);
// === Sky ===
const skyGeom = new THREE.SphereGeometry(450, 32, 16);
const skyMat = new THREE.ShaderMaterial({
uniforms: {
topColor: { value: new THREE.Color(0x0a0228) },
midColor: { value: new THREE.Color(0x552266) },
horizonColor: { value: new THREE.Color(0xff7733) },
bottomColor: { value: new THREE.Color(0xffaa66) },
sunPos: { value: new THREE.Vector3(0.25, 0.08, 0.96) },
sunColor: { value: new THREE.Color(0xffeecc) },
},
vertexShader: `varying vec3 vDir; void main() { vDir = normalize(position); gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`,
fragmentShader: `
uniform vec3 topColor; uniform vec3 midColor; uniform vec3 horizonColor; uniform vec3 bottomColor;
uniform vec3 sunPos; uniform vec3 sunColor; varying vec3 vDir;
void main() {
float h = vDir.y;
vec3 col = mix(bottomColor, horizonColor, smoothstep(-0.3, 0.0, h));
col = mix(col, midColor, smoothstep(0.0, 0.18, h));
col = mix(col, topColor, smoothstep(0.2, 0.7, h));
float sunDot = max(dot(vDir, normalize(sunPos)), 0.0);
col += sunColor * pow(sunDot, 64.0) * 2.0;
col += sunColor * 0.4 * pow(sunDot, 6.0);
col += vec3(0.3, 0.1, 0.2) * pow(sunDot, 2.0) * 0.2;
gl_FragColor = vec4(col, 1.0);
}
`,
side: THREE.BackSide,
depthWrite: false,
});
const sky = new THREE.Mesh(skyGeom, skyMat);
scene.add(sky);
// === Lighting ===
const sunLight = new THREE.DirectionalLight(0xffbb77, 1.9);
sunLight.position.set(80, 22, 60);
sunLight.target.position.set(0, 0, 0);
scene.add(sunLight.target);
sunLight.castShadow = true;
sunLight.shadow.mapSize.width = 1024;
sunLight.shadow.mapSize.height = 1024;
sunLight.shadow.camera.left = -70;
sunLight.shadow.camera.right = 70;
sunLight.shadow.camera.top = 70;
sunLight.shadow.camera.bottom = -70;
sunLight.shadow.camera.near = 1;
sunLight.shadow.camera.far = 220;
sunLight.shadow.bias = -0.0008;
scene.add(sunLight);
const fillLight = new THREE.HemisphereLight(0xff8855, 0x110033, 0.45);
scene.add(fillLight);
const rimLight = new THREE.DirectionalLight(0x8855ff, 0.6);
rimLight.position.set(-60, 30, -40);
scene.add(rimLight);
// === Ground (follows player) ===
const groundMat = new THREE.MeshStandardMaterial({ color: 0x0a0414, roughness: 1, metalness: 0 });
const ground = new THREE.Mesh(new THREE.PlaneGeometry(3000, 3000), groundMat);
ground.rotation.x = -Math.PI / 2;
ground.position.y = -28;
ground.receiveShadow = true;
scene.add(ground);
// === Player ===
const playerGroup = new THREE.Group();
scene.add(playerGroup);
const bodyMat = new THREE.MeshStandardMaterial({ color: 0xeaeaff, metalness: 0.85, roughness: 0.22, flatShading: true });
const accentMat = new THREE.MeshStandardMaterial({ color: 0x884422, metalness: 0.4, roughness: 0.6, flatShading: true });
// Body
const bodyGeom = new THREE.ConeGeometry(0.55, 2.6, 4);
bodyGeom.rotateX(Math.PI / 2);
const body = new THREE.Mesh(bodyGeom, bodyMat);
body.castShadow = true;
playerGroup.add(body);
// Wings
const wingGeom = new THREE.BoxGeometry(3.4, 0.08, 1.1);
const wing = new THREE.Mesh(wingGeom, bodyMat);
wing.position.z = 0.3;
wing.castShadow = true;
playerGroup.add(wing);
// Wing tips
const tipGeom = new THREE.BoxGeometry(0.6, 0.18, 0.8);
const tipL = new THREE.Mesh(tipGeom, accentMat);
tipL.position.set(-1.7, -0.05, 0.3);
tipL.castShadow = true;
playerGroup.add(tipL);
const tipR = new THREE.Mesh(tipGeom, accentMat);
tipR.position.set(1.7, -0.05, 0.3);
tipR.castShadow = true;
playerGroup.add(tipR);
// Tail fin
const finGeom = new THREE.BoxGeometry(0.08, 0.6, 0.7);
const fin = new THREE.Mesh(finGeom, bodyMat);
fin.position.set(0, 0.4, -0.7);
playerGroup.add(fin);
// Cockpit
const cockpitGeom = new THREE.SphereGeometry(0.35, 8, 6, 0, Math.PI * 2, 0, Math.PI / 2);
const cockpitMat = new THREE.MeshStandardMaterial({ color: 0x222244, metalness: 0.9, roughness: 0.1 });
const cockpit = new THREE.Mesh(cockpitGeom, cockpitMat);
cockpit.position.set(0, 0.3, 0.4);
playerGroup.add(cockpit);
// Engine glow
const engineMat = new THREE.MeshBasicMaterial({ color: 0xffcc66 });
const engine = new THREE.Mesh(new THREE.SphereGeometry(0.32, 12, 8), engineMat);
engine.position.set(0, 0, -1.4);
playerGroup.add(engine);
const engineHalo = new THREE.Mesh(
new THREE.SphereGeometry(0.7, 12, 8),
new THREE.MeshBasicMaterial({ color: 0xff8833, transparent: true, opacity: 0.5, blending: THREE.AdditiveBlending, depthWrite: false })
);
engineHalo.position.copy(engine.position);
playerGroup.add(engineHalo);
const engineLight = new THREE.PointLight(0xff7733, 2.5, 10);
engineLight.position.set(0, 0, -1.6);
playerGroup.add(engineLight);
// Trail cone
const trailConeGeom = new THREE.ConeGeometry(0.32, 2.2, 8, 1, true);
trailConeGeom.rotateX(-Math.PI / 2);
const trailConeMat = new THREE.MeshBasicMaterial({
color: 0xffaa44, transparent: true, opacity: 0.55,
blending: THREE.AdditiveBlending, side: THREE.DoubleSide, depthWrite: false,
});
const trailCone = new THREE.Mesh(trailConeGeom, trailConeMat);
trailCone.position.set(0, 0, -2.6);
playerGroup.add(trailCone);
const player = {
group: playerGroup,
position: new THREE.Vector3(0, 6, 0),
roll: 0, rollVel: 0,
pitch: 0, pitchVel: 0,
yaw: 0, yawVel: 0,
forwardSpeed: 55,
radius: PLAYER_RADIUS,
};
// === World: Chunk System ===
const chunks = new Map();
const chunkGroup = new THREE.Group();
scene.add(chunkGroup);
function createWallSegment(x, z, length, depth, height, baseY, side, rng) {
const geom = new THREE.BoxGeometry(depth, height, length);
const hueShift = rng();
const mat = new THREE.MeshStandardMaterial({
color: 0x6a2a18,
metalness: 0.15,
roughness: 0.85,
flatShading: true,
emissive: 0x220a05,
emissiveIntensity: 0.5,
});
mat.color.setHSL(0.04 + hueShift * 0.025, 0.55, 0.14 + hueShift * 0.08);
const mesh = new THREE.Mesh(geom, mat);
mesh.position.set(x, baseY + height / 2, z);
mesh.castShadow = true;
mesh.receiveShadow = true;
return mesh;
}
function createMonolith(x, z, width, depth, height, bottom) {
const geom = new THREE.BoxGeometry(width, height, depth);
const mat = new THREE.MeshStandardMaterial({
color: 0xaaaabb,
metalness: 0.75,
roughness: 0.32,
flatShading: true,
});
const mesh = new THREE.Mesh(geom, mat);
mesh.position.set(x, bottom + height / 2, z);
mesh.castShadow = true;
mesh.receiveShadow = true;
return mesh;
}
function createOrb() {
const group = new THREE.Group();
const core = new THREE.Mesh(
new THREE.OctahedronGeometry(0.5, 0),
new THREE.MeshBasicMaterial({ color: 0xffeeaa })
);
group.add(core);
const halo = new THREE.Mesh(
new THREE.SphereGeometry(1.0, 12, 8),
new THREE.MeshBasicMaterial({ color: 0xffaa44, transparent: true, opacity: 0.35, blending: THREE.AdditiveBlending, depthWrite: false })
);
group.add(halo);
const ring = new THREE.Mesh(
new THREE.TorusGeometry(0.75, 0.05, 6, 16),
new THREE.MeshBasicMaterial({ color: 0xffeecc, transparent: true, opacity: 0.6 })
);
ring.rotation.x = Math.PI / 2;
group.add(ring);
group.userData.core = core;
group.userData.halo = halo;
group.userData.ring = ring;
return group;
}
function generateChunk(index) {
if (chunks.has(index)) return;
const rng = mulberry32(index * 9173 + 7);
const group = new THREE.Group();
group.position.z = index * CHUNK_LENGTH;
const walls = [];
const monoliths = [];
const orbs = [];
const difficulty = Math.min(1, state.score / 12000);
const baseWidth = 48 - difficulty * 22;
const wallThick = 4;
const wallHeight = 32 + rng() * 22;
const wallBaseY = -16;
const numSegments = 6;
const segLength = CHUNK_LENGTH / numSegments;
// Global curve function - canyon snakes left/right smoothly
const curveAmp = 10 + difficulty * 8;
for (let i = 0; i < numSegments; i++) {
const zLocal = i * segLength + segLength / 2;
const globalZ = index * CHUNK_LENGTH + zLocal;
const curveX = Math.sin(globalZ * 0.022) * curveAmp;
const wobble = (rng() - 0.5) * 4;
const leftX = -baseWidth / 2 + curveX + (rng() - 0.5) * 3;
const rightX = baseWidth / 2 + curveX + (rng() - 0.5) * 3;
const segHeight = wallHeight + (rng() - 0.5) * 6;
const segBaseY = wallBaseY + (rng() - 0.5) * 2;
const wL = createWallSegment(leftX, zLocal, segLength + 1, wallThick, segHeight, segBaseY, 'left', rng);
const wR = createWallSegment(rightX, zLocal, segLength + 1, wallThick, segHeight, segBaseY, 'right', rng);
group.add(wL, wR);
walls.push({ x: leftX, z: zLocal, depth: wallThick, width: segLength, height: segHeight, bottom: segBaseY });
walls.push({ x: rightX, z: zLocal, depth: wallThick, width: segLength, height: segHeight, bottom: segBaseY });
}
// Monoliths
const monoChance = 0.25 + difficulty * 0.4;
let monoCount = 0;
if (rng() < monoChance) monoCount = 1;
if (rng() < difficulty * 0.35) monoCount++;
for (let m = 0; m < monoCount; m++) {
const monoZ = rng() * CHUNK_LENGTH;
const globalZ2 = index * CHUNK_LENGTH + monoZ;
const curveX2 = Math.sin(globalZ2 * 0.022) * curveAmp;
const monoX = curveX2 + (rng() - 0.5) * (baseWidth - 14);
const monoWidth = 3.5 + rng() * 4.5;
const monoDepth = 3.5 + rng() * 4.5;
const fromCeiling = rng() > 0.55;
const monoHeight = 9 + rng() * 18;
const monoBottom = fromCeiling ? 30 - monoHeight : -16;
group.add(createMonolith(monoX, monoZ, monoWidth, monoDepth, monoHeight, monoBottom));
monoliths.push({ x: monoX, z: monoZ, width: monoWidth, depth: monoDepth, height: monoHeight, bottom: monoBottom });
}
// Orbs
const orbCount = 1 + Math.floor(rng() * 3);
for (let i = 0; i < orbCount; i++) {
const orbZ = (i + 0.5) * (CHUNK_LENGTH / orbCount) + (rng() - 0.5) * 6;
const globalZ3 = index * CHUNK_LENGTH + orbZ;
const curveX3 = Math.sin(globalZ3 * 0.022) * curveAmp;
const orbX = curveX3 + (rng() - 0.5) * (baseWidth - 8);
const orbY = 4 + rng() * 14;
const orbMesh = createOrb();
orbMesh.position.set(orbX, orbY, orbZ);
group.add(orbMesh);
orbs.push({ x: orbX, y: orbY, z: orbZ, mesh: orbMesh, collected: false });
}
chunkGroup.add(group);
chunks.set(index, { index, group, walls, monoliths, orbs, z: index * CHUNK_LENGTH });
}
function despawnChunk(index) {
const chunk = chunks.get(index);
if (!chunk) return;
chunk.group.traverse(obj => {
if (obj.geometry) obj.geometry.dispose();
if (obj.material) {
if (Array.isArray(obj.material)) obj.material.forEach(m => m.dispose());
else obj.material.dispose();
}
});
chunkGroup.remove(chunk.group);
chunks.delete(index);
}
function updateChunks() {
const playerChunk = Math.floor(player.position.z / CHUNK_LENGTH);
for (let i = playerChunk - CHUNKS_BEHIND; i <= playerChunk + CHUNKS_AHEAD; i++) {
if (i >= 0) generateChunk(i);
}
for (const index of [...chunks.keys()]) {
if (index < playerChunk - CHUNKS_BEHIND || index > playerChunk + CHUNKS_AHEAD) despawnChunk(index);
}
}
// === Particles ===
const TRAIL_COUNT = 300;
const trailPositions = new Float32Array(TRAIL_COUNT * 3);
const trailColors = new Float32Array(TRAIL_COUNT * 3);
const trailSizes = new Float32Array(TRAIL_COUNT);
for (let i = 0; i < TRAIL_COUNT; i++) {
trailPositions[i * 3 + 1] = -1000;
trailSizes[i] = 0;
trailColors[i * 3] = 1; trailColors[i * 3 + 1] = 0.6; trailColors[i * 3 + 2] = 0.3;
}
const trailGeom = new THREE.BufferGeometry();
trailGeom.setAttribute('position', new THREE.BufferAttribute(trailPositions, 3));
trailGeom.setAttribute('color', new THREE.BufferAttribute(trailColors, 3));
trailGeom.setAttribute('size', new THREE.BufferAttribute(trailSizes, 1));
const trailMat = new THREE.ShaderMaterial({
vertexShader: `
attribute float size; attribute vec3 color; varying vec3 vColor;
void main() { vColor = color;
vec4 mv = modelViewMatrix * vec4(position, 1.0);
gl_PointSize = size * (320.0 / -mv.z);
gl_Position = projectionMatrix * mv;
}`,
fragmentShader: `
varying vec3 vColor;
void main() {
vec2 c = gl_PointCoord - vec2(0.5);
float d = length(c);
if (d > 0.5) discard;
float a = pow(1.0 - d * 2.0, 2.0);
gl_FragColor = vec4(vColor, a * 0.7);
}`,
transparent: true, depthWrite: false, blending: THREE.AdditiveBlending,
});
const trailPoints = new THREE.Points(trailGeom, trailMat);
scene.add(trailPoints);
let trailIndex = 0;
function spawnTrailParticle() {
const i = trailIndex;
trailPositions[i * 3] = player.position.x + (Math.random() - 0.5) * 0.4;
trailPositions[i * 3 + 1] = player.position.y + (Math.random() - 0.5) * 0.2;
trailPositions[i * 3 + 2] = player.position.z - 1.6 + (Math.random() - 0.5) * 0.6;
const boost = state.turbo > 0.05 && (keys[' '] || keys['space']) ? 1 : 0;
trailSizes[i] = 18 + Math.random() * 22 + boost * 15;
trailColors[i * 3] = 1;
trailColors[i * 3 + 1] = 0.5 + Math.random() * 0.25;
trailColors[i * 3 + 2] = 0.2 + Math.random() * 0.15;
trailIndex = (trailIndex + 1) % TRAIL_COUNT;
}
function updateTrail(dt) {
for (let i = 0; i < TRAIL_COUNT; i++) {
if (trailSizes[i] > 0.5) trailSizes[i] *= 0.94;
else trailSizes[i] = 0;
}
trailGeom.attributes.size.needsUpdate = true;
trailGeom.attributes.color.needsUpdate = true;
trailGeom.attributes.position.needsUpdate = true;
}
// Speed debris
const DEBRIS_COUNT = 200;
const debrisPositions = new Float32Array(DEBRIS_COUNT * 3);
const debrisColors = new Float32Array(DEBRIS_COUNT * 3);
const debrisSizes = new Float32Array(DEBRIS_COUNT);
for (let i = 0; i < DEBRIS_COUNT; i++) {
debrisPositions[i * 3 + 1] = -1000;
debrisSizes[i] = 0;
debrisColors[i * 3] = 1; debrisColors[i * 3 + 1] = 0.7; debrisColors[i * 3 + 2] = 0.4;
}
const debrisGeom = new THREE.BufferGeometry();
debrisGeom.setAttribute('position', new THREE.BufferAttribute(debrisPositions, 3));
debrisGeom.setAttribute('color', new THREE.BufferAttribute(debrisColors, 3));
debrisGeom.setAttribute('size', new THREE.BufferAttribute(debrisSizes, 1));
const debrisMat = trailMat.clone();
const debrisPoints = new THREE.Points(debrisGeom, debrisMat);
scene.add(debrisPoints);
let debrisIndex = 0;
function spawnDebris() {
const i = debrisIndex;
const angle = Math.random() * Math.PI * 2;
const radius = 5 + Math.random() * 28;
debrisPositions[i * 3] = player.position.x + Math.cos(angle) * radius;
debrisPositions[i * 3 + 1] = player.position.y + Math.sin(angle) * radius * 0.6 - 2;
debrisPositions[i * 3 + 2] = player.position.z + 8 + Math.random() * 50;
debrisSizes[i] = 4 + Math.random() * 8;
debrisColors[i * 3] = 1;
debrisColors[i * 3 + 1] = 0.55 + Math.random() * 0.3;
debrisColors[i * 3 + 2] = 0.3 + Math.random() * 0.2;
debrisIndex = (debrisIndex + 1) % DEBRIS_COUNT;
}
function updateDebris(dt) {
for (let i = 0; i < DEBRIS_COUNT; i++) {
if (debrisSizes[i] > 0) {
if (debrisPositions[i * 3 + 2] < player.position.z - 50) debrisSizes[i] = 0;
}
}
debrisGeom.attributes.position.needsUpdate = true;
debrisGeom.attributes.size.needsUpdate = true;
debrisGeom.attributes.color.needsUpdate = true;
}
// === Post Processing ===
const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
const bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 0.85, 0.5, 0.55);
composer.addPass(bloomPass);
composer.addPass(new OutputPass());
// === Audio ===
const audio = {
ctx: null, engineOsc: null, engineGain: null, filter: null,
init() {
try {
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
this.engineOsc = this.ctx.createOscillator();
this.engineOsc.type = 'sawtooth';
this.engineOsc.frequency.value = 70;
this.filter = this.ctx.createBiquadFilter();
this.filter.type = 'lowpass';
this.filter.frequency.value = 400;
this.filter.Q.value = 2;
this.engineGain = this.ctx.createGain();
this.engineGain.gain.value = 0;
this.engineOsc.connect(this.filter).connect(this.engineGain).connect(this.ctx.destination);
this.engineOsc.start();
} catch (e) { console.warn('Audio init failed', e); }
},
resume() { if (this.ctx && this.ctx.state === 'suspended') this.ctx.resume(); },
updateEngine(speed, boost) {
if (!this.engineOsc) return;
const targetFreq = 55 + speed * 0.55 + boost * 90;
const targetGain = 0.03 + speed * 0.0004 + boost * 0.05;
this.engineOsc.frequency.setTargetAtTime(targetFreq, this.ctx.currentTime, 0.08);
this.filter.frequency.setTargetAtTime(350 + speed * 4 + boost * 600, this.ctx.currentTime, 0.08);
this.engineGain.gain.setTargetAtTime(targetGain, this.ctx.currentTime, 0.08);
},
playOrb() {
if (!this.ctx) return;
const osc = this.ctx.createOscillator();
osc.type = 'sine';
osc.frequency.setValueAtTime(660, this.ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(1500, this.ctx.currentTime + 0.18);
const g = this.ctx.createGain();
g.gain.setValueAtTime(0.18, this.ctx.currentTime);
g.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.28);
osc.connect(g).connect(this.ctx.destination);
osc.start(); osc.stop(this.ctx.currentTime + 0.28);
},
playNearMiss() {
if (!this.ctx) return;
const osc = this.ctx.createOscillator();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(220, this.ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(900, this.ctx.currentTime + 0.13);
const f = this.ctx.createBiquadFilter();
f.type = 'bandpass'; f.frequency.value = 1100; f.Q.value = 5;
const g = this.ctx.createGain();
g.gain.setValueAtTime(0.14, this.ctx.currentTime);
g.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.22);
osc.connect(f).connect(g).connect(this.ctx.destination);
osc.start(); osc.stop(this.ctx.currentTime + 0.22);
},
playCrash() {
if (!this.ctx) return;
const sz = this.ctx.sampleRate * 0.7;
const buf = this.ctx.createBuffer(1, sz, this.ctx.sampleRate);
const d = buf.getChannelData(0);
for (let i = 0; i < sz; i++) d[i] = (Math.random() * 2 - 1) * (1 - i / sz) * 0.9;
const src = this.ctx.createBufferSource();
src.buffer = buf;
const g = this.ctx.createGain();
g.gain.value = 0.5;
const f = this.ctx.createBiquadFilter();
f.type = 'lowpass'; f.frequency.value = 1800;
src.connect(f).connect(g).connect(this.ctx.destination);
src.start();
// Engine stop
if (this.engineGain) this.engineGain.gain.setTargetAtTime(0, this.ctx.currentTime, 0.05);
},
};
// === Game Logic ===
function updatePlayer(dt) {
let rollInput = 0;
if (keys['a'] || keys['arrowleft']) rollInput += 1;
if (keys['d'] || keys['arrowright']) rollInput -= 1;
let pitchInput = 0;
if (keys['w'] || keys['arrowup']) pitchInput += 1;
if (keys['s'] || keys['arrowdown']) pitchInput -= 1;
const targetRollVel = rollInput * 4.5;
player.rollVel += (targetRollVel - player.rollVel) * Math.min(1, dt * 7);
player.roll += player.rollVel * dt;
player.roll = Math.max(-1.6, Math.min(1.6, player.roll));
if (rollInput === 0) player.roll *= (1 - dt * 1.8);
player.yawVel = -player.roll * 0.95;
player.yaw += player.yawVel * dt;
const targetPitchVel = pitchInput * 2.8;
player.pitchVel += (targetPitchVel - player.pitchVel) * Math.min(1, dt * 7);
player.pitch += player.pitchVel * dt;
player.pitch = Math.max(-0.75, Math.min(0.75, player.pitch));
if (pitchInput === 0) player.pitch *= (1 - dt * 1.8);
const difficulty = Math.min(1, state.score / 12000);
state.difficulty = difficulty;
let targetSpeed = 60 + difficulty * 40;
const boosting = (keys[' '] || keys['space']) && state.turbo > 0.04;
if (boosting) {
targetSpeed = 110 + difficulty * 30;
state.turbo = Math.max(0, state.turbo - dt * 0.4);
state.shake = Math.min(0.35, state.shake + dt * 0.7);
}
player.forwardSpeed += (targetSpeed - player.forwardSpeed) * Math.min(1, dt * 2.5);
state.speed = player.forwardSpeed;
state.maxSpeed = Math.max(state.maxSpeed, player.forwardSpeed);
const fx = Math.sin(player.yaw);
const fz = Math.cos(player.yaw);
player.position.x += fx * player.forwardSpeed * dt;
player.position.z += fz * player.forwardSpeed * dt;
player.position.y += Math.sin(player.pitch) * player.forwardSpeed * dt * 0.85;
player.position.y = Math.max(-2, Math.min(42, player.position.y));
playerGroup.rotation.set(player.pitch, player.yaw, player.roll);
playerGroup.position.copy(player.position);
const boostVis = boosting ? 1 : 0;
engine.material.color.setHex(boostVis ? 0xffeeaa : 0xffaa44);
engineHalo.material.opacity = 0.4 + boostVis * 0.5;
engineHalo.scale.setScalar(1 + boostVis * 0.6);
engineLight.intensity = 2.5 + boostVis * 4;
trailCone.scale.z = 1 + boostVis * 2;
trailCone.scale.x = 1 + boostVis * 0.5;
trailCone.material.opacity = 0.55 + boostVis * 0.45;
}
function updateCamera(dt) {
const offset = new THREE.Vector3(0, 3.8, -12);
const camYaw = player.yaw;
const camPitch = player.pitch * 0.45;
const offsetRotated = offset.clone().applyEuler(new THREE.Euler(camPitch, camYaw, 0));
const targetPos = player.position.clone().add(offsetRotated);
camera.position.lerp(targetPos, Math.min(1, dt * 6));
const lookAhead = new THREE.Vector3(0, 1.5, 28).applyEuler(new THREE.Euler(0, player.yaw, 0));
const lookTarget = player.position.clone().add(lookAhead);
camera.lookAt(lookTarget);
const boosting = (keys[' '] || keys['space']) && state.turbo > 0.04;
const targetFOV = 72 + (player.forwardSpeed - 55) * 0.42 + boosting * 14;
camera.fov += (targetFOV - camera.fov) * Math.min(1, dt * 3);
camera.updateProjectionMatrix();
if (state.shake > 0.01) {
camera.position.x += (Math.random() - 0.5) * state.shake;
camera.position.y += (Math.random() - 0.5) * state.shake;
camera.position.z += (Math.random() - 0.5) * state.shake * 0.4;
state.shake *= 0.91;
} else state.shake = 0;
}
function checkCollisions() {
const px = player.position.x;
const py = player.position.y;
const pz = player.position.z;
const r = player.radius;
const currentChunk = Math.floor(pz / CHUNK_LENGTH);
let nearMissed = false;
for (let i = currentChunk - 1; i <= currentChunk + 2; i++) {
const chunk = chunks.get(i);
if (!chunk) continue;
for (const w of chunk.walls) {
const dx = Math.abs(px - w.x);
const dz = Math.abs(pz - w.z);
const dy = Math.abs(py - (w.bottom + w.height / 2));
if (dz > w.width / 2 + 4) continue;
const xC = dx < w.depth / 2 + r;
const zC = dz < w.width / 2 + r;
const yC = dy < w.height / 2 + r;
if (xC && zC && yC) return { type: 'crash', obj: w };
const xN = dx < w.depth / 2 + r + 2.2;
const zN = dz < w.width / 2 + r + 1.8;
const yN = dy < w.height / 2 + r + 2;
if (xN && zN && yN && (pz - w.z) > -1.5 && (pz - w.z) < 1.5) nearMissed = true;
}
for (const m of chunk.monoliths) {
const dx = Math.abs(px - m.x);
const dz = Math.abs(pz - m.z);
const dy = Math.abs(py - (m.bottom + m.height / 2));
if (dz > m.depth / 2 + 4) continue;
const xC = dx < m.width / 2 + r;
const zC = dz < m.depth / 2 + r;
const yC = dy < m.height / 2 + r;
if (xC && zC && yC) return { type: 'crash', obj: m };
const xN = dx < m.width / 2 + r + 2;
const zN = dz < m.depth / 2 + r + 2;
const yN = dy < m.height / 2 + r + 2;
if (xN && zN && yN && (pz - m.z) > -1.8 && (pz - m.z) < 1.8) nearMissed = true;
}
for (const orb of chunk.orbs) {
if (orb.collected) continue;
const dSq = (px - orb.x) ** 2 + (py - orb.y) ** 2 + (pz - orb.z) ** 2;
if (dSq < 2.2 * 2.2) {
orb.collected = true;
orb.mesh.visible = false;
collectOrb();
}
}
}
if (nearMissed && state.nearMissCooldown <= 0) triggerNearMiss();
else if (!nearMissed) state.nearMissCooldown = Math.max(0, state.nearMissCooldown - dt);
return null;
}
function triggerNearMiss() {
state.multiplier = Math.min(99, state.multiplier + 0.5);
state.maxMultiplier = Math.max(state.maxMultiplier, state.multiplier);
state.score += Math.floor(state.multiplier * 40);
state.shake = Math.min(0.55, state.shake + 0.4);
state.nearMissCooldown = 0.45;
audio.playNearMiss();
const el = document.getElementById('multiplier');
el.classList.remove('pulse');
void el.offsetWidth;
el.classList.add('pulse');
}
function collectOrb() {
state.energy = Math.min(1, state.energy + 0.22);
state.score += Math.floor(80 * state.multiplier);
state.turbo = Math.min(1, state.turbo + 0.18);
audio.playOrb();
}
function updateGameLogic(dt) {
state.energy -= dt * 0.085;
if (state.energy <= 0) { gameOver(); return; }
state.score += player.forwardSpeed * dt * 0.12 * state.multiplier;
if (state.nearMissCooldown <= 0) state.multiplier = Math.max(1, state.multiplier - dt * 0.55);
state.turbo = Math.max(0, state.turbo - dt * 0.018);
state.spawnTrailDist += player.forwardSpeed * dt;
if (state.spawnTrailDist > 0.35) { spawnTrailParticle(); state.spawnTrailDist = 0; }
state.spawnDebrisDist += player.forwardSpeed * dt;
const debrisInterval = 1.4 - (player.forwardSpeed - 55) * 0.008;
if (state.spawnDebrisDist > Math.max(0.4, debrisInterval)) { spawnDebris(); state.spawnDebrisDist = 0; }
const t = state.frameTime;
for (const chunk of chunks.values()) {
for (const orb of chunk.orbs) {
if (!orb.collected) {
orb.mesh.position.y = orb.y + Math.sin(t * 2.5 + orb.z * 0.1) * 0.4;
orb.mesh.rotation.y = t * 1.8;
orb.mesh.userData.ring.rotation.z = t * 1.2;
}
}
}
ground.position.x = player.position.x;
ground.position.z = player.position.z;
const boosting = (keys[' '] || keys['space']) && state.turbo > 0.04;
audio.updateEngine(player.forwardSpeed, boosting ? 1 : 0);
}
function updateHUD() {
const speedPct = Math.min(100, Math.max(0, ((state.speed - 40) / 100) * 100));
document.getElementById('speed-fill').style.width = speedPct + '%';
document.getElementById('score').textContent = Math.floor(state.score).toString().padStart(5, '0');
document.getElementById('multiplier').textContent = state.multiplier.toFixed(1) + '×';
const energyPct = Math.max(0, state.energy * 100);
const ef = document.getElementById('energy-fill');
ef.style.width = energyPct + '%';
if (state.energy < 0.3) { ef.style.boxShadow = '0 0 24px #ff4466'; ef.style.background = 'linear-gradient(90deg, #ff4466, #ffaa44)'; }
else { ef.style.boxShadow = '0 0 10px #ffee66'; ef.style.background = 'linear-gradient(90deg, #44ddff, #ffee66)'; }
document.getElementById('warning').style.opacity = state.energy < 0.28 ? (Math.sin(state.frameTime * 8) > 0 ? 0.9 : 0.3) : '0';
const turboPct = state.turbo * 100;
const tf = document.getElementById('turbo-fill');
tf.style.width = turboPct + '%';
tf.classList.toggle('ready', state.turbo > 0.75);
}
function gameOver() {
state.playing = false;
audio.playCrash();
document.getElementById('final-score').textContent = Math.floor(state.score);
document.getElementById('final-speed').textContent = Math.floor(state.maxSpeed);
document.getElementById('final-mult').textContent = state.maxMultiplier.toFixed(1) + '×';
document.getElementById('gameover').style.display = 'flex';
state.multiplier = 1;
}
function restart() {
for (const idx of [...chunks.keys()]) despawnChunk(idx);
state.playing = true;
state.score = 0;
state.speed = 55;
state.maxSpeed = 0;
state.multiplier = 1;
state.maxMultiplier = 1;
state.energy = 1;
state.turbo = 0.3;
state.shake = 0;
state.nearMissCooldown = 0;
state.spawnTrailDist = 0;
state.spawnDebrisDist = 0;
player.position.set(0, 6, 0);
player.roll = 0; player.rollVel = 0;
player.pitch = 0; player.pitchVel = 0;
player.yaw = 0; player.yawVel = 0;
player.forwardSpeed = 55;
document.getElementById('gameover').style.display = 'none';
document.getElementById('start').style.display = 'none';
audio.resume();
updateChunks();
}
document.getElementById('start-btn').addEventListener('click', () => {
audio.init();
restart();
});
document.getElementById('restart-btn').addEventListener('click', () => { restart(); });
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
composer.setSize(window.innerWidth, window.innerHeight);
});
// Initial chunks for background scene
updateChunks();
// === Main Loop ===
let lastTime = performance.now();
function animate() {
requestAnimationFrame(animate);
const now = performance.now();
let dt = Math.min(0.05, (now - lastTime) / 1000);
lastTime = now;
state.frameTime += dt;
if (state.playing) {
updatePlayer(dt);
updateChunks();
const collision = checkCollisions();
if (collision && collision.type === 'crash') gameOver();
updateGameLogic(dt);
updateTrail(dt);
updateDebris(dt);
} else {
// Idle camera on start/gameover
const t = state.frameTime;
camera.position.x = Math.sin(t * 0.15) * 4;
camera.position.y = 6 + Math.sin(t * 0.2) * 0.5;
camera.position.z = -12;
camera.lookAt(0, 5, 8);
camera.fov = 72;
camera.updateProjectionMatrix();
}
updateHUD();
composer.render();
}
animate();
</script>
</body>
</html>
<!-- agent-meta {"model":"minimax/minimax-m3","provider":"openrouter","persona":"basic","sessionId":"6c1e5ea8-73ba-4d38-90e8-ba31de51d63a","tokensIn":1042,"tokensOut":48060,"tokensTotal":49102,"turns":1,"toolCalls":0,"failedToolCalls":0,"timestamp":"2026-07-13T22:15:06.359Z"} -->