Canyon Run
muse spark 1.1 · 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
- 3 / 5
- UX / feel
- 3 / 5
- Works
- 4 / 5
- Prompt spirit
- 3 / 5
- Delight
- 3 / 5
Pretty good design; backwards ship model; plain terrain with weird artifacting
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,175 bytes · SHA-256 cfb570e69692
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>CANYON RUNNER — Proximity Racer</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/0.160.0/three.min.js"></script>
<style>
/* ── css: base ── */
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700;800&family=Orbitron:wght@600;800&display=swap');
*{margin:0;padding:0;box-sizing:border-box;user-select:none}
html,body{width:100%;height:100%;overflow:hidden;background:#0b0713}
#app{position:relative;width:100vw;height:100vh}
#c{display:block;width:100%;height:100%}
#hud{position:absolute;inset:0;pointer-events:none;z-index:2}
#topBar{position:absolute;top:0;left:0;right:0;display:flex;justify-content:space-between;padding:18px 28px;align-items:flex-start}
.hud-group{font-family:'JetBrains Mono',monospace}
.left #score{font-family:'Orbitron',sans-serif;font-size:42px;font-weight:800;letter-spacing:-1px;color:#fff;text-shadow:0 0 12px rgba(255,126,41,0.8)}
#subScore{font-size:11px;color:rgba(255,255,255,0.55);letter-spacing:1.2px;margin-top:2px}
.center{display:flex;flex-direction:column;align-items:center;margin-top:6px}
#multiWrap{text-align:center}
#multiplier{font-family:'Orbitron',sans-serif;font-size:52px;font-weight:800;color:#ffe66d;transition:transform 0.1s ease;line-height:1;text-shadow:0 0 20px rgba(255,230,109,0.9)}
#multiplier.pulse{transform:scale(1.25)}
#multiLabel{font-size:9px;letter-spacing:3px;color:rgba(255,255,255,0.45);margin-top:4px}
.right{text-align:right}
#speedNum{font-family:'Orbitron',sans-serif;font-size:42px;font-weight:800;color:#fff;line-height:1}
#speedLabel{font-size:9px;letter-spacing:2.5px;color:rgba(255,255,255,0.4)}
#bottomBar{position:absolute;bottom:0;left:0;right:0;padding:18px 28px 22px;display:flex;gap:24px;align-items:flex-end}
#energyWrap{flex:1.2;max-width:420px}
#energyLabel,#turboLabel{font-family:'JetBrains Mono';font-size:9px;letter-spacing:2.5px;color:rgba(255,255,255,0.5);margin-bottom:8px}
#energyTrack{position:relative;height:6px;background:rgba(255,255,255,0.08);border-radius:99px;overflow:hidden}
#energyFill{position:absolute;left:0;top:0;bottom:0;width:100%;background:linear-gradient(90deg,#ff7b1a,#ffd23f);box-shadow:0 0 12px #ff7b1a;border-radius:99px;transition:width 0.15s linear}
#energyGlow{position:absolute;right:0;top:50%;width:22px;height:22px;transform:translate(50%,-50%);background:radial-gradient(circle,#ffdc6a 0%,transparent 70%);opacity:0.9;pointer-events:none}
#turboWrap{flex:0.9;max-width:320px}
#turboTrack{position:relative;height:6px;background:rgba(255,255,255,0.08);border-radius:99px;overflow:hidden}
#turboFill{position:absolute;left:0;top:0;bottom:0;width:0%;background:linear-gradient(90deg,#7afbff,#a78bfa);box-shadow:0 0 12px #7afbff;border-radius:99px;transition:width 0.1s linear}
#speedTrack{flex:0.6;max-width:220px;height:2px;background:rgba(255,255,255,0.06);position:relative;margin-bottom:2px;border-radius:99px;overflow:hidden}
#speedFill{position:absolute;left:0;top:0;bottom:0;width:40%;background:#fff;box-shadow:0 0 8px #fff;transition:width 0.2s ease}
#nearMiss{position:absolute;left:50%;top:48%;transform:translate(-50%,-50%) scale(0.8);font-family:'Orbitron';font-size:26px;font-weight:800;color:#fff;letter-spacing:2px;opacity:0;pointer-events:none;text-shadow:0 0 20px #ff7e2a;transition:all 0.18s cubic-bezier(.2,.8,.3,1)}
#nearMiss.show{opacity:1;transform:translate(-50%,-70%) scale(1.1)}
#centerReticle{position:absolute;left:50%;top:52%;width:26px;height:26px;border:1px solid rgba(255,255,255,0.13);border-radius:50%;transform:translate(-50%,-50%);pointer-events:none}
#centerReticle::after{content:'';position:absolute;left:50%;top:50%;width:4px;height:4px;background:rgba(255,255,255,0.3);transform:translate(-50%,-50%);border-radius:50%}
#overlay{position:absolute;inset:0;z-index:10;background:radial-gradient(120% 80% at 50% 10%, #2d1450 0%, #140a22 40%, #08050d 100%);display:flex;flex-direction:column;align-items:center;justify-content:center;transition:opacity 0.6s ease, transform 0.6s ease}
#overlay.hidden{opacity:0;pointer-events:none;transform:scale(1.05)}
#titleBlock{text-align:center;margin-bottom:38px}
#gameTitle{font-family:'Orbitron';font-size:76px;font-weight:800;letter-spacing:-2px;color:#fff;line-height:0.9}
#gameTitle span{font-weight:600;color:#ff7e2a;text-shadow:0 0 30px rgba(255,126,42,0.8)}
#tagline{font-family:'JetBrains Mono';margin-top:14px;font-size:11px;letter-spacing:3px;color:rgba(255,255,255,0.5)}
#startCard{background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.08);backdrop-filter:blur(18px);border-radius:16px;padding:22px 28px;min-width:420px}
.kbd{font-family:'JetBrains Mono';font-size:12px;color:rgba(255,255,255,0.7);margin:8px 0;letter-spacing:0.2px}
.kbd b{color:#fff;background:rgba(255,255,255,0.08);padding:2px 6px;border-radius:4px;font-size:11px;margin-right:4px}
.kbd i{color:#ffe66d;font-style:normal}
#playBtn{margin-top:18px;width:100%;font-family:'Orbitron';font-size:15px;font-weight:800;letter-spacing:2px;color:#0b0713;background:linear-gradient(90deg,#ff8c2a,#ffd23f);border:none;border-radius:10px;padding:14px;cursor:pointer;box-shadow:0 0 24px rgba(255,141,42,0.5);transition:transform 0.1s ease, box-shadow 0.2s ease}
#playBtn:hover{transform:translateY(-1px);box-shadow:0 0 34px rgba(255,141,42,0.7)}
#playBtn:active{transform:translateY(1px) scale(0.99)}
#statsEnd{margin-top:18px;border-top:1px solid rgba(255,255,255,0.08);padding-top:16px;display:grid;grid-template-columns:1fr 1fr;gap:10px}
#statsEnd.hidden{display:none}
.stat{display:flex;flex-direction:column;gap:2px}
.stat span{font-family:'JetBrains Mono';font-size:9px;letter-spacing:2px;color:rgba(255,255,255,0.4)}
.stat b{font-family:'Orbitron';font-size:18px;color:#fff}
#footerHint{position:absolute;bottom:16px;font-family:'JetBrains Mono';font-size:9px;letter-spacing:2.5px;color:rgba(255,255,255,0.22)}
@media (max-width:640px){#gameTitle{font-size:44px}#startCard{min-width:88vw}#bottomBar{flex-wrap:wrap}}
</style>
</head>
<body>
<div id="app">
<canvas id="c"></canvas>
<div id="hud">
<div id="topBar">
<div class="hud-group left">
<div id="score">00000</div>
<div id="subScore"><span id="dist">0m</span> • <span id="maxSpeed">0 km/h</span></div>
</div>
<div class="hud-group center">
<div id="multiWrap">
<div id="multiplier">x1.0</div>
<div id="multiLabel">MULTIPLIER</div>
</div>
</div>
<div class="hud-group right">
<div id="speedNum">0</div>
<div id="speedLabel">KM/H</div>
</div>
</div>
<div id="bottomBar">
<div id="energyWrap">
<div id="energyLabel">ENERGY</div>
<div id="energyTrack"><div id="energyFill"></div><div id="energyGlow"></div></div>
</div>
<div id="turboWrap">
<div id="turboLabel">TURBO <span id="turboKey">[ SHIFT ]</span></div>
<div id="turboTrack"><div id="turboFill"></div></div>
</div>
<div id="speedTrack"><div id="speedFill"></div></div>
</div>
<div id="nearMiss">NEAR MISS +<span id="nearVal">0</span></div>
<div id="centerReticle"></div>
</div>
<div id="overlay">
<div id="titleBlock">
<h1 id="gameTitle">CANYON<span>RUNNER</span></h1>
<p id="tagline">FLIGHT PHYSICS • PROXIMITY SCORING • INFINITE PROCEDURAL</p>
</div>
<div id="startCard">
<div class="kbd"><b>WASD / ARROWS / MOUSE</b> — BANK & PITCH</div>
<div class="kbd"><b>SHIFT / SPACE</b> — TURBO BURN</div>
<div class="kbd">GRAZE WALLS FOR <i>xMULTIPLIER</i> • COLLECT ORBS FOR ENERGY</div>
<button id="playBtn">IGNITE ENGINES [ ENTER ]</button>
<div id="statsEnd" class="hidden">
<div class="stat"><span>FINAL SCORE</span><b id="finalScore">0</b></div>
<div class="stat"><span>MAX SPEED</span><b id="finalMax">0</b></div>
<div class="stat"><span>MAX MULTIPLIER</span><b id="finalMulti">x1.0</b></div>
<div class="stat"><span>ORB COLLECTED</span><b id="finalOrbs">0</b></div>
</div>
</div>
<div id="footerHint">No assets • 60fps Low-Poly • Audio Synthetic</div>
</div>
</div>
<script>
'use strict';
// ── const: CFG ──
const CFG = {
CHUNK_LEN: 140,
VISIBLE_CHUNKS: 7,
CANYON_BASE_WIDTH: 52,
CANYON_MIN_WIDTH: 18,
PLAYER_BOUNDS_X: 22,
PLAYER_BOUNDS_Y: 14,
BASE_SPEED: 62,
MAX_SPEED: 260,
ACCEL: 18,
TURBO_SPEED: 120,
ENERGY_DECAY: 9.5,
ENERGY_ORB: 22,
TURBO_GAIN_NEAR: 12,
TURBO_GAIN_ORB: 8,
FOV_BASE: 74,
FOV_TURBO: 98,
};
// ── const: PAL ──
const PAL = {
fog: 0x130a28,
ground: 0x201436,
wall: 0x3a2a5e,
wall2: 0xff7e2a,
orb: 0xffd23f,
ship: 0xe8e6ef,
};
// ── function: lerp ──
function lerp(a,b,t){return a+(b-a)*t}
// ── function: clamp ──
function clamp(v,a,b){return Math.max(a,Math.min(b,v))}
// ── function: smoothstep ──
function smoothstep(e0,e1,x){ const t=clamp((x-e0)/(e1-e0),0,1); return t*t*(3-2*t)}
// ── function: rand ──
function rand(a=0,b=1){return a+(b-a)*Math.random()}
// ── function: hash ──
function hash(n){ // deterministic pseudo
const x = Math.sin(n*127.1)*43758.5453;
return x - Math.floor(x);
}
// ── class: AudioEngine ──
class AudioEngine {
constructor(){
this.ctx = null;
this.master = null;
this.engineOsc = null;
this.engineGain = null;
this.on = false;
}
init(){
if(this.ctx) return;
const AC = window.AudioContext || window.webkitAudioContext;
this.ctx = new AC();
this.master = this.ctx.createGain();
this.master.gain.value = 0.28;
this.master.connect(this.ctx.destination);
// engine hum
this.engineGain = this.ctx.createGain();
this.engineGain.gain.value = 0;
this.engineGain.connect(this.master);
this.engineOsc = this.ctx.createOscillator();
this.engineOsc.type = 'sawtooth';
this.engineOsc.frequency.value = 58;
const low = this.ctx.createBiquadFilter();
low.type='lowpass'; low.frequency.value=420;
this.engineOsc.connect(low);
low.connect(this.engineGain);
this.engineOsc.start();
// second layer
this.engineOsc2 = this.ctx.createOscillator();
this.engineOsc2.type='sine';
this.engineOsc2.frequency.value=110;
this.engineGain2 = this.ctx.createGain(); this.engineGain2.gain.value=0;
this.engineOsc2.connect(this.engineGain2);
this.engineGain2.connect(this.master);
this.engineOsc2.start();
this.on=true;
}
setEngine(speedNorm, turbo){
if(!this.ctx) return;
const f = 48 + speedNorm*140 + turbo*120;
if(this.engineOsc) this.engineOsc.frequency.linearRampToValueAtTime(f, this.ctx.currentTime+0.12);
if(this.engineOsc2) this.engineOsc2.frequency.linearRampToValueAtTime(f*1.015, this.ctx.currentTime+0.12);
this.engineGain.gain.linearRampToValueAtTime(0.06+speedNorm*0.16+turbo*0.12, this.ctx.currentTime+0.08);
this.engineGain2.gain.linearRampToValueAtTime(0.02+speedNorm*0.06, this.ctx.currentTime+0.08);
}
blip(type='orb'){
if(!this.ctx) return;
const o = this.ctx.createOscillator();
const g = this.ctx.createGain();
o.connect(g); g.connect(this.master);
const t = this.ctx.currentTime;
if(type==='orb'){
o.type='sine'; o.frequency.setValueAtTime(660,t); o.frequency.exponentialRampToValueAtTime(1320,t+0.18);
g.gain.setValueAtTime(0.6,t); g.gain.exponentialRampToValueAtTime(0.001,t+0.35);
o.start(t); o.stop(t+0.36);
}else if(type==='near'){
o.type='triangle'; o.frequency.setValueAtTime(300,t); o.frequency.linearRampToValueAtTime(880,t+0.12);
g.gain.setValueAtTime(0.25,t); g.gain.exponentialRampToValueAtTime(0.001,t+0.25);
o.start(t); o.stop(t+0.26);
}else if(type==='hit'){
o.type='sawtooth'; o.frequency.setValueAtTime(160,t); o.frequency.linearRampToValueAtTime(20,t+0.4);
const f = this.ctx.createBiquadFilter(); f.type='lowpass'; f.frequency.value=800;
o.disconnect(); o.connect(f); f.connect(g);
g.gain.setValueAtTime(0.7,t); g.gain.exponentialRampToValueAtTime(0.001,t+0.5);
o.start(t); o.stop(t+0.5);
}else if(type==='turbo'){
o.type='sine'; o.frequency.setValueAtTime(200,t); o.frequency.exponentialRampToValueAtTime(900,t+0.6);
g.gain.setValueAtTime(0.5,t); g.gain.linearRampToValueAtTime(0,t+0.6);
o.start(t); o.stop(t+0.6);
}
}
}
// ── class: InputSys ──
class InputSys {
constructor(){
this.x=0; this.y=0; // -1..1
this.turboHeld=false;
this._keys={};
this._mouseDown=false;
this._mx=0; this._my=0;
}
attach(canvas){
window.addEventListener('keydown', (e)=>{
this._keys[e.code]=true;
if(e.code==='Space'||e.code==='ShiftLeft'||e.code==='ShiftRight') this.turboHeld=true;
});
window.addEventListener('keyup', (e)=>{
this._keys[e.code]=false;
if(e.code==='Space'||e.code==='ShiftLeft'||e.code==='ShiftRight') this.turboHeld=false;
});
canvas.addEventListener('pointerdown', (e)=>{ this._mouseDown=true; canvas.setPointerCapture(e.pointerId); });
window.addEventListener('pointerup', ()=>{ this._mouseDown=false; });
canvas.addEventListener('pointermove', (e)=>{
if(this._mouseDown){
this._mx = (e.clientX / window.innerWidth -0.5)*2;
this._my = -(e.clientY / window.innerHeight -0.5)*2;
}
});
// gyro fallback not needed
}
update(dt){
let ix=0, iy=0;
if(this._keys['KeyA']||this._keys['ArrowLeft']) ix-=1;
if(this._keys['KeyD']||this._keys['ArrowRight']) ix+=1;
if(this._keys['KeyW']||this._keys['ArrowUp']) iy+=1;
if(this._keys['KeyS']||this._keys['ArrowDown']) iy-=1;
// blend keyboard and mouse
if(this._mouseDown){
ix = lerp(ix, this._mx*1.2, 0.5);
iy = lerp(iy, this._my*0.9, 0.5);
}
// smooth input
this.x = lerp(this.x, clamp(ix,-1,1), 1 - Math.pow(0.001, dt*60));
this.y = lerp(this.y, clamp(iy,-1,1), 1 - Math.pow(0.001, dt*60));
if(!this._mouseDown && Math.abs(ix)<0.01 && Math.abs(iy)<0.01){
// decay
this.x *= Math.pow(0.9, dt*60);
this.y *= Math.pow(0.9, dt*60);
}
}
}
// ── function: makeShipGroup ──
function makeShipGroup(){
const g = new THREE.Group();
// main hull - sleek dart
const hullGeo = new THREE.BufferGeometry();
// vertices for low-poly ship: front tip, back, wings
const verts = new Float32Array([
0,0,6, -1.6,0,0, 1.6,0,0, // top face?
0,0,6, 1.6,0,0, 0, -0.6, 1.2,
0,0,6, 0,-0.6,1.2, -1.6,0,0,
-1.6,0,0, 0,-0.6,1.2, 0,0,-3,
1.6,0,0, 0,0,-3, 0,-0.6,1.2,
-1.6,0,0, 0,0,-3, 0,0.45, -0.6,
1.6,0,0, 0,0.45,-0.6, 0,0,-3,
-1.6,0,0, 0,0.45,-0.6, 0,0,0,
1.6,0,0, 0,0,0, 0,0.45,-0.6,
]);
hullGeo.setAttribute('position', new THREE.BufferAttribute(verts,3));
hullGeo.computeVertexNormals();
const hullMat = new THREE.MeshStandardMaterial({color:PAL.ship, flatShading:true, metalness:0.3, roughness:0.45, emissive:0x101018, emissiveIntensity:0.2});
const hull = new THREE.Mesh(hullGeo, hullMat);
g.add(hull);
// cockpit glow
const cockGeo = new THREE.BoxGeometry(0.6,0.28,1.2);
const cockMat = new THREE.MeshStandardMaterial({color:0x89dfff, emissive:0x2ab8ff, emissiveIntensity:1.2, transparent:true, opacity:0.9});
const cock = new THREE.Mesh(cockGeo, cockMat);
cock.position.set(0,0.28,0.2);
g.add(cock);
// engine glows
const engGeo = new THREE.ConeGeometry(0.45,1.2,6);
engGeo.rotateX(Math.PI);
const engMat = new THREE.MeshStandardMaterial({color:0xff7e2a, emissive:0xff4f00, emissiveIntensity:2.2, transparent:true, opacity:0.9});
const eng = new THREE.Mesh(engGeo, engMat);
eng.position.set(0,-0.2,-3.2);
g.add(eng);
g.userData.engMat = engMat;
g.userData.hullMat = hullMat;
// wing tip lights
const lightGeo = new THREE.SphereGeometry(0.18,6,6);
const lightMat = new THREE.MeshStandardMaterial({emissive:0xffffff, emissiveIntensity:1.5, color:0xffffff});
const lw = new THREE.Mesh(lightGeo, lightMat); lw.position.set(-1.6,0,0); g.add(lw);
const rw = new THREE.Mesh(lightGeo, lightMat.clone()); rw.position.set(1.6,0,0); g.add(rw);
rw.material.emissive.setHex(0xff2a2a);
lw.material.emissive.setHex(0x2aff9c);
// trail plane
const trailGeo = new THREE.PlaneGeometry(1.2, 5);
const trailMat = new THREE.MeshBasicMaterial({color:0xff8a2a, transparent:true, opacity:0.0, blending:THREE.AdditiveBlending, side:THREE.DoubleSide});
const trail = new THREE.Mesh(trailGeo, trailMat);
trail.position.set(0,-0.2,-5.7);
trail.rotation.x = Math.PI*0.45;
g.add(trail);
g.userData.trailMat = trailMat;
return g;
}
// ── function: makeOrbMesh ──
function makeOrbMesh(){
const geo = new THREE.OctahedronGeometry(1.1,0);
const mat = new THREE.MeshStandardMaterial({color:0xffe9a0, emissive:0xff9b00, emissiveIntensity:1.8, flatShading:true});
const m = new THREE.Mesh(geo, mat);
// core
const coreGeo = new THREE.SphereGeometry(0.35,8,8);
const coreMat = new THREE.MeshBasicMaterial({color:0xfffff0});
const core = new THREE.Mesh(coreGeo, coreMat);
m.add(core);
// glow sprite
const sprMat = new THREE.MeshBasicMaterial({color:0xffb347, transparent:true, opacity:0.35, blending:THREE.AdditiveBlending});
const glowGeo = new THREE.SphereGeometry(2.2,10,10);
const glow = new THREE.Mesh(glowGeo, sprMat);
m.add(glow);
m.userData.glowMat = sprMat;
return m;
}
// ── class: Chunk ──
class Chunk {
constructor(index, zStart){
this.index=index;
this.zStart=zStart;
this.zEnd=zStart-CFG.CHUNK_LEN;
this.group=new THREE.Group();
this.group.position.z=zStart;
this.colliders=[]; // {box:THREE.Box3, type:'wall'|'obs', mesh}
this.orbs=[];
this.lights=[];
}
dispose(scene){
scene.remove(this.group);
this.group.traverse(o=>{
if(o.geometry) o.geometry.dispose();
});
}
}
// ── class: WorldGenerator ──
class WorldGenerator {
constructor(scene){
this.scene=scene;
this.chunks=[];
this.nextZ=0;
this.nextIndex=0;
}
drand(i, salt){
return hash(i*12.9898 + salt*78.233 + 0.123) ;
}
generateInitial(){
for(let k=0;k<CFG.VISIBLE_CHUNKS;k++){
this.pushChunk();
}
}
pushChunk(){
const idx=this.nextIndex++;
const z0=this.nextZ;
this.nextZ-=CFG.CHUNK_LEN;
const ch=new Chunk(idx,z0);
const difficulty = Math.min(1.9, 0.2 + idx*0.04);
const canyonGap = lerp(CFG.CANYON_BASE_WIDTH, CFG.CANYON_MIN_WIDTH, clamp(difficulty/1.6,0,1));
const centerOffset = (this.drand(idx,1)-0.5) * 14 * Math.min(1, difficulty*0.7);
const wallHeight=70 + this.drand(idx,2)*30;
const wallThickness=60;
const leftWallLen = CFG.CHUNK_LEN + 4;
const leftGeo = new THREE.BoxGeometry(wallThickness, wallHeight, leftWallLen, 2, 3, 4);
const posAttr = leftGeo.attributes.position;
for(let v=0; v<posAttr.count; v++){
const y = posAttr.getY(v);
if(y> wallHeight*0.3){
const disp = (this.drand(idx+v*0.1,9)-0.5)*8;
posAttr.setY(v, y+disp);
posAttr.setX(v, posAttr.getX(v)+ (this.drand(idx+v,11)-0.5)*4);
}
}
posAttr.needsUpdate=true; leftGeo.computeVertexNormals();
const hueShift = this.drand(idx,3) >0.5 ? 0x3d2e5e : 0x4a2d3a;
const matA = new THREE.MeshStandardMaterial({color:hueShift, flatShading:true, roughness:0.85, metalness:0.1});
const leftMesh = new THREE.Mesh(leftGeo, matA);
leftMesh.position.set(centerOffset - canyonGap/2 - wallThickness/2, wallHeight/2-12, -leftWallLen/2+2);
ch.group.add(leftMesh);
ch.colliders.push({box:new THREE.Box3().setFromCenterAndSize(leftMesh.position.clone(), new THREE.Vector3(wallThickness, wallHeight, leftWallLen)), mesh:leftMesh, type:'wall', half:new THREE.Vector3(wallThickness/2, wallHeight/2, leftWallLen/2)});
const rightGeo = new THREE.BoxGeometry(wallThickness, wallHeight, leftWallLen, 2,3,4);
const pos2 = rightGeo.attributes.position;
for(let v=0; v<pos2.count; v++){
const y = pos2.getY(v);
if(y> wallHeight*0.3){
const disp = (this.drand(idx+v*0.13,13)-0.5)*8;
pos2.setY(v, y+disp);
pos2.setX(v, pos2.getX(v)+ (this.drand(idx+v,17)-0.5)*4);
}
}
pos2.needsUpdate=true; rightGeo.computeVertexNormals();
const rightMesh = new THREE.Mesh(rightGeo, matA.clone());
rightMesh.position.set(centerOffset + canyonGap/2 + wallThickness/2, wallHeight/2-12, -leftWallLen/2+2);
ch.group.add(rightMesh);
ch.colliders.push({box:new THREE.Box3().setFromCenterAndSize(rightMesh.position.clone(), new THREE.Vector3(wallThickness, wallHeight, leftWallLen)), mesh:rightMesh, type:'wall', half:new THREE.Vector3(wallThickness/2, wallHeight/2, leftWallLen/2)});
const floorGeo = new THREE.PlaneGeometry(200, CFG.CHUNK_LEN+2, 4,2);
const floorMat = new THREE.MeshStandardMaterial({color:0x1a0f2f, roughness:1, metalness:0});
const fp = floorGeo.attributes.position;
for(let v=0; v<fp.count; v++){
const z = fp.getY(v);
fp.setZ(v, (this.drand(idx+v*0.21,19)-0.5)*3 + Math.sin(z*0.02+idx)*2);
}
fp.needsUpdate=true;
const floor = new THREE.Mesh(floorGeo, floorMat);
floor.rotation.x = -Math.PI/2;
floor.position.set(centerOffset, -8, -CFG.CHUNK_LEN/2);
ch.group.add(floor);
const obsCount = Math.floor(1 + difficulty*2.2 + this.drand(idx,5)*1.2);
for(let o=0;o<obsCount;o++){
const t = this.drand(idx*10+o,7);
const zLocal = - (t*0.78+0.08)*CFG.CHUNK_LEN - rand(0,10);
const typeRoll = this.drand(idx+o,23);
if(typeRoll < 0.44){
const w = 2.5 + this.drand(idx+o,29)*4;
const h = 10 + this.drand(idx+o,31)*24;
const d = 2.5 + this.drand(idx+o,37)*3;
const geo = new THREE.BoxGeometry(w,h,d);
const mat = new THREE.MeshStandardMaterial({color: typeRoll<0.22?0xff8552:0x6d507a, flatShading:true, roughness:0.7});
const m = new THREE.Mesh(geo, mat);
const xOff = (this.drand(idx+o*1.7,41)-0.5)* (canyonGap-8);
m.position.set(centerOffset + xOff, -4 + h/2, zLocal);
ch.group.add(m);
ch.colliders.push({box:new THREE.Box3().setFromCenterAndSize(m.position.clone(), new THREE.Vector3(w,h,d)), mesh:m, type:'obs', half:new THREE.Vector3(w/2,h/2,d/2)});
}else if(typeRoll < 0.74){
const gap = Math.max(7.5 - difficulty*2, 5.2) + this.drand(idx+o,43)*1.2;
const gapX = centerOffset + (this.drand(idx+o,47)-0.5)*(canyonGap*0.25);
const sideW=5, sideH=34, sideD=4;
for(let side of [-1,1]){
const geo = new THREE.BoxGeometry(sideW, sideH, sideD);
const mat = new THREE.MeshStandardMaterial({color:0xb86f4c, flatShading:true});
const mm = new THREE.Mesh(geo, mat);
mm.position.set(gapX + side*(gap/2+sideW/2), sideH/2-8, zLocal);
ch.group.add(mm);
ch.colliders.push({box:new THREE.Box3().setFromCenterAndSize(mm.position.clone(), new THREE.Vector3(sideW,sideH,sideD)), mesh:mm, type:'obs', half:new THREE.Vector3(sideW/2,sideH/2,sideD/2)});
}
if(this.drand(idx+o,53)>0.5){
const beamGeo = new THREE.BoxGeometry(gap+sideW*2, 2.2, 2.8);
const beamMat = new THREE.MeshStandardMaterial({color:0xffb074, emissive:0xff6a2a, emissiveIntensity:0.25, flatShading:true});
const beam = new THREE.Mesh(beamGeo, beamMat);
beam.position.set(gapX, 12 + this.drand(idx+o,59)*6, zLocal);
ch.group.add(beam);
ch.colliders.push({box:new THREE.Box3().setFromCenterAndSize(beam.position.clone(), new THREE.Vector3(gap+sideW*2,2.2,2.8)), mesh:beam, type:'obs', half:new THREE.Vector3(gap/2+sideW,1.1,1.4)});
}
}else{
const w=14, h=2.3, d=2.0;
const geo = new THREE.BoxGeometry(w,h,d);
const mat = new THREE.MeshStandardMaterial({color:0x8a5ae0, flatShading:true});
const mm = new THREE.Mesh(geo, mat);
const xOff = (this.drand(idx+o,61)-0.5)*(canyonGap-12);
mm.position.set(centerOffset+xOff, 2 + this.drand(idx+o,67)*10, zLocal);
mm.rotation.z = (this.drand(idx+o,71)-0.5)*0.9;
ch.group.add(mm);
ch.colliders.push({box:new THREE.Box3().setFromCenterAndSize(mm.position.clone(), new THREE.Vector3(w/2+1, h/2+1, d/2+1)), mesh:mm, type:'obs', half:new THREE.Vector3(w/2+1,h/2+1,d/2+1)});
}
}
const orbCount = 2 + Math.floor(this.drand(idx,83)*2 + difficulty*0.85);
for(let k=0;k<orbCount;k++){
const tk = (k+0.5)/orbCount + (this.drand(idx+k,89)-0.5)*0.18;
const zz = -tk*CFG.CHUNK_LEN;
const xx = centerOffset + (this.drand(idx+k,107)-0.5)*(canyonGap*0.62);
const yy = -1 + this.drand(idx+k,109)*12;
const orb = makeOrbMesh();
orb.position.set(xx, yy, zz);
orb.rotation.set(rand(0,Math.PI), rand(0,Math.PI), 0);
orb.userData.baseY = yy;
orb.userData.phase = rand(0,Math.PI*2);
orb.userData.collected=false;
ch.group.add(orb);
ch.orbs.push({mesh:orb, collected:false});
}
this.scene.add(ch.group);
this.chunks.push(ch);
}
// worldOffset is total forward distance traveled (positive). Chunks have been moved by +worldOffset?
updateCollidersWorld(worldOffsetZ){
for(const ch of this.chunks){
for(const c of ch.colliders){
c.box.setFromCenterAndSize(
new THREE.Vector3(c.mesh.position.x, c.mesh.position.y, c.mesh.position.z + ch.group.position.z + worldOffsetZ),
new THREE.Vector3(c.half.x*2, c.half.y*2, c.half.z*2)
);
}
for(const o of ch.orbs){
if(o.collected) continue;
const wp = o.mesh.getWorldPosition(new THREE.Vector3());
wp.z += worldOffsetZ;
o._wp = wp;
}
}
}
recycleIfNeeded(worldOffsetZ){
// chunks are at position.z + worldOffsetZ moving toward player. Player at 0,0,0. We want to remove chunks that are behind (z + offset > +60)
while(this.chunks.length && this.chunks[0].group.position.z + worldOffsetZ > 80){
const old = this.chunks.shift();
old.group.traverse(obj=>{ if(obj.geometry) obj.geometry.dispose(); });
this.scene.remove(old.group);
this.pushChunk();
// new chunk group position is already set, but needs to account for worldOffset? Its group pos is absolute; worldOffset is added externally via chunkGroupOffset? We'll keep worldOffset applied as translation of whole world manager: instead of shifting each chunk, we shift a parent. Simpler: after push, set its group.z += worldOffset? No.
// Let's adjust: new chunk's group.position.z is already this.nextZ (increasing negative). But worldOffset makes older chunks move positive. So we need to not re-add offset.
// For consistency, after push we must subtract worldOffset from its pos because update will add worldOffset later? Alternative: keep worldOffset applied as separate group offset to all chunks.
}
// Ensure we have enough chunks ahead
while(this.chunks.length < CFG.VISIBLE_CHUNKS){
this.pushChunk();
}
}
}
// ── class: DustSystem ──
class DustSystem {
constructor(scene, count=320){
this.scene=scene;
const geo = new THREE.BufferGeometry();
const pos = new Float32Array(count*3);
const vel = new Float32Array(count);
for(let i=0;i<count;i++){
pos[i*3+0]=rand(-50,50);
pos[i*3+1]=rand(-12,32);
pos[i*3+2]=rand(-240,40);
vel[i]=rand(0.8,1.6);
}
geo.setAttribute('position', new THREE.BufferAttribute(pos,3));
geo.setAttribute('vel', new THREE.BufferAttribute(vel,1));
const mat = new THREE.PointsMaterial({color:0xffc68a, size:0.22, transparent:true, opacity:0.55, depthWrite:false});
this.points = new THREE.Points(geo, mat);
this.scene.add(this.points);
this.count=count;
}
update(dt, speed, worldOffset){
const pos = this.points.geometry.attributes.position;
for(let i=0;i<this.count;i++){
let z = pos.getZ(i);
// move towards player (positive Z) due to speed
z += speed * dt * (0.6 + pos.getX(i)*0.002);
// wrap
if(z > 30){
z = -240 + rand(-10,0);
pos.setX(i, rand(-48,48));
pos.setY(i, rand(-10,30));
}
pos.setZ(i, z);
}
pos.needsUpdate=true;
}
}
// ── class: TrailSystem ──
class TrailSystem {
constructor(scene){
this.scene=scene;
this.particles=[];
const geo = new THREE.SphereGeometry(0.35,4,4);
const mat = new THREE.MeshBasicMaterial({color:0xff8a2a, transparent:true, opacity:0.9, blending:THREE.AdditiveBlending});
this.protoMat = mat;
this.geo = geo;
}
spawn(pos, speed){
if(this.particles.length>45) return;
const m = new THREE.Mesh(this.geo, this.protoMat.clone());
m.position.copy(pos);
m.scale.setScalar(rand(0.6,1.0));
this.scene.add(m);
this.particles.push({mesh:m, life:1.0, vel:new THREE.Vector3(rand(-0.8,0.8), rand(-0.8,0.8), rand(1.5,3.5))});
}
update(dt){
for(let i=this.particles.length-1;i>=0;i--){
const p=this.particles[i];
p.life-=dt*1.8;
p.mesh.position.addScaledVector(p.vel, dt*12);
p.mesh.scale.multiplyScalar(0.985);
p.mesh.material.opacity = p.life*0.7;
if(p.life<=0){
this.scene.remove(p.mesh);
this.particles.splice(i,1);
}
}
}
}
// ── main ──
(() => {
const canvas = document.getElementById('c');
const overlay = document.getElementById('overlay');
const playBtn = document.getElementById('playBtn');
const scoreEl = document.getElementById('score');
const distEl = document.getElementById('dist');
const maxSpeedEl = document.getElementById('maxSpeed');
const speedNumEl = document.getElementById('speedNum');
const multiEl = document.getElementById('multiplier');
const energyFill = document.getElementById('energyFill');
const energyGlow = document.getElementById('energyGlow');
const turboFill = document.getElementById('turboFill');
const speedFill = document.getElementById('speedFill');
const nearMissEl = document.getElementById('nearMiss');
const nearValEl = document.getElementById('nearVal');
const statsEnd = document.getElementById('statsEnd');
const scene = new THREE.Scene();
scene.fog = new THREE.Fog(PAL.fog, 60, 260);
scene.background = new THREE.Color(0x170a2e);
const renderer = new THREE.WebGLRenderer({canvas, antialias:true, powerPreference:'high-performance'});
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.8));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.outputColorSpace = THREE.SRGBColorSpace;
const camera = new THREE.PerspectiveCamera(CFG.FOV_BASE, window.innerWidth/window.innerHeight, 0.1, 1000);
camera.position.set(0, 4.8, 16);
camera.lookAt(0,1.5,0);
const ambient = new THREE.AmbientLight(0x8a6eff, 0.55);
scene.add(ambient);
const sun = new THREE.DirectionalLight(0xff9a3d, 1.35);
sun.position.set(-30, 60, 10);
scene.add(sun);
const rim = new THREE.DirectionalLight(0x7bf8ff, 0.6);
rim.position.set(20,10,-40);
scene.add(rim);
const worldGen = new WorldGenerator(scene);
worldGen.generateInitial();
const ship = makeShipGroup();
scene.add(ship);
ship.position.set(0, 1.5, 0);
const dust = new DustSystem(scene, 380);
const trail = new TrailSystem(scene);
const input = new InputSys();
input.attach(canvas);
const audio = new AudioEngine();
let worldOffset = 0;
let state = 'menu';
let vx=0, vy=0;
let px=0, py=1.5;
let roll=0, pitch=0;
let speed = CFG.BASE_SPEED;
let baseSpeed = CFG.BASE_SPEED;
let turboMeter = 35;
let turboActive=false;
let turboTime=0;
let energy=100;
let score=0, distance=0;
let multiplier=1.0;
let maxMultiplier=1.0;
let maxSpeed=0;
let orbCollected=0;
let nearTimer=0;
let nearAccum=0;
let shake=0;
let straightTime=0;
function resetGame(){
worldOffset=0;
worldGen.chunks.forEach(c=>{ scene.remove(c.group); c.group.traverse(o=>{ if(o.geometry) o.geometry.dispose(); }); });
worldGen.chunks=[]; worldGen.nextZ=0; worldGen.nextIndex=0;
worldGen.generateInitial();
px=0; py=1.5; vx=0; vy=0; roll=0; pitch=0;
baseSpeed=CFG.BASE_SPEED; speed=baseSpeed;
turboMeter=35; turboActive=false; turboTime=0;
energy=100; score=0; distance=0; multiplier=1.0; maxMultiplier=1.0; maxSpeed=0; orbCollected=0;
nearTimer=0; nearAccum=0; shake=0; straightTime=0;
statsEnd.classList.add('hidden');
playBtn.textContent='IGNITE ENGINES [ ENTER ]';
}
function startGame(){
if(state==='playing') return;
audio.init();
if(state==='over') resetGame();
state='playing';
overlay.classList.add('hidden');
}
function gameOver(){
if(state!=='playing') return;
state='over';
audio.blip('hit');
document.getElementById('finalScore').textContent = Math.floor(score).toString();
document.getElementById('finalMax').textContent = Math.floor(maxSpeed)+' km/h';
document.getElementById('finalMulti').textContent = 'x'+maxMultiplier.toFixed(1);
document.getElementById('finalOrbs').textContent = orbCollected.toString();
statsEnd.classList.remove('hidden');
playBtn.textContent='RETRY RUN [ ENTER ]';
overlay.classList.remove('hidden');
}
playBtn.addEventListener('click', startGame);
window.addEventListener('keydown', (e)=>{
if(e.code==='Enter') { if(state!=='playing') startGame(); }
if(e.code==='KeyR' && state==='over') { resetGame(); startGame(); }
});
let lastT = performance.now();
let nearDisplayT=0;
function loop(){
const now = performance.now();
let dt = (now-lastT)/1000;
dt = Math.min(dt, 0.033);
lastT = now;
input.update(dt);
if(state==='playing'){
baseSpeed = CFG.BASE_SPEED + distance*0.12 + Math.min(70, score*0.06);
baseSpeed = clamp(baseSpeed, CFG.BASE_SPEED, CFG.MAX_SPEED);
const wantsTurbo = input.turboHeld && turboMeter > 2;
if(wantsTurbo && !turboActive){
turboActive=true; turboTime=1.6; audio.blip('turbo');
}
if(turboActive){
turboTime-=dt;
turboMeter = Math.max(0, turboMeter - dt*34);
speed = baseSpeed + CFG.TURBO_SPEED;
if(turboTime<=0 || turboMeter<=0 || !input.turboHeld){
turboActive=false;
}
}else{
speed = lerp(speed, baseSpeed, dt*2.5);
turboMeter = clamp(turboMeter + dt*2.2, 0, 100);
}
const targetVX = input.x * 19;
const targetVY = input.y * 12;
const accel = 26 + speed*0.03;
vx = lerp(vx, targetVX, clamp(accel*dt*0.18,0,1));
vx += -vx*0.8*dt;
vy = lerp(vy, targetVY, clamp(accel*dt*0.18,0,1));
vy += -vy*0.9*dt;
px += vx*dt;
py += vy*dt;
px = clamp(px, -CFG.PLAYER_BOUNDS_X, CFG.PLAYER_BOUNDS_X);
py = clamp(py, -6, CFG.PLAYER_BOUNDS_Y);
if(Math.abs(px)>=CFG.PLAYER_BOUNDS_X-0.01) vx*= -0.2;
if(py<=-5.8 || py>=CFG.PLAYER_BOUNDS_Y-0.1) vy*= -0.25;
const targetRoll = -vx*0.11 - input.x*0.42;
const targetPitch = vy*0.11 + input.y*0.25 - speed*0.002;
roll = lerp(roll, targetRoll, dt*7);
pitch = lerp(pitch, targetPitch, dt*6);
ship.position.set(px, py, 0);
ship.rotation.set(pitch*0.6, -roll*0.25, roll);
if(speed>40 && (Math.abs(vx)>1 || turboActive)){
if(Math.random()<0.45) trail.spawn(new THREE.Vector3(px, py-0.25, -3.2), speed);
}
if(Math.abs(input.x)<0.15 && Math.abs(vx)<1.2){
straightTime+=dt;
if(straightTime>2.2){
turboMeter = clamp(turboMeter + dt*6,0,100);
}
}else straightTime=0;
const travel = speed * dt;
distance += travel*0.08;
worldOffset += travel;
maxSpeed = Math.max(maxSpeed, speed*5.2);
for(const ch of worldGen.chunks){
ch.group.position.z += travel;
}
while(worldGen.chunks.length && worldGen.chunks[0].group.position.z > 70){
const old = worldGen.chunks.shift();
old.group.traverse(o=>{ if(o.geometry) o.geometry.dispose(); });
scene.remove(old.group);
worldGen.pushChunk();
const newest = worldGen.chunks[worldGen.chunks.length-1];
newest.group.position.z = newest.zStart + worldOffset;
}
let closestDist = 1000;
let collided=false;
const playerSpherePos = new THREE.Vector3(px, py, 0);
const playerRadius = 1.05;
for(const ch of worldGen.chunks){
const gz = ch.group.position.z;
if(gz < -220 || gz > 70) continue;
for(const col of ch.colliders){
const wp = new THREE.Vector3();
col.mesh.getWorldPosition(wp);
const half = col.half;
const cx = clamp(playerSpherePos.x, wp.x-half.x, wp.x+half.x);
const cy = clamp(playerSpherePos.y, wp.y-half.y, wp.y+half.y);
const cz = clamp(playerSpherePos.z, wp.z-half.z, wp.z+half.z);
const dx = playerSpherePos.x - cx;
const dy = playerSpherePos.y - cy;
const dz = playerSpherePos.z - cz;
const dist = Math.sqrt(dx*dx+dy*dy+dz*dz);
if(dist < closestDist) closestDist = dist;
if(dist < playerRadius + 0.15){
collided=true; break;
}
}
if(collided) break;
for(const orb of ch.orbs){
if(orb.collected) continue;
const wp = new THREE.Vector3();
orb.mesh.getWorldPosition(wp);
const d = wp.distanceTo(playerSpherePos);
if(d < 2.2){
orb.collected=true;
orb.mesh.visible=false;
energy = clamp(energy + CFG.ENERGY_ORB, 0, 100);
turboMeter = clamp(turboMeter + CFG.TURBO_GAIN_ORB, 0, 100);
score += 120 * multiplier;
orbCollected++;
audio.blip('orb');
for(let b=0;b<8;b++) trail.spawn(wp, speed);
}else{
orb.mesh.rotation.y += dt*1.4;
orb.mesh.position.y = orb.mesh.userData.baseY + Math.sin(now*0.002 + orb.mesh.userData.phase)*0.6;
if(orb.mesh.userData.glowMat){
orb.mesh.userData.glowMat.opacity = 0.25 + Math.sin(now*0.005 + orb.mesh.userData.phase)*0.15;
}
}
}
}
if(!collided && closestDist < 4.8){
const prox = 1 - (closestDist/4.8);
nearAccum += prox*dt*4.5;
nearTimer += dt;
shake = Math.max(shake, prox*0.6);
if(nearTimer>0.18 && prox>0.35){
multiplier = clamp(multiplier + prox*0.14, 1, 9.9);
maxMultiplier = Math.max(maxMultiplier, multiplier);
turboMeter = clamp(turboMeter + prox*CFG.TURBO_GAIN_NEAR*dt*2.5,0,100);
if(nearTimer>0.45){
audio.blip('near');
nearTimer=0;
nearValEl.textContent = (prox*100).toFixed(0);
nearMissEl.classList.add('show');
nearDisplayT=0.55;
score += 35*multiplier*prox;
multiEl.classList.remove('pulse'); void multiEl.offsetWidth; multiEl.classList.add('pulse');
}
}
}else{
nearTimer = Math.max(0, nearTimer - dt*2);
multiplier = lerp(multiplier, 1.0, dt*0.22);
}
const decayMult = turboActive ? 1.75 : 1.0;
energy -= CFG.ENERGY_DECAY * decayMult * dt * (0.6 + speed/200);
if(energy<=0){ energy=0; gameOver(); }
if(collided){
energy -= 34;
score = Math.max(0, score - 80);
audio.blip('hit');
shake=1.2;
multiplier = Math.max(1, multiplier*0.6);
speed *= 0.65;
if(energy<=0) gameOver();
}
score += speed * dt * 0.22 * multiplier;
scoreEl.textContent = Math.floor(score).toString().padStart(5,'0');
distEl.textContent = Math.floor(distance)+'m';
maxSpeedEl.textContent = Math.floor(maxSpeed)+' km/h';
speedNumEl.textContent = Math.floor(speed*5.2).toString();
multiEl.textContent = 'x'+multiplier.toFixed(1);
energyFill.style.width = energy+'%';
energyGlow.style.left = energy+'%';
turboFill.style.width = turboMeter+'%';
speedFill.style.width = clamp((speed/CFG.MAX_SPEED)*100, 0,100)+'%';
if(energy<28) energyFill.style.filter='hue-rotate(300deg) brightness(1.2)';
else energyFill.style.filter='';
nearDisplayT -= dt;
if(nearDisplayT<=0) nearMissEl.classList.remove('show');
shake = lerp(shake, 0, dt*4.5);
}else{
ship.position.y = 1.5 + Math.sin(now*0.001)*0.2;
ship.rotation.y = Math.sin(now*0.0006)*0.15;
ship.rotation.z = Math.sin(now*0.0008)*0.12;
}
dust.update(dt, speed, worldOffset);
trail.update(dt);
const fovTarget = turboActive ? CFG.FOV_TURBO : CFG.FOV_BASE + speed*0.06;
camera.fov = lerp(camera.fov, fovTarget, dt*3);
camera.updateProjectionMatrix();
const camX = lerp(camera.position.x, px*0.26, dt*2.2) + (shake*(Math.random()-0.5))*0.9;
const camY = lerp(camera.position.y, py*0.32 + 3.8, dt*2.2) + (shake*(Math.random()-0.5))*0.6;
camera.position.x = camX;
camera.position.y = camY;
camera.lookAt(px*0.45, py*0.45+0.6, -14);
const glow = 0.4 + speed*0.006 + (turboActive?0.8:0);
if(ship.userData.engMat){
ship.userData.engMat.emissiveIntensity = glow*2.2;
ship.userData.trailMat.opacity = turboActive?0.85 : 0.15 + speed*0.0015;
}
if(state==='playing'){
audio.setEngine(speed/CFG.MAX_SPEED, turboActive?1:0);
}
renderer.render(scene, camera);
requestAnimationFrame(loop);
}
window.addEventListener('resize', ()=>{
camera.aspect = window.innerWidth/window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
resetGame();
loop();
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"meta/muse-spark-1.1","provider":"openrouter","persona":"composer","sessionId":"1218bdf4-6643-40d8-a01e-daf2abc4746b","tokensIn":352440,"tokensOut":25444,"tokensTotal":377884,"turns":19,"toolCalls":18,"failedToolCalls":1,"timestamp":"2026-07-17T04:16:37.304Z"} -->