Canyon Run
muse spark 1.2 · 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 44,883 bytes · SHA-256 6ce2aec04030
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1">
<title>CANYON RACER — Procedural Flow</title>
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=JetBrains+Mono:wght@400;700&family=Sora:wght@300;600;700&display=swap" rel="stylesheet">
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{--orange:#ff6b1a;--pink:#ff2e7a;--violet:#7c3aed;--cyan:#22d3ee;--gold:#fbbf24;--bg:#0a0a14}
html,body{width:100%;height:100%;overflow:hidden;background:var(--bg);font-family:'Sora',sans-serif}
#canvas-wrap{position:absolute;inset:0}
canvas{display:block;width:100%;height:100%}
#hud{position:absolute;inset:0;pointer-events:none;z-index:10}
#topbar{position:absolute;top:0;left:0;right:0;display:flex;justify-content:space-between;align-items:flex-start;padding:16px 18px;gap:12px}
.hud-panel{background:rgba(12,12,20,0.70);backdrop-filter:blur(14px);border:1px solid rgba(255,255,255,0.09);border-radius:16px;padding:11px 14px;box-shadow:0 10px 30px rgba(0,0,0,0.25),inset 0 1px 0 rgba(255,255,255,0.06)}
#energy-wrap{flex:1;max-width:440px;display:flex;flex-direction:column;gap:7px}
.label{font-family:'JetBrains Mono',monospace;font-size:8.5px;letter-spacing:0.18em;color:rgba(255,255,255,0.55);text-transform:uppercase}
#energy-bar-bg{height:10px;background:rgba(255,255,255,0.08);border-radius:999px;overflow:hidden;position:relative;border:1px solid rgba(255,255,255,0.06)}
#energy-bar{height:100%;width:72%;background:linear-gradient(90deg,var(--gold) 0%,var(--orange) 55%,var(--pink) 100%);border-radius:999px;box-shadow:0 0 14px rgba(251,191,36,0.65);transition:width 0.18s ease}
#energy-bar.low{animation:pulse 0.42s infinite alternate;box-shadow:0 0 16px rgba(255,46,122,0.9)!important}
@keyframes pulse{from{opacity:1}to{opacity:0.55}}
#energy-text{font-family:'JetBrains Mono',monospace;font-size:11px;color:#fff;display:flex;justify-content:space-between;align-items:center}
#energy-pct{font-weight:700}
#score-panel{text-align:right;min-width:176px}
#score{font-family:'Orbitron',monospace;font-weight:900;font-size:30px;color:#fff;letter-spacing:0.03em;line-height:1;text-shadow:0 2px 16px rgba(124,58,237,0.35)}
#score-sub{font-family:'JetBrains Mono',monospace;font-size:10px;color:rgba(255,255,255,0.62);letter-spacing:0.12em;margin-top:4px}
#mult{font-family:'Orbitron',monospace;font-weight:900;font-size:20px;color:var(--cyan);display:inline-block;transition:transform 0.12s, color 0.12s, text-shadow 0.12s}
#mult.pop{transform:scale(1.38);color:#fff;text-shadow:0 0 18px var(--cyan),0 0 32px var(--cyan)}
.center-msg{position:absolute;left:50%;top:41%;transform:translate(-50%,-50%);text-align:center;pointer-events:none}
#near{font-family:'Orbitron',monospace;font-weight:900;font-size:46px;color:#fff;opacity:0;transform:translateY(14px) scale(0.88);transition:all 0.28s cubic-bezier(.2,.8,.2,1);text-shadow:0 0 24px var(--orange),0 0 60px var(--pink);letter-spacing:-0.02em}
#near.show{opacity:1;transform:translateY(0) scale(1.08)}
#near-sub{font-family:'JetBrains Mono',monospace;font-size:11px;letter-spacing:0.22em;color:var(--gold);opacity:0;transition:0.28s 0.06s;margin-top:6px}
#near.show + #near-sub{opacity:1}
#crosshair{position:absolute;left:50%;top:56%;transform:translate(-50%,-50%);width:36px;height:36px;opacity:0.28}
#crosshair:before,#crosshair:after{content:"";position:absolute;background:rgba(255,255,255,0.9)}
#crosshair:before{left:50%;top:0;bottom:0;width:1px;transform:translateX(-50%)}
#crosshair:after{top:50%;left:0;right:0;height:1px;transform:translateY(-50%)}
#crosshair i{position:absolute;inset:6px;border:1px solid rgba(255,255,255,0.18);border-radius:50%}
#bottom-hud{position:absolute;bottom:16px;left:18px;right:18px;display:flex;justify-content:space-between;align-items:flex-end;gap:14px}
#speed-wrap{width:260px}
#speed-track{height:5px;background:rgba(255,255,255,0.10);border-radius:999px;overflow:hidden;margin-top:7px;position:relative;box-shadow:inset 0 1px 2px rgba(0,0,0,0.4)}
#speed-fill{height:100%;width:32%;background:linear-gradient(90deg,var(--violet),var(--cyan),#fff);border-radius:999px;box-shadow:0 0 12px var(--cyan);transition:width 0.12s}
#speed-val{font-family:'Orbitron',monospace;font-size:13px;color:#fff;letter-spacing:0.08em;font-weight:700}
#speed-label{font-family:'JetBrains Mono',monospace;font-size:9px;color:rgba(255,255,255,0.5);letter-spacing:0.15em}
#turbo-wrap{width:260px;text-align:right}
#turbo-track{height:9px;background:rgba(255,255,255,0.07);border-radius:999px;overflow:hidden;margin-top:7px;border:1px solid rgba(34,211,238,0.22);position:relative}
#turbo-fill{height:100%;width:0%;background:linear-gradient(90deg,var(--cyan),#e0f2fe 75%,#fff);box-shadow:0 0 14px var(--cyan);transition:width 0.12s}
#turbo-fill.full{animation: turboPulse 0.45s infinite alternate}
@keyframes turboPulse{from{filter:brightness(1)}to{filter:brightness(1.35)}}
#turbo-pct{font-family:'Orbitron',monospace;font-size:12px;color:#fff;font-weight:900}
#turbo-hint{font-family:'JetBrains Mono',monospace;font-size:8px;color:var(--cyan);letter-spacing:0.14em;margin-top:5px}
#motion-vignette{position:absolute;inset:0;pointer-events:none;opacity:0;transition:opacity 0.18s;background:
radial-gradient(ellipse at 50% 45%, transparent 58%, rgba(0,0,0,0.55) 100%),
linear-gradient(90deg, transparent 0%, rgba(34,211,238,0.07) 12%, transparent 22%, transparent 78%, rgba(255,107,26,0.06) 88%, transparent 100%);
}
#motion-vignette.on{opacity:1}
.overlay{position:absolute;inset:0;z-index:20;display:flex;align-items:center;justify-content:center;background:radial-gradient(ellipse at 50% 18%,rgba(124,58,237,0.20),transparent 62%),radial-gradient(ellipse at 20% 80%,rgba(255,107,26,0.14),transparent 55%),linear-gradient(180deg,rgba(10,6,20,0.9),rgba(10,6,20,0.97));backdrop-filter:blur(8px);padding:18px}
.card{width:min(580px,94vw);background:linear-gradient(180deg,rgba(255,255,255,0.07),rgba(255,255,255,0.02));border:1px solid rgba(255,255,255,0.11);border-radius:24px;padding:26px 24px;box-shadow:0 24px 70px rgba(0,0,0,0.55),inset 0 1px 0 rgba(255,255,255,0.09);text-align:center;max-height:92vh;overflow:auto}
.logo{font-family:'Orbitron',monospace;font-weight:900;letter-spacing:0.2em;font-size:10px;color:var(--cyan)}
h1{font-family:'Orbitron',monospace;font-weight:900;font-size:40px;line-height:0.88;color:#fff;margin:10px 0 8px;letter-spacing:-0.03em}
h1 span{background:linear-gradient(90deg,var(--orange),var(--pink) 55%,var(--violet));-webkit-background-clip:text;-webkit-text-fill-color:transparent}
.subtitle{font-family:'JetBrains Mono',monospace;font-size:11px;letter-spacing:0.14em;color:rgba(255,255,255,0.58)}
.divider{height:1px;background:linear-gradient(90deg,transparent,rgba(255,255,255,0.14),transparent);margin:16px 0}
.btn{pointer-events:auto;cursor:pointer;border:none;border-radius:999px;padding:14px 30px;font-family:'Orbitron',monospace;font-weight:900;letter-spacing:0.13em;font-size:13px;color:#0a0a14;background:linear-gradient(90deg,var(--gold),var(--orange));box-shadow:0 10px 28px rgba(255,107,26,0.42),0 2px 0 rgba(255,255,255,0.2) inset;transition:transform 0.13s,box-shadow 0.13s}
.btn:hover{transform:translateY(-2px);box-shadow:0 14px 36px rgba(255,107,26,0.52)}
.btn:active{transform:translateY(0) scale(0.99)}
.btn.secondary{background:rgba(255,255,255,0.06);color:#fff;border:1px solid rgba(255,255,255,0.18);box-shadow:none}
.btn.secondary:hover{background:rgba(255,255,255,0.10)}
.grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin:14px 0;text-align:left}
.stat{background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.07);border-radius:14px;padding:12px 13px}
.stat b{font-family:'Orbitron',monospace;font-size:13px;color:#fff;display:block;letter-spacing:0.02em}
.stat span{font-family:'JetBrains Mono',monospace;font-size:9px;letter-spacing:0.1em;color:rgba(255,255,255,0.52);line-height:1.4;display:block;margin-top:3px}
.controls{display:flex;flex-wrap:wrap;gap:8px;justify-content:center;margin:14px 0}
.kbd{font-family:'JetBrains Mono',monospace;font-size:11px;padding:7px 11px;border-radius:10px;background:rgba(255,255,255,0.08);border:1px solid rgba(255,255,255,0.12);color:#fff;display:inline-flex;align-items:center;gap:6px;box-shadow:0 4px 12px rgba(0,0,0,0.2)}
.kbd em{font-style:normal;opacity:0.6;font-size:10px}
#gameover{display:none}
#pause{display:none}
/* mobile touch pad */
#touch{position:absolute;inset:0;z-index:5;display:none;touch-action:none}
#mobile-ctrl{position:absolute;left:50%;bottom:108px;transform:translateX(-50%);z-index:6;display:none;gap:10px;pointer-events:none}
.mob-btn{width:54px;height:54px;border-radius:16px;background:rgba(255,255,255,0.08);border:1px solid rgba(255,255,255,0.14);backdrop-filter:blur(10px);display:flex;align-items:center;justify-content:center;color:#fff;font-family:'JetBrains Mono',monospace;font-size:10px;pointer-events:auto}
@media(max-width:760px){
#touch{display:block}
#mobile-ctrl{display:flex}
#topbar{padding:10px 10px;flex-direction:column}
#energy-wrap{max-width:none;width:100%}
#score-panel{min-width:0;width:100%;display:flex;justify-content:space-between;align-items:center}
#bottom-hud{left:10px;right:10px;bottom:10px}
#speed-wrap,#turbo-wrap{width:46vw}
h1{font-size:30px}
#crosshair{top:52%}
.center-msg{top:38%}
#near{font-size:32px}
}
</style>
</head>
<body>
<div id="canvas-wrap"></div>
<div id="motion-vignette"></div>
<div id="hud">
<div id="topbar">
<div id="energy-wrap" class="hud-panel">
<div class="label" style="display:flex;justify-content:space-between"><span>Energy — stay in the flow</span><span style="color:var(--gold)">◉ COLLECT ORBS</span></div>
<div id="energy-bar-bg"><div id="energy-bar"></div></div>
<div id="energy-text"><span id="energy-pct">72%</span><span id="energy-hint" style="font-size:9px;letter-spacing:0.12em;color:rgba(255,255,255,0.55)">DECAYING • GRAZE WALLS FOR FUEL</span></div>
</div>
<div id="score-panel" class="hud-panel">
<div>
<div class="label">Score & Multiplier</div>
<div id="score">0</div>
</div>
<div style="text-align:right">
<div id="mult">×1.0</div>
<div id="score-sub"><span id="dist">0 m</span></div>
</div>
</div>
</div>
<div class="center-msg">
<div id="near">NEAR MISS!</div>
<div id="near-sub">+ PROXIMITY BONUS</div>
</div>
<div id="crosshair"><i></i></div>
<div id="bottom-hud">
<div id="speed-wrap" class="hud-panel">
<div style="display:flex;justify-content:space-between;align-items:center">
<span id="speed-val">0 KM/H</span><span id="speed-label">VELOCITY</span>
</div>
<div id="speed-track"><div id="speed-fill"></div></div>
<div style="font-family:'JetBrains Mono',monospace;font-size:8px;color:rgba(255,255,255,0.42);margin-top:5px;letter-spacing:0.09em">A/D or ← → bank • W/S pitch • hold for weighty momentum</div>
</div>
<div id="turbo-wrap" class="hud-panel">
<div style="display:flex;justify-content:space-between;align-items:center">
<span class="label" style="color:var(--cyan)">Turbo</span><span id="turbo-pct">0%</span>
</div>
<div id="turbo-track"><div id="turbo-fill"></div></div>
<div id="turbo-hint">HOLD SHIFT / SPACE • FOV + MOTION BLUR</div>
</div>
</div>
</div>
<div id="touch"></div>
<div id="mobile-ctrl">
<div class="mob-btn" id="mLeft">◀</div>
<div class="mob-btn" id="mBoost" style="flex:1; min-width:90px;background:linear-gradient(90deg,var(--violet),var(--cyan));color:#fff;font-weight:800;letter-spacing:0.1em">BOOST</div>
<div class="mob-btn" id="mRight">▶</div>
</div>
<div id="start" class="overlay">
<div class="card">
<div class="logo">HIGH-SPEED PROCEDURAL • 60 FPS</div>
<h1>CANYON <span>RACER</span></h1>
<div class="subtitle">RIDE THE GOLDEN HOUR • FLOW IS SURVIVAL</div>
<div class="divider"></div>
<p style="color:rgba(255,255,255,0.74);font-size:13px;line-height:1.65">
Skim the walls. Kiss the stone. Energy drains every second — only <b style="color:var(--gold)">golden orbs</b> keep you alive.
The closer you fly, the higher your multiplier. Build <b style="color:var(--cyan)">Turbo</b> on clean lines, then burn it for extreme velocity.
</p>
<div class="grid">
<div class="stat"><b>Weighty Flight</b><span>bank • pitch • momentum rolls</span></div>
<div class="stat"><b>Near-Miss ×6.0</b><span>grazing = multiplier surge</span></div>
<div class="stat"><b>Infinite Chunks</b><span>procedural generation · no leaks</span></div>
<div class="stat"><b>Golden Hour Bloom</b><span>long shadows · dust · trail</span></div>
</div>
<div class="controls">
<span class="kbd">A / D <em>bank</em></span>
<span class="kbd">W / S <em>pitch</em></span>
<span class="kbd">SHIFT <em>turbo</em></span>
<span class="kbd">SPACE <em>pulse</em></span>
<span class="kbd">P <em>pause</em></span>
</div>
<button class="btn" id="playBtn">IGNITE ENGINES — PLAY</button>
<div style="margin-top:10px;font-family:'JetBrains Mono',monospace;font-size:10px;color:rgba(255,255,255,0.42)">Tip: thread tight squeezes — risky lines hold the most fuel. Use pitch to dive under arches.</div>
</div>
</div>
<div id="gameover" class="overlay">
<div class="card">
<div class="logo" style="color:var(--orange)">SIGNAL LOST • OUT OF ENERGY</div>
<h1 style="font-size:30px">OUT OF <span>FLOW</span></h1>
<div class="grid" style="grid-template-columns:1fr 1fr 1fr">
<div class="stat"><b id="finalScore">0</b><span>FINAL SCORE</span></div>
<div class="stat"><b id="finalDist">0 m</b><span>DISTANCE FLOWN</span></div>
<div class="stat"><b id="finalSpeed">0</b><span>MAX SPEED</span></div>
</div>
<div id="medal" style="font-family:'Orbitron',monospace;font-size:11px;letter-spacing:0.18em;color:var(--gold);margin:8px 0;min-height:16px"></div>
<div id="stats-extra" style="font-family:'JetBrains Mono',monospace;font-size:10px;color:rgba(255,255,255,0.5);margin-bottom:6px"></div>
<div style="display:flex;gap:10px;justify-content:center;margin-top:14px;flex-wrap:wrap">
<button class="btn" id="restartBtn">RETRY RUN</button>
<button class="btn secondary" id="menuBtn">TITLE</button>
</div>
</div>
</div>
<div id="pause" class="overlay">
<div class="card" style="width:min(420px,92vw)">
<div class="logo">PAUSED</div>
<h1 style="font-size:28px">HOLD <span>POSITION</span></h1>
<p style="color:rgba(255,255,255,0.6);font-size:12px;margin:8px 0 16px">Take a breath. The canyon waits.</p>
<div style="display:flex;gap:10px;justify-content:center">
<button class="btn" id="resumeBtn">RESUME</button>
<button class="btn secondary" id="quitBtn">QUIT</button>
</div>
</div>
</div>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
import { AfterimagePass } from 'three/addons/postprocessing/AfterimagePass.js';
// ---------- Audio ----------
let audioCtx=null;
function ensureAudio(){ if(!audioCtx) audioCtx=new (window.AudioContext||window.webkitAudioContext)(); if(audioCtx.state==='suspended') audioCtx.resume(); }
function tone(freq,dur,type='sine',gain=0.18,slideTo=null){
if(!audioCtx) return;
const o=audioCtx.createOscillator(), g=audioCtx.createGain(), f=audioCtx.createBiquadFilter();
o.type=type; o.frequency.value=freq; f.type='lowpass'; f.frequency.value=3000;
o.connect(f); f.connect(g); g.connect(audioCtx.destination);
const t=audioCtx.currentTime;
g.gain.setValueAtTime(0.0001,t); g.gain.linearRampToValueAtTime(gain,t+0.015); g.gain.exponentialRampToValueAtTime(0.0001,t+dur);
if(slideTo!==null) o.frequency.linearRampToValueAtTime(slideTo,t+dur);
o.start(t); o.stop(t+dur+0.02);
}
function playCollect(){ tone(880,0.16,'sine',0.22,1320); setTimeout(()=>tone(1320,0.18,'triangle',0.13,1760),70); }
function playNear(){ tone(200,0.24,'sawtooth',0.10,520); tone(380,0.18,'sine',0.08); }
function playCrash(){ tone(140,0.5,'square',0.22,28); setTimeout(()=>tone(70,0.6,'sawtooth',0.14,30),40); }
function playBoost(){ tone(90,0.5,'sawtooth',0.16,340); tone(180,0.35,'triangle',0.10,260); }
let engineOsc, engineGain, engineFilter;
function startEngine(){
ensureAudio(); if(engineOsc) return;
engineOsc=audioCtx.createOscillator(); engineGain=audioCtx.createGain(); engineFilter=audioCtx.createBiquadFilter();
engineFilter.type='lowpass'; engineFilter.frequency.value=880;
engineOsc.type='sawtooth'; engineOsc.frequency.value=58;
engineGain.gain.value=0.0;
engineOsc.connect(engineFilter); engineFilter.connect(engineGain); engineGain.connect(audioCtx.destination);
engineOsc.start();
}
function updateEngine(speed, boosting){
if(!engineGain||!audioCtx) return;
const target = 0.055 + speed*0.00013 + (boosting?0.075:0);
engineGain.gain.linearRampToValueAtTime(target, audioCtx.currentTime+0.09);
engineOsc.frequency.linearRampToValueAtTime(48 + speed*0.19 + (boosting?28:0), audioCtx.currentTime+0.09);
engineFilter.frequency.linearRampToValueAtTime(800 + speed*2.2 + (boosting?600:0), audioCtx.currentTime+0.09);
}
function stopEngine(){ if(engineOsc){ try{engineOsc.stop()}catch(e){} engineOsc=null; engineGain=null; } }
// ---------- Three ----------
const wrap=document.getElementById('canvas-wrap');
const scene=new THREE.Scene();
scene.fog=new THREE.Fog(0x140a2a, 145, 980);
const camera=new THREE.PerspectiveCamera(74, innerWidth/innerHeight, 0.1, 2600);
const renderer=new THREE.WebGLRenderer({antialias:true, powerPreference:'high-performance'});
renderer.setPixelRatio(Math.min(devicePixelRatio,1.85));
renderer.setSize(innerWidth,innerHeight);
renderer.shadowMap.enabled=true; renderer.shadowMap.type=THREE.PCFSoftShadowMap;
renderer.toneMapping=THREE.ACESFilmicToneMapping; renderer.toneMappingExposure=1.06;
wrap.appendChild(renderer.domElement);
scene.background=new THREE.Color(0x1b0f2c);
const hemi=new THREE.HemisphereLight(0xffb86a, 0x1a0f2e, 0.92); scene.add(hemi);
const sun=new THREE.DirectionalLight(0xff8a2a, 1.95); sun.position.set(240,320,-190); sun.castShadow=true;
sun.shadow.mapSize.set(2048,2048); sun.shadow.camera.near=1; sun.shadow.camera.far=1300;
sun.shadow.camera.left=-340; sun.shadow.camera.right=340; sun.shadow.camera.top=260; sun.shadow.camera.bottom=-360;
sun.shadow.bias=-0.0006; scene.add(sun);
const rim=new THREE.DirectionalLight(0x7c3aed,0.68); rim.position.set(-200,140,160); scene.add(rim);
const fill=new THREE.DirectionalLight(0x22d3ee,0.22); fill.position.set(0, 40, -260); scene.add(fill);
// fog gradient mesh
const floorGeo=new THREE.PlaneGeometry(5000,5000);
const floorMat=new THREE.MeshLambertMaterial({color:0x29182e});
const floor=new THREE.Mesh(floorGeo,floorMat); floor.rotation.x=-Math.PI/2; floor.position.y=-44; floor.receiveShadow=true; scene.add(floor);
// composer with bloom + afterimage (motion blur)
let composer, bloomPass, afterimagePass;
try{
composer=new EffectComposer(renderer);
composer.addPass(new RenderPass(scene,camera));
bloomPass=new UnrealBloomPass(new THREE.Vector2(innerWidth,innerHeight),0.66,0.52,0.16);
composer.addPass(bloomPass);
afterimagePass=new AfterimagePass();
afterimagePass.uniforms['damp'].value=0.84;
composer.addPass(afterimagePass);
}catch(e){ composer=null; }
// --- Craft ---
const craftGroup=new THREE.Group(); scene.add(craftGroup);
const craft=new THREE.Group(); craftGroup.add(craft);
const bodyGeo=new THREE.ConeGeometry(3.1,14,6); bodyGeo.rotateX(Math.PI/2);
const bodyMat=new THREE.MeshPhongMaterial({color:0xeef2ff, flatShading:true, shininess:80, emissive:0x0f1422, emissiveIntensity:0.2});
const body=new THREE.Mesh(bodyGeo,bodyMat); body.castShadow=true; craft.add(body);
const wingGeo=new THREE.BoxGeometry(15,0.62,4.4);
const wingMat=new THREE.MeshPhongMaterial({color:0xcbd5e1, flatShading:true});
const wings=new THREE.Mesh(wingGeo,wingMat); wings.position.set(0,-0.6,0.8); wings.castShadow=true; craft.add(wings);
const canardGeo=new THREE.BoxGeometry(7.5,0.42,2.3);
const canard=new THREE.Mesh(canardGeo,wingMat); canard.position.set(0,0.22,-3.2); craft.add(canard);
const engineGeo=new THREE.ConeGeometry(1.75,4.2,8); engineGeo.rotateX(-Math.PI/2);
const engineMat=new THREE.MeshBasicMaterial({color:0x22d3ee});
const engineMesh=new THREE.Mesh(engineGeo,engineMat); engineMesh.position.set(0,-0.3,7.2); craft.add(engineMesh);
const engineLight=new THREE.PointLight(0x22d3ee, 2.4, 64); engineLight.position.set(0,0,7); craft.add(engineLight);
const engineGlow=new THREE.Sprite(new THREE.SpriteMaterial({color:0x22d3ee, transparent:true, opacity:0.55, blending:THREE.AdditiveBlending}));
engineGlow.scale.set(11,11,1); engineGlow.position.set(0,0,8.6); craft.add(engineGlow);
const cockGeo=new THREE.SphereGeometry(1.35,10,8);
const cockMat=new THREE.MeshPhongMaterial({color:0x0ea5e9, emissive:0x08324d, shininess:90});
const cockpit=new THREE.Mesh(cockGeo,cockMat); cockpit.position.set(0,0.9,-1.15); cockpit.scale.set(1,0.68,1.32); craft.add(cockpit);
craftGroup.position.set(0,6,0);
// trail
const trailCount=80;
const trailGeo=new THREE.BufferGeometry();
const trailPos=new Float32Array(trailCount*3);
const trailCol=new Float32Array(trailCount*3);
const trailSize=new Float32Array(trailCount);
for(let i=0;i<trailCount;i++){ trailPos[i*3]=0; trailPos[i*3+1]=0; trailPos[i*3+2]=0; trailCol[i*3]=0.14; trailCol[i*3+1]=0.82; trailCol[i*3+2]=1; trailSize[i]=1.6; }
trailGeo.setAttribute('position', new THREE.BufferAttribute(trailPos,3));
trailGeo.setAttribute('color', new THREE.BufferAttribute(trailCol,3));
const trailMat=new THREE.PointsMaterial({size:2.0, vertexColors:true, transparent:true, opacity:0.9, blending:THREE.AdditiveBlending, depthWrite:false, sizeAttenuation:true});
const trailPoints=new THREE.Points(trailGeo,trailMat); scene.add(trailPoints);
let trailHead=0;
// dust
const dustCount=520;
const dustGeo=new THREE.BufferGeometry();
const dustPos=new Float32Array(dustCount*3);
const dustVel=new Float32Array(dustCount);
for(let i=0;i<dustCount;i++){ dustPos[i*3]=(Math.random()-0.5)*360; dustPos[i*3+1]=Math.random()*130-12; dustPos[i*3+2]=Math.random()*2000-260; dustVel[i]=0.9+Math.random()*2.0; }
dustGeo.setAttribute('position', new THREE.BufferAttribute(dustPos,3));
const dustMat=new THREE.PointsMaterial({color:0xffc89a, size:1.18, transparent:true, opacity:0.5});
const dustPoints=new THREE.Points(dustGeo,dustMat); scene.add(dustPoints);
// streaks for high speed
const streakCount=22;
const streakGeo=new THREE.BufferGeometry();
const streakPos=new Float32Array(streakCount*3);
for(let i=0;i<streakCount;i++){ streakPos[i*3]=(Math.random()-0.5)*90; streakPos[i*3+1]=Math.random()*40-6; streakPos[i*3+2]=Math.random()*160-40; }
streakGeo.setAttribute('position', new THREE.BufferAttribute(streakPos,3));
const streakMat=new THREE.PointsMaterial({color:0xffffff, size:0.6, transparent:true, opacity:0});
const streakPoints=new THREE.Points(streakGeo,streakMat); scene.add(streakPoints);
// ---------- Game state ----------
const CHUNK_LEN=140;
const VISIBLE_CHUNKS=13;
let chunks=[];
let zPos=0;
let posX=0, posY=6, velX=0, velY=0, roll=0, pitch=0;
let speed=64, actualSpeed=64, energy=78, turbo=0, score=0, dist=0, multiplier=1.0, maxSpeed=0;
let isBoosting=false, boostingTime=0;
let nearCooldown=0, lastNear=0;
let alive=true, started=false, pausedState=false;
let difficulty=0, orbsCollected=0, nearMisses=0;
let keys={left:false,right:false,up:false,down:false,boost:false};
let shake=0;
const energyBar=document.getElementById('energy-bar'), energyPct=document.getElementById('energy-pct');
const scoreEl=document.getElementById('score'), multEl=document.getElementById('mult'), distEl=document.getElementById('dist');
const speedFill=document.getElementById('speed-fill'), speedVal=document.getElementById('speed-val');
const turboFill=document.getElementById('turbo-fill'), turboPct=document.getElementById('turbo-pct');
const nearEl=document.getElementById('near');
const vignette=document.getElementById('motion-vignette');
function rnd(a,b){return a+Math.random()*(b-a)}
function hash(n){ return Math.abs(Math.sin(n*127.1)*43758.5453)%1 }
// shared materials to avoid leaks
const wallMatA=new THREE.MeshLambertMaterial({color:0x6b3a2a, flatShading:true});
const wallMatB=new THREE.MeshLambertMaterial({color:0x5a2f7a, flatShading:true});
const ridgeMatA=new THREE.MeshLambertMaterial({color:0x8a4a2e, flatShading:true});
const ridgeMatB=new THREE.MeshLambertMaterial({color:0x7a3b6a, flatShading:true});
function createWallChunk(z, width, heightL, heightR){
const group=new THREE.Group();
const leftGeo=new THREE.BoxGeometry(30, heightL, CHUNK_LEN+6);
const left=new THREE.Mesh(leftGeo, wallMatA); left.position.set(-width/2-15, heightL/2-44, z); left.castShadow=true; left.receiveShadow=true; group.add(left);
const rightGeo=new THREE.BoxGeometry(30, heightR, CHUNK_LEN+6);
const right=new THREE.Mesh(rightGeo, wallMatB); right.position.set(width/2+15, heightR/2-44, z); right.castShadow=true; right.receiveShadow=true; group.add(right);
// ridges
for(let i=0;i<2;i++){
const h=rnd(8,18), w=rnd(6,10);
const ridge=new THREE.Mesh(new THREE.BoxGeometry(w,h, CHUNK_LEN*0.58), i===0?ridgeMatA:ridgeMatB);
ridge.position.set((i===0?-width/2+7:width/2-7), h/2-44, z+rnd(-20,20)); ridge.castShadow=true; group.add(ridge);
}
// top dust caps
const capGeo=new THREE.BoxGeometry(width+62, 2, CHUNK_LEN);
const cap=new THREE.Mesh(capGeo, new THREE.MeshLambertMaterial({color:0x1e1020, transparent:true, opacity:0.0})); cap.position.set(0,-44,z); group.add(cap);
scene.add(group);
return {group, width};
}
function addObstacle(chunkZ, width, listMeshes, listObstacles){
const t=Math.random();
let mesh;
if(t<0.42){
const h=rnd(18,40), w=rnd(5,9), d=rnd(5,9);
const x=rnd(-width/2+12, width/2-12);
const y=rnd(-10, 16);
const geo=new THREE.BoxGeometry(w,h,d);
const hue= rnd(0.05,0.10);
const mat=new THREE.MeshLambertMaterial({color:new THREE.Color().setHSL(hue,0.55,0.48), flatShading:true});
mesh=new THREE.Mesh(geo,mat); mesh.position.set(x, y+h/2-32, chunkZ+rnd(-46,46)); mesh.castShadow=true; mesh.receiveShadow=true;
mesh.rotation.y=rnd(0,Math.PI);
scene.add(mesh); listMeshes.push(mesh);
listObstacles.push({x, y:y+h/2-32, z:mesh.position.z, hw:w/2+3.4, hh:h/2+2.4, hd:d/2+4.2, nearDist:11});
} else if(t<0.72){
const gap=rnd(17,25) - difficulty*2.2; const gapClamped=Math.max(12.5, gap);
const cx=rnd(-width/3.2, width/3.2);
const h=rnd(24,44);
const gate=new THREE.Group();
const mat=new THREE.MeshLambertMaterial({color:0x4a395a, flatShading:true});
const leftP=new THREE.Mesh(new THREE.BoxGeometry(5.8,h,5.8), mat); leftP.position.set(cx-gapClamped/2-2.9, h/2-32, 0); leftP.castShadow=true;
const rightP=new THREE.Mesh(new THREE.BoxGeometry(5.8,h,5.8), mat); rightP.position.set(cx+gapClamped/2+2.9, h/2-32, 0); rightP.castShadow=true;
const topH=rnd(2,3.2);
const topBar=new THREE.Mesh(new THREE.BoxGeometry(gapClamped+13,topH,4.2), new THREE.MeshLambertMaterial({color:0x8a5a2a})); topBar.position.set(cx, h-30,0);
topBar.castShadow=true;
gate.add(leftP,rightP,topBar);
gate.position.set(0,0,chunkZ+rnd(-40,40));
scene.add(gate); listMeshes.push(gate);
listObstacles.push({x:cx-gapClamped/2-2.9, y:h/2-32, z:gate.position.z, hw:2.9+3, hh:h/2+2.2, hd:2.9+4, nearDist:11});
listObstacles.push({x:cx+gapClamped/2+2.9, y:h/2-32, z:gate.position.z, hw:2.9+3, hh:h/2+2.2, hd:2.9+4, nearDist:11});
} else {
const w=rnd(18,32), h=rnd(3.2,5.2), d=rnd(6,9);
const x=rnd(-width/2+14, width/2-14);
const y=rnd(5,20);
const geo=new THREE.BoxGeometry(w,h,d);
const mat=new THREE.MeshLambertMaterial({color:0x9a6a3a, flatShading:true});
mesh=new THREE.Mesh(geo,mat); mesh.position.set(x,y,chunkZ+rnd(-34,34)); mesh.castShadow=true;
scene.add(mesh); listMeshes.push(mesh);
listObstacles.push({x, y, z:mesh.position.z, hw:w/2+3.8, hh:h/2+3.2, hd:d/2+5.2, nearDist:11});
}
}
function addOrb(chunkZ, width, listMeshes, listOrbs){
const x=rnd(-width/2+10, width/2-10);
const y=rnd(-1,17);
const z=chunkZ+rnd(-52,52);
const g=new THREE.Group();
const core=new THREE.Mesh(new THREE.IcosahedronGeometry(2.05,0), new THREE.MeshPhongMaterial({color:0xffd84d, emissive:0xff8e00, emissiveIntensity:0.95, flatShading:true}));
const halo=new THREE.Sprite(new THREE.SpriteMaterial({color:0xffd84d, transparent:true, opacity:0.30, blending:THREE.AdditiveBlending}));
halo.scale.set(15,15,1);
const ring=new THREE.Mesh(new THREE.RingGeometry(2.75,3.25,16), new THREE.MeshBasicMaterial({color:0xffd84d, side:THREE.DoubleSide, transparent:true, opacity:0.55}));
ring.rotation.x=Math.PI/2;
const light=new THREE.PointLight(0xffb000, 1.8, 28); light.position.set(0,0,0);
g.add(core,ring,halo,light); g.position.set(x,y,z);
g.userData={spin:rnd(0.02,0.05)};
scene.add(g); listMeshes.push(g); listOrbs.push({x,y,z, mesh:g, collected:false});
}
function generateChunk(idx){
const z=idx*CHUNK_LEN;
const t=idx*0.07;
const width = Math.max(44, 92 - difficulty*9 + Math.sin(t*1.7)*8 + Math.cos(t*0.9)*6 + hash(idx+7)*6 -3);
const hL= 44 + Math.sin(t*2.3)*18 + hash(idx)*24;
const hR= 44 + Math.cos(t*1.9)*18 + hash(idx+99)*24;
const wall=createWallChunk(z, width, hL, hR);
const meshes=[wall.group];
const obstacles=[], orbs=[];
const obsCount = Math.floor(1 + difficulty*1.55 + Math.random()*1.1);
for(let i=0;i<obsCount;i++) addObstacle(z, width, meshes, obstacles);
const orbCount = 1 + (Math.random()<0.58?1:0) + (difficulty>2.5 && Math.random()<0.38?1:0);
for(let i=0;i<orbCount;i++) addOrb(z, width, meshes, orbs);
chunks.push({idx, z, width, meshes, obstacles, orbs});
}
function ensureChunks(){
const currentIdx=Math.floor(zPos/CHUNK_LEN);
const needMin=currentIdx-2, needMax=currentIdx+VISIBLE_CHUNKS;
for(let i=needMin;i<=needMax;i++) if(!chunks.find(c=>c.idx===i)) generateChunk(i);
chunks = chunks.filter(c=>{
if(c.idx < needMin-1){
c.meshes.forEach(m=>{
scene.remove(m);
m.traverse?.(o=>{ if(o.geometry) o.geometry.dispose(); });
});
return false;
}
return true;
});
}
function resetGame(){
chunks.forEach(c=>c.meshes.forEach(m=>{ scene.remove(m); m.traverse?.(o=>{ if(o.geometry) o.geometry.dispose(); })}));
chunks=[]; zPos=0; posX=0; posY=6; velX=0; velY=0; roll=0; pitch=0;
speed=64; actualSpeed=64; energy=78; turbo=0; score=0; dist=0; multiplier=1; maxSpeed=64;
isBoosting=false; boostingTime=0; difficulty=0; shake=0; nearCooldown=0; orbsCollected=0; nearMisses=0;
alive=true;
for(let i=-1;i<14;i++) generateChunk(i);
updateHUD(0);
}
// ---------- Controls ----------
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==='ShiftLeft'||e.code==='ShiftRight') keys.boost=true;
if(e.code==='Space'){ keys.boost=true; e.preventDefault(); }
if(e.code==='KeyP' && started && alive){ togglePause(); }
if(e.code==='Escape' && pausedState) togglePause();
});
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;
if(e.code==='ShiftLeft'||e.code==='ShiftRight') keys.boost=false;
if(e.code==='Space') keys.boost=false;
});
const touchEl=document.getElementById('touch');
let touchActive=false;
touchEl.addEventListener('pointerdown',e=>{ touchActive=true; ensureAudio(); handleTouch(e); });
touchEl.addEventListener('pointermove',e=>{ if(touchActive) handleTouch(e); });
addEventListener('pointerup',()=>{ touchActive=false; keys.left=false; keys.right=false; keys.up=false; keys.down=false; });
function handleTouch(e){
const dx=(e.clientX - innerWidth/2)/(innerWidth/2);
const dy=(e.clientY - innerHeight/2)/(innerHeight/2);
keys.left=dx<-0.16; keys.right=dx>0.16;
keys.up=dy<-0.20; keys.down=dy>0.20;
}
touchEl.addEventListener('click',()=>{ if(started && !pausedState){ keys.boost=true; setTimeout(()=>keys.boost=false,360); }});
document.getElementById('mLeft').addEventListener('pointerdown',()=>{keys.left=true});
document.getElementById('mLeft').addEventListener('pointerup',()=>{keys.left=false});
document.getElementById('mLeft').addEventListener('pointerleave',()=>{keys.left=false});
document.getElementById('mRight').addEventListener('pointerdown',()=>{keys.right=true});
document.getElementById('mRight').addEventListener('pointerup',()=>{keys.right=false});
document.getElementById('mRight').addEventListener('pointerleave',()=>{keys.right=false});
const mBoost=document.getElementById('mBoost');
mBoost.addEventListener('pointerdown',()=>{keys.boost=true});
mBoost.addEventListener('pointerup',()=>{keys.boost=false});
mBoost.addEventListener('pointerleave',()=>{keys.boost=false});
document.getElementById('playBtn').addEventListener('click',()=> startGame());
document.getElementById('restartBtn').addEventListener('click',()=>{ hideGameover(); startGame(); });
document.getElementById('menuBtn').addEventListener('click',()=>{ hideGameover(); showStart(); });
document.getElementById('resumeBtn').addEventListener('click',()=> togglePause());
document.getElementById('quitBtn').addEventListener('click',()=>{ document.getElementById('pause').style.display='none'; pausedState=false; hideGameover(); showStart(); stopEngine(); });
function showStart(){ document.getElementById('start').style.display='flex'; started=false; pausedState=true; }
function hideStart(){ document.getElementById('start').style.display='none'; }
function showGameover(){
document.getElementById('finalScore').textContent=Math.floor(score).toLocaleString();
document.getElementById('finalDist').textContent=Math.floor(dist)+' m';
document.getElementById('finalSpeed').textContent=Math.floor(maxSpeed)+' KM/H';
const m=document.getElementById('medal');
if(score>20000) m.textContent='◆ AURORA ACE — FLAWLESS FLOW';
else if(score>10000) m.textContent='◇ CANYON VETERAN — SHARP LINES';
else if(score>4500) m.textContent='○ DUST RUNNER — KEEP GRAZING';
else m.textContent='— STAY IN THE FLOW —';
document.getElementById('stats-extra').textContent=`${orbsCollected} orbs • ${nearMisses} near-misses • ${Math.floor(dist)}m carved`;
document.getElementById('gameover').style.display='flex';
}
function hideGameover(){ document.getElementById('gameover').style.display='none'; }
function togglePause(){
if(!started || !alive) return;
pausedState=!pausedState;
document.getElementById('pause').style.display= pausedState? 'flex':'none';
if(pausedState) { engineGain && (engineGain.gain.value*=0.35); }
else { ensureAudio(); }
}
function startGame(){
hideStart(); hideGameover(); document.getElementById('pause').style.display='none';
resetGame(); started=true; pausedState=false; alive=true;
ensureAudio(); startEngine();
}
resetGame(); showStart();
function updateHUD(dt){
energyPct.textContent=Math.floor(energy)+'%';
energyBar.style.width=Math.max(0,energy)+'%';
const low=energy<24;
energyBar.classList.toggle('low', low);
energyBar.style.boxShadow= low ? '0 0 16px rgba(255,46,122,0.9)' : '0 0 14px rgba(251,191,36,0.62)';
scoreEl.textContent=Math.floor(score).toLocaleString();
multEl.textContent='×'+multiplier.toFixed(1);
distEl.textContent=Math.floor(dist)+' m';
speedVal.textContent=Math.floor(actualSpeed)+' KM/H';
const pct=Math.min(100, Math.max(0, ((actualSpeed-42)/260)*100));
speedFill.style.width=pct+'%';
turboFill.style.width=turbo+'%';
turboPct.textContent=Math.floor(turbo)+'%';
turboFill.classList.toggle('full', turbo>=99);
document.getElementById('turbo-hint').style.opacity= turbo>96 ? '1' : '0.55';
vignette.classList.toggle('on', isBoosting || actualSpeed>210);
vignette.style.opacity= isBoosting? '1' : (actualSpeed>210? '0.55':'0');
}
// ---------- Collision & Near Miss ----------
function checkCollisions(){
const px=posX, py=posY, pz=zPos;
let collided=false, closest=Infinity;
for(const c of chunks){
if(Math.abs(c.z - pz) > 95) continue;
for(const o of c.orbs){
if(o.collected) continue;
const dx=o.x - px, dy=o.y - py, dz=o.z - pz;
const d=Math.hypot(dx,dy,dz);
if(d < 6.6){
o.collected=true; o.mesh.visible=false; orbsCollected++;
energy=Math.min(100, energy+20);
score+= 135*multiplier;
turbo=Math.min(100, turbo+15);
multiplier=Math.min(6, multiplier+0.13);
playCollect(); shake=Math.max(shake,0.42);
for(let i=0;i<7;i++){ const id=(trailHead+i)%trailCount; trailPos[id*3]=o.x+rnd(-1,1); trailPos[id*3+1]=o.y+rnd(-1,1); trailPos[id*3+2]=o.z+rnd(-1,1); }
// flash bloom
if(bloomPass) { bloomPass.strength=0.88; setTimeout(()=>{ if(bloomPass) bloomPass.strength=0.66; },160); }
}
}
for(const ob of c.obstacles){
const dx=Math.abs(ob.x - px), dy=Math.abs(ob.y - py), dz=Math.abs(ob.z - pz);
const inside = dx < ob.hw && dy < ob.hh && dz < ob.hd;
if(inside) collided=true;
if(!inside){
const surfDist = Math.hypot(Math.max(0,dx-ob.hw), Math.max(0,dy-ob.hh), Math.max(0,dz-ob.hd));
if(surfDist < ob.nearDist) closest=Math.min(closest, surfDist);
}
}
const wallEdge = c.width/2;
const distToWall = wallEdge - Math.abs(px);
if(distToWall < 9 && distToWall> -2) closest=Math.min(closest, Math.max(0,distToWall));
if(Math.abs(px) > wallEdge+2.2) collided=true;
}
if(closest < 8 && closest!==Infinity){
if(performance.now()-lastNear>420 && nearCooldown<=0){
const bonus = closest<3.2? 300 : closest<5.8? 150 : 75;
score+= bonus * multiplier * 0.65;
turbo=Math.min(100, turbo + (closest<3.2?8:3.5));
multiplier=Math.min(6, multiplier + (closest<3.2?0.20:0.07));
nearMisses++;
const label = closest<3.2? 'RAZOR GRAZE!' : closest<5.8? 'NEAR MISS!' : 'CLOSE!';
nearEl.textContent=label;
nearEl.classList.add('show'); multEl.classList.add('pop');
setTimeout(()=>{nearEl.classList.remove('show'); multEl.classList.remove('pop');}, 580);
lastNear=performance.now(); nearCooldown=0.24; shake=Math.max(shake, closest<3.2?1.05:0.52);
playNear();
if(bloomPass) { bloomPass.strength=0.90; setTimeout(()=>{ if(bloomPass) bloomPass.strength=0.66; },190); }
if(afterimagePass) afterimagePass.uniforms['damp'].value=0.72;
setTimeout(()=>{ if(afterimagePass) afterimagePass.uniforms['damp'].value=0.84; },220);
}
}
return collided;
}
function randSpread(s){ return (Math.random()-0.5)*s*2 }
// ---------- Loop ----------
let lastT=performance.now();
function frame(now){
requestAnimationFrame(frame);
const dt=Math.min(0.033, (now-lastT)/1000); lastT=now;
if(pausedState){ render(); return; }
if(!started){
zPos+= 13*dt; ensureChunks();
craft.rotation.z=Math.sin(now*0.00055)*0.11;
craft.rotation.x=Math.sin(now*0.0007)*0.06;
render(); return;
}
if(!alive){ render(); return; }
difficulty = Math.min(8.5, dist/880 + score/11500);
const baseTarget = 64 + difficulty*23 + Math.min(32, dist*0.032);
const wantBoost = keys.boost && turbo>0.5;
if(wantBoost && !isBoosting && turbo>6) playBoost();
isBoosting = wantBoost;
if(isBoosting){
turbo=Math.max(0, turbo - 23*dt);
boostingTime+=dt;
} else {
boostingTime=Math.max(0, boostingTime - dt*1.35);
if(actualSpeed>115) turbo=Math.min(100, turbo + 6.5*dt);
}
const boostAdd = isBoosting? 84 + Math.min(30, boostingTime*14) : 0;
speed += (baseTarget + boostAdd - speed)* 0.92*dt;
speed=Math.max(46, speed);
actualSpeed = speed + (isBoosting? 20:0);
maxSpeed=Math.max(maxSpeed, actualSpeed);
const decay = (3.9 + difficulty*0.62 + (isBoosting?4.6:0) + actualSpeed*0.0065) * dt;
energy-= decay;
if(energy<=0){ energy=0; alive=false; stopEngine(); playCrash(); showGameover(); }
const steer = (keys.right?1:0) - (keys.left?1:0);
const pitchIn = (keys.up?1:0) - (keys.down?1:0);
const targetVelX = steer * (38 + actualSpeed*0.10 + (isBoosting?11:0));
const targetVelY = pitchIn * 24;
velX += (targetVelX - velX)* 2.9*dt;
velY += (targetVelY - velY)* 2.3*dt;
velX *= (1 - 0.92*dt); velY *= (1 - 0.92*dt);
posX += velX * dt * (0.64+ actualSpeed*0.0022);
posY += velY * dt * 0.66;
posY = Math.max(-16, Math.min(36, posY));
const targetRoll = -steer*0.72 - velX*0.013;
const targetPitch = pitchIn*0.45 - (isBoosting?0.2:0);
roll += (targetRoll - roll)* 6.2*dt;
pitch += (targetPitch - pitch)* 5.2*dt;
craft.rotation.z=roll;
craft.rotation.x=pitch;
craft.rotation.y= -steer*0.20;
const worldSpeed = (actualSpeed*0.065);
zPos += worldSpeed * dt * 18.5;
dist = zPos*0.9;
score += worldSpeed*dt* 9.2 * multiplier + (isBoosting? 7*dt*multiplier:0);
if(nearCooldown>0) nearCooldown-=dt; else multiplier = Math.max(1, multiplier - 0.19*dt);
shake*=(1-4.2*dt);
if(checkCollisions()){
alive=false; energy=0; playCrash(); shake=1.5; stopEngine(); showGameover();
}
ensureChunks();
for(const c of chunks) for(const o of c.orbs) if(!o.collected){
o.mesh.rotation.y+= o.mesh.userData.spin;
o.mesh.position.y += Math.sin(now*0.0032 + o.z*0.01)*0.018;
// bob light
o.mesh.children[0].material.emissiveIntensity=0.85+Math.sin(now*0.005+o.z)*0.15;
}
// trail
trailHead=(trailHead+1)%trailCount;
trailPos[trailHead*3]=posX + randSpread(0.35);
trailPos[trailHead*3+1]=posY + randSpread(0.35);
trailPos[trailHead*3+2]=zPos - 9.5;
trailCol[trailHead*3]= 0.12 + (isBoosting?0.35:0);
trailCol[trailHead*3+1]= 0.80;
trailCol[trailHead*3+2]= 1;
trailGeo.attributes.position.needsUpdate=true;
trailGeo.attributes.color.needsUpdate=true;
trailMat.opacity= 0.78 + (isBoosting?0.15:0);
trailMat.size= isBoosting? 2.8 : 2.0;
const dPos=dustGeo.attributes.position.array;
const boostFactor=isBoosting? 2.35:1;
for(let i=0;i<dustCount;i++){
dPos[i*3+2] += dustVel[i]*boostFactor* (0.92+actualSpeed*0.007);
if(dPos[i*3+2] > zPos+70){
dPos[i*3+2]=zPos - 980 + Math.random()*140;
dPos[i*3]=(Math.random()-0.5)*300;
dPos[i*3+1]=Math.random()*100-18;
}
}
dustGeo.attributes.position.needsUpdate=true;
dustMat.opacity=0.30 + actualSpeed*0.00125 + (isBoosting?0.12:0);
// streaks visibility
streakMat.opacity= Math.min(0.45, Math.max(0, (actualSpeed-120)/280)) + (isBoosting?0.18:0);
const sPos=streakGeo.attributes.position.array;
for(let i=0;i<streakCount;i++){
sPos[i*3+2]+= (28 + actualSpeed*0.18) * dt * 18;
if(sPos[i*3+2] > zPos+60){ sPos[i*3+2]=zPos - 80 - Math.random()*40; sPos[i*3]=(Math.random()-0.5)*70; sPos[i*3+1]=Math.random()*34-8;}
}
streakGeo.attributes.position.needsUpdate=true;
engineLight.intensity=1.7 + isBoosting*1.9 + Math.sin(now*0.022)*0.22;
engineGlow.material.opacity=0.52 + isBoosting*0.38 + Math.sin(now*0.04)*0.05;
engineMesh.material.color.setHSL(0.52 + isBoosting*0.07,1,0.56);
const camX = posX*0.38 + (Math.random()-0.5)*shake*1.35;
const camY = posY*0.44 + 18.5 + (Math.random()-0.5)*shake*0.9;
const camZ = zPos - 70 - (isBoosting? 10:0) + (Math.random()-0.5)*shake*0.7;
camera.position.set(camX, camY, camZ);
camera.lookAt(posX*0.58, posY*0.52+4.2, zPos+56);
const targetFov= 74 + actualSpeed*0.065 + (isBoosting?10:0) - shake*1.6;
camera.fov += (targetFov - camera.fov)*0.085;
camera.updateProjectionMatrix();
if(afterimagePass) afterimagePass.uniforms['damp'].value = isBoosting? 0.76 : 0.84;
updateEngine(actualSpeed, isBoosting);
updateHUD(dt);
render();
}
function render(){
if(composer) composer.render();
else renderer.render(scene,camera);
}
addEventListener('resize',()=>{
camera.aspect=innerWidth/innerHeight; camera.updateProjectionMatrix();
renderer.setSize(innerWidth,innerHeight);
if(composer) composer.setSize(innerWidth,innerHeight);
});
frame(performance.now());
</script>
</body>
</html>