Canyon Run
Deepseek v4.1 Flash · 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 57,244 bytes · SHA-256 936d709027a4
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
<title>CANYON RUN — Procedural Canyon Racer</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
html,body{width:100%;height:100%;overflow:hidden;background:#0a0614}
body{
font-family:"Helvetica Neue",Helvetica,Arial,system-ui,sans-serif;
color:#f6ecff;user-select:none;-webkit-user-select:none;-webkit-font-smoothing:antialiased;
background:
radial-gradient(70% 42% at 62% 63%, rgba(255,214,150,.55) 0%, rgba(255,140,80,.22) 34%, rgba(255,90,60,0) 68%),
linear-gradient(to bottom,
#07030f 0%, #150a28 16%, #361141 32%, #6b2250 46%, #ab3f4e 55%,
#dd6a44 61%, #f2a463 65%, #b35640 71%, #55163a 82%, #1d0a24 93%, #0a0614 100%);
background-attachment:fixed;
}
canvas{display:block;position:fixed;inset:0;width:100%;height:100%}
/* ---------- full screen fx layers ---------- */
#fx{position:fixed;inset:0;pointer-events:none;z-index:5}
#vignette{position:absolute;inset:0;background:radial-gradient(120% 95% at 50% 50%, rgba(0,0,0,0) 42%, rgba(10,2,20,.55) 78%, rgba(6,1,14,.92) 100%)}
#danger{position:absolute;inset:0;opacity:0;background:radial-gradient(130% 100% at 50% 50%, rgba(255,0,60,0) 45%, rgba(255,20,50,.35) 82%, rgba(180,0,40,.6) 100%);transition:opacity .25s}
#flash{position:absolute;inset:0;background:#fff;opacity:0;transition:opacity .35s}
/* ---------- HUD ---------- */
#hud{position:fixed;inset:0;z-index:10;pointer-events:none;opacity:0;transition:opacity .8s}
#hud.on{opacity:1}
.lbl{font-size:9.5px;letter-spacing:.34em;text-transform:uppercase;opacity:.5;font-weight:600}
#tl{position:absolute;top:24px;left:30px}
#score{font-size:52px;font-weight:200;letter-spacing:.02em;line-height:.95;font-variant-numeric:tabular-nums;
text-shadow:0 0 26px rgba(255,180,120,.55),0 0 60px rgba(255,120,80,.25)}
#sub{display:flex;gap:16px;margin-top:8px}
#sub div{font-size:10px;letter-spacing:.18em;opacity:.55;font-variant-numeric:tabular-nums}
#sub b{font-weight:600;opacity:1;color:#ffd9ac}
#tr{position:absolute;top:20px;right:32px;text-align:right}
#mult{font-size:70px;font-weight:700;line-height:.9;letter-spacing:-.02em;color:#fff;
text-shadow:0 0 18px rgba(255,170,110,.9),0 0 54px rgba(255,110,60,.55);
transform-origin:100% 60%;transition:transform .18s cubic-bezier(.2,1.6,.4,1),color .2s}
#mult.hot{color:#fff3d6;transform:scale(1.28)}
#mbar{margin-top:8px;width:130px;height:2px;margin-left:auto;background:rgba(255,255,255,.12);overflow:hidden}
#mbarFill{height:100%;width:10%;background:linear-gradient(90deg,#ffb26b,#ff6a3d)}
#bottom{position:absolute;left:0;right:0;bottom:26px;display:flex;align-items:flex-end;
justify-content:space-between;padding:0 34px;gap:28px}
.gauge{width:230px}
.gtrack{position:relative;height:9px;border-radius:5px;background:rgba(255,255,255,.09);
box-shadow:inset 0 0 0 1px rgba(255,255,255,.08);overflow:hidden;margin-top:7px}
.gfill{position:absolute;inset:0;width:100%;transform-origin:0 50%;border-radius:5px}
#energyFill{background:linear-gradient(90deg,#59f7d4,#a6ffdf);box-shadow:0 0 16px rgba(90,250,215,.85)}
#turboFill{background:linear-gradient(90deg,#ff9d5c,#ffe6a8);box-shadow:0 0 16px rgba(255,180,110,.8)}
#gaugeR{text-align:right}
#turboFill{transform-origin:100% 50%}
.gauge .val{font-size:11px;letter-spacing:.2em;opacity:.75;margin-top:7px;font-variant-numeric:tabular-nums}
.gauge.armed .lbl{color:#ffe1ae;opacity:1;text-shadow:0 0 12px rgba(255,180,110,.9)}
#dial{flex:0 0 auto;width:400px;text-align:center}
#dial .num{display:flex;align-items:baseline;justify-content:center;gap:8px}
#spdNum{font-size:44px;font-weight:200;line-height:1;font-variant-numeric:tabular-nums;
text-shadow:0 0 22px rgba(255,190,140,.5)}
#dial .unit{font-size:9.5px;letter-spacing:.3em;opacity:.5}
#track{position:relative;height:3px;margin-top:12px;background:rgba(255,255,255,.1);border-radius:2px}
#spdFill{height:100%;width:0%;border-radius:2px;background:linear-gradient(90deg,#8ffff0,#ffd7a0 55%,#ff6a3d);
box-shadow:0 0 14px rgba(255,170,110,.9)}
#spdKnob{position:absolute;top:-3px;left:0;width:9px;height:9px;border-radius:50%;background:#fff;
box-shadow:0 0 16px rgba(255,220,180,1);transform:translateX(-50%)}
#ticks{display:flex;justify-content:space-between;margin-top:6px}
#ticks span{width:1px;height:5px;background:rgba(255,255,255,.25)}
#nm{position:absolute;top:48%;left:50%;transform:translate(-50%,-50%) scale(.9);
font-size:15px;letter-spacing:.5em;font-weight:600;color:#fff2d8;opacity:0;text-transform:uppercase;
text-shadow:0 0 22px rgba(255,150,90,.95),0 0 60px rgba(255,90,50,.6)}
#nm.show{animation:nmpop .5s ease-out}
@keyframes nmpop{0%{opacity:0;transform:translate(-50%,-50%) scale(.75)}
18%{opacity:1;transform:translate(-50%,-50%) scale(1.05)}
100%{opacity:0;transform:translate(-50%,-55%) scale(1.1)}}
#boost{position:absolute;bottom:78px;left:50%;transform:translateX(-50%);font-size:11px;letter-spacing:.6em;
opacity:0;color:#ffe3b8;text-shadow:0 0 18px rgba(255,170,110,.9)}
#boost.on{opacity:1;animation:bl .55s infinite alternate}
@keyframes bl{from{opacity:.45}to{opacity:1}}
/* ---------- Overlays ---------- */
#overlay{position:fixed;inset:0;z-index:20;display:flex;align-items:center;justify-content:center;
background:radial-gradient(70% 70% at 50% 45%, rgba(20,6,26,.35) 0%, rgba(8,2,14,.72) 70%, rgba(6,1,12,.88) 100%);
opacity:0;pointer-events:none;transition:opacity .5s;backdrop-filter:blur(3px)}
#overlay.show{opacity:1;pointer-events:auto}
.panel{width:min(620px,88vw);text-align:center;padding:44px 46px 34px;border:1px solid rgba(255,190,140,.18);
border-radius:4px;background:linear-gradient(180deg,rgba(30,12,36,.62),rgba(14,4,22,.72));
box-shadow:0 0 80px rgba(255,120,60,.12),inset 0 1px 0 rgba(255,220,180,.09)}
.kicker{font-size:10px;letter-spacing:.55em;opacity:.55;text-transform:uppercase}
h1{font-size:min(76px,13vw);font-weight:200;letter-spacing:.06em;margin:12px 0 6px;line-height:1;
background:linear-gradient(180deg,#fff6e6,#ffb26b 55%,#ff6a3d);
-webkit-background-clip:text;background-clip:text;color:transparent;
filter:drop-shadow(0 0 32px rgba(255,130,70,.45))}
#ovtitle.dead{background:linear-gradient(180deg,#fff,#ff8ea0 60%,#c33a5c);-webkit-background-clip:text;background-clip:text}
.body-text{font-size:12px;letter-spacing:.13em;line-height:2;opacity:.7;margin-top:14px;text-transform:uppercase}
.stats{display:grid;grid-template-columns:1fr 1fr;gap:14px 26px;margin:26px 0 6px;text-align:left}
.stats .s{border-left:1px solid rgba(255,170,120,.3);padding-left:12px}
.stats .s i{display:block;font-size:9px;letter-spacing:.28em;opacity:.5;font-style:normal;text-transform:uppercase}
.stats .s b{font-size:26px;font-weight:300;font-variant-numeric:tabular-nums;color:#ffe0b8}
#startBtn{margin-top:24px;cursor:pointer;pointer-events:auto;font-family:inherit;font-size:12px;letter-spacing:.34em;
text-transform:uppercase;color:#1b0812;background:linear-gradient(180deg,#ffe6bb,#ffab6b 60%,#ff7a45);
border:0;border-radius:2px;padding:15px 44px;font-weight:700;
box-shadow:0 0 34px rgba(255,150,90,.45),0 8px 26px rgba(0,0,0,.4);transition:transform .16s,box-shadow .16s}
#startBtn:hover{transform:translateY(-2px);box-shadow:0 0 46px rgba(255,170,110,.7),0 10px 30px rgba(0,0,0,.45)}
.hint{margin-top:18px;font-size:9.5px;letter-spacing:.3em;opacity:.42;text-transform:uppercase}
.keys{display:flex;justify-content:center;gap:22px;margin-top:18px;flex-wrap:wrap}
.key{font-size:9.5px;letter-spacing:.22em;opacity:.62;text-transform:uppercase}
.key b{display:inline-block;min-width:22px;padding:3px 7px;margin-right:6px;border:1px solid rgba(255,200,150,.3);
border-radius:3px;font-weight:600;opacity:1;color:#ffd9ac}
#loading{position:fixed;inset:0;z-index:30;display:flex;align-items:center;justify-content:center;
background:#0a0614;font-size:11px;letter-spacing:.4em;text-transform:uppercase;opacity:.7}
</style>
</head>
<body>
<div id="loading">initialising canyon…</div>
<canvas id="gl"></canvas>
<div id="fx">
<div id="danger"></div>
<div id="vignette"></div>
<div id="flash"></div>
</div>
<div id="hud">
<div id="tl">
<div class="lbl">Score</div>
<div id="score">0</div>
<div id="sub">
<div>DIST <b id="dist">0</b>m</div>
<div>MAX <b id="maxspd">0</b></div>
<div>ORBS <b id="orbs">0</b></div>
</div>
</div>
<div id="tr">
<div class="lbl">Multiplier</div>
<div id="mult">×1.0</div>
<div id="mbar"><div id="mbarFill"></div></div>
</div>
<div id="bottom">
<div class="gauge" id="energyGauge">
<div class="lbl">Energy</div>
<div class="gtrack"><div class="gfill" id="energyFill"></div></div>
</div>
<div id="dial">
<div class="num"><b id="spdNum">0</b><span class="unit">KM/H</span></div>
<div id="track"><div id="spdFill"></div><div id="spdKnob"></div></div>
<div id="ticks"><span></span><span></span><span></span><span></span><span></span><span></span><span></span></div>
</div>
<div class="gauge" id="gaugeR">
<div class="lbl">Turbo</div>
<div class="gtrack"><div class="gfill" id="turboFill"></div></div>
</div>
</div>
<div id="nm">Near Miss</div>
<div id="boost">TURBO ENGAGED</div>
</div>
<div id="overlay" class="show">
<div class="panel">
<div class="kicker" id="ovkicker">Endless procedural racer</div>
<h1 id="ovtitle">Canyon Run</h1>
<div class="body-text" id="ovbody">Fly the canyon. Skim the walls. Feed on light.</div>
<div class="stats" id="ovstats" style="display:none"></div>
<button id="startBtn">Launch</button>
<div class="keys">
<div class="key"><b>A</b><b>D</b> bank left / right</div>
<div class="key"><b>W</b><b>S</b> climb / dive</div>
<div class="key"><b>⇧</b> turbo</div>
<div class="key"><b>R</b> restart</div>
</div>
<div class="hint">Collect orbs to stay charged · hug the rock to multiply your score</div>
</div>
</div>
<script type="importmap">{"imports":{"three":"https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js"}}</script>
<script type="module">
import * as THREE from 'three';
/* ============================================================
CANYON RUN — high-speed procedural canyon racer
============================================================ */
const CFG = {
CHUNK: 62,
SPAWN_AHEAD: 13,
KILL_BEHIND: 2,
PLAYER_R: 2.3,
NEAR_DIST: 7.0,
MAX_VX: 84,
LAT_ACC: 175,
VERT_ACC: 155,
CEIL: 56,
FLOOR: -78,
BASE_SPEED: 66,
SPEED_GAIN: 66,
ATTRACT_SPEED: 58,
TURBO_MULT: 1.8,
DRAIN: 5.6,
DRAIN_SPD: 0.035,
ORB_ENERGY: 22,
MAX_MULT: 10
};
/* ---------------- utils ---------------- */
const clamp=(v,a,b)=>v<a?a:(v>b?b:v);
const lerp=(a,b,t)=>a+(b-a)*t;
const ss=t=>t*t*(3-2*t);
function hash(n){ const s=Math.sin(n*127.1+n*311.7+n*n*0.0731+1.3)*43758.5453; return s-Math.floor(s); }
const rnd=(s)=>hash(s);
/* ---------------- canyon path ---------------- */
function pathX(z){
return Math.sin(z*0.0042)*38 + Math.sin(z*0.0113+1.7)*13 + Math.sin(z*0.0191+0.6)*4;
}
const nodeWCache = new Map();
let diffGlobal = 0;
function nodeW(i){
let v = nodeWCache.get(i);
if(v===undefined){
const h=hash(i*7.13+2.3), h2=hash(i*3.71+9.1), h3=hash(i*11.77+5.5);
let base = 15 + h*21; // 15 .. 36
base *= (1 - 0.32*clamp(diffGlobal,0,1)); // narrows as you improve
if(h2 > 0.70) base *= 0.55; // tight squeeze region
else if(h2 < 0.16) base *= 1.18; // wide basin
if(h3 > 0.93) base *= 0.8;
v = Math.max(8.0, base);
nodeWCache.set(i,v);
}
return v;
}
function widthAt(z){
const t=-z/CFG.CHUNK, i=Math.floor(t), f=t-i;
return lerp(nodeW(i), nodeW(i+1), ss(f));
}
/* ---------------- palette ---------------- */
const C = (h)=>new THREE.Color(h);
const PAL = {
deep: C('#241a35'), mid: C('#4a3457'), top: C('#9c5560'), crest: C('#c9846f'),
monoA: C('#2b2340'), monoB: C('#3d2b4d')
};
/* ============================================================
RENDERER / SCENE
============================================================ */
const canvas = document.getElementById('gl');
const renderer = new THREE.WebGLRenderer({canvas, antialias:true, alpha:true, powerPreference:'high-performance'});
renderer.setPixelRatio(Math.min(devicePixelRatio||1, 1.6));
renderer.setSize(innerWidth, innerHeight);
renderer.setClearAlpha(0);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(C('#9e4a3c'), 0.0042);
const camera = new THREE.PerspectiveCamera(74, innerWidth/innerHeight, 0.3, 2600);
camera.position.set(0,6,14);
/* lights — low golden sun, cool ambient bounce */
const sun = new THREE.DirectionalLight(C('#ffc089'), 2.35);
sun.position.set(-1, 0.42, -0.5);
scene.add(sun); scene.add(sun.target);
sun.castShadow = true;
sun.shadow.mapSize.set(1024,1024);
const sc = sun.shadow.camera;
sc.left=-140; sc.right=140; sc.top=140; sc.bottom=-140; sc.near=1; sc.far=900;
sun.shadow.bias = -0.0009;
sun.shadow.normalBias = 1.2;
const hemi = new THREE.HemisphereLight(C('#ff9a6a'), C('#180a2a'), 0.85);
scene.add(hemi);
const fill = new THREE.DirectionalLight(C('#9a7bff'), 0.45);
fill.position.set(1,0.6,0.7); scene.add(fill);
/* ============================================================
SHARED GEOMETRY / MATERIALS
============================================================ */
const wallMat = new THREE.MeshLambertMaterial({
vertexColors:true, flatShading:true, side:THREE.DoubleSide,
emissive:C('#2a0f2e'), emissiveIntensity:0.35
});
const boxGeo = new THREE.BoxGeometry(1,1,1);
const monoMats = [
new THREE.MeshLambertMaterial({color:C('#332a4a'), flatShading:true, emissive:C('#180c26'), emissiveIntensity:.5}),
new THREE.MeshLambertMaterial({color:C('#453055'), flatShading:true, emissive:C('#1c0e2c'), emissiveIntensity:.5}),
new THREE.MeshLambertMaterial({color:C('#5b3a52'), flatShading:true, emissive:C('#241028'), emissiveIntensity:.5})
];
const orbGeo = new THREE.IcosahedronGeometry(1.15, 0);
const orbMat = new THREE.MeshBasicMaterial({color:C('#c9fff6')});
const orbHaloMat = new THREE.MeshBasicMaterial({color:C('#3ef0d8'), transparent:true, opacity:0.28,
blending:THREE.AdditiveBlending, depthWrite:false});
const haloGeo = new THREE.IcosahedronGeometry(3.1, 0);
/* ============================================================
CANYON WALL GEOMETRY
============================================================ */
const PROF = [
{y:-108, d:0.0},
{y:-56, d:1.4},
{y:-16, d:3.0},
{y: 18, d:6.0},
{y: 44, d:10.5}
];
function flareD(y){
if(y<=PROF[0].y) return PROF[0].d;
for(let i=0;i<PROF.length-1;i++){
const a=PROF[i], b=PROF[i+1];
if(y>=a.y && y<=b.y) return lerp(a.d, b.d, (y-a.y)/(b.y-a.y));
}
return PROF[PROF.length-1].d;
}
function wallRadius(z,y){ return widthAt(z) + flareD(y); }
const _c = new THREE.Color();
function faceColor(yc, h, out){
const t = clamp((yc+80)/160, 0, 1);
if(t<0.5) out.copy(PAL.deep).lerp(PAL.mid, t*2*1.05 + h*0.15);
else out.copy(PAL.mid).lerp(PAL.crest, (t-0.5)*2*(0.55+h*0.75));
if(h>0.88) out.lerp(PAL.top, 0.45);
out.multiplyScalar(0.88 + h*0.26);
return out;
}
function buildWallGeo(idx){
const z0 = -idx*CFG.CHUNK, NS = 6;
const pos=[], col=[], nor=[];
const cols = PROF.length + 3;
const grid = [];
for(let side=-1; side<=1; side+=2){
const g = [];
for(let i=0;i<=NS;i++){
const z = z0 - (i/NS)*CFG.CHUNK;
const cx = pathX(z), w = widthAt(z);
const row = [];
for(let j=0;j<PROF.length;j++){
const pr = PROF[j];
const jit = (hash(i*3.13+j*7.7+side*17.3+idx*0.37)-0.5) * (2.0 + j*1.9);
const yr = (hash(i*5.11+j*2.93+side*3.1+idx*0.91)-0.5) * (j>1?5:1.5);
row.push(new THREE.Vector3(cx + side*(w + pr.d + jit), pr.y + yr, z));
}
const pk = 14 + hash(i*9.41+side*4.4+idx*0.113)*46;
const off = w + 11 + hash(i*2.23+side*1.7+idx*0.31)*12;
row.push(new THREE.Vector3(cx + side*(off+7), 44 + pk*0.45 + (hash(i*4.2+side)-0.5)*8, z));
row.push(new THREE.Vector3(cx + side*(off+30), 44 + pk*1.0, z));
row.push(new THREE.Vector3(cx + side*(off+86), -108, z));
g.push(row);
}
grid.push(g);
}
const tri = (a,b,c,i,j)=>{}; // placeholder (unused)
function pushTri(p1,p2,p3,i,j,side){
pos.push(p1.x,p1.y,p1.z, p2.x,p2.y,p2.z, p3.x,p3.y,p3.z);
const yc=(p1.y+p2.y+p3.y)/3;
const h = hash(i*13.11+j*3.73+side*5.53+idx*2.17);
faceColor(yc, h, _c);
for(let k=0;k<3;k++) col.push(_c.r,_c.g,_c.b);
}
for(let s=0;s<2;s++){
const g = grid[s], side = s===0?-1:1;
for(let i=0;i<NS;i++){
const a=g[i], b=g[i+1];
for(let j=0;j<cols-1;j++){
pushTri(a[j], a[j+1], b[j+1], i, j, side);
pushTri(a[j], b[j+1], b[j], i, j, side);
}
}
}
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.Float32BufferAttribute(pos,3));
geo.setAttribute('color', new THREE.Float32BufferAttribute(col,3));
geo.computeVertexNormals();
geo.computeBoundingSphere();
return geo;
}
/* ============================================================
PLAYER CRAFT
============================================================ */
const craft = new THREE.Group();
(function buildCraft(){
const bodyMat = new THREE.MeshLambertMaterial({color:C('#3a3a52'), flatShading:true,
emissive:C('#1a1030'), emissiveIntensity:.6});
const plateMat = new THREE.MeshLambertMaterial({color:C('#c9743e'), flatShading:true,
emissive:C('#5a1e08'), emissiveIntensity:.9});
const glowMat = new THREE.MeshBasicMaterial({color:C('#ffe0b0'), transparent:true, opacity:.95,
blending:THREE.AdditiveBlending, depthWrite:false});
const hull = new THREE.Mesh(new THREE.ConeGeometry(1.5, 6.2, 4), bodyMat);
hull.rotation.x = -Math.PI/2; hull.rotation.y = Math.PI/4;
hull.castShadow = true; craft.add(hull);
const spine = new THREE.Mesh(new THREE.BoxGeometry(0.9, 0.7, 3.2), plateMat);
spine.position.set(0,0.5,-1.1); craft.add(spine);
const wing = new THREE.BoxGeometry(4.4, 0.22, 1.7);
const wl = new THREE.Mesh(wing, bodyMat); wl.position.set(-2.4,0,0.5); wl.rotation.z = 0.30;
wl.rotation.y = 0.22; wl.castShadow = true; craft.add(wl);
const wr = new THREE.Mesh(wing, bodyMat); wr.position.set( 2.4,0,0.5); wr.rotation.z = -0.30;
wr.rotation.y = -0.22; wr.castShadow = true; craft.add(wr);
const tipL = new THREE.Mesh(new THREE.BoxGeometry(0.28,1.1,0.8), plateMat);
tipL.position.set(-4.35,0.35,0.72); tipL.rotation.z=0.30; craft.add(tipL);
const tipR = tipL.clone(); tipR.position.x = 4.35; tipR.rotation.z=-0.30; craft.add(tipR);
const fin = new THREE.Mesh(new THREE.ConeGeometry(0.5,2.2,3), plateMat);
fin.rotation.x = -Math.PI/2; fin.position.set(0,0.9,1.4); craft.add(fin);
const engine = new THREE.Mesh(new THREE.SphereGeometry(1.05,8,6), glowMat);
engine.position.set(0,0,2.5); engine.scale.set(1,1,1.7); craft.add(engine);
craft.userData.engine = engine;
craft.userData.wings = [wl,wr,tipL,tipR];
})();
scene.add(craft);
/* ============================================================
CHUNK WORLD
============================================================ */
const world = {
chunks: new Map(),
nextIdx: 0,
colliders: [],
spawn(idx){
const group = new THREE.Group();
const walls = new THREE.Mesh(buildWallGeo(idx), wallMat);
walls.receiveShadow = true; walls.castShadow = true;
group.add(walls);
const colliders = [];
const r0 = hash(idx*3.77+1.1);
const count = r0<0.32 ? 1 : (r0<0.78 ? 2 : 3);
for(let k=0;k<count;k++){
const h1=hash(idx*11.3+k*5.7), h2=hash(idx*7.9+k*3.1), h3=hash(idx*17.1+k*9.3);
const nz = -((idx + (k+0.5)/count + (h1-0.5)*0.34) * CFG.CHUNK);
const w = widthAt(nz), cx = pathX(nz);
const mw = Math.min(2.6 + h2*5.2, Math.max(1.8, w-6.2), 7.0);
const mh = 16 + h3*52;
const md = 5 + h2*9;
const maxOff = Math.max(0, w - mw - 6.0);
const x = cx + (h3-0.5)*2*maxOff;
const y = -18 + h1*44;
const m = new THREE.Mesh(boxGeo, monoMats[(idx+k)%3]);
m.position.set(x,y,nz);
m.scale.set(mw*2, mh, md);
m.rotation.set((h1-0.5)*0.10, (h2-0.5)*0.30, (h3-0.5)*0.22);
m.castShadow = true; m.receiveShadow = true;
group.add(m);
colliders.push({x, y, z:nz, hx:mw+1.4, hy:mh/2+1.0, hz:md/2+1.4});
}
const orbs = [];
const oc = 2 + Math.floor(hash(idx*5.51+2.2)*3);
for(let k=0;k<oc;k++){
const h1=hash(idx*9.11+k*7.3), h2=hash(idx*4.19+k*3.9), h3=hash(idx*13.7+k*1.7);
const nz = -((idx + (k+0.35+h2*0.4)/oc) * CFG.CHUNK);
const w = widthAt(nz), cx = pathX(nz);
let x = cx + (h1-0.5)*2*(w-4.0);
const y = clamp((h3-0.5)*46, -26, 24);
let ok = true;
for(const cd of colliders){
if(Math.abs(nz-cd.z) < cd.hz+3 && Math.abs(x-cd.x) < cd.hx+3 && Math.abs(y-cd.y) < cd.hy+3){ ok=false; break; }
}
if(!ok) x += (x>cx?1:-1) * (w*0.55);
const mesh = new THREE.Mesh(orbGeo, orbMat);
const halo = new THREE.Mesh(haloGeo, orbHaloMat);
const base = new THREE.Vector3(x,y,nz);
mesh.position.copy(base); halo.position.copy(base);
group.add(mesh); group.add(halo);
orbs.push({mesh, halo, base, phase: hash(idx*2.7+k)*6.283, taken:false});
}
scene.add(group);
this.chunks.set(idx, {idx, group, colliders, orbs});
for(const c of colliders) this.colliders.push({...c, chunk:idx});
},
clear(){
for(const [,ch] of this.chunks){
ch.group.traverse(o=>{ if(o.isMesh && o.geometry && o.geometry !== boxGeo && o.geometry !== orbGeo && o.geometry !== haloGeo) o.geometry.dispose(); });
scene.remove(ch.group);
}
this.chunks.clear(); this.colliders.length = 0; this.nextIdx = 0;
nodeWCache.clear();
},
update(pz, spawnAll){
const ci = Math.floor(-pz/CFG.CHUNK);
let guard = 0;
while(this.nextIdx <= ci + CFG.SPAWN_AHEAD && guard++ < 40) this.spawn(this.nextIdx++);
for(const [idx,ch] of this.chunks){
if(idx < ci - CFG.KILL_BEHIND){
ch.group.traverse(o=>{ if(o.isMesh && o.geometry && o.geometry !== boxGeo && o.geometry !== orbGeo && o.geometry !== haloGeo) o.geometry.dispose(); });
scene.remove(ch.group);
this.chunks.delete(idx);
}
}
for(let i=this.colliders.length-1;i>=0;i--) if(this.colliders[i].chunk < ci-3) this.colliders.splice(i,1);
if(nodeWCache.size > 60){ for(const k of nodeWCache.keys()) if(k < ci-10) nodeWCache.delete(k); }
}
};
/* ============================================================
SKY DOME (golden-hour gradient + sun disc)
============================================================ */
const SUN_DIR = new THREE.Vector3(-1, 0.30, -0.55).normalize();
const skyMat = new THREE.ShaderMaterial({
side: THREE.BackSide, depthWrite:false, fog:false,
uniforms:{
uTop:{value:C('#150a2b')}, uHor:{value:C('#e8734a')}, uBot:{value:C('#200c26')},
uSun:{value:C('#ffd9a4')}, uSunDir:{value:SUN_DIR.clone()}
},
vertexShader:`
varying vec3 vDir;
void main(){ vDir = position; gl_Position = projectionMatrix*modelViewMatrix*vec4(position,1.0); }`,
fragmentShader:`
uniform vec3 uTop,uHor,uBot,uSun,uSunDir; varying vec3 vDir;
void main(){
vec3 d = normalize(vDir);
float h = d.y;
vec3 c = h>0.0 ? mix(uHor,uTop,pow(clamp(h,0.0,1.0),0.42)) : mix(uHor,uBot,pow(clamp(-h,0.0,1.0),0.5));
c = mix(c, uHor, smoothstep(0.30,0.0,abs(h-0.02))*0.55);
float sd = max(dot(d, uSunDir), 0.0);
c += uSun * smoothstep(0.99930,0.99985,sd) * 9.0;
c += uSun * pow(sd, 9.0) * 0.30;
c += uSun * pow(sd, 2.0) * 0.07;
gl_FragColor = vec4(c,1.0);
}`
});
const sky = new THREE.Mesh(new THREE.SphereGeometry(1500, 32, 20), skyMat);
sky.frustumCulled = false;
scene.add(sky);
/* ============================================================
PARTICLES
============================================================ */
const P_VERT = `
attribute float aSize; attribute float aAlpha; attribute vec3 aColor;
varying float vA; varying vec3 vC;
void main(){
vA = aAlpha; vC = aColor;
vec4 mv = modelViewMatrix*vec4(position,1.0);
gl_PointSize = aSize*(430.0/max(1.0,-mv.z));
gl_Position = projectionMatrix*mv;
}`;
const P_FRAG = `
varying float vA; varying vec3 vC;
void main(){
vec2 d = gl_PointCoord-0.5;
float r = length(d);
float a = smoothstep(0.5,0.02,r);
if(a*vA < 0.004) discard;
gl_FragColor = vec4(vC, a*vA);
}`;
function pMaterial(){ return new THREE.ShaderMaterial({
vertexShader:P_VERT, fragmentShader:P_FRAG, transparent:true, depthWrite:false,
blending:THREE.AdditiveBlending
}); }
class Burst {
constructor(max){
this.max=max; this.cursor=0;
this.pos=new Float32Array(max*3); this.col=new Float32Array(max*3);
this.size=new Float32Array(max); this.alpha=new Float32Array(max);
this.vel=new Float32Array(max*3); this.life=new Float32Array(max);
this.maxLife=new Float32Array(max); this.drag=new Float32Array(max); this.grow=new Float32Array(max);
const g=new THREE.BufferGeometry();
g.setAttribute('position', new THREE.BufferAttribute(this.pos,3));
g.setAttribute('aColor', new THREE.BufferAttribute(this.col,3));
g.setAttribute('aSize', new THREE.BufferAttribute(this.size,1));
g.setAttribute('aAlpha', new THREE.BufferAttribute(this.alpha,1));
this.geo=g;
this.points=new THREE.Points(g, pMaterial());
this.points.frustumCulled=false;
scene.add(this.points);
this._c=new THREE.Color();
}
emit(x,y,z,vx,vy,vz,size,life,color,drag,grow){
const i=this.cursor; this.cursor=(this.cursor+1)%this.max;
const i3=i*3;
this.pos[i3]=x; this.pos[i3+1]=y; this.pos[i3+2]=z;
this.vel[i3]=vx; this.vel[i3+1]=vy; this.vel[i3+2]=vz;
this._c.set(color);
this.col[i3]=this._c.r; this.col[i3+1]=this._c.g; this.col[i3+2]=this._c.b;
this.size[i]=size; this.alpha[i]=1; this.life[i]=life; this.maxLife[i]=life;
this.drag[i]=drag||1.6; this.grow[i]=grow||0;
}
update(dt){
const {pos,vel,life,maxLife,size,alpha,drag,grow,max}=this;
for(let i=0;i<max;i++){
if(life[i]<=0){ if(alpha[i]!==0){alpha[i]=0; size[i]=0;} continue; }
life[i]-=dt;
const i3=i*3;
const k=Math.exp(-drag[i]*dt);
const t=life[i]/maxLife[i];
if(life[i]<=0){ alpha[i]=0; size[i]=0; continue; }
vel[i3]*=k; vel[i3+1]*=k; vel[i3+2]*=k;
pos[i3]+=vel[i3]*dt; pos[i3+1]+=vel[i3+1]*dt; pos[i3+2]+=vel[i3+2]*dt;
alpha[i]=t*t*0.95+0.05*t;
size[i]*=(1+grow[i]*dt);
}
this.geo.attributes.position.needsUpdate=true;
this.geo.attributes.aAlpha.needsUpdate=true;
this.geo.attributes.aSize.needsUpdate=true;
this.geo.attributes.aColor.needsUpdate=true;
}
clear(){
this.life.fill(0); this.alpha.fill(0); this.size.fill(0);
this.geo.attributes.aAlpha.needsUpdate=true;
this.geo.attributes.aSize.needsUpdate=true;
}
}
class Streaks {
constructor(n){
this.n=n;
this.pos=new Float32Array(n*3); this.col=new Float32Array(n*3);
this.size=new Float32Array(n); this.alpha=new Float32Array(n);
this.tw=new Float32Array(n);
const c=new THREE.Color();
for(let i=0;i<n;i++){
this.pos[i*3]=0; this.pos[i*3+1]=0; this.pos[i*3+2]=0;
const warm = Math.random()<0.72;
c.set(warm ? '#ffd2a0' : '#a8ecff');
this.col[i*3]=c.r; this.col[i*3+1]=c.g; this.col[i*3+2]=c.b;
this.size[i]=0.9+Math.random()*2.0;
this.alpha[i]=0.20+Math.random()*0.45;
this.tw[i]=Math.random()*6.28;
}
const g=new THREE.BufferGeometry();
g.setAttribute('position', new THREE.BufferAttribute(this.pos,3));
g.setAttribute('aColor', new THREE.BufferAttribute(this.col,3));
g.setAttribute('aSize', new THREE.BufferAttribute(this.size,1));
g.setAttribute('aAlpha', new THREE.BufferAttribute(this.alpha,1));
this.geo=g;
this.points=new THREE.Points(g, pMaterial());
this.points.frustumCulled=false;
scene.add(this.points);
}
reset(px,py,pz){
for(let i=0;i<this.n;i++) this.place(i,px,py,pz,true);
}
place(i,px,py,pz,anywhere){
const i3=i*3;
const far = anywhere ? Math.random()*520 : 380+Math.random()*320;
this.pos[i3] = px + (Math.random()-0.5)*2*95;
this.pos[i3+1] = py + (Math.random()-0.5)*2*70;
this.pos[i3+2] = pz - far;
}
update(dt,px,py,pz,t){
for(let i=0;i<this.n;i++){
const i3=i*3;
if(this.pos[i3+2] > pz + 40 || this.alpha[i]<=0) this.place(i,px,py,pz,false);
this.alpha[i]=0.16+0.30*(0.5+0.5*Math.sin(t*3.0+this.tw[i]));
}
this.geo.attributes.position.needsUpdate=true;
this.geo.attributes.aAlpha.needsUpdate=true;
}
}
const burst = new Burst(760);
const streaks = new Streaks(300);
/* ============================================================
AUDIO (procedural, Web Audio API)
============================================================ */
const Sfx = {
ready:false, muted:false,
init(){
if(this.ready) return;
const AC = window.AudioContext || window.webkitAudioContext;
if(!AC) return;
const ctx = new AC(); this.ctx=ctx;
const master=ctx.createGain(); master.gain.value=0.9;
const comp=ctx.createDynamicsCompressor();
comp.threshold.value=-14; comp.knee.value=20; comp.ratio.value=7;
master.connect(comp); comp.connect(ctx.destination);
this.master=master;
const len=Math.floor(ctx.sampleRate*2);
const buf=ctx.createBuffer(1,len,ctx.sampleRate);
const d=buf.getChannelData(0);
for(let i=0;i<len;i++) d[i]=Math.random()*2-1;
this.noise=buf;
const o1=ctx.createOscillator(); o1.type='sawtooth'; o1.frequency.value=44;
const o2=ctx.createOscillator(); o2.type='square'; o2.frequency.value=66;
const eg=ctx.createGain(); eg.gain.value=0.0;
const lp=ctx.createBiquadFilter(); lp.type='lowpass'; lp.frequency.value=340; lp.Q.value=8;
o1.connect(eg); o2.connect(eg); eg.connect(lp); lp.connect(master);
o1.start(); o2.start();
const wind=ctx.createBufferSource(); wind.buffer=buf; wind.loop=true;
const wf=ctx.createBiquadFilter(); wf.type='bandpass'; wf.frequency.value=520; wf.Q.value=0.8;
const wg=ctx.createGain(); wg.gain.value=0.0;
wind.connect(wf); wf.connect(wg); wg.connect(master); wind.start();
this.eng={o1,o2,eg,lp,wind,wf,wg};
this.ready=true;
},
resume(){ if(this.ctx && this.ctx.state==='suspended') this.ctx.resume(); },
engine(speed,turbo,active){
if(!this.ready || this.muted) return;
const e=this.eng, t=this.ctx.currentTime;
const base=34+speed*0.40;
e.o1.frequency.setTargetAtTime(base, t, 0.07);
e.o2.frequency.setTargetAtTime(base*1.503, t, 0.07);
e.lp.frequency.setTargetAtTime(240+speed*7.5+(turbo?1100:0), t, 0.12);
e.eg.gain.setTargetAtTime(active?0.055+Math.min(speed/230,1)*0.06:0.0, t, 0.18);
e.wg.gain.setTargetAtTime(active?0.010+Math.min(speed/260,1)*0.05:0.0, t, 0.25);
e.wf.frequency.setTargetAtTime(380+speed*6.5, t, 0.25);
},
ping(){
if(!this.ready) return;
const ctx=this.ctx, t=ctx.currentTime;
const o=ctx.createOscillator(); o.type='sine';
const o2=ctx.createOscillator(); o2.type='triangle';
const g=ctx.createGain();
o.frequency.setValueAtTime(1180,t); o.frequency.exponentialRampToValueAtTime(2050,t+0.09);
o2.frequency.setValueAtTime(1760,t); o2.frequency.exponentialRampToValueAtTime(2980,t+0.09);
g.gain.setValueAtTime(0.001,t); g.gain.exponentialRampToValueAtTime(0.28,t+0.012);
g.gain.exponentialRampToValueAtTime(0.0008,t+0.30);
o.connect(g); o2.connect(g); g.connect(this.master);
o.start(t); o2.start(t); o.stop(t+0.34); o2.stop(t+0.34);
},
whoosh(){
if(!this.ready) return;
const ctx=this.ctx, t=ctx.currentTime;
const s=ctx.createBufferSource(); s.buffer=this.noise;
const bp=ctx.createBiquadFilter(); bp.type='bandpass'; bp.Q.value=1.6;
bp.frequency.setValueAtTime(300,t); bp.frequency.exponentialRampToValueAtTime(2600,t+0.30);
const g=ctx.createGain();
g.gain.setValueAtTime(0.0008,t); g.gain.exponentialRampToValueAtTime(0.30,t+0.10);
g.gain.exponentialRampToValueAtTime(0.0008,t+0.42);
s.connect(bp); bp.connect(g); g.connect(this.master);
s.start(t); s.stop(t+0.45);
},
boost(){
if(!this.ready) return;
const ctx=this.ctx, t=ctx.currentTime;
const o=ctx.createOscillator(); o.type='sawtooth';
const f=ctx.createBiquadFilter(); f.type='lowpass'; f.frequency.value=1800;
o.frequency.setValueAtTime(150,t); o.frequency.exponentialRampToValueAtTime(880,t+0.45);
const g=ctx.createGain();
g.gain.setValueAtTime(0.0008,t); g.gain.exponentialRampToValueAtTime(0.16,t+0.06);
g.gain.exponentialRampToValueAtTime(0.0008,t+0.55);
o.connect(f); f.connect(g); g.connect(this.master);
o.start(t); o.stop(t+0.6);
},
beep(){
if(!this.ready) return;
const ctx=this.ctx, t=ctx.currentTime;
const o=ctx.createOscillator(); o.type='square'; o.frequency.value=760;
const g=ctx.createGain();
g.gain.setValueAtTime(0.0008,t); g.gain.exponentialRampToValueAtTime(0.075,t+0.01);
g.gain.exponentialRampToValueAtTime(0.0008,t+0.13);
o.connect(g); g.connect(this.master);
o.start(t); o.stop(t+0.15);
},
crash(){
if(!this.ready) return;
const ctx=this.ctx, t=ctx.currentTime;
const s=ctx.createBufferSource(); s.buffer=this.noise;
const lp=ctx.createBiquadFilter(); lp.type='lowpass';
lp.frequency.setValueAtTime(1600,t); lp.frequency.exponentialRampToValueAtTime(90,t+1.1);
const g=ctx.createGain();
g.gain.setValueAtTime(0.55,t); g.gain.exponentialRampToValueAtTime(0.0008,t+1.3);
s.connect(lp); lp.connect(g); g.connect(this.master);
s.start(t); s.stop(t+1.35);
const o=ctx.createOscillator(); o.type='sine';
o.frequency.setValueAtTime(120,t); o.frequency.exponentialRampToValueAtTime(34,t+0.9);
const g2=ctx.createGain();
g2.gain.setValueAtTime(0.5,t); g2.gain.exponentialRampToValueAtTime(0.0008,t+1.0);
o.connect(g2); g2.connect(this.master);
o.start(t); o.stop(t+1.05);
}
};
/* ============================================================
POST PROCESSING (bright-pass → blur → composite/trail → blit)
============================================================ */
const PostFX = (function(){
const quadGeo = new THREE.PlaneGeometry(2,2);
const fsScene = new THREE.Scene();
const fsCam = new THREE.OrthographicCamera(-1,1,1,-1,0,1);
const fsMesh = new THREE.Mesh(quadGeo, null);
fsMesh.frustumCulled = false;
fsScene.add(fsMesh);
const VERT = `varying vec2 vUv; void main(){ vUv=uv; gl_Position=vec4(position.xy,0.0,1.0); }`;
const brightMat = new THREE.ShaderMaterial({
uniforms:{ tDiffuse:{value:null}, uThresh:{value:0.52} },
vertexShader:VERT,
fragmentShader:`
uniform sampler2D tDiffuse; uniform float uThresh; varying vec2 vUv;
void main(){
vec3 c=texture2D(tDiffuse,vUv).rgb;
float l=dot(c,vec3(0.2126,0.7152,0.0722));
float k=max(0.0,l-uThresh)/max(l,1e-4);
gl_FragColor=vec4(c*k,1.0);
}`
});
const blurMat = new THREE.ShaderMaterial({
uniforms:{ tDiffuse:{value:null}, uDir:{value:new THREE.Vector2()} },
vertexShader:VERT,
fragmentShader:`
uniform sampler2D tDiffuse; uniform vec2 uDir; varying vec2 vUv;
void main(){
vec3 s=texture2D(tDiffuse,vUv).rgb*0.227;
s+=(texture2D(tDiffuse,vUv+uDir*1.0).rgb+texture2D(tDiffuse,vUv-uDir*1.0).rgb)*0.194;
s+=(texture2D(tDiffuse,vUv+uDir*2.0).rgb+texture2D(tDiffuse,vUv-uDir*2.0).rgb)*0.121;
s+=(texture2D(tDiffuse,vUv+uDir*3.0).rgb+texture2D(tDiffuse,vUv-uDir*3.0).rgb)*0.054;
s+=(texture2D(tDiffuse,vUv+uDir*4.0).rgb+texture2D(tDiffuse,vUv-uDir*4.0).rgb)*0.016;
gl_FragColor=vec4(s,1.0);
}`
});
const compMat = new THREE.ShaderMaterial({
uniforms:{
tScene:{value:null}, tBloom:{value:null}, tPrev:{value:null},
uBloom:{value:0.9}, uTrail:{value:0.45}, uBlur:{value:0.0}, uTime:{value:0}
},
vertexShader:VERT,
fragmentShader:`
uniform sampler2D tScene,tBloom,tPrev;
uniform float uBloom,uTrail,uBlur,uTime;
varying vec2 vUv;
void main(){
vec2 dir=vUv-0.5;
float amt=uBlur*dot(dir,dir)*4.0;
vec3 s=texture2D(tScene,vUv).rgb*0.34;
s+=texture2D(tScene,vUv-dir*amt*0.25).rgb*0.24;
s+=texture2D(tScene,vUv-dir*amt*0.50).rgb*0.18;
s+=texture2D(tScene,vUv-dir*amt*0.75).rgb*0.14;
s+=texture2D(tScene,vUv-dir*amt*1.00).rgb*0.10;
vec3 c=s+texture2D(tBloom,vUv).rgb*uBloom;
vec3 p=texture2D(tPrev,vUv).rgb;
c=max(c, p*uTrail);
gl_FragColor=vec4(c,1.0);
}`
});
const blitMat = new THREE.ShaderMaterial({
uniforms:{ tDiffuse:{value:null}, uExp:{value:1.25}, uVig:{value:0.85},
uRed:{value:0.0}, uTime:{value:0.0}, uHurt:{value:0.0} },
vertexShader:VERT,
fragmentShader:`
uniform sampler2D tDiffuse; uniform float uExp,uVig,uRed,uTime,uHurt;
varying vec2 vUv;
float rand(vec2 c){ return fract(sin(dot(c,vec2(12.9898,78.233)))*43758.5453); }
void main(){
vec2 uv=vUv;
vec2 d=uv-0.5;
float r2=dot(d,d);
vec2 ca=d*r2*(0.014+uHurt*0.05);
vec3 c;
c.r=texture2D(tDiffuse,uv+ca).r;
c.g=texture2D(tDiffuse,uv).g;
c.b=texture2D(tDiffuse,uv-ca).b;
c=vec3(1.0)-exp(-max(c,0.0)*uExp);
float v=smoothstep(1.05,0.28,length(d*vec2(1.06,1.0))*1.42);
c*=mix(1.0,v,uVig);
c+=vec3(0.65,0.05,0.12)*uRed*(1.0-v);
float g=rand(uv*vec2(1024.0,760.0)+fract(uTime)*37.0)-0.5;
c+=g*0.035;
c=pow(max(c,0.0),vec3(1.0/2.2));
gl_FragColor=vec4(c,1.0);
}`
});
let sceneRT=null, brightRT=null, blurRT=null, compA=null, compB=null;
let halfW=2, halfH=2;
const _size = new THREE.Vector2();
function makeRT(w,h,depth,samples){
return new THREE.WebGLRenderTarget(Math.max(2,w|0), Math.max(2,h|0), {
minFilter:THREE.LinearFilter, magFilter:THREE.LinearFilter,
type:THREE.HalfFloatType, depthBuffer:!!depth, stencilBuffer:false,
samples:samples||0
});
}
function disposeAll(){
[sceneRT,brightRT,blurRT,compA,compB].forEach(rt=>{ if(rt) rt.dispose(); });
}
function resize(){
renderer.getDrawingBufferSize(_size);
const w=Math.max(2,_size.x|0), h=Math.max(2,_size.y|0);
const hw=Math.max(2,(w/2)|0), hh=Math.max(2,(h/2)|0);
disposeAll();
sceneRT = makeRT(w,h,true,4);
brightRT = makeRT(hw,hh,false,0);
blurRT = makeRT(hw,hh,false,0);
compA = makeRT(w,h,false,0);
compB = makeRT(w,h,false,0);
compA = makeRT(w,h,false,0); compB = makeRT(w,h,false,0);
[compA,compB].forEach(rt=>{
renderer.setRenderTarget(rt); renderer.setClearColor(0x000000,1); renderer.clear();
});
renderer.setRenderTarget(null);
blurMat.uniforms.uDir.value.set(1/hw, 0);
halfW=hw; halfH=hh;
}
function pass(mat,target){
fsMesh.material=mat;
renderer.setRenderTarget(target||null);
renderer.render(fsScene, fsCam);
}
return {
resize,
render(dt, p){
if(!sceneRT) resize();
renderer.setClearColor(0x000000,1);
renderer.setRenderTarget(sceneRT);
renderer.clear();
renderer.render(scene, camera);
brightMat.uniforms.tDiffuse.value = sceneRT.texture;
pass(brightMat, brightRT);
blurMat.uniforms.tDiffuse.value = brightRT.texture;
blurMat.uniforms.uDir.value.set(1/this._hw, 0);
pass(blurMat, blurRT);
blurMat.uniforms.tDiffuse.value = blurRT.texture;
blurMat.uniforms.uDir.value.set(0, 1/this._hh);
pass(blurMat, brightRT);
compMat.uniforms.tScene.value = sceneRT.texture;
compMat.uniforms.tBloom.value = brightRT.texture;
compMat.uniforms.tPrev.value = compA.texture;
compMat.uniforms.uBloom.value = 0.85 + (p.turbo?0.5:0);
compMat.uniforms.uTrail.value = p.trail;
compMat.uniforms.uBlur.value = p.blur;
pass(compMat, compB);
const t=compA; compA=compB; compB=t;
blitMat.uniforms.tDiffuse.value = compA.texture;
blitMat.uniforms.uRed.value = p.red;
blitMat.uniforms.uHurt.value = p.hurt;
blitMat.uniforms.uTime.value = p.time;
pass(blitMat, null);
renderer.setRenderTarget(null);
}
};
})();
/* ============================================================
STATE / DOM
============================================================ */
const player = {x:0,y:6,z:0,vx:0,vy:0,roll:0,yaw:0,pitch:0};
const G = {
state:'menu', score:0, best:0, distance:0, speed:CFG.BASE_SPEED, maxSpeed:0,
mult:1, energy:100, turbo:35, turboActive:false, orbCount:0,
t:0, nearCd:0, shake:0, deathT:0, reason:'', hudT:0, beepT:0
};
const $=id=>document.getElementById(id);
const hud = {
score:$('score'), dist:$('dist'), maxspd:$('maxspd'), orbs:$('orbs'),
mult:$('mult'), mb:$('mbarFill'), energy:$('energyFill'), turbo:$('turboFill'),
spdNum:$('spdNum'), spdFill:$('spdFill'), spdKnob:$('spdKnob'),
nm:$('nm'), boost:$('boost'), danger:$('danger'), flash:$('flash'),
hud:$('hud'), overlay:$('overlay'), ovkicker:$('ovkicker'), ovtitle:$('ovtitle'),
ovbody:$('ovbody'), ovstats:$('ovstats'), startBtn:$('startBtn'),
energyGauge:$('energyGauge'), gaugeR:$('gaugeR'), loading:$('loading')
};
/* ============================================================
INPUT
============================================================ */
const keys={left:false,right:false,up:false,down:false,turbo:false};
const KMAP={ArrowLeft:'left',KeyA:'left',ArrowRight:'right',KeyD:'right',
ArrowUp:'up',KeyW:'up',ArrowDown:'down',KeyS:'down',
ShiftLeft:'turbo',ShiftRight:'turbo',Space:'turbo'};
addEventListener('keydown', e=>{
const k=KMAP[e.code];
if(k){ keys[k]=true; e.preventDefault(); }
if(e.code==='KeyR' && G.state!=='menu'){ start(); e.preventDefault(); }
if(G.state==='menu' && (k || e.code==='Enter')) start();
}, {passive:false});
addEventListener('keyup', e=>{ const k=KMAP[e.code]; if(k){ keys[k]=false; e.preventDefault(); } }, {passive:false});
addEventListener('blur', ()=>{ for(const k in keys) keys[k]=false; ptrOn=false; });
let ptrOn=false, ptrX=0, ptrY=0;
canvas.addEventListener('pointerdown', e=>{ ptrOn=true; ptrX=e.clientX; ptrY=e.clientY; });
addEventListener('pointerup', ()=>{ ptrOn=false; });
addEventListener('pointermove', e=>{ if(ptrOn){ ptrX=e.clientX; ptrY=e.clientY; } });
/* ============================================================
COLLISION HELPERS
============================================================ */
function wallDistAndX(){
const rad = wallRadius(player.z, player.y);
const cx = pathX(player.z);
const dL = player.x - (cx - rad);
const dR = (cx + rad) - player.x;
return dL<dR ? {d:dL, x:cx-rad} : {d:dR, x:cx+rad};
}
function boxDist(c){
const dx=Math.abs(player.x-c.x)-c.hx;
const dy=Math.abs(player.y-c.y)-c.hy;
const dz=Math.abs(player.z-c.z)-c.hz;
const ox=dx>0?dx:0, oy=dy>0?dy:0, oz=dz>0?dz:0;
const outside=Math.sqrt(ox*ox+oy*oy+oz*oz);
const inside=Math.min(Math.max(dx,dy,dz),0);
return outside+inside;
}
/* ============================================================
EVENTS
============================================================ */
function nearMiss(x,y){
G.mult = Math.min(CFG.MAX_MULT, G.mult+0.16);
G.score += 30*G.mult;
G.turbo = Math.min(100, G.turbo+7);
G.shake = Math.min(1.0, G.shake+0.42);
Sfx.whoosh();
hud.nm.classList.remove('show'); void hud.nm.offsetWidth; hud.nm.classList.add('show');
hud.mult.classList.add('hot');
clearTimeout(nearMiss._t);
nearMiss._t = setTimeout(()=>hud.mult.classList.remove('hot'), 180);
for(let i=0;i<12;i++){
burst.emit(x+(Math.random()-0.5)*3, y+(Math.random()-0.5)*9, player.z-4-Math.random()*16,
(Math.random()-0.5)*22, (Math.random()-0.5)*24, 26+Math.random()*34,
0.7+Math.random()*1.4, 0.32+Math.random()*0.5, '#ffd6ac', 1.5, 0.7);
}
}
function die(reason){
if(G.state!=='play') return;
G.state='dying'; G.deathT=0; G.reason=reason; G.shake=1.15; G.turboActive=false;
G.best = Math.max(G.best, G.score);
craft.visible=false;
Sfx.crash(); Sfx.engine(0,false,false);
hud.flash.style.transition='opacity .05s'; hud.flash.style.opacity='0.9';
setTimeout(()=>{ hud.flash.style.transition='opacity .9s'; hud.flash.style.opacity='0'; }, 70);
const col=new THREE.Color();
for(let i=0;i<180;i++){
const a=Math.random()*6.283, b=Math.acos(2*Math.random()-1), sp=10+Math.random()*75;
col.setHSL(0.03+Math.random()*0.10, 1, 0.5+Math.random()*0.4);
burst.emit(player.x, player.y, player.z,
Math.sin(b)*Math.cos(a)*sp, Math.cos(b)*sp*0.7+10, Math.sin(b)*Math.sin(a)*sp+34,
1.0+Math.random()*2.8, 0.7+Math.random()*1.5, '#'+col.getHexString(), 0.75, -0.25);
}
}
function statBlock(label, val){
return '<div class="s"><i>'+label+'</i><b>'+val+'</b></div>';
}
function showDead(){
G.state='dead';
hud.ovkicker.textContent = G.reason || 'RUN TERMINATED';
hud.ovtitle.textContent = 'RUN OVER';
hud.ovtitle.classList.add('dead');
hud.ovbody.textContent = 'The canyon swallowed you. Fly tighter next time.';
hud.ovstats.innerHTML =
statBlock('Final Score', Math.floor(G.score).toLocaleString('en-US')) +
statBlock('Max Speed', Math.round(G.maxSpeed*3.6)+' km/h') +
statBlock('Distance', Math.floor(G.distance).toLocaleString('en-US')+' m') +
statBlock('Orbs Collected', G.orbCount);
hud.ovstats.style.display='grid';
hud.startBtn.textContent='Retry';
hud.overlay.classList.add('show');
}
function resetWorld(){
world.clear();
diffGlobal=0;
player.x=pathX(0); player.y=8; player.z=0; player.vx=0; player.vy=0;
player.roll=0; player.yaw=0; player.pitch=0;
G.score=0; G.distance=0; G.speed=CFG.BASE_SPEED; G.maxSpeed=0; G.mult=1;
G.energy=100; G.turbo=35; G.turboActive=false; G.orbCount=0;
G.shake=0; G.nearCd=0; G.deathT=0; G.reason=''; G.beepT=0;
world.nextIdx=-2;
world.update(player.z);
burst.clear();
streaks.reset(player.x, player.y, player.z);
craft.visible=true;
craft.position.set(player.x,player.y,player.z);
camera.position.set(player.x, player.y+5, player.z+15);
camera.fov=74; camera.updateProjectionMatrix();
sun.position.set(player.x-190, player.y+80, player.z-105);
sun.target.position.set(player.x, player.y-6, player.z);
sun.target.updateMatrixWorld();
}
function start(){
Sfx.init(); Sfx.resume();
resetWorld();
hud.overlay.classList.remove('show');
hud.hud.classList.add('on');
hud.ovtitle.classList.remove('dead');
G.state='play';
}
hud.startBtn.addEventListener('click', start);
/* ============================================================
UPDATE
============================================================ */
const _look=new THREE.Vector3(), _des=new THREE.Vector3();
function update(dt){
const playing = G.state==='play';
const dying = G.state==='dying';
let inX=0, inY=0;
if(playing){
inX = (keys.left?-1:0)+(keys.right?1:0);
inY = (keys.up?1:0)+(keys.down?-1:0);
if(ptrOn && inX===0 && inY===0){
inX = clamp((ptrX/innerWidth-0.5)*3.2,-1,1);
inY = clamp(-(ptrY/innerHeight-0.5)*3.2,-1,1);
}
} else if(!dying){
const cx=pathX(player.z), w=widthAt(player.z);
const tx = cx + Math.sin(G.t*0.33)*w*0.30;
const ty = 8 + Math.sin(G.t*0.23)*14;
inX = clamp((tx-player.x)*0.10 - player.vx*0.055, -1,1);
inY = clamp((ty-player.y)*0.10 - player.vy*0.050, -1,1);
}
const diff = clamp(G.distance/15000,0,1);
diffGlobal = diff;
if(dying){
G.speed *= Math.exp(-dt*2.6);
G.deathT += dt;
player.z -= G.speed*dt;
if(G.deathT>0.85) showDead();
} else {
const pitchFactor = 1 - 0.26*inY;
const baseSpd = (CFG.BASE_SPEED + CFG.SPEED_GAIN*diff) * (playing?1:0.9);
const target = baseSpd * pitchFactor * (G.turboActive?CFG.TURBO_MULT:1);
G.speed += (target-G.speed)*(1-Math.exp(-dt*1.7));
G.speed = clamp(G.speed, 16, 430);
G.distance += G.speed*dt;
player.z -= G.speed*dt;
if(G.speed>G.maxSpeed) G.maxSpeed=G.speed;
}
/* --- lateral flight (momentum + drag) --- */
player.vx += inX*CFG.LAT_ACC*dt;
player.vx *= Math.exp(-dt*(playing?2.0:3.0));
player.vx = clamp(player.vx,-CFG.MAX_VX,CFG.MAX_VX);
player.x += player.vx*dt;
/* --- vertical flight --- */
player.vy += inY*CFG.VERT_ACC*dt;
player.vy *= Math.exp(-dt*2.7);
player.vy = clamp(player.vy,-72,72);
player.y += player.vy*dt;
if(!dying){
/* --- attitude: bank into the turn, pitch with climb/dive --- */
const tRoll = clamp(-player.vx*0.0088,-0.85,0.85);
const tYaw = clamp(-player.vx*0.0060,-0.55,0.55);
const tPit = clamp(-player.vy*0.0055,-0.40,0.40);
const kr=1-Math.exp(-dt*6.5), ky=1-Math.exp(-dt*7.5), kp=1-Math.exp(-dt*6.0);
player.roll += (tRoll-player.roll)*kr;
player.yaw += (tYaw -player.yaw )*ky;
player.pitch+= (tPit -player.pitch)*kp;
craft.position.set(player.x, player.y, player.z);
craft.rotation.set(player.pitch, player.yaw, player.roll);
} else {
player.roll += dt*1.6; player.pitch += dt*0.9;
craft.rotation.set(player.pitch, player.yaw, player.roll);
}
const eng = craft.userData.engine;
const puls = 1 + Math.sin(G.t*40)*0.06;
const es = (G.turboActive?2.7:1.7) + G.speed*0.006;
eng.scale.set(puls, puls, es*puls);
eng.material.opacity = 0.75 + (G.turboActive?0.25:0);
/* --- collisions --- */
let dMin = 9999, wallX = 0;
if(playing){
const wd = wallDistAndX();
dMin = wd.d; wallX = wd.x;
if(wd.d < CFG.PLAYER_R+1.6) die('WALL IMPACT');
if(player.y > CFG.CEIL) die('LOST TO THE SKY');
if(player.y < CFG.FLOOR) die('LOST TO THE VOID');
for(const c of world.colliders){
if(Math.abs(c.z-player.z) > 80) continue;
const d = boxDist(c);
if(d<dMin){ dMin=d; wallX=c.x; }
if(d < CFG.PLAYER_R) die('MONOLITH IMPACT');
}
}
/* --- near miss / multiplier --- */
G.nearCd -= dt;
if(G.state==='play'){
if(dMin < CFG.NEAR_DIST && dMin > CFG.PLAYER_R){
if(G.nearCd<=0){ nearMiss(wallX, player.y); G.nearCd = 0.40; }
}
G.mult = Math.max(1, G.mult - dt*0.10);
G.score += G.speed*dt*0.09*G.mult;
}
/* --- energy / turbo --- */
const wantBoost = G.state==='play' && keys.turbo && G.turbo>0.5;
G.turboActive = wantBoost;
if(G.state==='play'){
G.energy -= (CFG.DRAIN + G.speed*CFG.DRAIN_SPD + (G.turboActive?7.5:0))*dt;
if(G.energy<=0){ G.energy=0; die('ENERGY DEPLETED'); }
}
if(G.turboActive) G.turbo = Math.max(0, G.turbo-24*dt);
else if(G.state==='play' && G.speed>115) G.turbo = Math.min(100, G.turbo+3.6*dt);
/* --- low energy warning --- */
if(G.state==='play' && G.energy<28){
G.beepT-=dt;
if(G.beepT<=0){ Sfx.beep(); G.beepT = 0.35 + (G.energy/28)*0.7; }
}
/* --- orbs --- */
for(const ch of world.chunks.values()){
for(const o of ch.orbs){
if(o.taken) continue;
const bob = Math.sin(G.t*2.2+o.phase)*1.7;
o.mesh.position.y = o.base.y + bob;
o.halo.position.copy(o.mesh.position);
o.mesh.rotation.y += dt*1.6; o.mesh.rotation.x += dt*0.7;
const hs = 1 + Math.sin(G.t*3.4+o.phase)*0.10;
o.halo.scale.setScalar(hs);
o.halo.rotation.y -= dt*0.6;
if(!playing) continue;
const dx=o.mesh.position.x-player.x, dy=o.mesh.position.y-player.y, dz=o.mesh.position.z-player.z;
if(dx*dx+dy*dy+dz*dz < 20){
o.taken=true; o.mesh.visible=false; o.halo.visible=false;
G.energy = Math.min(100, G.energy+CFG.ORB_ENERGY);
G.turbo = Math.min(100, G.turbo+6);
G.orbCount++; G.score += 25*G.mult;
Sfx.ping();
for(let i=0;i<14;i++){
const a=Math.random()*6.283, b=Math.acos(2*Math.random()-1), sp=8+Math.random()*26;
burst.emit(o.mesh.position.x, o.mesh.position.y, o.mesh.position.z,
Math.sin(b)*Math.cos(a)*sp, Math.cos(b)*sp, Math.sin(b)*Math.sin(a)*sp+18,
0.6+Math.random()*1.2, 0.35+Math.random()*0.5, '#8ffbe4', 1.8, 0.3);
}
}
}
}
/* --- world streaming --- */
world.update(player.z);
/* --- engine trail --- */
if(G.state==='play' || G.state==='menu'){
const tn = G.turboActive?4:2;
for(let i=0;i<tn;i++){
burst.emit(
player.x - Math.sin(player.yaw)*2.6 + (Math.random()-0.5)*1.1,
player.y + (Math.random()-0.5)*1.1,
player.z + 2.6 + (Math.random()-0.5)*0.6,
-player.vx*0.15, -player.vy*0.1, 26+Math.random()*24,
1.6+Math.random()*1.4, 0.30+Math.random()*0.35,
G.turboActive?'#ffd39a':'#ff9f5e', 2.4, 1.4);
}
}
burst.update(dt);
streaks.update(dt, player.x, player.y, player.z, G.t);
/* --- camera --- */
const camLag = 1-Math.exp(-dt*(playing?8:3));
const back = 13.2 + G.speed*0.011 + (G.turboActive?3.4:0) + (dying?18:0);
_des.set(player.x + player.vx*0.05, player.y + 4.5 - player.vy*0.02, player.z + back);
camera.position.lerp(_des, camLag);
_look.set(player.x + player.vx*0.28, player.y + player.vy*0.06, player.z - 36);
camera.lookAt(_look);
camera.rotateZ(-player.roll*0.40);
if(G.shake>0.002){
const s=G.shake;
camera.position.x += (Math.random()-0.5)*s*2.4;
camera.position.y += (Math.random()-0.5)*s*2.4;
camera.position.z += (Math.random()-0.5)*s*2.0;
camera.rotateZ((Math.random()-0.5)*s*0.05);
G.shake *= Math.exp(-dt*5.0);
} else G.shake=0;
const tFov = 72 + clamp((G.speed-60)/170,0,1)*11 + (G.turboActive?15:0);
camera.fov += (tFov-camera.fov)*(1-Math.exp(-dt*4.5));
camera.updateProjectionMatrix();
/* --- sun shadows follow the player --- */
sun.position.set(player.x-190, player.y+80, player.z-105);
sun.target.position.set(player.x, player.y-6, player.z);
sun.target.updateMatrixWorld();
sky.position.copy(camera.position);
Sfx.engine(G.speed, G.turboActive, G.state==='play'||G.state==='menu');
/* --- HUD (throttled) --- */
G.hudT -= dt;
if(G.hudT<=0){ G.hudT=0.06; updateHUD(); }
}
function updateHUD(){
const kmh = G.speed*3.6;
hud.score.textContent = Math.floor(G.score).toLocaleString('en-US');
hud.dist.textContent = Math.floor(G.distance).toLocaleString('en-US');
hud.maxspd.textContent= Math.round(G.maxSpeed*3.6);
hud.orbs.textContent = G.orbCount;
hud.mult.textContent = '×'+G.mult.toFixed(1);
hud.mb.style.width = clamp(G.mult/CFG.MAX_MULT,0,1)*100+'%';
hud.energy.style.width= clamp(G.energy/100,0,1)*100+'%';
hud.turbo.style.width = clamp(G.turbo/100,0,1)*100+'%';
hud.spdNum.textContent= Math.round(kmh);
const f = clamp(kmh/950,0,1);
hud.spdFill.style.width = (f*100)+'%';
hud.spdKnob.style.left = (f*100)+'%';
hud.danger.style.opacity = G.energy<32 ? ((32-G.energy)/32)*0.7 : 0;
hud.boost.classList.toggle('on', G.turboActive);
hud.gaugeR.classList.toggle('armed', G.turbo>18);
hud.energyGauge.classList.toggle('armed', G.energy<30);
}
/* ============================================================
RESIZE + LOOP
============================================================ */
function onResize(){
camera.aspect = innerWidth/innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
PostFX.resize();
}
addEventListener('resize', onResize);
let last = performance.now();
let booted = false;
function frame(now){
requestAnimationFrame(frame);
let dt = (now-last)/1000; last = now;
if(!(dt>0)) dt = 0.016;
dt = Math.min(dt, 0.05);
G.t += dt;
update(dt);
const spdF = clamp((G.speed-70)/210, 0, 1);
const trail = 0.30 + spdF*0.34 + (G.turboActive?0.10:0);
const blur = 0.22 + spdF*0.55 + (G.turboActive?0.30:0);
const red = (G.state==='play' && G.energy<32) ? (1-G.energy/32)*0.85 : 0;
const hurt = Math.min(1.4, G.shake*0.9 + (G.state==='dying'?0.7:0));
PostFX.render(dt, {trail, blur, red, hurt, turbo:G.turboActive, time:G.t});
if(!booted){
booted = true;
hud.loading.style.display='none';
}
}
/* ============================================================
BOOT
============================================================ */
try{
onResize();
resetWorld();
updateHUD();
requestAnimationFrame(frame);
}catch(err){
hud.loading.textContent = 'ERROR: ' + err.message;
console.error(err);
}
</script>
</body>
</html>