Canyon Run
Minimax m3 · composer

Reviews & generation details
Reviews
Human review · Jul 17, 2026 · rubric 1
Historical path-based review. Association with these exact bytes is unverified.
- Visual design
- 4 / 5
- UX / feel
- 4 / 5
- Works
- 4 / 5
- Prompt spirit
- 4 / 5
- Delight
- 4 / 5
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 48,488 bytes · SHA-256 dd1a816eba28
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Solar Drift — Canyon Racer</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<style>
/* ── css: theme ── */
:root {
--orange: #ff8a3c;
--magenta: #ff4d8f;
--silver: #d8e2ff;
--deep: #1a0820;
--cyan: #6ff0ff;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #0a0410; font-family: 'Courier New', monospace; color: var(--silver); user-select: none; }
canvas#game { position: fixed; inset: 0; display: block; }
#hud { position: fixed; inset: 0; pointer-events: none; z-index: 5; }
#hud-top {
position: absolute; top: 24px; left: 0; right: 0;
display: flex; justify-content: space-between; align-items: flex-start;
padding: 0 36px;
}
#score-block, #multiplier-block, #speed-block {
text-align: left; line-height: 1;
}
#score-label, #mult-label, #speed-label {
font-size: 11px; letter-spacing: 4px; opacity: 0.55; margin-bottom: 8px;
}
#score-value {
font-size: 38px; font-weight: 700; letter-spacing: 2px;
text-shadow: 0 0 18px rgba(255,138,60,0.5);
}
#mult-value {
font-size: 64px; font-weight: 800; letter-spacing: -2px;
color: var(--magenta);
text-shadow: 0 0 24px rgba(255,77,143,0.7);
transition: transform 0.15s ease-out, color 0.2s;
}
#mult-value.pulse { transform: scale(1.25); color: #fff; }
#speed-value { font-size: 28px; font-weight: 700; letter-spacing: 1px; }
#speed-bar {
width: 180px; height: 3px; background: rgba(255,255,255,0.1);
margin-top: 8px; position: relative;
}
#speed-bar-fill {
position: absolute; left: 0; top: 0; bottom: 0;
background: linear-gradient(90deg, var(--orange), var(--magenta));
width: 0%; transition: width 0.1s;
box-shadow: 0 0 10px var(--orange);
}
#energy-block {
position: absolute; bottom: 36px; left: 50%; transform: translateX(-50%);
width: 380px; text-align: center;
}
#energy-bar {
width: 100%; height: 8px; background: rgba(255,255,255,0.07);
border: 1px solid rgba(111,240,255,0.25);
position: relative; overflow: hidden;
}
#energy-fill {
position: absolute; left: 0; top: 0; bottom: 0;
background: linear-gradient(90deg, #ff4d8f, #ff8a3c, #ffd66f);
width: 100%;
box-shadow: 0 0 14px var(--orange);
transition: width 0.18s;
}
#energy-label { font-size: 10px; letter-spacing: 6px; opacity: 0.5; margin-top: 8px; }
#boost-block {
position: absolute; bottom: 36px; right: 36px;
width: 220px; text-align: right;
}
#boost-bar {
width: 100%; height: 4px; background: rgba(255,255,255,0.07);
border: 1px solid rgba(255,138,60,0.3);
position: relative; overflow: hidden;
}
#boost-fill {
position: absolute; left: 0; top: 0; bottom: 0;
background: linear-gradient(90deg, #6ff0ff, #d8e2ff);
width: 0%;
box-shadow: 0 0 12px var(--cyan);
transition: width 0.15s;
}
#boost-label { font-size: 10px; letter-spacing: 6px; opacity: 0.5; margin-top: 8px; }
#nearmiss-flash {
position: fixed; inset: 0; pointer-events: none; z-index: 4;
box-shadow: inset 0 0 0 0px rgba(255,77,143,0);
transition: box-shadow 0.4s;
}
#nearmiss-flash.active { box-shadow: inset 0 0 200px 30px rgba(255,77,143,0.35); transition: box-shadow 0.05s; }
#gameover {
position: fixed; inset: 0; z-index: 10;
display: none; align-items: center; justify-content: center;
background: rgba(10,4,16,0.85);
backdrop-filter: blur(8px);
}
#gameover.show { display: flex; }
#gameover-inner {
text-align: center; padding: 48px 64px;
border: 1px solid rgba(255,138,60,0.3);
background: linear-gradient(135deg, rgba(26,8,32,0.9), rgba(10,4,16,0.9));
box-shadow: 0 0 60px rgba(255,77,143,0.2);
}
#gameover h1 {
font-size: 56px; letter-spacing: 8px; margin-bottom: 32px;
color: var(--magenta); text-shadow: 0 0 24px rgba(255,77,143,0.6);
}
.stat {
display: flex; justify-content: space-between; gap: 64px;
font-size: 13px; letter-spacing: 3px; opacity: 0.6;
margin: 10px 0; padding: 10px 0;
border-bottom: 1px solid rgba(255,255,255,0.08);
}
.stat b { font-size: 24px; opacity: 1; color: var(--orange); letter-spacing: 1px; }
#restart-btn {
margin-top: 32px; padding: 14px 40px;
background: transparent; color: var(--silver);
border: 1px solid var(--orange); font-family: inherit;
font-size: 14px; letter-spacing: 6px; cursor: pointer;
transition: all 0.2s;
}
#restart-btn:hover { background: var(--orange); color: var(--deep); box-shadow: 0 0 24px var(--orange); }
#title-screen {
position: fixed; inset: 0; z-index: 12;
display: flex; align-items: center; justify-content: center;
background: radial-gradient(ellipse at center, #2a0e2e 0%, #0a0410 80%);
}
#title-screen.hide { display: none; }
#title-inner { text-align: center; }
#title-inner h1 {
font-size: 84px; letter-spacing: 14px;
background: linear-gradient(90deg, #ff8a3c, #ff4d8f, #6ff0ff);
-webkit-background-clip: text; background-clip: text; color: transparent;
margin-bottom: 12px; font-weight: 800;
}
#title-inner p { font-size: 13px; letter-spacing: 4px; opacity: 0.6; margin: 6px 0; }
#title-inner .keys { font-size: 11px; opacity: 0.4; margin-top: 4px; }
#start-btn {
margin-top: 36px; padding: 16px 56px;
background: transparent; color: var(--orange);
border: 1px solid var(--orange); font-family: inherit;
font-size: 16px; letter-spacing: 8px; cursor: pointer;
transition: all 0.2s;
}
#start-btn:hover { background: var(--orange); color: var(--deep); box-shadow: 0 0 32px var(--orange); }
/* ── css: fx ── */
/* Vignette overlay for cinematic feel */
#vignette {
position: fixed; inset: 0; z-index: 3; pointer-events: none;
background: radial-gradient(ellipse at center,
transparent 40%,
rgba(0,0,0,0.4) 90%,
rgba(0,0,0,0.7) 100%);
}
/* Danger warning when close to obstacles */
#danger-warn {
position: fixed; inset: 0; z-index: 4; pointer-events: none;
box-shadow: inset 0 0 0 0px rgba(255,60,60,0);
transition: box-shadow 0.15s;
}
#danger-warn.active {
box-shadow: inset 0 0 120px 20px rgba(255,30,30,0.4);
transition: box-shadow 0.05s;
}
/* Speed lines effect */
#speed-lines {
position: fixed; inset: 0; z-index: 2; pointer-events: none;
background: radial-gradient(ellipse at center,
transparent 30%,
rgba(255,200,100,0.04) 60%,
rgba(255,200,100,0.12) 100%);
opacity: 0;
transition: opacity 0.3s;
}
#speed-lines.active { opacity: 1; }
</style>
</head>
<body>
<div id="app"><div id="hud">
<div id="hud-top">
<div id="score-block">
<div id="score-label">SCORE</div>
<div id="score-value">0</div>
</div>
<div id="multiplier-block">
<div id="mult-label">× MULTIPLIER</div>
<div id="mult-value">1.0</div>
</div>
<div id="speed-block">
<div id="speed-label">VELOCITY</div>
<div id="speed-value">0</div>
<div id="speed-bar"><div id="speed-bar-fill"></div></div>
</div>
</div>
<div id="energy-block">
<div id="energy-bar"><div id="energy-fill"></div></div>
<div id="energy-label">ENERGY</div>
</div>
<div id="boost-block">
<div id="boost-bar"><div id="boost-fill"></div></div>
<div id="boost-label">TURBO</div>
</div>
</div>
<div id="nearmiss-flash"></div>
<div id="gameover">
<div id="gameover-inner">
<h1>SIGNAL LOST</h1>
<div class="stat"><span>FINAL SCORE</span><b id="final-score">0</b></div>
<div class="stat"><span>MAX VELOCITY</span><b id="final-speed">0</b></div>
<div class="stat"><span>ORBS COLLECTED</span><b id="final-orbs">0</b></div>
<button id="restart-btn">REBOOT</button>
</div>
</div>
<div id="title-screen">
<div id="title-inner">
<h1>SOLAR DRIFT</h1>
<p>Pitch · Bank · Boost</p>
<p class="keys">W/S · PITCH A/D · BANK SHIFT · TURBO</p>
<p class="keys">ARROWS · MOVEMENT SPACE · START</p>
<button id="start-btn">ENGAGE</button>
</div>
</div>
<div id="vignette"></div>
<div id="danger-warn"></div>
<div id="speed-lines"></div><canvas id="game"></canvas></div>
<script>
'use strict';
// ── const: CONFIG ──
// Tunable constants
const CONFIG = {
// World
CHUNK_LENGTH: 70,
CHUNKS_AHEAD: 4,
CHUNKS_BEHIND: 2,
CANYON_HALF_WIDTH: 45,
CANYON_HEIGHT: 50,
CANYON_FLOOR_Y: -28,
// Player
BASE_SPEED: 90,
MAX_SPEED: 220,
BOOST_MULT: 1.65,
ACCEL_RATE: 18,
PITCH_RATE: 1.6,
ROLL_RATE: 2.4,
PITCH_ACCEL: 90,
ROLL_TURN: 1.8,
DRAG: 0.985,
BOUND_RADIUS: 1.6,
// Energy
ENERGY_MAX: 100,
ENERGY_DECAY: 2.4,
ORB_RECHARGE: 20,
// Boost
BOOST_MAX: 100,
BOOST_GAIN: 18,
BOOST_COST: 55,
// Scoring
NEARMISS_BASE: 5,
NEARMISS_RANGE: 10.0,
// Difficulty
DIFF_SCALE: 0.000018,
OBSTACLE_DENSITY: 0.65,
};
// ── function: makeRng ──
// Seeded RNG (mulberry32) for reproducible chunks at a given index
function makeRng(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;
};
}
// ── function: chunkSeed ──
function chunkSeed(index) {
return (index * 2654435761) ^ 0x9E3779B9;
}
// ── function: rngRange ──
function rngRange(rng, a, b) { return a + (b - a) * rng(); }
// ── function: rngInt ──
function rngInt(rng, a, b) { return Math.floor(a + (b - a + 1) * rng()); }
// ── function: rngPick ──
function rngPick(rng, arr) { return arr[Math.floor(rng() * arr.length)]; }
// ── const: STATE ──
// Global state object — created in main
const STATE = {
scene: null, camera: null, renderer: null, clock: null,
player: null, // THREE.Object3D
playerVel: new THREE.Vector3(0, 0, 0),
forwardDir: new THREE.Vector3(0, 0, -1),
rightDir: new THREE.Vector3(1, 0, 0),
upDir: new THREE.Vector3(0, 1, 0),
pitch: 0, roll: 0, yaw: 0,
speed: 0,
energy: CONFIG.ENERGY_MAX,
boost: 0,
boosting: false,
score: 0,
multiplier: 1.0,
multiplierTimer: 0,
orbs: 0,
maxSpeed: 0,
alive: false,
started: false,
// Chunks
chunks: new Map(), // index -> { group, walls, monoliths, squeezes, orbs }
firstChunkIndex: 0,
lastChunkIndex: -1,
// Input
keys: {},
// Camera FX
baseFov: 75,
shakeAmount: 0, shakeTime: 0,
// Misc
trailCooldown: 0,
lastNearMiss: 0,
prevPlayerPos: new THREE.Vector3(),
};
// ── const: Audio ──
// Web Audio API — procedural sound effects
const Audio = (() => {
let ctx = null;
let master = null;
let engineOsc = null, engineGain = null, engineFilter = null;
let windOsc = null, windGain = null;
let initialized = false;
function init() {
if (initialized) return;
try {
const AC = window.AudioContext || window.webkitAudioContext;
ctx = new AC();
master = ctx.createGain();
master.gain.value = 0.55;
master.connect(ctx.destination);
// Engine hum
engineOsc = ctx.createOscillator();
engineGain = ctx.createGain();
engineFilter = ctx.createBiquadFilter();
engineFilter.type = 'lowpass';
engineFilter.frequency.value = 380;
engineOsc.type = 'sawtooth';
engineOsc.frequency.value = 70;
engineGain.gain.value = 0.0;
engineOsc.connect(engineFilter);
engineFilter.connect(engineGain);
engineGain.connect(master);
engineOsc.start();
// Wind noise
windOsc = ctx.createOscillator();
windGain = ctx.createGain();
const windFilter = ctx.createBiquadFilter();
windFilter.type = 'bandpass';
windFilter.frequency.value = 900;
windFilter.Q.value = 0.4;
windOsc.type = 'sawtooth';
windOsc.frequency.value = 50;
windGain.gain.value = 0.0;
windOsc.connect(windFilter);
windFilter.connect(windGain);
windGain.connect(master);
windOsc.start();
initialized = true;
} catch (e) { console.warn('Audio init failed', e); }
}
function setEngine(speed01, boosting) {
if (!initialized) return;
const targetFreq = 60 + speed01 * 180 + (boosting ? 60 : 0);
engineOsc.frequency.setTargetAtTime(targetFreq, ctx.currentTime, 0.08);
engineFilter.frequency.setTargetAtTime(280 + speed01 * 1200, ctx.currentTime, 0.1);
engineGain.gain.setTargetAtTime(0.05 + speed01 * 0.18, ctx.currentTime, 0.1);
windGain.gain.setTargetAtTime(0.02 + speed01 * 0.14, ctx.currentTime, 0.1);
}
function setBoost(active) {
if (!initialized) return;
engineGain.gain.setTargetAtTime(active ? 0.32 : 0.18, ctx.currentTime, 0.05);
}
function ping(freq, dur, type) {
if (!initialized) return;
type = type || 'sine';
const o = ctx.createOscillator();
const g = ctx.createGain();
o.type = type;
o.frequency.value = freq;
g.gain.setValueAtTime(0, ctx.currentTime);
g.gain.linearRampToValueAtTime(0.35, ctx.currentTime + 0.01);
g.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + dur);
o.connect(g); g.connect(master);
o.start();
o.stop(ctx.currentTime + dur + 0.05);
}
function whoosh(intensity) {
if (!initialized) return;
const o = ctx.createOscillator();
const g = ctx.createGain();
const f = ctx.createBiquadFilter();
f.type = 'bandpass';
f.frequency.setValueAtTime(300 + intensity * 1500, ctx.currentTime);
f.frequency.exponentialRampToValueAtTime(80, ctx.currentTime + 0.35);
f.Q.value = 1.2;
o.type = 'sawtooth';
o.frequency.setValueAtTime(120, ctx.currentTime);
o.frequency.exponentialRampToValueAtTime(40, ctx.currentTime + 0.35);
g.gain.setValueAtTime(0, ctx.currentTime);
g.gain.linearRampToValueAtTime(0.15 + intensity * 0.2, ctx.currentTime + 0.02);
g.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.35);
o.connect(f); f.connect(g); g.connect(master);
o.start();
o.stop(ctx.currentTime + 0.4);
}
function orb() { ping(880, 0.18, 'triangle'); setTimeout(() => ping(1320, 0.22, 'sine'), 50); }
function nearMiss() {
ping(220, 0.4, 'square');
setTimeout(() => whoosh(0.7), 30);
}
function crash() {
if (!initialized) return;
const o = ctx.createOscillator();
const g = ctx.createGain();
o.type = 'sawtooth';
o.frequency.setValueAtTime(280, ctx.currentTime);
o.frequency.exponentialRampToValueAtTime(40, ctx.currentTime + 0.6);
g.gain.setValueAtTime(0.5, ctx.currentTime);
g.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.6);
o.connect(g); g.connect(master);
o.start();
o.stop(ctx.currentTime + 0.65);
}
return { init, setEngine, setBoost, orb, nearMiss, crash };
})();
// ── function: buildCraft ──
// Build player craft — sleek, low-poly geometric ship
function buildCraft() {
const group = new THREE.Group();
// Main hull — sharp triangular wedge
const hullMat = new THREE.MeshStandardMaterial({
color: 0xd8e2ff, metalness: 0.85, roughness: 0.28, flatShading: true,
});
const hull = new THREE.Mesh(
new THREE.ConeGeometry(0.7, 2.4, 4, 1),
hullMat
);
hull.rotation.x = -Math.PI / 2;
hull.rotation.y = Math.PI / 4;
group.add(hull);
// Top spine
const spine = new THREE.Mesh(
new THREE.BoxGeometry(0.18, 0.5, 2.6),
new THREE.MeshStandardMaterial({ color: 0x4a3a6a, metalness: 0.6, roughness: 0.4, flatShading: true })
);
spine.position.y = 0.32;
group.add(spine);
// Wings — swept delta
const wingMat = new THREE.MeshStandardMaterial({
color: 0xff8a3c, metalness: 0.7, roughness: 0.35, flatShading: true,
emissive: 0x4a1a00, emissiveIntensity: 0.4,
});
const wingGeo = new THREE.BufferGeometry();
// single delta wing on +X side
const wingVerts = new Float32Array([
0, 0, 0.6, 1.4, -0.05, -1.0, 0, 0, -1.2,
0, 0, 0.6, 0, 0.15, 0.2, 1.4, -0.05, -1.0,
]);
wingGeo.setAttribute('position', new THREE.BufferAttribute(wingVerts, 3));
wingGeo.computeVertexNormals();
const wingR = new THREE.Mesh(wingGeo, wingMat);
group.add(wingR);
const wingL = wingR.clone();
wingL.scale.x = -1;
group.add(wingL);
// Tail fin
const fin = new THREE.Mesh(
new THREE.BoxGeometry(0.06, 0.7, 0.5),
new THREE.MeshStandardMaterial({ color: 0xff4d8f, metalness: 0.5, roughness: 0.4, flatShading: true, emissive: 0x4a0a1a, emissiveIntensity: 0.6 })
);
fin.position.set(0, 0.55, 1.0);
group.add(fin);
// Engine glow at back
const engineMat = new THREE.MeshBasicMaterial({ color: 0x6ff0ff });
const engineGlow = new THREE.Mesh(new THREE.SphereGeometry(0.22, 8, 8), engineMat);
engineGlow.position.set(0, 0, 1.25);
engineGlow.name = 'engine';
group.add(engineGlow);
// Engine halo
const haloMat = new THREE.MeshBasicMaterial({ color: 0x6ff0ff, transparent: true, opacity: 0.4 });
const halo = new THREE.Mesh(new THREE.SphereGeometry(0.4, 8, 8), haloMat);
halo.position.set(0, 0, 1.25);
halo.name = 'halo';
group.add(halo);
group.userData.engineGlow = engineGlow;
group.userData.halo = halo;
return group;
}
// ── function: initScene ──
// Initialize the Three.js scene
function initScene() {
const canvas = document.getElementById('game');
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, powerPreference: 'high-performance' });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.outputEncoding = THREE.sRGBEncoding;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.05;
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
const scene = new THREE.Scene();
const canvas2 = document.createElement('canvas');
canvas2.width = 4; canvas2.height = 256;
const ctx2 = canvas2.getContext('2d');
const grad = ctx2.createLinearGradient(0, 0, 0, 256);
grad.addColorStop(0.0, '#1a0830');
grad.addColorStop(0.35, '#3a1248');
grad.addColorStop(0.65, '#a83a52');
grad.addColorStop(0.85, '#ff7a3c');
grad.addColorStop(1.0, '#ffd680');
ctx2.fillStyle = grad;
ctx2.fillRect(0, 0, 4, 256);
const skyTex = new THREE.CanvasTexture(canvas2);
skyTex.magFilter = THREE.LinearFilter;
scene.background = skyTex;
scene.fog = new THREE.Fog(0x3a1248, 80, 320);
const camera = new THREE.PerspectiveCamera(STATE.baseFov, window.innerWidth / window.innerHeight, 0.5, 800);
camera.position.set(0, 0, 0);
// Lighting
const sun = new THREE.DirectionalLight(0xffb070, 1.4);
sun.position.set(60, 40, 30);
sun.castShadow = true;
sun.shadow.mapSize.set(512, 512);
sun.shadow.camera.near = 0.5;
sun.shadow.camera.far = 250;
sun.shadow.camera.left = -60;
sun.shadow.camera.right = 60;
sun.shadow.camera.top = 60;
sun.shadow.camera.bottom = -60;
sun.shadow.bias = -0.0005;
scene.add(sun);
const fill = new THREE.DirectionalLight(0x6f4dff, 0.4);
fill.position.set(-50, 30, -20);
scene.add(fill);
const rim = new THREE.DirectionalLight(0x6ff0ff, 0.3);
rim.position.set(0, -10, 50);
scene.add(rim);
scene.add(new THREE.AmbientLight(0x4a2860, 0.5));
// Distant sun disc
const sunDisc = new THREE.Mesh(
new THREE.CircleGeometry(28, 32),
new THREE.MeshBasicMaterial({ color: 0xffd680, transparent: true, opacity: 0.95 })
);
sunDisc.position.set(180, 35, -400);
sunDisc.lookAt(0, 35, 0);
scene.add(sunDisc);
const sunHalo = new THREE.Mesh(
new THREE.RingGeometry(28, 44, 32),
new THREE.MeshBasicMaterial({ color: 0xff8a3c, transparent: true, opacity: 0.45, side: THREE.DoubleSide })
);
sunHalo.position.copy(sunDisc.position);
sunHalo.lookAt(0, 35, 0);
scene.add(sunHalo);
// Distance haze planes for parallax depth
const hazeMat = new THREE.MeshBasicMaterial({ color: 0x6a2a4a, transparent: true, opacity: 0.25, side: THREE.DoubleSide });
for (let i = 0; i < 5; i++) {
const h = new THREE.Mesh(new THREE.PlaneGeometry(800, 100), hazeMat.clone());
h.position.set(0, 20, -200 - i * 200);
h.material.opacity = 0.05 + i * 0.03;
scene.add(h);
}
STATE.renderer = renderer;
STATE.scene = scene;
STATE.camera = camera;
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
return { scene, camera, renderer };
}
// ── function: buildChunk ──
// Build one chunk of canyon: walls, monoliths, squeezes, orbs
function buildChunk(index) {
const group = new THREE.Group();
const localZBase = -index * CONFIG.CHUNK_LENGTH;
group.position.z = localZBase;
const rng = makeRng(chunkSeed(index));
const data = { index, group, walls: [], monoliths: [], squeezes: [], orbs: [], colliders: [] };
// Difficulty scales with distance
const dist = index * CONFIG.CHUNK_LENGTH;
const diff = Math.min(1, dist * CONFIG.DIFF_SCALE);
const wallNarrow = CONFIG.CANYON_HALF_WIDTH * (0.75 + diff * 0.15);
const wallHeight = CONFIG.CANYON_HEIGHT * (0.7 + diff * 0.4);
// ---- Canyon walls ----
const wallMatA = new THREE.MeshStandardMaterial({ color: 0x2a0820, metalness: 0.05, roughness: 0.9, flatShading: true });
const wallMatB = new THREE.MeshStandardMaterial({ color: 0x4a1230, metalness: 0.05, roughness: 0.9, flatShading: true });
const wallMatC = new THREE.MeshStandardMaterial({ color: 0x6a1f3a, metalness: 0.05, roughness: 0.9, flatShading: true });
const slabsPerSide = 5;
for (let s = 0; s < slabsPerSide; s++) {
const localZ = -s * (CONFIG.CHUNK_LENGTH / slabsPerSide) + CONFIG.CHUNK_LENGTH / 2;
const worldZ = localZBase + localZ;
const centerX = getCenterOffset(worldZ);
for (let side of [-1, 1]) {
const w = wallNarrow + rngRange(rng, -3, 3);
const h = wallHeight + rngRange(rng, -6, 10);
const tilt = rngRange(rng, -0.18, 0.18);
const mat = [wallMatA, wallMatB, wallMatC][rngInt(rng, 0, 2)];
const geo = new THREE.BoxGeometry(6 + rngRange(rng, 0, 6), h, CONFIG.CHUNK_LENGTH / slabsPerSide + 1);
const mesh = new THREE.Mesh(geo, mat);
const wx = centerX + side * (w + 3);
mesh.position.set(wx, CONFIG.CANYON_FLOOR_Y + h / 2, localZ);
mesh.rotation.z = tilt;
mesh.castShadow = true;
mesh.receiveShadow = true;
group.add(mesh);
data.walls.push(mesh);
data.colliders.push({
type: 'wall', mesh,
x: wx, y: CONFIG.CANYON_FLOOR_Y + h / 2, z: worldZ,
hx: 3 + rngRange(rng, 0, 3), hy: h / 2, hz: CONFIG.CHUNK_LENGTH / slabsPerSide / 2 + 0.5,
rotZ: tilt
});
}
}
// ---- Floor ----
const floorMat = new THREE.MeshStandardMaterial({ color: 0x1a0612, metalness: 0.0, roughness: 1.0, flatShading: true });
const floor = new THREE.Mesh(
new THREE.PlaneGeometry(wallNarrow * 2.2, CONFIG.CHUNK_LENGTH),
floorMat
);
floor.rotation.x = -Math.PI / 2;
floor.position.y = CONFIG.CANYON_FLOOR_Y;
floor.position.z = CONFIG.CHUNK_LENGTH / 2;
floor.position.x = getCenterOffset(localZBase + CONFIG.CHUNK_LENGTH / 2);
floor.receiveShadow = true;
group.add(floor);
// ---- Floating monoliths — densely placed in the flight corridor ----
// Early chunks: lots of monoliths. Later: fewer but more dangerous.
const monoCount = index < 4
? rngInt(rng, 3, 5)
: 2 + rngInt(rng, 0, 2) + (diff > 0.2 ? rngInt(rng, 0, 1) : 0);
for (let m = 0; m < monoCount; m++) {
const localZ = -rngRange(rng, 6, CONFIG.CHUNK_LENGTH - 6);
const worldZ = localZBase + localZ;
const centerX = getCenterOffset(worldZ);
// Place pillars across the navigable band so the player must bank
// 50% directly in the path (within 8m of centerline)
// 50% off to one side (within 8-20m)
let x;
const r = rng();
if (r < 0.5) {
x = centerX + rngRange(rng, -8, 8);
} else {
const sideSign = rng() < 0.5 ? -1 : 1;
x = centerX + sideSign * rngRange(rng, 8, 18);
}
const y = rngRange(rng, -3, 4);
const w = rngRange(rng, 1.4, 2.6);
const h = rngRange(rng, 5, 10);
const d = rngRange(rng, 3, 6); // deeper in z to extend near-miss window
const mat = new THREE.MeshStandardMaterial({
color: rngPick(rng, [0x3a1248, 0x5a1f3a, 0x6a2a4a, 0x4a1a4a, 0x2a0a2a]),
metalness: 0.4, roughness: 0.5, flatShading: true
});
const mesh = new THREE.Mesh(new THREE.BoxGeometry(w, h, d), mat);
mesh.position.set(x, y, localZ);
mesh.rotation.y = rngRange(rng, 0, Math.PI);
mesh.rotation.z = rngRange(rng, -0.2, 0.2);
mesh.castShadow = true;
group.add(mesh);
data.monoliths.push(mesh);
data.colliders.push({
type: 'monolith', mesh,
x, y, z: worldZ, hx: w / 2 + 0.3, hy: h / 2, hz: d / 2 + 0.3
});
}
// ---- Wall spikes: protrude from the walls into the canyon ----
// Creates near-miss opportunities with the walls themselves
const spikeCount = rngInt(rng, 0, 2);
for (let sp = 0; sp < spikeCount; sp++) {
const localZ = -rngRange(rng, 6, CONFIG.CHUNK_LENGTH - 6);
const worldZ = localZBase + localZ;
const centerX = getCenterOffset(worldZ);
const side = rng() < 0.5 ? -1 : 1;
const spikeLen = rngRange(rng, 6, 14);
const spikeW = 4;
const spikeH = rngRange(rng, 4, 10);
const geo = new THREE.BoxGeometry(spikeLen, spikeH, 5);
const mat = new THREE.MeshStandardMaterial({
color: 0x3a1248, metalness: 0.3, roughness: 0.6, flatShading: true
});
const mesh = new THREE.Mesh(geo, mat);
const wx = centerX + side * (wallNarrow - spikeLen / 2 - 2);
mesh.position.set(wx, CONFIG.CANYON_FLOOR_Y + spikeH / 2 + 4, localZ);
mesh.castShadow = true;
group.add(mesh);
data.colliders.push({
type: 'spike', mesh,
x: wx, y: mesh.position.y, z: worldZ,
hx: spikeLen / 2 + 0.3, hy: spikeH / 2 + 0.3, hz: 2.8
});
}
// ---- Tight squeezes ----
if (rng() < 0.5 + diff * 0.3) {
const localZ = -rngRange(rng, 12, CONFIG.CHUNK_LENGTH - 12);
const worldZ = localZBase + localZ;
const centerX = getCenterOffset(worldZ);
const gap = 6.5 - diff * 2.5 + rngRange(rng, -0.8, 0.8);
const archH = 9 + rngRange(rng, 0, 3);
const fullW = wallNarrow;
for (let side of [-1, 1]) {
const w = (fullW - gap) / 2 + 3;
const geo = new THREE.BoxGeometry(w, archH, 4);
const mat = new THREE.MeshStandardMaterial({
color: 0x2a0820, metalness: 0.1, roughness: 0.85, flatShading: true
});
const mesh = new THREE.Mesh(geo, mat);
const wx = centerX + side * (fullW - w / 2);
mesh.position.set(wx, archH / 2 - 3, localZ);
mesh.castShadow = true;
mesh.receiveShadow = true;
group.add(mesh);
data.squeezes.push(mesh);
data.colliders.push({
type: 'squeeze', mesh,
x: wx, y: mesh.position.y, z: worldZ,
hx: w / 2 + 0.3, hy: archH / 2 + 0.3, hz: 2.3
});
}
}
// ---- Orbs ----
const orbCount = rngInt(rng, 5, 8);
for (let o = 0; o < orbCount; o++) {
const localZ = -rngRange(rng, 4, CONFIG.CHUNK_LENGTH - 4);
const worldZ = localZBase + localZ;
const centerX = getCenterOffset(worldZ);
const onPath = rng() < 0.5;
const ox = onPath
? centerX + rngRange(rng, -3, 3)
: rngRange(rng, -wallNarrow * 0.5, wallNarrow * 0.5);
const oy = rngRange(rng, -2, 5);
const orb = makeOrb();
orb.position.set(ox, oy, localZ);
group.add(orb);
data.orbs.push({ mesh: orb, x: ox, y: oy, z: worldZ, alive: true, t: rng() * Math.PI * 2 });
}
STATE.scene.add(group);
STATE.chunks.set(index, data);
STATE.lastChunkIndex = Math.max(STATE.lastChunkIndex, index);
return data;
}
// ── function: makeOrb ──
// Glowing energy orb — single mesh with sprite halo to keep draw calls low
function makeOrb() {
const g = new THREE.Group();
const core = new THREE.Mesh(
new THREE.IcosahedronGeometry(0.55, 0),
new THREE.MeshBasicMaterial({ color: 0xfff0a0 })
);
g.add(core);
const halo = new THREE.Mesh(
new THREE.SphereGeometry(1.1, 8, 8),
new THREE.MeshBasicMaterial({ color: 0xff8a3c, transparent: true, opacity: 0.35 })
);
g.add(halo);
g.userData.core = core;
g.userData.halo = halo;
return g;
}
// ── function: updateChunks ──
// Manage chunk lifecycle: spawn ahead, despawn behind
function updateChunks() {
const pz = STATE.player.position.z;
// Current chunk index based on z (z is negative forward)
const curIdx = Math.max(0, Math.floor(-pz / CONFIG.CHUNK_LENGTH));
STATE.firstChunkIndex = curIdx;
// Spawn forward
for (let i = curIdx; i <= curIdx + CONFIG.CHUNKS_AHEAD; i++) {
if (!STATE.chunks.has(i)) buildChunk(i);
}
// Despawn behind
for (const [idx, data] of STATE.chunks) {
if (idx < curIdx - CONFIG.CHUNKS_BEHIND) {
// Dispose
STATE.scene.remove(data.group);
data.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();
}
});
STATE.chunks.delete(idx);
}
}
}
// ── const: Particles ──
// Particle systems — engine trail + speed debris
const Particles = (() => {
let trailGeo, trailMat, trailPoints;
let debrisPool = [];
const DEBRIS_MAX = 60;
const TRAIL_MAX = 180;
const positions = new Float32Array(TRAIL_MAX * 3);
const colors = new Float32Array(TRAIL_MAX * 3);
let trailHead = 0, trailCount = 0;
function init(scene) {
// Trail as Points
trailGeo = new THREE.BufferGeometry();
trailGeo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
trailGeo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
trailMat = new THREE.PointsMaterial({
size: 0.7, vertexColors: true, transparent: true, opacity: 0.9,
blending: THREE.AdditiveBlending, depthWrite: false,
sizeAttenuation: true,
});
trailPoints = new THREE.Points(trailGeo, trailMat);
trailPoints.frustumCulled = false;
scene.add(trailPoints);
// Debris pool
const dGeo = new THREE.BoxGeometry(0.2, 0.2, 1.0);
const dMat = new THREE.MeshBasicMaterial({ color: 0xffd680, transparent: true, opacity: 0.7 });
for (let i = 0; i < DEBRIS_MAX; i++) {
const m = new THREE.Mesh(dGeo, dMat.clone());
m.visible = false;
m.userData = { vx: 0, vy: 0, vz: 0, life: 0, max: 0 };
scene.add(m);
debrisPool.push(m);
}
}
function emitTrail(x, y, z, intensity, boosting) {
const idx = trailHead;
positions[idx * 3] = x;
positions[idx * 3 + 1] = y;
positions[idx * 3 + 2] = z;
if (boosting) {
colors[idx * 3] = 1.0; colors[idx * 3 + 1] = 0.6; colors[idx * 3 + 2] = 0.15;
} else {
const t = intensity;
colors[idx * 3] = 0.3 + t * 0.3;
colors[idx * 3 + 1] = 0.7 + t * 0.3;
colors[idx * 3 + 2] = 1.0;
}
trailHead = (trailHead + 1) % TRAIL_MAX;
if (trailCount < TRAIL_MAX) trailCount++;
}
function spawnDebris(camera, count, speed) {
for (let n = 0; n < count; n++) {
const m = debrisPool.find(d => !d.visible);
if (!m) return;
m.position.set(
camera.position.x + (Math.random() - 0.5) * 80,
camera.position.y + (Math.random() - 0.5) * 30,
camera.position.z - 60 - Math.random() * 30
);
m.userData.vx = (Math.random() - 0.5) * 1.5;
m.userData.vy = (Math.random() - 0.5) * 1.5;
m.userData.vz = speed * (0.8 + Math.random() * 0.4);
m.userData.life = 0;
m.userData.max = 0.6 + Math.random() * 0.4;
m.material.color.setHex(Math.random() < 0.5 ? 0xff8a3c : 0x6ff0ff);
m.material.opacity = 0.9;
m.visible = true;
}
}
function updateTrail(dt) {
trailGeo.attributes.position.needsUpdate = true;
trailGeo.attributes.color.needsUpdate = true;
}
function updateDebris(dt) {
for (const m of debrisPool) {
if (!m.visible) continue;
m.userData.life += dt;
m.position.x += m.userData.vx * dt;
m.position.y += m.userData.vy * dt;
m.position.z += m.userData.vz * dt;
m.material.opacity = Math.max(0, 0.9 * (1 - m.userData.life / m.userData.max));
if (m.userData.life >= m.userData.max) m.visible = false;
}
}
return { init, emitTrail, spawnDebris, updateTrail, updateDebris, trailHead, TRAIL_MAX };
})();
// ── function: checkCollisions ──
// Collision detection: player (sphere) vs OBBs (with optional Z-rotation)
function checkCollisions() {
const p = STATE.player.position;
const r = CONFIG.BOUND_RADIUS;
for (const [, data] of STATE.chunks) {
for (const c of data.colliders) {
// World-space AABB check (ignores rotation for monoliths/squeezes — fine for this aesthetic)
const dx = Math.abs(p.x - c.x);
const dy = Math.abs(p.y - c.y);
const dz = Math.abs(p.z - c.z);
if (dx < c.hx + r && dy < c.hy + r && dz < c.hz + r) {
return true;
}
}
}
return false;
}
// ── function: checkOrbPickup ──
// Orb collection: simple sphere-sphere with active orbs
function checkOrbPickup() {
const p = STATE.player.position;
const r2 = (CONFIG.BOUND_RADIUS + 1.0) * (CONFIG.BOUND_RADIUS + 1.0);
for (const [, data] of STATE.chunks) {
for (const o of data.orbs) {
if (!o.alive) continue;
const dx = p.x - o.x, dy = p.y - o.y, dz = p.z - o.z;
if (dx * dx + dy * dy + dz * dz < r2) {
o.alive = false;
o.mesh.visible = false;
STATE.energy = Math.min(CONFIG.ENERGY_MAX, STATE.energy + CONFIG.ORB_RECHARGE);
STATE.orbs++;
STATE.boost = Math.min(CONFIG.BOOST_MAX, STATE.boost + CONFIG.BOOST_GAIN);
Audio.orb();
}
}
}
}
// ── function: checkNearMiss ──
// Near-miss detection: considers the "passing window" in z, with x/y proximity
function checkNearMiss() {
const p = STATE.player.position;
let bestIntensity = 0;
const speedFactor = STATE.speed / 120;
// Larger pass window to accommodate lower frame rates
const passWindow = Math.max(8, speedFactor * 8);
for (const [, data] of STATE.chunks) {
for (const c of data.colliders) {
const dz = Math.abs(p.z - c.z);
if (dz > passWindow + c.hz) continue;
const dx = Math.max(0, Math.abs(p.x - c.x) - c.hx);
const dy = Math.max(0, Math.abs(p.y - c.y) - c.hy);
// Compute proximity in the x/y plane
const dPerp = Math.sqrt(dx * dx + dy * dy);
if (dPerp > CONFIG.NEARMISS_RANGE) continue;
const intensity = 1 - (dPerp / CONFIG.NEARMISS_RANGE);
if (intensity > bestIntensity) {
bestIntensity = intensity;
}
}
}
if (bestIntensity > 0) {
const ddt = dt();
// Punchy multiplier gain
STATE.multiplier = Math.min(8, STATE.multiplier + bestIntensity * bestIntensity * 1.2 * ddt * 6);
STATE.multiplierTimer = 1.5;
// Award points
const gained = CONFIG.NEARMISS_BASE * bestIntensity * STATE.multiplier * ddt * 15;
STATE.score += gained;
if (bestIntensity > 0.3) {
triggerNearMissFlash(bestIntensity);
STATE.shakeAmount = Math.max(STATE.shakeAmount, bestIntensity * 0.35);
if (performance.now() - STATE.lastNearMiss > 120) {
Audio.nearMiss();
STATE.lastNearMiss = performance.now();
}
}
}
// Decay multiplier slowly
STATE.multiplierTimer -= dt();
if (STATE.multiplierTimer <= 0) {
STATE.multiplier = Math.max(1.0, STATE.multiplier - dt() * 0.2);
}
}
// ── let: _lastTime ──
let _lastTime = performance.now();
// ── function: dt ──
function dt() {
const now = performance.now();
const d = (now - _lastTime) / 1000;
_lastTime = now;
return Math.min(0.05, d);
}
// ── function: resetDt ──
function resetDt() { _lastTime = performance.now(); }
// ── function: triggerNearMissFlash ──
function triggerNearMissFlash(intensity) {
const el = document.getElementById('nearmiss-flash');
if (!el) return;
el.classList.add('active');
clearTimeout(triggerNearMissFlash._t);
triggerNearMissFlash._t = setTimeout(() => el.classList.remove('active'), 120);
const m = document.getElementById('mult-value');
if (m) {
m.classList.add('pulse');
clearTimeout(triggerNearMissFlash._m);
triggerNearMissFlash._m = setTimeout(() => m.classList.remove('pulse'), 140);
}
}
// ── function: updatePlayer ──
// Player flight physics — pitch/bank-based, not axis-based
function updatePlayer(dt) {
const k = STATE.keys;
// Pitch input: W/S or Up/Down
const pitchIn = (k['KeyW'] || k['ArrowUp'] ? 1 : 0) - (k['KeyS'] || k['ArrowDown'] ? 1 : 0);
// Roll input: A/D or Left/Right (banking)
const rollIn = (k['KeyA'] || k['ArrowLeft'] ? 1 : 0) - (k['KeyD'] || k['ArrowRight'] ? 1 : 0);
// Boost: Shift
STATE.boosting = !!(k['ShiftLeft'] || k['ShiftRight']) && STATE.boost > 0;
if (STATE.boosting) {
STATE.boost = Math.max(0, STATE.boost - CONFIG.BOOST_COST * dt);
}
// Target speed
const diff = Math.min(1, STATE.score * CONFIG.DIFF_SCALE);
const baseMax = CONFIG.MAX_SPEED * (0.55 + diff * 0.55);
const targetSpeed = (STATE.boosting ? baseMax * CONFIG.BOOST_MULT : baseMax);
STATE.speed += (targetSpeed - STATE.speed) * Math.min(1, dt * 2.2);
STATE.maxSpeed = Math.max(STATE.maxSpeed, STATE.speed);
// Pitch angle (climb/dive) — affects y velocity
STATE.pitch -= pitchIn * CONFIG.PITCH_RATE * dt;
STATE.pitch *= 0.96;
STATE.pitch = Math.max(-0.9, Math.min(0.9, STATE.pitch));
// Roll (banking) — affects yaw/turn
STATE.roll -= rollIn * CONFIG.ROLL_RATE * dt;
STATE.roll *= 0.92;
// Yaw driven by roll
STATE.yaw -= STATE.roll * CONFIG.ROLL_TURN * dt;
// Self-level roll toward 0
STATE.roll += (0 - STATE.roll) * dt * 1.4;
// Velocity from orientation
const cosY = Math.cos(STATE.yaw), sinY = Math.sin(STATE.yaw);
const cosP = Math.cos(STATE.pitch), sinP = Math.sin(STATE.pitch);
// Forward vector
const fx = -sinY * cosP;
const fy = sinP;
const fz = -cosY * cosP;
STATE.forwardDir.set(fx, fy, fz);
STATE.rightDir.set(cosY, 0, -sinY);
STATE.upDir.crossVectors(STATE.rightDir, STATE.forwardDir).normalize();
// Apply forward speed as displacement
const p = STATE.player.position;
p.x += fx * STATE.speed * dt;
p.y += fy * STATE.speed * dt;
p.z += fz * STATE.speed * dt;
// Vertical bounds
const yMax = CONFIG.CANYON_FLOOR_Y + CONFIG.CANYON_HEIGHT * 0.7;
const yMin = CONFIG.CANYON_FLOOR_Y + 3;
if (p.y > yMax) p.y = yMax;
if (p.y < yMin) {
p.y = yMin;
STATE.pitch = Math.max(0, STATE.pitch);
}
// Lateral bounds — soft, follow the meandering centerline
const centerX = getCenterOffset(p.z);
const diffScale = Math.min(1, STATE.score * CONFIG.DIFF_SCALE);
const wallNarrow = CONFIG.CANYON_HALF_WIDTH * (0.75 + diffScale * 0.15);
const latMax = centerX + wallNarrow * 0.78;
const latMin = centerX - wallNarrow * 0.78;
if (p.x > latMax) {
p.x = latMax;
STATE.roll = Math.min(0, STATE.roll); // prevent further right roll
}
if (p.x < latMin) {
p.x = latMin;
STATE.roll = Math.max(0, STATE.roll); // prevent further left roll
}
// Apply orientation to craft mesh
STATE.player.rotation.set(0, 0, 0);
STATE.player.rotateY(STATE.yaw);
STATE.player.rotateX(STATE.pitch);
STATE.player.rotateZ(STATE.roll);
// Engine glow pulse
const engineGlow = STATE.player.userData.engineGlow;
const halo = STATE.player.userData.halo;
if (engineGlow) {
const s = 0.9 + (STATE.speed / CONFIG.MAX_SPEED) * 0.7 + (STATE.boosting ? 0.6 : 0);
engineGlow.scale.setScalar(s);
engineGlow.material.color.setHex(STATE.boosting ? 0xff8a3c : 0x6ff0ff);
}
if (halo) {
const s = 1 + Math.sin(performance.now() * 0.012) * 0.15 + (STATE.boosting ? 0.4 : 0);
halo.scale.setScalar(s);
halo.material.opacity = 0.3 + (STATE.boosting ? 0.3 : 0);
}
}
// ── function: updateHUD ──
// Update HUD elements
function updateHUD() {
const score = Math.floor(STATE.score);
const speed = Math.floor(STATE.speed);
document.getElementById('score-value').textContent = score.toLocaleString();
document.getElementById('speed-value').textContent = speed;
document.getElementById('mult-value').textContent = STATE.multiplier.toFixed(1);
document.getElementById('energy-fill').style.width = (STATE.energy / CONFIG.ENERGY_MAX * 100) + '%';
document.getElementById('boost-fill').style.width = (STATE.boost / CONFIG.BOOST_MAX * 100) + '%';
document.getElementById('speed-bar-fill').style.width = (STATE.speed / CONFIG.MAX_SPEED * 100) + '%';
}
// ── function: updateCamera ──
// Update camera: follow, shake, FOV pump
function updateCamera(dt) {
const cam = STATE.camera;
const p = STATE.player;
// Third-person chase position — slightly behind & above
const offset = new THREE.Vector3(0, 3.2, 9.5);
// Apply roll rotation to offset
const cs = Math.cos(STATE.roll * 0.5), sn = Math.sin(STATE.roll * 0.5);
const ox = offset.x * cs - offset.y * sn;
const oy = offset.x * sn + offset.y * cs;
const target = new THREE.Vector3(p.position.x + ox, p.position.y + oy, p.position.z + offset.z);
cam.position.lerp(target, Math.min(1, dt * 6));
// Look ahead
const look = new THREE.Vector3(
p.position.x + STATE.forwardDir.x * 8,
p.position.y + STATE.forwardDir.y * 4,
p.position.z + STATE.forwardDir.z * 8
);
cam.lookAt(look);
// FOV pump with speed + boost
const targetFov = STATE.baseFov + (STATE.speed / CONFIG.MAX_SPEED) * 32 + (STATE.boosting ? 22 : 0);
cam.fov += (targetFov - cam.fov) * Math.min(1, dt * 3);
cam.updateProjectionMatrix();
// Camera shake
if (STATE.shakeAmount > 0.001) {
cam.position.x += (Math.random() - 0.5) * STATE.shakeAmount;
cam.position.y += (Math.random() - 0.5) * STATE.shakeAmount;
STATE.shakeAmount *= Math.pow(0.001, dt); // decay
}
}
// ── function: updateOrbs ──
// Animate orbs (rotation, bobbing, pulse)
function updateOrbs(t) {
for (const [, data] of STATE.chunks) {
for (const o of data.orbs) {
if (!o.alive) continue;
o.t += dt();
o.mesh.userData.core.scale.setScalar(0.85 + Math.sin(o.t * 4) * 0.15);
o.mesh.userData.halo.scale.setScalar(1 + Math.sin(o.t * 3) * 0.2);
o.mesh.rotation.y = o.t * 0.8;
}
}
}
// ── function: triggerGameOver ──
// Game over flow
function triggerGameOver() {
STATE.alive = false;
Audio.crash();
document.getElementById('final-score').textContent = Math.floor(STATE.score).toLocaleString();
document.getElementById('final-speed').textContent = Math.floor(STATE.maxSpeed);
document.getElementById('final-orbs').textContent = STATE.orbs;
document.getElementById('gameover').classList.add('show');
}
// ── function: restartGame ──
function restartGame() {
// Clear chunks
for (const [, data] of STATE.chunks) {
STATE.scene.remove(data.group);
data.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();
}
});
}
STATE.chunks.clear();
STATE.lastChunkIndex = -1;
// Reset state
STATE.player.position.set(0, 0, 0);
STATE.player.rotation.set(0, 0, 0);
STATE.pitch = 0; STATE.roll = 0; STATE.yaw = 0;
STATE.speed = 0;
STATE.energy = CONFIG.ENERGY_MAX;
STATE.boost = CONFIG.BOOST_MAX * 0.4;
STATE.score = 0;
STATE.multiplier = 1.0;
STATE.multiplierTimer = 0;
STATE.orbs = 0;
STATE.maxSpeed = 0;
STATE.alive = true;
STATE.shakeAmount = 0;
resetDt();
// Pre-build a few chunks
updateChunks();
document.getElementById('gameover').classList.remove('show');
}
// ── function: setupInput ──
// Input wiring
function setupInput() {
window.addEventListener('keydown', e => {
STATE.keys[e.code] = true;
if (e.code === 'Space') {
if (!STATE.started) startGame();
e.preventDefault();
}
});
window.addEventListener('keyup', e => { STATE.keys[e.code] = false; });
}
// ── function: startGame ──
function startGame() {
Audio.init();
document.getElementById('title-screen').classList.add('hide');
STATE.started = true;
STATE.alive = true;
resetDt();
}
// ── function: loop ──
// Main animation loop
function loop() {
requestAnimationFrame(loop);
const d = Math.min(0.05, dt());
// Animate orbs always
updateOrbs(performance.now() / 1000);
Particles.updateTrail(d);
Particles.updateDebris(d);
if (STATE.started && STATE.alive) {
updatePlayer(d);
updateChunks();
checkOrbPickup();
checkNearMiss();
// Energy decay
STATE.energy -= CONFIG.ENERGY_DECAY * d * (STATE.boosting ? 1.6 : 1);
if (STATE.energy <= 0) {
STATE.energy = 0;
triggerGameOver();
}
// Score for distance
STATE.score += d * (10 * STATE.multiplier);
// Engine trail
STATE.trailCooldown -= d;
if (STATE.trailCooldown <= 0) {
const p = STATE.player.position;
const back = STATE.forwardDir.clone().multiplyScalar(-1.4);
Particles.emitTrail(
p.x + back.x + (Math.random() - 0.5) * 0.3,
p.y + back.y,
p.z + back.z,
STATE.speed / CONFIG.MAX_SPEED,
STATE.boosting
);
STATE.trailCooldown = 0.04;
}
// Speed debris
const speedRatio = STATE.speed / CONFIG.MAX_SPEED;
if (speedRatio > 0.5 && Math.random() < (speedRatio - 0.4) * 0.7) {
Particles.spawnDebris(STATE.camera, 2, STATE.speed);
}
// Audio engine
Audio.setEngine(speedRatio, STATE.boosting);
Audio.setBoost(STATE.boosting);
updateCamera(d);
updateHUD();
updateDangerWarning();
} else if (STATE.started) {
updateCamera(d);
}
if (STATE.renderer && STATE.scene && STATE.camera) {
STATE.renderer.render(STATE.scene, STATE.camera);
}
}
// ── function: bootGame ──
// Initialize everything and kick off the loop
function bootGame() {
const { scene, camera, renderer } = initScene();
// Build player
const craft = buildCraft();
craft.position.set(0, 0, 0);
scene.add(craft);
STATE.player = craft;
// Pre-spawn first chunks
for (let i = 0; i <= CONFIG.CHUNKS_AHEAD; i++) buildChunk(i);
Particles.init(scene);
setupInput();
document.getElementById('start-btn').addEventListener('click', () => {
Audio.init();
startGame();
});
document.getElementById('restart-btn').addEventListener('click', () => {
Audio.init();
restartGame();
});
resetDt();
STATE.clock = new THREE.Clock();
loop();
}
// ── function: getCenterOffset ──
// Meandering centerline — tight enough to force near-misses with the walls
function getCenterOffset(zWorld) {
const t = zWorld * 0.0035;
return Math.sin(t) * 8 + Math.sin(t * 0.43 + 1.3) * 4;
}
// ── function: updateDangerWarning ──
// Update the danger warning overlay based on proximity to obstacles
function updateDangerWarning() {
const el = document.getElementById('danger-warn');
const sl = document.getElementById('speed-lines');
if (!el) return;
const p = STATE.player.position;
let minDist = Infinity;
for (const [, data] of STATE.chunks) {
for (const c of data.colliders) {
const dz = Math.abs(p.z - c.z);
if (dz > 12) continue;
const dx = Math.max(0, Math.abs(p.x - c.x) - c.hx);
const dy = Math.max(0, Math.abs(p.y - c.y) - c.hy);
const d = Math.sqrt(dx * dx + dy * dy);
if (d < minDist) minDist = d;
}
}
if (minDist < 5) {
el.classList.add('active');
} else {
el.classList.remove('active');
}
// Speed lines
const speedRatio = STATE.speed / CONFIG.MAX_SPEED;
if (sl) {
if (speedRatio > 0.75 || STATE.boosting) sl.classList.add('active');
else sl.classList.remove('active');
}
}
// ── main ──
window.addEventListener('load', () => {
window.STATE = STATE;
window.CONFIG = CONFIG;
bootGame();
});
</script>
</body>
</html>
<!-- agent-meta {"model":"minimax/minimax-m3","provider":"openrouter","persona":"composer","sessionId":"7330b0be-0800-47ae-a8b2-84c80000481a","tokensIn":6179161,"tokensOut":85571,"tokensTotal":6264732,"turns":97,"toolCalls":130,"failedToolCalls":4,"timestamp":"2026-07-13T21:45:50.216Z"} -->