Canyon Run
Qwen 3.8 · full
◈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
No human review yet. Unreviewed does not mean unsuccessful.
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 42,055 bytes · SHA-256 5eb5e76d5601
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>CANYON RUSH — High-Speed Procedural Canyon Racer</title>
<style>
:root{
--orange:#ff8a4d; --gold:#ffd9a0; --silver:#cfd3e8; --purple:#8a63c9; --deep:#120820;
}
*{margin:0;padding:0;box-sizing:border-box}
html,body{width:100%;height:100%;overflow:hidden;background:var(--deep)}
body{font-family:'Segoe UI',system-ui,-apple-system,sans-serif;color:var(--silver);user-select:none;-webkit-user-select:none;touch-action:none}
canvas{display:block}
.mono{font-family:ui-monospace,'Cascadia Code','SF Mono',Menlo,Consolas,monospace}
[hidden]{display:none !important}
#edgeVig{position:fixed;inset:0;pointer-events:none;z-index:9;box-shadow:inset 0 0 130px 24px rgba(8,2,18,.6)}
#vignette{position:fixed;inset:0;pointer-events:none;z-index:9;box-shadow:inset 0 0 170px 46px rgba(255,92,40,.9);opacity:0}
#hud{position:fixed;inset:0;pointer-events:none;z-index:10}
#multBlock{position:absolute;top:24px;left:32px}
#mult{font-size:54px;font-weight:800;font-style:italic;line-height:1;color:var(--silver);text-shadow:0 0 24px rgba(255,138,77,.55);transform-origin:left top}
#mult.pulse{animation:mpulse .35s ease-out}
@keyframes mpulse{0%{transform:scale(1)}35%{transform:scale(1.34);color:var(--gold)}100%{transform:scale(1)}}
#multLabel{font-size:10px;letter-spacing:.34em;color:var(--purple);margin-top:6px}
#scoreBlock{position:absolute;top:26px;right:32px;text-align:right}
#score{font-size:34px;font-weight:700;letter-spacing:.05em;color:var(--gold);text-shadow:0 0 18px rgba(255,138,77,.5)}
#best{font-size:11px;letter-spacing:.28em;color:var(--purple);margin-top:3px}
#gauge{position:absolute;bottom:26px;left:50%;transform:translateX(-50%);display:flex;flex-direction:column;align-items:center;gap:9px}
#energyWrap{position:relative;width:92px;height:92px}
#energyWrap svg{width:92px;height:92px;transform:rotate(-90deg);filter:drop-shadow(0 0 6px rgba(255,217,160,.55))}
#energyText{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:20px;font-weight:700;color:var(--gold)}
#energyWrap.low #energyText{color:#ff5a3c;animation:blink .5s infinite}
#energyWrap.low svg{filter:drop-shadow(0 0 8px rgba(255,90,60,.8))}
@keyframes blink{50%{opacity:.35}}
#speedLine{position:relative;width:240px;height:3px;background:rgba(207,211,232,.16)}
#speedFill{position:absolute;left:0;top:0;height:100%;width:0;background:linear-gradient(90deg,#8a63c9,#cfd3e8);box-shadow:0 0 8px rgba(207,211,232,.7)}
#turboFill{position:absolute;left:0;top:-2px;height:7px;width:0;background:linear-gradient(90deg,#ff8a4d,#ffd9a0);box-shadow:0 0 10px rgba(255,138,77,.85)}
#speedText{font-size:13px;letter-spacing:.24em;color:var(--silver)}
#boostReady{font-size:10px;letter-spacing:.4em;color:var(--gold);opacity:0;transition:opacity .25s;height:12px}
#boostBtn{position:absolute;bottom:30px;right:32px;font-size:11px;letter-spacing:.32em;color:var(--purple);border:1px solid rgba(138,99,201,.35);padding:10px 18px 10px 22px;background:rgba(18,8,32,.55)}
#boostBtn.ready{color:var(--gold);border-color:var(--orange);box-shadow:0 0 18px rgba(255,138,77,.45);text-shadow:0 0 10px rgba(255,138,77,.8)}
.screen{position:fixed;inset:0;z-index:20;display:flex;flex-direction:column;align-items:center;justify-content:center;background:rgba(12,4,22,.74);backdrop-filter:blur(3px);text-align:center;cursor:pointer;padding:20px}
.screen h1{font-family:'Arial Black','Segoe UI',sans-serif;font-size:clamp(40px,7vw,72px);font-weight:900;font-style:italic;letter-spacing:.12em;color:var(--gold);text-shadow:0 0 32px rgba(255,138,77,.85),0 0 90px rgba(255,138,77,.4)}
.screen .sub{margin-top:12px;font-size:12px;letter-spacing:.42em;color:var(--purple)}
.controls{margin-top:38px;display:flex;gap:30px;font-size:11px;letter-spacing:.2em;color:var(--silver);flex-wrap:wrap;justify-content:center}
.controls b{display:block;color:var(--gold);font-size:13px;letter-spacing:.3em;margin-bottom:6px}
.cta{margin-top:46px;padding:15px 46px;border:1px solid var(--orange);color:var(--gold);font-size:13px;letter-spacing:.42em;background:transparent;cursor:pointer;font-family:inherit;pointer-events:auto}
.cta:hover{background:rgba(255,138,77,.16);box-shadow:0 0 26px rgba(255,138,77,.4)}
.statRow{display:flex;gap:44px;margin-top:34px;flex-wrap:wrap;justify-content:center}
.stat .v{font-size:32px;font-weight:700;color:var(--gold);text-shadow:0 0 16px rgba(255,138,77,.5)}
.stat .k{font-size:10px;letter-spacing:.3em;color:var(--purple);margin-top:6px}
#loadFail{z-index:30}
</style>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/shaders/CopyShader.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/shaders/LuminosityHighPassShader.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/shaders/GammaCorrectionShader.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/shaders/AfterimageShader.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/EffectComposer.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/RenderPass.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/ShaderPass.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/UnrealBloomPass.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/AfterimagePass.js"></script>
</head>
<body>
<div id="edgeVig"></div>
<div id="vignette"></div>
<div id="hud">
<div id="multBlock">
<div id="mult" class="mono">×1.0</div>
<div id="multLabel">MULTIPLIER</div>
</div>
<div id="scoreBlock">
<div id="score" class="mono">0</div>
<div id="best">BEST 0</div>
</div>
<div id="gauge">
<div id="energyWrap">
<svg viewBox="0 0 92 92">
<circle cx="46" cy="46" r="40" fill="none" stroke="rgba(207,211,232,.15)" stroke-width="5"/>
<circle id="energyArc" cx="46" cy="46" r="40" fill="none" stroke="#ffd9a0" stroke-width="5" stroke-linecap="round" stroke-dasharray="251.33" stroke-dashoffset="0"/>
</svg>
<div id="energyText" class="mono">100</div>
</div>
<div id="speedLine"><div id="turboFill"></div><div id="speedFill"></div></div>
<div id="speedText" class="mono">0</div>
<div id="boostReady">BOOST READY</div>
</div>
<div id="boostBtn">BOOST</div>
</div>
<div id="startScreen" class="screen">
<h1>CANYON RUSH</h1>
<div class="sub">HIGH-SPEED PROCEDURAL CANYON RACER</div>
<div class="controls">
<div><b>MOUSE</b>STEER</div>
<div><b>SHIFT</b>BOOST</div>
<div><b>ORBS</b>ENERGY</div>
<div><b>WALLS</b>MULTIPLIER</div>
</div>
<button class="cta" id="startBtn">CLICK TO LAUNCH</button>
</div>
<div id="overScreen" class="screen" hidden>
<h1 id="overTitle">CRASHED</h1>
<div class="sub" id="overSub">SIGNAL LOST</div>
<div class="statRow">
<div class="stat"><div class="v mono" id="finalScore">0</div><div class="k">FINAL SCORE</div></div>
<div class="stat"><div class="v mono" id="finalSpeed">0</div><div class="k">MAX SPEED</div></div>
<div class="stat"><div class="v mono" id="finalNear">0</div><div class="k">NEAR MISSES</div></div>
<div class="stat"><div class="v mono" id="finalOrbs">0</div><div class="k">ORBS</div></div>
</div>
<button class="cta" id="restartBtn">RESTART</button>
</div>
<div id="loadFail" class="screen" hidden>
<h1 style="font-size:34px">ENGINE OFFLINE</h1>
<div class="sub" id="loadFailMsg">Failed to load Three.js</div>
</div>
<script>
'use strict';
var $ = id => document.getElementById(id);
var clamp = (v, a, b) => v < a ? a : v > b ? b : v;
// ================= Procedural Audio (Web Audio API) =================
var sfx = {
ctx: null, master: null, engGain: null, engOsc1: null, engOsc2: null, engFilter: null, noiseBuf: null,
init(){
if (this.ctx){ if (this.ctx.state === 'suspended') this.ctx.resume(); return; }
const AC = window.AudioContext || window.webkitAudioContext;
if (!AC) return;
this.ctx = new AC();
this.master = this.ctx.createGain();
this.master.gain.value = 0.5;
this.master.connect(this.ctx.destination);
const len = this.ctx.sampleRate;
this.noiseBuf = this.ctx.createBuffer(1, len, this.ctx.sampleRate);
const d = this.noiseBuf.getChannelData(0);
for (let i = 0; i < len; i++) d[i] = Math.random() * 2 - 1;
// Engine: detuned saw + square through lowpass
this.engGain = this.ctx.createGain();
this.engGain.gain.value = 0;
this.engFilter = this.ctx.createBiquadFilter();
this.engFilter.type = 'lowpass';
this.engFilter.frequency.value = 300;
this.engOsc1 = this.ctx.createOscillator();
this.engOsc1.type = 'sawtooth';
this.engOsc1.frequency.value = 46;
this.engOsc2 = this.ctx.createOscillator();
this.engOsc2.type = 'square';
this.engOsc2.frequency.value = 23;
const g2 = this.ctx.createGain();
g2.gain.value = 0.35;
this.engOsc1.connect(this.engFilter);
this.engOsc2.connect(g2);
g2.connect(this.engFilter);
this.engFilter.connect(this.engGain);
this.engGain.connect(this.master);
this.engOsc1.start();
this.engOsc2.start();
},
engine(f, boosting){
if (!this.ctx) return;
const t = this.ctx.currentTime;
this.engGain.gain.setTargetAtTime(0.03 + f * 0.11 + (boosting ? 0.05 : 0), t, 0.12);
const base = 40 + f * 72 + (boosting ? 30 : 0);
this.engOsc1.frequency.setTargetAtTime(base, t, 0.12);
this.engOsc2.frequency.setTargetAtTime(base * 0.5, t, 0.12);
this.engFilter.frequency.setTargetAtTime(240 + f * 1300 + (boosting ? 900 : 0), t, 0.15);
},
engineOff(){
if (!this.ctx) return;
this.engGain.gain.setTargetAtTime(0, this.ctx.currentTime, 0.25);
},
whoosh(i){
if (!this.ctx) return;
const t = this.ctx.currentTime;
const src = this.ctx.createBufferSource();
src.buffer = this.noiseBuf;
const f = this.ctx.createBiquadFilter();
f.type = 'bandpass'; f.Q.value = 1.3;
f.frequency.setValueAtTime(280 + i * 420, t);
f.frequency.exponentialRampToValueAtTime(1100 + i * 1600, t + 0.16);
const g = this.ctx.createGain();
g.gain.setValueAtTime(0.0001, t);
g.gain.exponentialRampToValueAtTime(0.2 + i * 0.3, t + 0.05);
g.gain.exponentialRampToValueAtTime(0.0001, t + 0.3);
src.connect(f); f.connect(g); g.connect(this.master);
src.start(t); src.stop(t + 0.32);
},
ping(){
if (!this.ctx) return;
const t = this.ctx.currentTime;
[660, 990].forEach((fr, k) => {
const o = this.ctx.createOscillator();
o.type = 'sine'; o.frequency.value = fr;
const g = this.ctx.createGain();
const t0 = t + k * 0.07;
g.gain.setValueAtTime(0.0001, t0);
g.gain.exponentialRampToValueAtTime(0.2, t0 + 0.02);
g.gain.exponentialRampToValueAtTime(0.0001, t0 + 0.22);
o.connect(g); g.connect(this.master);
o.start(t0); o.stop(t0 + 0.26);
});
},
boost(){
if (!this.ctx) return;
const t = this.ctx.currentTime;
const src = this.ctx.createBufferSource();
src.buffer = this.noiseBuf;
const f = this.ctx.createBiquadFilter();
f.type = 'bandpass'; f.Q.value = 0.8;
f.frequency.setValueAtTime(240, t);
f.frequency.exponentialRampToValueAtTime(2600, t + 0.5);
const g = this.ctx.createGain();
g.gain.setValueAtTime(0.0001, t);
g.gain.exponentialRampToValueAtTime(0.28, t + 0.12);
g.gain.exponentialRampToValueAtTime(0.0001, t + 0.55);
src.connect(f); f.connect(g); g.connect(this.master);
src.start(t); src.stop(t + 0.6);
},
explode(){
if (!this.ctx) return;
const t = this.ctx.currentTime;
const src = this.ctx.createBufferSource();
src.buffer = this.noiseBuf;
const f = this.ctx.createBiquadFilter();
f.type = 'lowpass';
f.frequency.setValueAtTime(1200, t);
f.frequency.exponentialRampToValueAtTime(60, t + 0.9);
const g = this.ctx.createGain();
g.gain.setValueAtTime(0.5, t);
g.gain.exponentialRampToValueAtTime(0.0001, t + 1);
src.connect(f); f.connect(g); g.connect(this.master);
src.start(t); src.stop(t + 1.05);
const o = this.ctx.createOscillator();
o.type = 'sine';
o.frequency.setValueAtTime(110, t);
o.frequency.exponentialRampToValueAtTime(28, t + 0.7);
const og = this.ctx.createGain();
og.gain.setValueAtTime(0.4, t);
og.gain.exponentialRampToValueAtTime(0.0001, t + 0.8);
o.connect(og); og.connect(this.master);
o.start(t); o.stop(t + 0.85);
}
};
</script>
<script>
'use strict';
// ================= Guard: Three.js CDN loaded? =================
if (typeof THREE === 'undefined' || !THREE.EffectComposer || !THREE.UnrealBloomPass || !THREE.AfterimagePass) {
document.getElementById('loadFail').hidden = false;
document.getElementById('loadFailMsg').textContent = 'Failed to load Three.js from CDN';
throw new Error('THREE not available');
}
// ================= Tunables =================
const CFG = {
baseSpeed: 34, maxSpeed: 86, boostSpeed: 150,
energyDecay: 3.2, orbEnergy: 24,
multDecay: 1.6, multMax: 10,
chunkLen: 60, chunksAhead: 10,
playerRadius: 1.15, nearMissDist: 4.2,
};
const hash = n => { const s = Math.sin(n) * 43758.5453; return s - Math.floor(s); };
const damp = (a, b, l, dt) => a + (b - a) * (1 - Math.exp(-l * dt));
const difficultyAt = z => clamp(z * 0.00035, 0, 0.85);
// ================= Renderer / scene =================
const renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: 'high-performance' });
let pixelCap = Math.min(window.devicePixelRatio, 1.75);
let shadowRes = 1024;
{
const gl = renderer.getContext();
const dbg = gl.getExtension('WEBGL_debug_renderer_info');
const rstr = dbg ? String(gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL)) : '';
if (/swiftshader|llvmpipe|software/i.test(rstr)) { pixelCap = 1; shadowRes = 512; }
}
renderer.setPixelRatio(pixelCap);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.outputEncoding = THREE.sRGBEncoding;
renderer.toneMappingExposure = 1.05;
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.fog = new THREE.Fog(0x8a3550, 70, 380);
// Sky gradient (cyber-sunset)
{
const c = document.createElement('canvas');
c.width = 2; c.height = 512;
const g = c.getContext('2d');
const grad = g.createLinearGradient(0, 0, 0, 512);
grad.addColorStop(0, '#150a28');
grad.addColorStop(0.42, '#3d1c4e');
grad.addColorStop(0.68, '#8a2f56');
grad.addColorStop(0.84, '#e05a3e');
grad.addColorStop(1, '#ffb377');
g.fillStyle = grad;
g.fillRect(0, 0, 2, 512);
const tex = new THREE.CanvasTexture(c);
tex.colorSpace = THREE.SRGBColorSpace;
scene.background = tex;
}
const camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 700);
camera.position.set(0, 8, -12);
// Sun disc at the end of the canyon
const sun = new THREE.Mesh(new THREE.CircleGeometry(34, 32), new THREE.MeshBasicMaterial({ color: new THREE.Color(2.3, 1.85, 1.15), fog: false }));
const sunGlow = new THREE.Mesh(new THREE.CircleGeometry(74, 32), new THREE.MeshBasicMaterial({ color: new THREE.Color(1.6, 0.9, 0.5), transparent: true, opacity: 0.3, fog: false }));
sunGlow.rotation.y = Math.PI;
sun.rotation.y = Math.PI;
scene.add(sun, sunGlow);
// Lights — low golden-hour sun for long shadows
const hemi = new THREE.HemisphereLight(0xffb070, 0x2a1430, 0.75);
scene.add(hemi);
const sunLight = new THREE.DirectionalLight(0xffb070, 2.7);
sunLight.castShadow = true;
sunLight.shadow.mapSize.set(shadowRes, shadowRes);
sunLight.shadow.camera.left = -55;
sunLight.shadow.camera.right = 55;
sunLight.shadow.camera.top = 55;
sunLight.shadow.camera.bottom = -55;
sunLight.shadow.camera.near = 5;
sunLight.shadow.camera.far = 140;
sunLight.shadow.bias = -0.002;
scene.add(sunLight, sunLight.target);
// ================= Shared geometry / materials =================
const wallMat = new THREE.MeshStandardMaterial({ vertexColors: true, flatShading: true, roughness: 0.92, metalness: 0.05, side: THREE.DoubleSide });
const monoMat = new THREE.MeshStandardMaterial({ color: 0x6b4a86, flatShading: true, roughness: 0.7, metalness: 0.25, emissive: 0x180d2c, emissiveIntensity: 0.9 });
const orbMat = new THREE.MeshBasicMaterial({ color: new THREE.Color(2.4, 1.8, 1.1) });
const orbGeo = new THREE.IcosahedronGeometry(0.8, 0);
const monoGeo = new THREE.CylinderGeometry(1, 1, 30, 8, 1);
// ================= Procedural corridor =================
function pathX(z) {
const s = difficultyAt(z);
const amp = 6 + s * 9;
return Math.sin(z * 0.011) * amp + Math.sin(z * 0.0037 + 1.7) * amp * 0.6;
}
function pathY(z) {
const s = difficultyAt(z);
return 4 + Math.sin(z * 0.007 + 0.8) * (2 + s * 3);
}
function corridor(z, s) {
const cx = pathX(z) + Math.sin(z * 0.021 + 2.1) * 4;
const cy = pathY(z) + Math.sin(z * 0.017 + 0.5) * 2.5;
let halfGap = (14.5 + s * 5) * (0.78 + 0.3 * Math.sin(z * 0.033 + 1.2));
const sq = (Math.sin(z * 0.013 + 4.2) + 1) * 0.5;
const squeeze = clamp((sq - 0.8) / 0.2, 0, 1);
if (s > 0.12) halfGap *= 1 - squeeze * (0.28 + s * 0.14);
halfGap = Math.max(halfGap, 5.6 - s * 1.2);
return { cx, cy, halfGap };
}
// ================= Chunk building =================
const SURF_BASE = [new THREE.Color(0x7b4a8e), new THREE.Color(0x9a5548), new THREE.Color(0x3b2549), new THREE.Color(0x2b1a3d)];
function genSurface(kind, i, cz, s) {
const steps = 48;
const P = [], C = [], I = [];
const base = SURF_BASE[kind];
const outSign = kind === 0 ? -1 : kind === 1 ? 1 : kind === 2 ? -1 : 1;
for (let k = 0; k <= steps; k++) {
const z = cz + (k / steps) * CFG.chunkLen;
const { cx, cy, halfGap } = corridor(z, s);
const a0 = kind < 2 ? cy - halfGap : cx - halfGap;
const a1 = kind < 2 ? cy + halfGap : cx + halfGap;
const innerX = kind === 0 ? cx - halfGap : kind === 1 ? cx + halfGap : 0;
const innerY = kind === 2 ? cy - halfGap : kind === 3 ? cy + halfGap : 0;
for (let j = 0; j < 3; j++) {
const n1 = hash(i * 57.3 + k * 2.7 + j * 9.1);
const n2 = hash(i * 31.7 + k * 4.3 + j * 15.7);
const out = j === 0 ? 0 : j === 1 ? 4 + 4 * n1 : 9 + 7 * n2;
for (let r = 0; r < 2; r++) {
const lift = j === 0 ? 0 : j === 1 ? (r === 0 ? 3 + 3 * n1 : 1.5 + 2 * n2) : (r === 0 ? 7 + 5 * n2 : 4 + 4 * n1);
const span = r === 0 ? a0 : a1;
let x, y;
if (kind < 2) { x = innerX + outSign * (out + lift); y = span; }
else { y = innerY + outSign * (out + lift); x = span; }
P.push(x, y, z);
const m = 0.72 + 0.55 * hash(i * 13.1 + k * 1.3 + j * 3.7 + r * 2.1);
C.push(base.r * m, base.g * m, base.b * m);
}
}
}
for (let k = 0; k < steps; k++) {
for (let j = 0; j < 2; j++) {
const a = k * 6 + j * 2, b = a + 1, c = a + 6, d = c + 1;
I.push(a, c, b, b, c, d);
}
}
return { P, C, I };
}
function buildChunk(i) {
const cz = i * CFG.chunkLen;
const s = difficultyAt(cz);
const group = new THREE.Group();
let P = [], C = [], I = [], offset = 0;
for (let kind = 0; kind < 4; kind++) {
const g = genSurface(kind, i, cz, s);
for (let n = 0; n < g.I.length; n++) I.push(g.I[n] + offset);
P = P.concat(g.P);
C = C.concat(g.C);
offset += g.P.length / 3;
}
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.Float32BufferAttribute(P, 3));
geo.setAttribute('color', new THREE.Float32BufferAttribute(C, 3));
geo.setIndex(I);
const mesh = new THREE.Mesh(geo, wallMat);
mesh.receiveShadow = true;
group.add(mesh);
scene.add(group);
const chunk = { group, wallGeo: geo, orbs: [], monos: [] };
// Floating monolith (bank to avoid)
if (i > 2 && i % 3 === 1) {
const z = cz + CFG.chunkLen * 0.5;
const { cx, cy, halfGap } = corridor(z, s);
const r = 2.2 + hash(i * 3.3) * 1.4;
const maxOff = Math.max(1, halfGap - r - 2);
const off = (hash(i * 7.7) - 0.5) * 2 * maxOff;
const m = new THREE.Mesh(monoGeo, monoMat);
m.scale.set(r, 1, r);
m.position.set(cx + off, cy, z);
m.castShadow = true;
m.receiveShadow = true;
group.add(m);
chunk.monos.push({ x: cx + off, y: cy, z, r, mesh: m, spin: (hash(i * 9.1) - 0.5) * 0.8 });
}
// Energy orbs — biased toward the risky edges
const nOrbs = 5 + (i % 3);
for (let k = 0; k < nOrbs; k++) {
const z = cz + ((k + 0.5) / (nOrbs + 1)) * CFG.chunkLen;
const { cx, cy, halfGap } = corridor(z, s);
const side = hash(i * 11.3 + k) < 0.5 ? -1 : 1;
const x = cx + side * halfGap * (0.55 + 0.3 * hash(i * 5.1 + k * 2.2));
const y = cy + (hash(i * 13.7 + k * 3.3) - 0.5) * halfGap * 0.7;
const o = new THREE.Mesh(orbGeo, orbMat);
o.position.set(x, y, z);
group.add(o);
chunk.orbs.push({ mesh: o, baseY: y, phase: hash(i * 17.1 + k) * 6.28, taken: false });
}
return chunk;
}
function disposeChunk(c) {
scene.remove(c.group);
c.wallGeo.dispose();
}
const chunks = new Map();
let chunkIndex = -2;
function syncChunks() {
const pz = player.pos.z;
const minI = Math.floor((pz - 130) / CFG.chunkLen);
const maxI = Math.floor((pz + CFG.chunksAhead * CFG.chunkLen) / CFG.chunkLen);
while (chunkIndex <= maxI) {
chunks.set(chunkIndex, buildChunk(chunkIndex));
chunkIndex++;
}
for (const [k, c] of chunks) {
if (k < minI) { disposeChunk(c); chunks.delete(k); }
}
}
// ================= Player craft =================
function buildCraft() {
const g = new THREE.Group();
const bodyMat = new THREE.MeshStandardMaterial({ color: 0xd9dcef, flatShading: true, metalness: 0.6, roughness: 0.3, emissive: 0x241536, emissiveIntensity: 0.7 });
const darkMat = new THREE.MeshStandardMaterial({ color: 0x3a2a55, flatShading: true, metalness: 0.4, roughness: 0.5 });
const accentMat = new THREE.MeshStandardMaterial({ color: 0xff8a4d, flatShading: true, emissive: 0xff5a2a, emissiveIntensity: 2.4 });
const glowMat = new THREE.MeshBasicMaterial({ color: new THREE.Color(3.2, 2.1, 1.1) });
const fus = new THREE.Mesh(new THREE.OctahedronGeometry(1, 0), bodyMat);
fus.scale.set(0.55, 0.38, 2.4);
fus.castShadow = true;
const wing = new THREE.Mesh(new THREE.BoxGeometry(3.6, 0.1, 1.5), bodyMat);
wing.position.set(0, -0.05, -0.4);
wing.castShadow = true;
const tipGeo = new THREE.BoxGeometry(0.35, 0.22, 0.9);
const tipL = new THREE.Mesh(tipGeo, accentMat);
tipL.position.set(-1.7, 0.02, -0.5);
const tipR = tipL.clone();
tipR.position.x = 1.7;
const tail = new THREE.Mesh(new THREE.BoxGeometry(0.1, 0.7, 1.1), darkMat);
tail.position.set(0, 0.45, -1.1);
const cockpit = new THREE.Mesh(new THREE.TetrahedronGeometry(0.5, 0), darkMat);
cockpit.position.set(0, 0.32, 0.5);
const glow = new THREE.Mesh(new THREE.IcosahedronGeometry(0.42, 0), glowMat);
glow.position.set(0, 0, -1.55);
g.add(fus, wing, tipL, tipR, tail, cockpit, glow);
scene.add(g);
return { group: g, glow };
}
const craft = buildCraft();
// ================= Particles =================
// Engine trail
const TRAIL_N = 90;
const trailPos = new Float32Array(TRAIL_N * 3);
for (let i = 0; i < TRAIL_N; i++) trailPos[i * 3 + 1] = -9999;
const trailGeo = new THREE.BufferGeometry();
trailGeo.setAttribute('position', new THREE.BufferAttribute(trailPos, 3));
const trailMat = new THREE.LineBasicMaterial({ color: 0xffb37a, transparent: true, opacity: 0.5 });
const trail = new THREE.Line(trailGeo, trailMat);
trail.frustumCulled = false;
scene.add(trail);
// Speed streaks
const LINE_N = 110;
const linePos = new Float32Array(LINE_N * 6);
const lineGeo = new THREE.BufferGeometry();
lineGeo.setAttribute('position', new THREE.BufferAttribute(linePos, 3));
const lineMat = new THREE.LineBasicMaterial({ color: 0xcfd3e8, transparent: true, opacity: 0 });
const speedLines = new THREE.LineSegments(lineGeo, lineMat);
speedLines.frustumCulled = false;
scene.add(speedLines);
// Dust
const DUST_N = 220;
const dustPos = new Float32Array(DUST_N * 3);
for (let i = 0; i < DUST_N; i++) {
const a = hash(i * 3.1) * 6.283, rad = 14 + hash(i * 4.7) * 22;
dustPos[i * 3] = Math.cos(a) * rad;
dustPos[i * 3 + 1] = 4 + Math.sin(a) * rad * 0.7;
dustPos[i * 3 + 2] = -30 + hash(i * 7.3) * 110;
}
const dustGeo = new THREE.BufferGeometry();
dustGeo.setAttribute('position', new THREE.BufferAttribute(dustPos, 3));
const dust = new THREE.Points(dustGeo, new THREE.PointsMaterial({ color: 0xffc9a0, size: 0.22, transparent: true, opacity: 0.5 }));
dust.frustumCulled = false;
scene.add(dust);
// Explosion
const EXP_N = 150;
const expPos = new Float32Array(EXP_N * 3);
const expVel = new Float32Array(EXP_N * 3);
const expLife = new Float32Array(EXP_N);
for (let i = 0; i < EXP_N; i++) expPos[i * 3 + 1] = -9999;
const expGeo = new THREE.BufferGeometry();
expGeo.setAttribute('position', new THREE.BufferAttribute(expPos, 3));
const explosion = new THREE.Points(expGeo, new THREE.PointsMaterial({ color: new THREE.Color(2.5, 1.5, 0.8), size: 0.6, transparent: true, opacity: 0.95 }));
explosion.frustumCulled = false;
scene.add(explosion);
const ring = new THREE.Mesh(new THREE.RingGeometry(1, 1.4, 48), new THREE.MeshBasicMaterial({ color: 0xffb37a, transparent: true, opacity: 0.8, side: THREE.DoubleSide }));
ring.visible = false;
scene.add(ring);
let ringLife = 1;
function explodeAt(p) {
for (let i = 0; i < EXP_N; i++) {
expPos[i * 3] = p.x; expPos[i * 3 + 1] = p.y; expPos[i * 3 + 2] = p.z;
const a = Math.random() * Math.PI * 2, b = Math.acos(2 * Math.random() - 1), sp = 6 + Math.random() * 26;
expVel[i * 3] = Math.sin(b) * Math.cos(a) * sp;
expVel[i * 3 + 1] = Math.cos(b) * sp;
expVel[i * 3 + 2] = Math.sin(b) * Math.sin(a) * sp;
expLife[i] = 0.8 + Math.random() * 0.8;
}
ring.visible = true;
ring.position.copy(p);
ring.scale.set(0.1, 0.1, 0.1);
ringLife = 0;
}
// ================= Post-processing =================
const composer = new THREE.EffectComposer(renderer);
composer.addPass(new THREE.RenderPass(scene, camera));
const afterimage = new THREE.AfterimagePass(0.82);
composer.addPass(afterimage);
const bloom = new THREE.UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 0.7, 0.5, 0.6);
composer.addPass(bloom);
composer.addPass(new THREE.ShaderPass(THREE.GammaCorrectionShader));
// ================= State =================
const state = {
mode: 'menu',
score: 0,
best: 0,
speed: CFG.baseSpeed,
energy: 100,
mult: 1,
nearMisses: 0,
orbs: 0,
maxSpeed: 0,
turbo: 0,
boosting: false,
boostT: 0,
shake: 0,
time: 0,
lastWhoosh: -1,
crashReason: 'CRASHED',
};
try { state.best = parseInt(localStorage.getItem('cr_best') || '0', 10) || 0; } catch (e) {}
const player = {
pos: new THREE.Vector3(0, 5, 0),
vel: new THREE.Vector3(0, 0, 0),
bank: 0,
pitch: 0,
};
// ================= Input =================
let targetMX = 0, targetMY = 0, mouseX = 0, mouseY = 0, rawMX = 0, rawMY = 0;
const keys = {};
window.addEventListener('pointermove', e => {
rawMX = (e.clientX / window.innerWidth) * 2 - 1;
rawMY = (e.clientY / window.innerHeight) * 2 - 1;
targetMX = rawMX;
targetMY = rawMY;
});
window.addEventListener('keydown', e => {
if (e.code === 'ArrowLeft' || e.code === 'KeyA') keys.left = true;
if (e.code === 'ArrowRight' || e.code === 'KeyD') keys.right = true;
if (e.code === 'ArrowUp' || e.code === 'KeyW') keys.up = true;
if (e.code === 'ArrowDown' || e.code === 'KeyS') keys.down = true;
if (e.code === 'Space' || e.code === 'ShiftLeft' || e.code === 'ShiftRight') { e.preventDefault(); tryBoost(); }
});
window.addEventListener('keyup', e => {
if (e.code === 'ArrowLeft' || e.code === 'KeyA') keys.left = false;
if (e.code === 'ArrowRight' || e.code === 'KeyD') keys.right = false;
if (e.code === 'ArrowUp' || e.code === 'KeyW') keys.up = false;
if (e.code === 'ArrowDown' || e.code === 'KeyS') keys.down = false;
});
function tryBoost() {
if (state.mode !== 'playing' || state.boosting || state.turbo < 100) return;
state.boosting = true;
state.boostT = 2.3;
state.turbo = 0;
state.shake = Math.max(state.shake, 0.5);
sfx.boost();
}
// ================= HUD refs =================
const elMult = $('mult'), elScore = $('score'), elBest = $('best');
const elEnergyArc = $('energyArc'), elEnergyText = $('energyText'), elEnergyWrap = $('energyWrap');
const elSpeedFill = $('speedFill'), elTurboFill = $('turboFill'), elSpeedText = $('speedText');
const elBoostReady = $('boostReady'), elBoostBtn = $('boostBtn'), elVignette = $('vignette');
const ARCL = 251.33;
elBest.textContent = 'BEST ' + state.best;
function pulseMult() {
elMult.classList.remove('pulse');
void elMult.offsetWidth;
elMult.classList.add('pulse');
}
// ================= Game flow =================
function reset() {
for (const c of chunks.values()) disposeChunk(c);
chunks.clear();
chunkIndex = -2;
state.score = 0; state.speed = CFG.baseSpeed; state.energy = 100; state.mult = 1;
state.nearMisses = 0; state.orbs = 0; state.maxSpeed = 0; state.turbo = 0;
state.boosting = false; state.boostT = 0; state.shake = 0; state.time = 0; state.lastWhoosh = -1;
player.pos.set(0, pathY(0), 0);
player.vel.set(0, 0, 0);
player.bank = 0; player.pitch = 0;
craft.group.visible = true;
trail.visible = true;
for (let i = 0; i < TRAIL_N; i++) trailPos[i * 3 + 1] = -9999;
for (let i = 0; i < EXP_N; i++) expPos[i * 3 + 1] = -9999;
ring.visible = false;
camera.position.set(player.pos.x, player.pos.y + 2.6, player.pos.z - 9.5);
syncChunks();
state.mode = 'playing';
}
function crash(reason) {
if (state.mode !== 'playing') return;
state.mode = 'over';
state.crashReason = reason;
sfx.explode();
sfx.engineOff();
state.shake = 1.2;
explodeAt(player.pos);
craft.group.visible = false;
trail.visible = false;
state.best = Math.max(state.best, Math.floor(state.score));
try { localStorage.setItem('cr_best', String(state.best)); } catch (e) {}
setTimeout(showOver, 900);
}
function showOver() {
$('overTitle').textContent = state.crashReason;
$('overSub').textContent = state.crashReason === 'CRASHED' ? 'SIGNAL LOST' : 'POWER DEPLETED';
$('finalScore').textContent = Math.floor(state.score).toLocaleString();
$('finalSpeed').textContent = Math.round(state.maxSpeed * 3.4);
$('finalNear').textContent = state.nearMisses;
$('finalOrbs').textContent = state.orbs;
elBest.textContent = 'BEST ' + state.best;
$('overScreen').hidden = false;
}
$('startScreen').addEventListener('click', () => {
sfx.init();
reset();
$('startScreen').hidden = true;
});
$('restartBtn').addEventListener('click', e => {
e.stopPropagation();
sfx.init();
reset();
$('overScreen').hidden = true;
});
// ================= Update =================
function update(dt) {
state.time += dt;
const t = state.time;
const speedFrac = clamp((state.speed - CFG.baseSpeed) / (CFG.boostSpeed - CFG.baseSpeed), 0, 1);
// Sun stays ahead
sun.position.set(player.pos.x * 0.4, 9, player.pos.z + 430);
sunGlow.position.set(player.pos.x * 0.4, 9, player.pos.z + 429);
// Light follows player (long shadows)
sunLight.position.set(player.pos.x - 28, player.pos.y + 20, player.pos.z + 26);
sunLight.target.position.copy(player.pos);
sunLight.target.updateMatrixWorld();
if (state.mode === 'menu') {
const p0 = pathY(0);
craft.group.position.set(0, p0 + Math.sin(t * 1.1) * 0.5, 0);
craft.group.rotation.set(Math.sin(t * 0.5) * 0.12, Math.sin(t * 0.35) * 0.4, Math.sin(t * 0.7) * 0.28);
const a = t * 0.35;
camera.position.set(4 + Math.cos(a) * 7, p0 + 1.5 + Math.sin(t * 0.6) * 1.2, Math.sin(a) * 7 - 1);
camera.lookAt(0, p0, 0);
camera.fov = damp(camera.fov, 70, 4, dt);
camera.updateProjectionMatrix();
updateOrbSpin(dt);
updateExplosion(dt);
state.shake = Math.max(0, state.shake - dt * 1.8);
updateHUD();
return;
}
if (state.mode === 'over') {
camera.position.z += 10 * dt;
camera.lookAt(player.pos.x, player.pos.y, player.pos.z);
updateExplosion(dt);
state.shake = Math.max(0, state.shake - dt * 1.8);
updateHUD();
return;
}
// ---------- playing ----------
// Steering (mouse + keys), smoothed
if (keys.left) targetMX = -1;
else if (keys.right) targetMX = 1;
else targetMX = rawMX;
if (keys.up) targetMY = -1;
else if (keys.down) targetMY = 1;
else targetMY = rawMY;
mouseX = damp(mouseX, targetMX, 10, dt);
mouseY = damp(mouseY, targetMY, 10, dt);
const s = difficultyAt(player.pos.z);
let targetSpeed = CFG.baseSpeed + s * (CFG.maxSpeed - CFG.baseSpeed);
if (state.boosting) {
state.boostT -= dt;
if (state.boostT <= 0) state.boosting = false;
targetSpeed = CFG.boostSpeed;
}
state.speed = damp(state.speed, targetSpeed, 1.5, dt);
state.maxSpeed = Math.max(state.maxSpeed, state.speed);
// Flight physics: momentum-based lateral/vertical velocity
const steer = mouseX, pitchIn = mouseY;
player.vel.x = damp(player.vel.x, steer * state.speed * 0.42, 6, dt);
player.vel.y = damp(player.vel.y, pitchIn * state.speed * 0.30, 6, dt);
player.pos.x += player.vel.x * dt;
player.pos.y += player.vel.y * dt;
player.pos.z += state.speed * dt;
// Proximity / collision vs corridor walls
const cor = corridor(player.pos.z, s);
const dx = player.pos.x - cor.cx, dy = player.pos.y - cor.cy;
const proxX = cor.halfGap - Math.abs(dx);
const proxY = cor.halfGap - Math.abs(dy);
let prox = Math.min(proxX, proxY);
if (prox < CFG.playerRadius) { crash('CRASHED'); updateHUD(); return; }
// Monoliths
for (const [k, c] of chunks) {
const cz = k * CFG.chunkLen;
if (Math.abs(cz + CFG.chunkLen * 0.5 - player.pos.z) > CFG.chunkLen) continue;
for (const m of c.monos) {
if (Math.abs(m.z - player.pos.z) > 18) continue;
const mdx = player.pos.x - m.x, mdy = player.pos.y - m.y;
const d = Math.sqrt(mdx * mdx + mdy * mdy) - (m.r + CFG.playerRadius);
if (d < 0) { crash('CRASHED'); updateHUD(); return; }
if (d < CFG.nearMissDist) {
prox = Math.min(prox, d);
}
}
}
// Near-miss reward
if (prox < CFG.nearMissDist) {
state.mult = Math.min(CFG.multMax, state.mult + (CFG.nearMissDist - prox) * 3.5 * dt);
if (prox < 2.4 && t - state.lastWhoosh > 0.35) {
state.lastWhoosh = t;
state.nearMisses++;
state.shake = Math.max(state.shake, 0.22 + (2.4 - prox) * 0.25);
sfx.whoosh(clamp((2.4 - prox) / 2.4, 0, 1));
pulseMult();
}
} else {
state.mult = Math.max(1, state.mult - CFG.multDecay * dt);
}
// Orbs
for (const [k, c] of chunks) {
const cz = k * CFG.chunkLen;
if (Math.abs(cz + CFG.chunkLen * 0.5 - player.pos.z) > CFG.chunkLen) continue;
for (const o of c.orbs) {
if (o.taken) continue;
const odx = player.pos.x - o.mesh.position.x;
const ody = player.pos.y - o.mesh.position.y;
const odz = player.pos.z - o.mesh.position.z;
if (odx * odx + ody * ody + odz * odz < 3.6) {
o.taken = true;
o.mesh.visible = false;
state.orbs++;
state.energy = Math.min(100, state.energy + CFG.orbEnergy);
state.score += 250 * state.mult;
state.turbo = Math.min(100, state.turbo + 8);
state.shake = Math.max(state.shake, 0.12);
sfx.ping();
}
}
}
// Energy decay
state.energy -= (CFG.energyDecay + s * 2.6 + (state.boosting ? 4.5 : 0)) * dt;
if (state.energy <= 0) { state.energy = 0; crash('POWER DEPLETED'); updateHUD(); return; }
// Score + turbo
state.score += state.speed * dt * state.mult * 0.6;
state.turbo = clamp(state.turbo + ((state.speed > 55 ? 16 : 6) + (prox < 3 ? 12 : 0)) * dt, 0, 100);
// Craft transform: bank + pitch
player.bank = damp(player.bank, -steer * 0.85, 8, dt);
player.pitch = damp(player.pitch, pitchIn * 0.55, 8, dt);
craft.group.position.copy(player.pos);
craft.group.rotation.set(-player.pitch, -steer * 0.35, player.bank);
craft.glow.scale.setScalar(1 + speedFrac * 1.3 + Math.sin(t * 22) * 0.15);
// Trail
trailGeo.attributes.position.array.copyWithin(3, 6);
trailPos[0] = player.pos.x;
trailPos[1] = player.pos.y - 0.3;
trailPos[2] = player.pos.z - 1.7;
trailGeo.attributes.position.needsUpdate = true;
trailMat.opacity = 0.35 + speedFrac * 0.45;
// Camera follow + shake + graze offset
const graze = prox < 3 ? (dx > 0 ? -1 : 1) * (3 - prox) * 0.35 : 0;
const camTX = player.pos.x * 0.82 + graze;
const camTY = player.pos.y * 0.7 + 2.6;
const camTZ = player.pos.z - 9.5;
camera.position.x = damp(camera.position.x, camTX, 6, dt);
camera.position.y = damp(camera.position.y, camTY, 6, dt);
camera.position.z = damp(camera.position.z, camTZ, 6, dt);
const shakeAmp = state.shake * 0.5;
camera.position.x += (Math.random() - 0.5) * shakeAmp;
camera.position.y += (Math.random() - 0.5) * shakeAmp;
camera.lookAt(player.pos.x + player.vel.x * 0.8, player.pos.y + player.vel.y * 0.8, player.pos.z + 34);
const targetFov = 70 + speedFrac * 16 + (state.boosting ? 8 : 0);
camera.fov = damp(camera.fov, targetFov, 5, dt);
camera.updateProjectionMatrix();
// Speed streaks
lineMat.opacity = clamp((state.speed - 42) / 70, 0, 0.4) * (state.boosting ? 1.5 : 1);
for (let i = 0; i < LINE_N; i++) {
const a = (i * 97.31) % 6.283;
const rad = 10 + ((i * 57.31) % 26);
const zOff = ((i * 131.7 + t * state.speed * 2.2) % 70) - 35;
const x = player.pos.x + Math.cos(a) * rad;
const y = player.pos.y + Math.sin(a) * rad;
const len = 3 + state.speed * 0.09;
linePos[i * 6] = x; linePos[i * 6 + 1] = y; linePos[i * 6 + 2] = player.pos.z + zOff;
linePos[i * 6 + 3] = x; linePos[i * 6 + 4] = y; linePos[i * 6 + 5] = player.pos.z + zOff - len;
}
lineGeo.attributes.position.needsUpdate = true;
// Dust
for (let i = 0; i < DUST_N; i++) {
dustPos[i * 3 + 2] -= state.speed * 1.5 * dt;
if (dustPos[i * 3 + 2] < player.pos.z - 55) {
const a = Math.random() * 6.283, rad = 14 + Math.random() * 22;
dustPos[i * 3] = player.pos.x + Math.cos(a) * rad;
dustPos[i * 3 + 1] = player.pos.y + Math.sin(a) * rad * 0.7;
dustPos[i * 3 + 2] = player.pos.z + 45 + Math.random() * 20;
}
}
dustGeo.attributes.position.needsUpdate = true;
// Post FX intensity
afterimage.uniforms.damp.value = clamp(0.72 + speedFrac * 0.15 + (state.boosting ? 0.07 : 0), 0, 0.93);
bloom.strength = 0.55 + speedFrac * 0.35 + (state.boosting ? 0.35 : 0);
sfx.engine(clamp(state.speed / CFG.boostSpeed, 0, 1), state.boosting);
state.shake = Math.max(0, state.shake - dt * 1.8);
updateOrbSpin(dt);
updateExplosion(dt);
syncChunks();
updateHUD();
}
function updateOrbSpin(dt) {
const t = state.time;
for (const c of chunks.values()) {
for (const o of c.orbs) {
if (o.taken) continue;
o.mesh.rotation.y += dt * 2.2;
o.mesh.position.y = o.baseY + Math.sin(t * 2 + o.phase) * 0.5;
}
for (const m of c.monos) m.mesh.rotation.y += m.spin * dt;
}
}
function updateExplosion(dt) {
let any = false;
for (let i = 0; i < EXP_N; i++) {
if (expLife[i] > 0) {
any = true;
expLife[i] -= dt;
expPos[i * 3] += expVel[i * 3] * dt;
expPos[i * 3 + 1] += expVel[i * 3 + 1] * dt;
expPos[i * 3 + 2] += expVel[i * 3 + 2] * dt;
const dr = 1 - 1.5 * dt;
expVel[i * 3] *= dr; expVel[i * 3 + 1] *= dr; expVel[i * 3 + 2] *= dr;
if (expLife[i] <= 0) expPos[i * 3 + 1] = -9999;
}
}
expGeo.attributes.position.needsUpdate = true;
if (ring.visible) {
ringLife += dt;
const sc = 1 + ringLife * 90;
ring.scale.set(sc, sc, sc);
ring.material.opacity = Math.max(0, 0.8 - ringLife);
ring.lookAt(camera.position);
if (ringLife > 0.9) ring.visible = false;
}
}
function updateHUD() {
const playing = state.mode === 'playing';
document.getElementById('hud').style.opacity = playing ? 1 : 0.35;
elMult.textContent = '\u00d7' + state.mult.toFixed(1);
elScore.textContent = Math.floor(state.score).toLocaleString();
elSpeedFill.style.width = (state.speed / CFG.boostSpeed * 100).toFixed(1) + '%';
elTurboFill.style.width = state.turbo.toFixed(1) + '%';
elSpeedText.textContent = Math.round(state.speed * 3.4);
const e = state.energy;
elEnergyArc.style.strokeDashoffset = (ARCL * (1 - e / 100)).toFixed(1);
elEnergyText.textContent = Math.round(e);
elEnergyWrap.classList.toggle('low', e < 30 && playing);
const ready = state.turbo >= 100 && !state.boosting;
elBoostReady.style.opacity = ready ? 1 : 0;
elBoostBtn.classList.toggle('ready', ready);
elVignette.style.opacity = playing && e < 30 ? ((0.5 + 0.5 * Math.sin(state.time * 6)) * (1 - e / 30)).toFixed(2) : 0;
}
// ================= Main loop =================
let last = performance.now();
function loop(now) {
requestAnimationFrame(loop);
const dt = Math.min((now - last) / 1000, 0.05);
last = now;
update(dt);
composer.render();
}
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
composer.setSize(window.innerWidth, window.innerHeight);
});
// Init: build initial chunks, start menu
player.pos.set(0, pathY(0), 0);
syncChunks();
requestAnimationFrame(loop);
</script>
</body>
</html>
<!-- agent-meta {"model":"Qwen3.8","provider":"brigid","persona":"full","sessionId":"7ee28c59-dcb0-40e1-a4fd-0a0c9abd7156","tokensIn":4233021,"tokensOut":123062,"tokensTotal":4356083,"cost":0,"turns":50,"toolCalls":56,"failedToolCalls":2,"timestamp":"2026-09-01T20:51:17.690Z"} -->