Canyon Run
Mimo v2.6 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 72,770 bytes · SHA-256 c7ab82adfd96
<!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>HELIOS CANYON — Procedural Sunset Racer</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Rajdhani:wght@500;600;700&family=Orbitron:wght@600;800&display=swap" rel="stylesheet">
<style>
:root{
--or:#ff7b2e; --or2:#ffb066; --mg:#ff3d81; --pu:#a24bff; --sil:#e6ecff;
--glass:rgba(12,8,24,.5);
--edge:rgba(255,178,110,.32);
}
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%;width:100%;overflow:hidden;background:#06040c;color:#e9e2ff;
font-family:'Rajdhani',"Segoe UI",system-ui,sans-serif;-webkit-font-smoothing:antialiased;
text-rendering:optimizeLegibility;user-select:none;touch-action:none;cursor:crosshair}
canvas#view{position:fixed;inset:0;width:100%;height:100%;display:block;z-index:0}
#vig{position:fixed;inset:0;z-index:1;pointer-events:none;
background:radial-gradient(ellipse at 50% 52%,rgba(0,0,0,0) 38%,rgba(6,3,14,.55) 82%,rgba(4,2,10,.9) 100%);}
#vig::after{content:"";position:absolute;inset:0;
background:repeating-linear-gradient(to bottom,rgba(255,255,255,.022) 0 1px,rgba(0,0,0,0) 1px 3px);
mix-blend-mode:overlay;opacity:.5}
/* ---------------- HUD ---------------- */
#hud{position:fixed;inset:0;z-index:3;pointer-events:none;opacity:0;transition:opacity .5s ease}
#hud.on{opacity:1}
.tnum{font-variant-numeric:tabular-nums}
.lbl{font-size:11px;letter-spacing:.34em;font-weight:600;color:rgba(233,226,255,.5);text-transform:uppercase}
#scoreBox{position:absolute;top:26px;left:30px;padding-left:14px;border-left:2px solid var(--or)}
#scoreBox .val{font-family:'Orbitron',sans-serif;font-weight:800;font-size:40px;line-height:1;
letter-spacing:.04em;color:#fff;text-shadow:0 0 24px rgba(255,122,46,.55);margin-top:4px}
#mult{margin-top:6px;font-family:'Orbitron',sans-serif;font-weight:800;font-size:26px;line-height:1;
color:var(--or2);text-shadow:0 0 18px rgba(255,140,60,.7);transform-origin:left center;letter-spacing:.02em}
#mult.hot{color:#fff;text-shadow:0 0 30px rgba(255,90,140,.95),0 0 60px rgba(255,140,60,.6)}
#mult.pulse{animation:mpulse .42s cubic-bezier(.2,1.6,.4,1)}
@keyframes mpulse{0%{transform:scale(1)}28%{transform:scale(1.45)}100%{transform:scale(1)}}
#topRight{position:absolute;top:26px;right:30px;text-align:right;display:flex;gap:26px;align-items:flex-start}
#topRight .cell .v{font-family:'Orbitron',sans-serif;font-weight:600;font-size:17px;color:var(--sil);
letter-spacing:.06em;margin-top:3px;opacity:.9}
#snd{pointer-events:auto;cursor:pointer;background:var(--glass);border:1px solid var(--edge);
color:var(--or2);font-family:inherit;font-size:10px;letter-spacing:.2em;padding:5px 9px;border-radius:2px;
transition:.2s;opacity:.8}
#snd:hover{background:rgba(255,122,46,.18);opacity:1}
#snd.off{color:rgba(233,226,255,.35);border-color:rgba(255,255,255,.12)}
/* bottom left bars */
#bars{position:absolute;left:30px;bottom:30px;width:290px}
.barrow{display:flex;align-items:center;gap:10px;margin-top:9px}
.barrow .bl{width:52px;font-size:10px;letter-spacing:.2em;font-weight:600;color:rgba(233,226,255,.45)}
.bar{position:relative;flex:1;height:7px;background:rgba(255,255,255,.07);
border:1px solid rgba(255,255,255,.09);overflow:hidden;transform:skewX(-18deg)}
.bar i{position:absolute;inset:0 auto 0 0;width:50%;display:block;
background:linear-gradient(90deg,#ffd166,#ff7b2e 42%,#ff3d81);transition:width .09s linear;
box-shadow:0 0 14px rgba(255,110,60,.85)}
.bar.t{height:5px}
.bar.t i{background:linear-gradient(90deg,#7ef9ff,#49b6ff 55%,#a24bff);box-shadow:0 0 14px rgba(90,190,255,.85)}
.bar.low i{background:linear-gradient(90deg,#ff3d81,#ff2b2b);animation:lowblink .55s steps(2) infinite}
@keyframes lowblink{50%{opacity:.35}}
/* bottom right speedo */
#speedo{position:absolute;right:26px;bottom:22px;width:214px;height:132px}
#speedo svg{position:absolute;inset:0;width:100%;height:100%;overflow:visible}
#spdNum{position:absolute;left:0;right:0;bottom:26px;text-align:center;font-family:'Orbitron',sans-serif;
font-weight:800;font-size:38px;line-height:1;color:#fff;text-shadow:0 0 22px rgba(120,200,255,.55)}
#spdUnit{position:absolute;left:0;right:0;bottom:8px;text-align:center;font-size:10px;letter-spacing:.4em;
color:rgba(233,226,255,.42)}
#boostTag{position:absolute;left:0;right:0;bottom:112px;text-align:center;font-size:11px;letter-spacing:.36em;
color:#7ef9ff;opacity:0;transition:opacity .15s;font-weight:700}
#boostTag.on{opacity:1;animation:bst .5s linear infinite}
@keyframes bst{50%{opacity:.35}}
/* centre popups + alerts */
#pops{position:absolute;inset:0;overflow:hidden}
.pop{position:absolute;transform:translate(-50%,-50%);font-family:'Orbitron',sans-serif;font-weight:800;
font-size:17px;color:#fff;text-shadow:0 0 16px rgba(255,120,60,.9);white-space:nowrap;
animation:pop .95s cubic-bezier(.15,.85,.3,1) forwards}
.pop.nm{font-size:13px;letter-spacing:.28em;color:var(--or2);text-shadow:0 0 14px rgba(255,140,60,.9)}
@keyframes pop{0%{opacity:0;transform:translate(-50%,-50%) scale(.5)}
16%{opacity:1;transform:translate(-50%,-50%) scale(1.22)}
100%{opacity:0;transform:translate(-50%,-140%) scale(1)}}
#alert{position:absolute;left:0;right:0;top:19%;text-align:center;font-family:'Orbitron',sans-serif;
font-weight:800;font-size:clamp(20px,3.4vw,40px);letter-spacing:.3em;opacity:0;transition:opacity .25s;
color:#fff;text-shadow:0 0 30px rgba(255,80,120,.8)}
#alert.on{opacity:1}
#alert.warn{color:#ff5c7a;text-shadow:0 0 34px rgba(255,60,90,.9)}
#alert.good{color:#7ef9ff;text-shadow:0 0 34px rgba(90,220,255,.9)}
#hit{position:absolute;inset:0;background:radial-gradient(ellipse at 50% 50%,rgba(255,40,60,0) 45%,rgba(255,30,50,.55) 100%);
opacity:0;transition:opacity .5s;pointer-events:none}
#hit.on{opacity:1;transition:opacity .05s}
/* ---------------- OVERLAYS ---------------- */
#ov{position:fixed;inset:0;z-index:5;display:flex;align-items:center;justify-content:center;
background:radial-gradient(ellipse at 50% 45%,rgba(24,10,44,.42),rgba(4,2,10,.9));
backdrop-filter:blur(3px);opacity:0;visibility:hidden;transition:opacity .45s ease,visibility .45s}
#ov.show{opacity:1;visibility:visible}
.panel{display:none;flex-direction:column;align-items:center;text-align:center;padding:38px 44px;max-width:640px}
#ov[data-mode="menu"] .p-menu{display:flex}
#ov[data-mode="over"] .p-over{display:flex}
#ov[data-mode="pause"] .p-pause{display:flex}
.kicker{font-size:11px;letter-spacing:.55em;color:var(--or2);font-weight:600;margin-bottom:16px;opacity:.85}
.kicker.red{color:#ff5c7a}
h1{font-family:'Orbitron',sans-serif;font-weight:800;font-size:clamp(38px,7.4vw,84px);line-height:.94;
letter-spacing:.04em;color:#fff;text-shadow:0 0 44px rgba(255,110,50,.55),0 0 90px rgba(162,75,255,.35)}
h1 span{display:block;color:transparent;-webkit-text-stroke:1.6px var(--or2);text-shadow:0 0 34px rgba(255,140,60,.5)}
.tag{margin-top:16px;font-size:16px;letter-spacing:.16em;color:rgba(233,226,255,.6);font-weight:500}
.btn{margin-top:30px;pointer-events:auto;cursor:pointer;font-family:'Orbitron',sans-serif;font-weight:800;
font-size:15px;letter-spacing:.3em;color:#0b0714;background:linear-gradient(100deg,#ffd166,#ff7b2e 60%,#ff3d81);
border:none;padding:15px 42px;position:relative;transition:.22s;clip-path:polygon(12px 0,100% 0,calc(100% - 12px) 100%,0 100%)}
.btn:hover{filter:brightness(1.15);transform:translateY(-2px);box-shadow:0 12px 40px rgba(255,110,50,.4)}
.btn:active{transform:translateY(0)}
.btn.ghost{background:transparent;color:var(--or2);border:1px solid var(--edge);box-shadow:none}
.btn.ghost:hover{background:rgba(255,122,46,.14)}
.hints{margin-top:26px;display:flex;gap:24px;flex-wrap:wrap;justify-content:center}
.hints div{font-size:11.5px;letter-spacing:.16em;color:rgba(233,226,255,.42)}
.hints b{color:var(--or2);font-weight:700;letter-spacing:.2em}
.bestline{margin-top:22px;font-size:11px;letter-spacing:.4em;color:rgba(233,226,255,.4)}
.bestline span{color:#fff;font-family:'Orbitron',sans-serif;letter-spacing:.06em;margin-left:8px}
.final{margin-top:6px;display:flex;flex-direction:column;gap:4px}
.final .lbl{letter-spacing:.42em}
.final .num{font-family:'Orbitron',sans-serif;font-weight:800;font-size:clamp(46px,9vw,88px);line-height:1;
color:#fff;text-shadow:0 0 40px rgba(255,110,50,.65)}
.stats{margin-top:26px;display:flex;gap:14px;flex-wrap:wrap;justify-content:center}
.stat{min-width:140px;padding:14px 20px;background:rgba(255,255,255,.045);border:1px solid rgba(255,255,255,.09);
display:flex;flex-direction:column;gap:6px}
.stat span{font-size:10px;letter-spacing:.26em;color:rgba(233,226,255,.45)}
.stat b{font-family:'Orbitron',sans-serif;font-weight:800;font-size:22px;color:var(--or2);letter-spacing:.04em}
#fatal{position:fixed;inset:0;z-index:9;display:none;align-items:center;justify-content:center;background:#06040c}
#fatal.on{display:flex}
#fatal .\boxed{max-width:520px;padding:36px 40px;border:1px solid rgba(255,120,60,.3);background:rgba(255,120,60,.05);text-align:center}
#fatal h2{font-family:'Orbitron',sans-serif;font-size:20px;letter-spacing:.24em;color:var(--or2);margin-bottom:14px}
#fatal p{font-size:14px;line-height:1.7;color:rgba(233,226,255,.6);letter-spacing:.06em}
#hint{position:absolute;left:50%;bottom:96px;transform:translateX(-50%);font-size:11px;letter-spacing:.34em;
color:rgba(233,226,255,.4);transition:opacity .6s;opacity:0}
#hint.on{opacity:1}
@media (max-width:760px){
#scoreBox{top:16px;left:16px}#scoreBox .val{font-size:30px}#mult{font-size:20px}
#topRight{top:16px;right:16px;gap:14px}
#bars{left:16px;bottom:18px;width:200px}
#speedo{right:12px;bottom:12px;width:158px;height:104px}
#spdNum{font-size:28px;bottom:22px}#spdUnit{bottom:6px;font-size:9px}#boostTag{bottom:88px;font-size:9px}
}
</style>
</head>
<body>
<canvas id="view"></canvas>
<div id="vig"></div>
<div id="hud">
<div id="scoreBox">
<div class="lbl">Score</div>
<div class="val tnum" id="score">0</div>
<div id="mult">×1.0</div>
</div>
<div id="topRight">
<div class="cell"><div class="lbl">Distance</div><div class="v tnum" id="dist">0 m</div></div>
<div class="cell"><div class="lbl">Best</div><div class="v tnum" id="best">0</div></div>
<button id="snd" title="toggle sound (M)">SND</button>
</div>
<div id="bars">
<div class="barrow"><span class="bl">Energy</span><div class="bar" id="eBar"><i id="eFill" style="width:100%"></i></div></div>
<div class="barrow"><span class="bl">Turbo</span><div class="bar t"><i id="tFill" style="width:0%"></i></div></div>
</div>
<div id="speedo">
<svg viewBox="0 0 214 132">
<defs>
<linearGradient id="gs" x1="0" y1="1" x2="1" y2="0">
<stop offset="0" stop-color="#49b6ff"/><stop offset=".55" stop-color="#7ef9ff"/>
<stop offset="1" stop-color="#ff7b2e"/>
</linearGradient>
</defs>
<path d="M 22 112 A 85 85 0 0 1 192 112" fill="none" stroke="rgba(255,255,255,.1)" stroke-width="4" stroke-linecap="round"/>
<path id="spdArc" d="M 22 112 A 85 85 0 0 1 192 112" fill="none" stroke="url(#gs)" stroke-width="4"
stroke-linecap="round" stroke-dasharray="267" stroke-dashoffset="267"/>
<g id="ticks" stroke="rgba(255,255,255,.28)" stroke-width="1.4"></g>
</svg>
<div id="boostTag">TURBO</div>
<div id="spdNum" class="tnum">0</div>
<div id="spdUnit">KM / H</div>
</div>
<div id="pops"></div>
<div id="alert"></div>
<div id="hit"></div>
<div id="hint"></div>
</div>
<div id="ov" class="show" data-mode="menu">
<div class="panel p-menu">
<div class="kicker">Procedural Sunset Run</div>
<h1>HELIOS<span>CANYON</span></h1>
<p class="tag">Skim the rock. Steal the light. Outrun the dark.</p>
<button class="btn" id="startBtn">Begin Run</button>
<div class="hints">
<div><b>MOUSE / ← → ↑ ↓</b> bank & pitch</div>
<div><b>SPACE</b> turbo</div>
<div><b>P</b> pause</div>
<div><b>M</b> sound</div>
</div>
<div class="bestline">BEST <span id="menuBest">0</span></div>
</div>
<div class="panel p-over">
<div class="kicker red">Run Terminated</div>
<div class="final"><span class="lbl">Final Score</span><span class="num tnum" id="finalScore">0</span></div>
<div class="stats">
<div class="stat"><span>MAX SPEED</span><b class="tnum" id="finalSpeed">0</b></div>
<div class="stat"><span>DISTANCE</span><b class="tnum" id="finalDist">0</b></div>
<div class="stat"><span>NEAR MISSES</span><b class="tnum" id="finalNM">0</b></div>
</div>
<button class="btn" id="retryBtn">Restart</button>
<div class="bestline">BEST <span id="overBest">0</span></div>
</div>
<div class="panel p-pause">
<div class="kicker">System Hold</div>
<h1>PAUSED</h1>
<button class="btn ghost" id="resumeBtn">Resume</button>
<button class="btn ghost" id="quitBtn" style="margin-top:14px">Abandon Run</button>
</div>
</div>
<div id="fatal"><div class="box">
<h2>RENDERER OFFLINE</h2>
<p>The Three.js library could not be fetched from the CDN. This build streams all geometry
through Three.js — connect to the network and reload to enter the canyon.</p>
</div></div>
<script>
window.__THREE_SRC = [
"https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js",
"https://unpkg.com/three@0.128.0/build/three.min.js",
"https://cdn.jsdelivr.net/npm/three@0.128.0/build/three.min.js"
];
</script>
<script>
"use strict";
/* ============================================================================
HELIOS CANYON — high-speed procedural canyon racer
Single file. Three.js (CDN) + Web Audio + custom post-processing chain.
========================================================================== */
/* ------------------------------ CONSTANTS ------------------------------- */
const CH_LEN = 216; // chunk length (world units)
const CH_SEG = 18; // longitudinal samples per chunk
const CH_STEP = CH_LEN / CH_SEG; // 12
const CH_RING = 5; // vertical colour strata per wall
const CH_SHELF = 17; // wall rim thickness
const AHEAD = 6, BEHIND = 2; // chunk residency
const P_R = 2.05; // craft collision radius
const NEAR_WALL = 7.0; // near-miss distance for canyon walls
const NEAR_OBS = 5.2; // near-miss distance for monoliths
const ORB_R = 4.6; // orb pickup radius
const COL = {
band: [[0.125,0.088,0.170],[0.215,0.135,0.230],[0.335,0.198,0.262],
[0.475,0.300,0.282],[0.615,0.438,0.340]],
shelf:[0.780,0.598,0.440],
floorA:[0.098,0.068,0.140], floorB:[0.168,0.110,0.205],
obsSide:[0.255,0.245,0.360], obsSide2:[0.345,0.325,0.455],
obsTop:[0.740,0.575,0.430], obsEnd:[0.300,0.282,0.405], obsBot:[0.062,0.050,0.100],
rim:[1.0,0.56,0.20],
orb:[1.0,0.72,0.36], orbTurbo:[0.42,0.95,1.0]
};
/* -------------------------------- UTILS --------------------------------- */
const clamp=(v,a,b)=>v<a?a:(v>b?b:v);
const lerp=(a,b,t)=>a+(b-a)*t;
const sm=(c,t,r,dt)=>lerp(c,t,1-Math.exp(-r*dt));
function mulberry32(a){return function(){a|=0;a=a+0x6D2B79F5|0;let t=Math.imul(a^a>>>15,1|a);
t=t+Math.imul(t^t>>>7,61|t)^t;return((t^t>>>14)>>>0)/4294967296;};}
function hashInt(n){n=(n^61)^(n>>>16);n=n+(n<<3);n^=n>>>4;n=Math.imul(n,0x27d4eb2d);n^=n>>>15;return n>>>0;}
const fmt=n=>Math.round(n).toString().replace(/\B(?=(\d{3})+(?!\d))/g,",");
const $=id=>document.getElementById(id);
function loadThree(i){
if(window.THREE) return Promise.resolve();
if(i>=window.__THREE_SRC.length) return Promise.reject(new Error("cdn"));
return new Promise((res,rej)=>{
const s=document.createElement('script');
s.src=window.__THREE_SRC[i];
s.onload=()=>{window.THREE?res():rej(new Error("bad"));};
s.onerror=()=>rej(new Error("fail"));
document.head.appendChild(s);
}).catch(()=>loadThree(i+1));
}
/* --------------------------------- AUDIO -------------------------------- */
const AU = {
ctx:null, muted:false, ready:false,
init(){
if(this.ctx){ if(this.ctx.state==='suspended') this.ctx.resume(); return; }
const AC = window.AudioContext||window.webkitAudioContext; if(!AC) return;
try{
const ctx=this.ctx=new AC();
const master=this.master=ctx.createGain(); master.gain.value=0.5; master.connect(ctx.destination);
const len=Math.floor(ctx.sampleRate*2), buf=ctx.createBuffer(1,len,ctx.sampleRate);
const dat=buf.getChannelData(0);
for(let i=0;i<len;i++) dat[i]=Math.random()*2-1;
this.nbuf=buf;
/* engine: saw + sub through resonant lowpass */
const engG=this.engG=ctx.createGain(); engG.gain.value=0.0;
const lp=this.engLP=ctx.createBiquadFilter(); lp.type='lowpass'; lp.frequency.value=420; lp.Q.value=5;
const o1=this.o1=ctx.createOscillator(); o1.type='sawtooth'; o1.frequency.value=58;
const o2=this.o2=ctx.createOscillator(); o2.type='sine'; o2.frequency.value=29;
const o2g=ctx.createGain(); o2g.gain.value=0.7;
o1.connect(lp); o2.connect(o2g); o2g.connect(lp); lp.connect(engG); engG.connect(master);
o1.start(); o2.start();
/* wind: looped noise through bandpass */
const ns=this.wind=ctx.createBufferSource(); ns.buffer=buf; ns.loop=true;
const bp=this.windBP=ctx.createBiquadFilter(); bp.type='bandpass'; bp.frequency.value=760; bp.Q.value=0.6;
const wg=this.windG=ctx.createGain(); wg.gain.value=0;
ns.connect(bp); bp.connect(wg); wg.connect(master); ns.start();
this.ready=true;
}catch(e){ this.ready=false; }
},
setMuted(m){ this.muted=m; if(this.master) this.master.gain.value = m?0:0.5; },
update(sn,boost,active){
if(!this.ready) return; const t=this.ctx.currentTime, T=(p,v,c)=>p.setTargetAtTime(v,t,c||0.09);
if(!active){ T(this.engG.gain,0,0.2); T(this.windG.gain,0,0.2); return; }
T(this.o1.frequency, 46+sn*96+(boost?34:0),0.08);
T(this.o2.frequency, 23+sn*48+(boost?17:0),0.08);
T(this.engLP.frequency, 320+sn*1500+(boost?900:0),0.1);
T(this.engG.gain, 0.10+sn*0.13+(boost?0.05:0),0.12);
T(this.windG.gain, 0.02+sn*0.17+(boost?0.08:0),0.15);
T(this.windBP.frequency, 500+sn*1400,0.15);
},
env(g,t,a,peak,dur){ g.gain.setValueAtTime(0.0001,t); g.gain.exponentialRampToValueAtTime(peak,t+a);
g.gain.exponentialRampToValueAtTime(0.0001,t+dur); },
ping(kind){
if(!this.ready||this.muted) return; const ctx=this.ctx,t=ctx.currentTime;
const base = kind==='turbo' ? 660 : 880;
const g=ctx.createGain(); g.connect(this.master); this.env(g,t,0.012,0.24,0.5);
const o=ctx.createOscillator(); o.type='sine'; o.frequency.setValueAtTime(base,t);
o.frequency.exponentialRampToValueAtTime(base*2.0,t+0.16);
const o2=ctx.createOscillator(); o2.type='triangle'; o2.frequency.setValueAtTime(base*1.5,t);
const g2=ctx.createGain(); g2.gain.value=0.35; o2.connect(g2); g2.connect(g);
o.connect(g); o.start(t); o2.start(t); o.stop(t+0.55); o2.stop(t+0.55);
},
whoosh(pitch){
if(!this.ready||this.muted) return; const ctx=this.ctx,t=ctx.currentTime;
const s=ctx.createBufferSource(); s.buffer=this.nbuf;
const bp=ctx.createBiquadFilter(); bp.type='bandpass'; bp.Q.value=1.5;
const p=pitch||1;
bp.frequency.setValueAtTime(380*p,t);
bp.frequency.exponentialRampToValueAtTime(3200*p,t+0.16);
bp.frequency.exponentialRampToValueAtTime(620*p,t+0.44);
const g=ctx.createGain(); this.env(g,t,0.05,0.30,0.46);
s.connect(bp); bp.connect(g); g.connect(this.master); s.start(t); s.stop(t+0.5);
},
thud(){
if(!this.ready||this.muted) return; const ctx=this.ctx,t=ctx.currentTime;
const s=ctx.createBufferSource(); s.buffer=this.nbuf;
const lp=ctx.createBiquadFilter(); lp.type='lowpass'; lp.frequency.setValueAtTime(900,t);
lp.frequency.exponentialRampToValueAtTime(120,t+0.35);
const g=ctx.createGain(); this.env(g,t,0.008,0.5,0.42);
s.connect(lp); lp.connect(g); g.connect(this.master); s.start(t); s.stop(t+0.45);
const o=ctx.createOscillator(); o.type='sine';
o.frequency.setValueAtTime(120,t); o.frequency.exponentialRampToValueAtTime(34,t+0.3);
const g2=ctx.createGain(); this.env(g2,t,0.006,0.55,0.36);
o.connect(g2); g2.connect(this.master); o.start(t); o.stop(t+0.4);
},
sweep(up){
if(!this.ready||this.muted) return; const ctx=this.ctx,t=ctx.currentTime;
const o=ctx.createOscillator(); o.type='sawtooth';
const f=up?180:900; o.frequency.setValueAtTime(f,t);
o.frequency.exponentialRampToValueAtTime(up?980:150,t+0.55);
const lp=ctx.createBiquadFilter(); lp.type='lowpass'; lp.frequency.value=2400;
const g=ctx.createGain(); this.env(g,t,0.03,0.26,0.6);
o.connect(lp); lp.connect(g); g.connect(this.master); o.start(t); o.stop(t+0.65);
}
};
/* --------------------------------- INPUT -------------------------------- */
const IN = { bank:0, pitch:0, boost:false, keys:Object.create(null),
mouse:{x:0,y:0,active:false}, touch:null, anyKey:false };
function initInput(){
const kd=e=>{
const k=e.key.toLowerCase();
if(['arrowleft','arrowright','arrowup','arrowdown',' ','spacebar'].includes(k)) e.preventDefault();
IN.keys[k]=true; IN.anyKey=true; AU.init();
if(k==='m'){ AU.setMuted(!AU.muted); $('snd').classList.toggle('off',AU.muted); }
if(k==='p'||k==='escape'){ togglePause(); }
if(k===' '||k==='shift') IN.boost=true;
};
const ku=e=>{ const k=e.key.toLowerCase(); IN.keys[k]=false;
if(k===' '||k==='shift') IN.boost=false; };
window.addEventListener('keydown',kd,{passive:false});
window.addEventListener('keyup',ku);
window.addEventListener('blur',()=>{ IN.keys=Object.create(null); IN.boost=false; });
const cv=$('view');
cv.addEventListener('mousemove',e=>{
IN.mouse.x=(e.clientX/window.innerWidth)*2-1;
IN.mouse.y=(e.clientY/window.innerHeight)*2-1;
IN.mouse.active=true; AU.init();
});
cv.addEventListener('mouseleave',()=>{ IN.mouse.active=false; });
const tl=e=>{ const t=e.touches[0];
if(t) IN.touch={x:(t.clientX/window.innerWidth)*2-1, y:(t.clientY/window.innerHeight)*2-1};
IN.boost=e.touches.length>=2;
AU.init(); e.preventDefault(); };
const te=e=>{ if(!e.touches.length){ IN.touch=null; IN.boost=false; }
else { const t=e.touches[0];
IN.touch={x:(t.clientX/window.innerWidth)*2-1, y:(t.clientY/window.innerHeight)*2-1};
IN.boost=e.touches.length>=2; } };
window.addEventListener('touchstart',tl,{passive:false});
window.addEventListener('touchmove',tl,{passive:false});
window.addEventListener('touchend',te);
window.addEventListener('mousedown',()=>AU.init());
}
function readInput(){
let b=0,p=0;
if(IN.keys['arrowleft']||IN.keys['a']) b-=1;
if(IN.keys['arrowright']||IN.keys['d']) b+=1;
if(IN.keys['arrowup']||IN.keys['w']) p+=1;
if(IN.keys['arrowdown']||IN.keys['s']) p-=1;
let src=b, srcP=p;
if(IN.touch){ src=IN.touch.x; srcP=-IN.touch.y; }
else{
if(Math.abs(IN.mouse.x)>Math.abs(b)) src=IN.mouse.x;
if(Math.abs(-IN.mouse.y)>Math.abs(p)) srcP=-IN.mouse.y;
}
IN.bank=clamp(src,-1,1);
IN.pitch=clamp(srcP,-1,1);
IN.boost=!!(IN.boost||IN.keys[' ']||IN.keys['shift']);
}
/* ---------------------------------- HUD --------------------------------- */
const HUD = {
score:null, mult:null, eFill:null, tFill:null, eBar:null, spdNum:null,
arc:null, dist:null, best:null, pops:null, alert:null, hit:null, boostTag:null,
lastAlert:0, flashT:0,
init(){
this.score=$('score'); this.mult=$('mult'); this.eFill=$('eFill'); this.tFill=$('tFill');
this.eBar=$('eBar'); this.spdNum=$('spdNum'); this.arc=$('spdArc'); this.dist=$('dist');
this.best=$('best'); this.pops=$('pops'); this.alert=$('alert'); this.hit=$('hit');
this.boostTag=$('boostTag');
// speedo ticks
const g=$('ticks'); let s='';
for(let i=0;i<=10;i++){
const a=Math.PI-(i/10)*Math.PI;
const r1=i%5===0?66:72, r2=79;
const cx=107, cy=112;
s+=`<line x1="${(cx+Math.cos(a)*r1).toFixed(1)}" y1="${(cy-Math.sin(a)*r1).toFixed(1)}"
x2="${(cx+Math.cos(a)*r2).toFixed(1)}" y2="${(cy-Math.sin(a)*r2).toFixed(1)}"
opacity="${i%5===0?.85:.4}"/>`;
}
g.innerHTML=s;
this.arcLen=this.arc.getTotalLength?this.arc.getTotalLength():267;
this.arc.style.strokeDasharray=this.arcLen;
},
update(){
const S=window.__S, P=window.__P;
this.score.textContent=fmt(S.score);
const m='×'+S.mult.toFixed(1);
if(this.mult.textContent!==m) this.mult.textContent=m;
this.mult.classList.toggle('hot',S.mult>=3);
this.eFill.style.width=clamp(S.energy,0,100)+'%';
this.eBar.classList.toggle('low',S.energy<26);
this.tFill.style.width=clamp(S.turbo,0,100)+'%';
const kmh=Math.round(S.speed*3);
this.spdNum.textContent=kmh;
const f=clamp(S.speed/Math.max(1,S.vMax),0,1);
this.arc.style.strokeDashoffset=this.arcLen*(1-f);
this.dist.textContent = P.z>=1000 ? (P.z/1000).toFixed(2)+' km' : Math.round(P.z)+' m';
this.best.textContent = fmt(Math.max(S.best||0,S.score));
this.boostTag.classList.toggle('on',S.boosting);
if(S.flashHit>0){ this.hit.classList.add('on'); } else this.hit.classList.remove('on');
},
pulseMult(){
const el=this.mult; el.classList.remove('pulse'); void el.offsetWidth; el.classList.add('pulse');
},
pop(txt,x,y,cls){
const d=document.createElement('div');
d.className='pop'+(cls?' '+cls:''); d.textContent=txt;
d.style.left=x+'px'; d.style.top=y+'px';
this.pops.appendChild(d);
setTimeout(()=>{ d.remove(); },980);
},
say(txt,kind,hold){
const a=this.alert; a.textContent=txt; a.className='on'+(kind?' '+kind:'');
clearTimeout(this._at);
this._at=setTimeout(()=>{ a.className=''; }, hold||1400);
}
};
</script>
<script>
"use strict";
/* ============================ PROCEDURAL WORLD =========================== */
let SEED = 1337; // per-run seed: shifts terrain phases + obstacle layout
function dOf(z){ return clamp(z/17000,0,1); } // difficulty 0..1
/* --- analytic terrain fields (pure functions of z, shared by all chunks) --- */
function cX(z){ const s=SEED*0.017;
return Math.sin(z*0.0040+s)*40 + Math.sin(z*0.0107+1.7+s*1.3)*16 + Math.sin(z*0.0019+4.1+s*0.7)*26; }
function halfW(z){ const d=dOf(z);
return (48-17*d) + Math.sin(z*0.0061+0.6+SEED*0.011)*8 + Math.sin(z*0.0167+2.2)*4; }
function wallH(z){ const d=dOf(z);
return clamp((118 + 40*Math.sin(z*0.0034+2.0+SEED*0.009) + 22*Math.sin(z*0.0091+0.4)) *
(0.95+0.22*d), 92, 190); }
function ceilY(z){ return Math.min(wallH(z)*0.62, 76); }
function corrX(z){ const w=halfW(z);
let x = cX(z) + Math.sin(z*0.0073+1.1+SEED*0.02)*w*0.45 + Math.sin(z*0.0031+2.6)*w*0.25;
return clamp(x, cX(z)-w+9, cX(z)+w-9); }
function corrY(z){ const c=ceilY(z);
return clamp(21 + Math.sin(z*0.0058+0.4+SEED*0.013)*12 + Math.sin(z*0.0131+2.0)*6, 8, c-7); }
/* Linear-interpolated evaluation on the exact sample grid used by the meshes,
so collision matches the rendered surface to the fraction of a unit. */
function terrainAt(z){
let i=Math.floor(z/CH_STEP); if(i<0) i=0;
const za=i*CH_STEP, zb=za+CH_STEP;
let t=(z-za)/CH_STEP; t=clamp(t,0,1);
const ax=cX(za), bx=cX(zb), ah=halfW(za), bh=halfW(zb);
const ha=wallH(za), hb=wallH(zb);
return { x:lerp(ax,bx,t), hw:lerp(ah,bh,t), h:lerp(ha,hb,t),
ceil:Math.min(lerp(ha,hb,t)*0.62,76) };
}
/* ------------------------------ geometry helpers ------------------------- */
function triPush(P,C,a,b,c,col){
P.push(a[0],a[1],a[2],b[0],b[1],b[2],c[0],c[1],c[2]);
for(let i=0;i<3;i++) C.push(col[0],col[1],col[2]);
}
function quadPush(P,C,a,b,c,d,col){ triPush(P,C,a,b,c,col); triPush(P,C,a,c,d,col); }
function jit(col,k,r){ const m=1+(r()-0.5)*k; return [clamp(col[0]*m,0,1),clamp(col[1]*m,0,1),clamp(col[2]*m,0,1)]; }
function pushBox(P,C,x0,x1,y0,y1,z0,z1,cs,ct,ce,cb){
const S=cs,T=ct,E=ce||cs,B=cb||COL.obsBot;
quadPush(P,C,[x0,y1,z0],[x1,y1,z0],[x1,y1,z1],[x0,y1,z1],T); // top
quadPush(P,C,[x0,y0,z1],[x1,y0,z1],[x1,y0,z0],[x0,y0,z0],B); // bottom
quadPush(P,C,[x1,y0,z0],[x1,y0,z1],[x1,y1,z1],[x1,y1,z0],S); // +x
quadPush(P,C,[x0,y0,z1],[x0,y0,z0],[x0,y1,z0],[x0,y1,z1],S); // -x
quadPush(P,C,[x0,y0,z1],[x1,y0,z1],[x1,y1,z1],[x0,y1,z1],E); // +z
quadPush(P,C,[x1,y0,z0],[x0,y0,z0],[x0,y1,z0],[x1,y1,z0],E); // -z
}
/* is the intended flight corridor clear of this box (with padding)? */
function corridorClear(x0,x1,y0,y1,z0,z1,pad){
const zA=Math.floor(z0/6)*6;
for(let z=zA; z<=z1; z+=6){
if(z<z0-6) continue;
const cx=corrX(z), cy=corrY(z);
if(cx>x0-pad && cx<x1+pad && cy>y0-pad && cy<y1+pad) return false;
}
return true;
}
/* ------------------------------ chunk builder ---------------------------- */
function buildChunkData(idx){
const z0=idx*CH_LEN, z1=z0+CH_LEN;
const rnd=mulberry32(hashInt(Math.imul(idx,2654435761)^SEED));
const wP=[],wC=[],fP=[],fC=[],gP=[],gC=[];
const obstacles=[], orbs=[];
/* --- canyon walls + floor --- */
for(let i=0;i<CH_SEG;i++){
const za=z0+i*CH_STEP, zb=za+CH_STEP;
const ta=terrainAt(za), tb=terrainAt(zb);
const mid=(i%2===0)?COL.floorA:COL.floorB;
quadPush(fP,fC,
[ta.x-ta.hw,0,za],[ta.x+ta.hw,0,za],
[tb.x+tb.hw,0,zb],[tb.x-tb.hw,0,zb], jit(mid,0.10,rnd));
for(const s of [-1,1]){
const ax=ta.x+s*ta.hw, bx=tb.x+s*tb.hw;
for(let r=0;r<CH_RING;r++){
const ya0=ta.h*r/CH_RING, ya1=ta.h*(r+1)/CH_RING;
const yb0=tb.h*r/CH_RING, yb1=tb.h*(r+1)/CH_RING;
quadPush(wP,wC,[ax,ya0,za],[ax,ya1,za],[bx,yb1,zb],[bx,yb0,zb],
jit(COL.band[r],0.20,rnd));
}
const axo=ax+s*CH_SHELF, bxo=bx+s*CH_SHELF;
quadPush(wP,wC,[ax,ta.h,za],[axo,ta.h*1.05,za],[bxo,tb.h*1.05,zb],[bx,tb.h,zb],
jit(COL.shelf,0.14,rnd));
quadPush(wP,wC,[axo,ta.h*1.05,za],[axo+s*22,ta.h*1.05-46,za],
[bxo+s*22,tb.h*1.05-46,zb],[bxo,tb.h*1.05,zb],[0.07,0.05,0.11]);
}
}
/* --- difficulty-scaled obstacle clusters --- */
const d=dOf(z0);
const nCl=Math.max(1,Math.round(1+rnd()*1.6+d*2.1));
let lastZ=-1e9;
for(let k=0;k<nCl;k++){
const tz=z0+26+rnd()*(CH_LEN-52);
if(tz-lastZ<44) continue;
lastZ=tz;
const r=rnd(); let type='monolith';
if(d>0.10 && r<0.20+0.22*d) type='window';
else if(r<0.56) type='pillar';
else if(r<0.79) type='overhead';
const t=terrainAt(tz), cy=ceilY(tz), w=t.hw;
if(type==='window'){ /* TIGHT SQUEEZE */
const hw2=30-13*d, hh=17-6*d, dp=6;
const gxMin=t.x-w+hw2/2+1, gxMax=t.x+w-hw2/2-1;
const gyMin=hh/2+1, gyMax=Math.max(cy-hh/2-1,gyMin+2);
let gx=clamp(corrX(tz),gxMin,gxMax);
let gy=clamp(corrY(tz),gyMin,gyMax);
const zA=tz-dp, zB=tz+dp;
const L=t.x-w-9, R=t.x+w+9, top=t.h+5;
const push=(a,b,c,dd)=>{ pushBox(wP,wC,a,b,c,dd,zA,zB,COL.obsSide,COL.obsTop,COL.obsEnd);
obstacles.push({x0:a,x1:b,y0:c,y1:dd,z0:zA,z1:zB,nm:false}); };
push(L, gx-hw2/2, 0, top);
push(gx+hw2/2, R, 0, top);
push(gx-hw2/2, gx+hw2/2, 0, gy-hh/2);
push(gx-hw2/2, gx+hw2/2, gy+hh/2, top);
/* glowing rim marks the gap without encroaching on it */
const th=1.2;
pushBox(gP,gC, gx-hw2/2, gx+hw2/2, gy+hh/2, gy+hh/2+th, zA-0.6, zB+0.6, COL.rim, COL.rim, COL.rim, COL.rim);
pushBox(gP,gC, gx-hw2/2, gx+hw2/2, gy-hh/2-th, gy-hh/2, zA-0.6, zB+0.6, COL.rim, COL.rim, COL.rim, COL.rim);
pushBox(gP,gC, gx-hw2/2-th, gx-hw2/2, gy-hh/2, gy+hh/2, zA-0.6, zB+0.6, COL.rim, COL.rim, COL.rim, COL.rim);
pushBox(gP,gC, gx+hw2/2, gx+hw2/2+th, gy-hh/2, gy+hh/2, zA-0.6, zB+0.6, COL.rim, COL.rim, COL.rim, COL.rim);
} else {
let bw, bh, bz0, bz1, bx, by0, by1;
if(type==='pillar'){ /* grounded monolith */
bw=6+rnd()*11; bh=12+rnd()*40; bz0=tz-(6+rnd()*9)/2; bz1=tz+(bz0-tz)*-1;
bz1=tz+(6+rnd()*9)/2;
const side=rnd()<0.5?-1:1;
bx=t.x+side*(w-bw/2-3-rnd()*9); by0=0; by1=bh;
} else if(type==='overhead'){ /* ceiling fang */
bw=11+rnd()*15; const dp=7+rnd()*9; bz0=tz-dp/2; bz1=tz+dp/2;
const side=rnd()<0.5?-1:1;
bx=t.x+side*(w-bw/2-2);
by1=t.h+4; by0=cy*(0.30+rnd()*0.34);
} else { /* floating monolith */
bw=7+rnd()*13; const dp=7+rnd()*13; bz0=tz-dp/2; bz1=tz+dp/2;
bx=t.x+(rnd()*2-1)*(w-bw/2-7);
bh=8+rnd()*22; by0=8+rnd()*Math.max(4,(cy-26)); by1=by0+bh;
}
let x0=bx-bw/2, x1=bx+bw/2;
if(!corridorClear(x0,x1,by0,by1,bz0,bz1,8)){
const cg=corrX(tz), dir=(bx>=cg)?1:-1;
const nx0=(dir>0? cg+8 : x0-0), nx1=(dir>0? x1 : cg-8);
if(dir>0){ x0=Math.max(x0, cg+8); } else { x1=Math.min(x1, cg-8); }
if(x1-x0<3){ // shift whole box away from corridor instead
const need=(cg>x1)? (cg-8-x1) : (x0-(cg+8));
x0+= (cg>x1? need : -need); x1+=(cg>x1? need : -need);
}
const inside=clamp(x0, t.x-w+2, t.x+w-2);
x1+=inside-x0; x0=inside;
if(!corridorClear(x0,x1,by0,by1,bz0,bz1,7)){
by1=Math.min(by1, corrY(tz)-9);
if(by1-by0<5) by0=Math.max(0,by1-6);
if(!corridorClear(x0,x1,by0,by1,bz0,bz1,6)) continue;
}
}
pushBox(wP,wC,x0,x1,by0,by1,bz0,bz1,COL.obsSide,COL.obsTop,COL.obsEnd);
obstacles.push({x0,x1,y0:by0,y1:by1,z0:bz0,z1:bz1,nm:false});
}
}
/* --- energy orbs along the corridor (risk = they hug the walls) --- */
const step=40-8*d;
for(let z=z0+8; z<z1-4; z+=step+rnd()*14){
const t=terrainAt(z), cy=ceilY(z);
let ox=corrX(z)+(rnd()*2-1)*4, oy=corrY(z)+(rnd()*2-1)*5, kind='energy';
if(rnd()<0.24){ const side=rnd()<0.5?-1:1;
ox=t.x+side*(t.hw-7.5); oy=6+rnd()*Math.max(6,cy-14); }
if(rnd()<0.10) kind='turbo';
ox=clamp(ox, t.x-t.hw+4, t.x+t.hw-4);
oy=clamp(oy, 4.5, cy-4);
orbs.push({x:ox,y:oy,z:z,kind:kind,taken:false,ph:rnd()*6.283});
}
return { idx,z0,z1,
wP:new Float32Array(wP), wC:new Float32Array(wC),
fP:new Float32Array(fP), fC:new Float32Array(fC),
gP:new Float32Array(gP), gC:new Float32Array(gC),
obstacles, orbs };
}
/* --------------------------- collision primitives ------------------------ */
function boxDist(px,py,pz,o){
const dx=Math.max(o.x0-px,0,px-o.x1);
const dy=Math.max(o.y0-py,0,py-o.y1);
const dz=Math.max(o.z0-pz,0,pz-o.z1);
return Math.sqrt(dx*dx+dy*dy+dz*dz);
}
function boxInside(px,py,pz,o){
return px>o.x0&&px<o.x1&&py>o.y0&&py<o.y1&&pz>o.z0&&pz<o.z1;
}
</script>
<script>
"use strict";
/* ============================== RENDER LAYER ============================= */
let renderer, scene, camera, canvas;
let matWorld, matFloor, matGlow, matCraft;
let chunkMeshes=[]; // flat list of live meshes for disposal
const chunks=new Map(); // idx -> {data, meshes:[]}
let sky, skyMat, sunLight, sunSprite, ground, engineLight, engineSprite;
let craft, craftGlow;
let ptGeo, ptMat, orbGeo, orbMat;
let rtScene, rtPrev, rtCur, rtB1, rtB2, fsScene, fsCam, fsQuad;
let matBright, matBlur, matGrade, matScreen;
let sceneW=1, sceneH=1, bloomW=1, bloomH=1;
let QSCALE=1;
/* ------------------------------ state ----------------------------------- */
const S = {
mode:'menu', prevMode:'menu', time:0, seedTime:0,
score:0, mult:1, nmTimer:0, nmCount:0,
energy:100, turbo:0, boosting:false, boostHeld:false,
speed:44, vMax:100, dist:0, maxSpeed:0,
bankIn:0, pitchIn:0, bankVis:0, pitchVis:0, yaw:0,
shake:0, flashHit:0, invuln:0, turb:0, flash:1, flashCol:[0,0,0],
auto:true, overT:0, armed:[true,true], fbHold:0, best:0, muted:false
};
const P = { x:0, y:20, z:0, vx:0, vy:0, r:P_R };
window.__S=S; window.__P=P;
/* ------------------------------ textures -------------------------------- */
function glowTexture(){
const c=document.createElement('canvas'); c.width=c.height=64;
const g=c.getContext('2d');
const rg=g.createRadialGradient(32,32,0,32,32,32);
rg.addColorStop(0,'rgba(255,255,255,1)');
rg.addColorStop(0.16,'rgba(255,240,220,0.92)');
rg.addColorStop(0.42,'rgba(255,170,90,0.42)');
rg.addColorStop(1,'rgba(255,120,40,0)');
g.fillStyle=rg; g.fillRect(0,0,64,64);
const t=new THREE.CanvasTexture(c); t.needsUpdate=true; return t;
}
/* ------------------------------ the craft ------------------------------- */
function extrudeY(P,C,pts,y0,y1,col,capT,capB){
const n=pts.length;
for(let i=0;i<n;i++){ const j=(i+1)%n;
quadPush(P,C,[pts[i][0],y1,pts[i][1]],[pts[j][0],y1,pts[j][1]],
[pts[j][0],y0,pts[j][1]],[pts[i][0],y0,pts[i][1]],col); }
for(let i=1;i<n-1;i++){
triPush(P,C,[pts[0][0],y1,pts[0][1]],[pts[i][0],y1,pts[i][1]],[pts[i+1][0],y1,pts[i+1][1]],capT||col);
triPush(P,C,[pts[0][0],y0,pts[0][1]],[pts[i+1][0],y0,pts[i+1][1]],[pts[i][0],y0,pts[i][1]],capB||col);
}
}
function extrudeX(P,C,pts,x0,x1,col,capT,capB){
const n=pts.length;
for(let i=0;i<n;i++){ const j=(i+1)%n;
quadPush(P,C,[x1,pts[i][1],pts[i][0]],[x1,pts[j][1],pts[j][0]],
[x0,pts[j][1],pts[j][0]],[x0,pts[i][1],pts[i][0]],col); }
for(let i=1;i<n-1;i++){
triPush(P,C,[x1,pts[0][1],pts[0][0]],[x1,pts[i][1],pts[i][0]],[x1,pts[i+1][1],pts[i+1][0]],capT||col);
triPush(P,C,[x0,pts[0][1],pts[0][0]],[x0,pts[i+1][1],pts[i+1][0]],[x0,pts[i][1],pts[i][0]],capB||col);
}
}
function buildCraftGeometry(){
const P=[],C=[];
const body=[0.40,0.44,0.56], bodyD=[0.17,0.19,0.28], wing=[0.30,0.33,0.44];
const tip=[1.0,0.50,0.15], fin=[1.0,0.44,0.14], eng=[0.12,0.11,0.19];
const nose=[0,0.06,6.7], tail=[0,0.12,-5.0];
const rA=[[1.18,0,1.0],[0,0.92,1.0],[-1.18,0,1.0],[0,-0.82,1.0]];
const rB=[[0.74,0,-3.3],[0,0.56,-3.3],[-0.74,0,-3.3],[0,-0.5,-3.3]];
for(let i=0;i<4;i++){ const j=(i+1)%4;
triPush(P,C,nose,rA[i],rA[j], body);
triPush(P,C,rA[i],rB[i],rB[j], body);
triPush(P,C,rA[i],rB[j],rA[j], i===1?bodyD:body);
triPush(P,C,rB[i],tail,rB[j], bodyD);
}
extrudeY(P,C,[[-0.95,2.3],[-0.95,-2.7],[-6.5,-4.0],[-5.0,0.7]],-0.15,0.15,wing, body, bodyD);
extrudeY(P,C,[[0.95,2.3],[5.0,0.7],[6.5,-4.0],[0.95,-2.7]],-0.15,0.15,wing, body, bodyD);
extrudeY(P,C,[[-6.6,-3.15],[-6.6,-4.05],[-4.9,-3.75],[-4.9,-3.15]],-0.17,0.17,tip, tip, tip);
extrudeY(P,C,[[6.6,-3.15],[4.9,-3.15],[4.9,-3.75],[6.6,-4.05]],-0.17,0.17,tip, tip, tip);
extrudeX(P,C,[[-3.3,0.55],[-4.7,0.6],[-5.0,3.1],[-3.5,2.7]],-0.14,0.14,fin, fin, fin);
pushBox(P,C,-0.78,0.78,-0.66,0.66,-5.1,-3.6, eng, eng, eng, eng);
pushBox(P,C,-0.5,0.5,-0.44,0.44,2.6,4.4, [0.30,0.34,0.46],[0.9,0.55,0.25],[0.30,0.34,0.46]);
const g=new THREE.BufferGeometry();
g.setAttribute('position',new THREE.BufferAttribute(new Float32Array(P),3));
g.setAttribute('color',new THREE.BufferAttribute(new Float32Array(C),3));
g.computeVertexNormals();
return g;
}
/* ------------------------------ particle system -------------------------- */
const PN=1500, DN=520;
const pa={ px:new Float32Array(PN),py:new Float32Array(PN),pz:new Float32Array(PN),
vx:new Float32Array(PN),vy:new Float32Array(PN),vz:new Float32Array(PN),
life:new Float32Array(PN),max:new Float32Array(PN),bs:new Float32Array(PN),
dr:new Float32Array(PN),dg:new Float32Array(PN),db:new Float32Array(PN),
bsx:new Float32Array(PN),
pos:new Float32Array(PN*3), col:new Float32Array(PN*3),
siz:new Float32Array(PN), alp:new Float32Array(PN) };
let pHead=DN;
function spawnPart(x,y,z,vx,vy,vz,life,size,r,g,b,drag){
const i=pHead; pHead=(pHead+1)%PN; if(pHead<DN) pHead=DN;
pa.px[i]=x;pa.py[i]=y;pa.pz[i]=z;pa.vx[i]=vx;pa.vy[i]=vy;pa.vz[i]=vz;
pa.life[i]=life;pa.max[i]=life;pa.bs[i]=size;pa.dr[i]=r;pa.dg[i]=g;pa.db[i]=b;
pa.bsx[i]=drag||1;
}
function respawnDust(i,initial){
const z = initial ? P.z-70+Math.random()*470 : P.z+160+Math.random()*280;
const t=terrainAt(z);
pa.px[i]=t.x+(Math.random()*2-1)*(t.hw+8);
pa.py[i]=1+Math.random()*Math.max(6,ceilY(z)+6);
pa.pz[i]=z;
pa.vx[i]=0; pa.vy[i]=0; pa.vz[i]=-2-Math.random()*4;
pa.life[i]=1e9; pa.max[i]=1e9;
pa.bs[i]=0.16+Math.random()*0.30;
const w=0.55+Math.random()*0.45;
pa.dr[i]=w; pa.dg[i]=w*0.82; pa.db[i]=w*0.72;
pa.bsx[i]=2;
}
function initParticles(){
for(let i=0;i<DN;i++){ pa.life[i]=0; respawnDust(i,true); }
for(let i=DN;i<PN;i++) pa.life[i]=0;
pHead=DN;
}
function clearParticles(){
for(let i=DN;i<PN;i++) pa.life[i]=0;
pHead=DN;
}
function updateParticles(dt){
for(let i=0;i<DN;i++){
const z=pa.pz[i];
if(z<P.z-70||z>P.z+430){ respawnDust(i,false); }
else { pa.px[i]+=pa.vx[i]*dt; pa.py[i]+=pa.vy[i]*dt; pa.pz[i]+=pa.vz[i]*dt; }
const d=pa.pz[i]-P.z;
pa.alp[i]=0.55*clamp((d-7)/9,0,1)*clamp((440-d)/120,0,1);
pa.siz[i]=pa.bs[i];
pa.pos[i*3]=pa.px[i]; pa.pos[i*3+1]=pa.py[i]; pa.pos[i*3+2]=pa.pz[i];
pa.col[i*3]=pa.dr[i]; pa.col[i*3+1]=pa.dg[i]; pa.col[i*3+2]=pa.db[i];
}
for(let i=DN;i<PN;i++){
if(pa.life[i]<=0){ pa.alp[i]=0; continue; }
pa.life[i]-=dt;
if(pa.life[i]<=0){ pa.alp[i]=0; continue; }
const k=pa.bsx[i];
pa.vx[i]*=(1-2.4*dt*k); pa.vy[i]*=(1-2.4*dt*k); pa.vz[i]*=(1-2.4*dt*k);
pa.vy[i]-=6*dt*k*0.35;
pa.px[i]+=pa.vx[i]*dt; pa.py[i]+=pa.vy[i]*dt; pa.pz[i]+=pa.vz[i]*dt;
const t=pa.life[i]/pa.max[i];
pa.alp[i]=t*t;
pa.siz[i]=pa.bs[i]*(0.35+0.65*t);
pa.pos[i*3]=pa.px[i]; pa.pos[i*3+1]=pa.py[i]; pa.pos[i*3+2]=pa.pz[i];
const f=0.35+0.65*t;
pa.col[i*3]=pa.dr[i]*f+0.25*(1-t); pa.col[i*3+1]=pa.dg[i]*f*f; pa.col[i*3+2]=pa.db[i]*f*t;
}
ptGeo.attributes.position.needsUpdate=true;
ptGeo.attributes.aColor.needsUpdate=true;
ptGeo.attributes.aSize.needsUpdate=true;
ptGeo.attributes.aAlpha.needsUpdate=true;
}
function spawnSparks(x,y,z,n,r,g,b,spd){
for(let i=0;i<n;i++){
const a=Math.random()*6.283, b2=Math.acos(Math.random()*2-1), s=(0.4+Math.random())*(spd||26);
spawnPart(x,y,z,Math.sin(b2)*Math.cos(a)*s, Math.cos(b2)*s*0.7, Math.sin(b2)*Math.sin(a)*s,
0.4+Math.random()*0.5, 0.55+Math.random()*0.7, r,g,b, 1.6);
}
}
/* ------------------------------ scene objects ---------------------------- */
function initScene(){
scene=new THREE.Scene();
scene.fog=new THREE.Fog(0xff9a63,140,820);
camera=new THREE.PerspectiveCamera(70,innerWidth/innerHeight,0.5,6000);
camera.rotation.order='YXZ';
/* sky dome */
skyMat=new THREE.ShaderMaterial({
side:THREE.BackSide, depthWrite:false, fog:false,
uniforms:{ uTop:{value:new THREE.Color(0x2b1b52)}, uMid:{value:new THREE.Color(0x8a3f7a)},
uBot:{value:new THREE.Color(0xff8a4a)}, uGlow:{value:new THREE.Color(0xffd9a0)},
uSun:{value:new THREE.Vector3(-0.78,0.30,0.55)} },
vertexShader:`varying vec3 vD; void main(){ vD=normalize(position);
gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.0); }`,
fragmentShader:`varying vec3 vD; uniform vec3 uTop,uMid,uBot,uGlow,uSun;
void main(){ float h=vD.y;
vec3 c=mix(uBot,uMid,smoothstep(-0.12,0.16,h));
c=mix(c,uTop,smoothstep(0.08,0.62,h));
float s=max(dot(normalize(vD),normalize(uSun)),0.0);
c+=uGlow*pow(s,7.0)*0.60;
c+=uGlow*pow(s,150.0)*1.35;
gl_FragColor=vec4(c,1.0); }`
});
sky=new THREE.Mesh(new THREE.SphereGeometry(3200,24,16), skyMat);
sky.renderOrder=-1000; sky.frustumCulled=false;
scene.add(sky);
/* lights */
sunLight=new THREE.DirectionalLight(0xffc490,1.35);
sunLight.castShadow=true;
const sm2=sunLight.shadow;
sm2.mapSize.set(2048,2048);
sm2.camera.left=-135; sm2.camera.right=135; sm2.camera.top=150; sm2.camera.bottom=-150;
sm2.camera.near=1; sm2.camera.far=760;
sm2.bias=-0.0008; sm2.normalBias=0.9; sm2.radius=1.6;
sm2.camera.updateProjectionMatrix();
scene.add(sunLight); scene.add(sunLight.target);
const hemi=new THREE.HemisphereLight(0xa070ff,0x59291c,0.70); scene.add(hemi);
const amb=new THREE.AmbientLight(0x43306a,0.52); scene.add(amb);
window.__hemi=hemi; window.__amb=amb;
/* distant ground so no gaps show through */
const gg=new THREE.PlaneGeometry(7000,7000);
ground=new THREE.Mesh(gg,new THREE.MeshLambertMaterial({color:0x120c20,fog:true}));
ground.rotation.x=-Math.PI/2; ground.position.y=-1.4; ground.receiveShadow=true;
scene.add(ground);
/* materials */
matWorld=new THREE.MeshLambertMaterial({vertexColors:true,side:THREE.DoubleSide,fog:true});
matWorld.shadowSide=THREE.DoubleSide;
matFloor=new THREE.MeshLambertMaterial({vertexColors:true,side:THREE.DoubleSide,fog:true});
matGlow=new THREE.MeshBasicMaterial({vertexColors:true,side:THREE.DoubleSide,fog:true});
matCraft=new THREE.MeshLambertMaterial({vertexColors:true,side:THREE.DoubleSide,fog:true});
/* craft */
craft=new THREE.Mesh(buildCraftGeometry(),matCraft);
craft.castShadow=true; craft.rotation.order='YXZ';
scene.add(craft);
const tex=glowTexture();
engineSprite=new THREE.Sprite(new THREE.SpriteMaterial({map:tex,color:0xffa040,
blending:THREE.AdditiveBlending,depthWrite:false,transparent:true,opacity:0.95}));
engineSprite.position.set(0,0,-5.4); engineSprite.scale.set(6,6,1);
craft.add(engineSprite);
engineLight=new THREE.PointLight(0xff8a3c,1.6,70,2);
engineLight.position.set(0,0,-5); craft.add(engineLight);
sunSprite=new THREE.Sprite(new THREE.SpriteMaterial({map:tex,color:0xffe6bc,
blending:THREE.AdditiveBlending,depthWrite:false,transparent:true,opacity:0.95,fog:false}));
sunSprite.scale.set(340,340,1); sunSprite.renderOrder=-900;
scene.add(sunSprite);
/* particles */
ptGeo=new THREE.BufferGeometry();
ptGeo.setAttribute('position',new THREE.BufferAttribute(pa.pos,3));
ptGeo.setAttribute('aColor',new THREE.BufferAttribute(pa.col,3));
ptGeo.setAttribute('aSize',new THREE.BufferAttribute(pa.siz,1));
ptGeo.setAttribute('aAlpha',new THREE.BufferAttribute(pa.alp,1));
ptMat=new THREE.ShaderMaterial({
transparent:true, depthWrite:false, depthTest:true, blending:THREE.AdditiveBlending,
uniforms:{ uMap:{value:tex}, uScale:{value:600}, uFogNear:{value:140}, uFogFar:{value:820},
uFogCol:{value:new THREE.Color(0xff9a63)} },
vertexShader:`attribute float aSize; attribute vec3 aColor; attribute float aAlpha;
uniform float uScale,uFogNear,uFogFar; varying vec3 vC; varying float vA,vF;
void main(){ vC=aColor; vA=aAlpha;
vec4 mv=modelViewMatrix*vec4(position,1.0); float d=max(-mv.z,1.0);
vF=smoothstep(uFogNear,uFogFar,d);
gl_PointSize=clamp(aSize*uScale/d,1.0,140.0);
gl_Position=projectionMatrix*mv; }`,
fragmentShader:`uniform sampler2D uMap; uniform vec3 uFogCol; varying vec3 vC; varying float vA,vF;
void main(){ float m=texture2D(uMap,gl_PointCoord).a;
float a=vA*m*(1.0-vF); if(a<0.004) discard;
gl_FragColor=vec4(vC,a); }`
});
const pts=new THREE.Points(ptGeo,ptMat); pts.frustumCulled=false; scene.add(pts);
/* orbs */
const ON=320;
orbGeo=new THREE.BufferGeometry();
orbGeo.setAttribute('position',new THREE.BufferAttribute(new Float32Array(ON*3),3));
orbGeo.setAttribute('aColor',new THREE.BufferAttribute(new Float32Array(ON*3),3));
orbGeo.setAttribute('aSize',new THREE.BufferAttribute(new Float32Array(ON),1));
orbGeo.setAttribute('aAlpha',new THREE.BufferAttribute(new Float32Array(ON),1));
orbGeo.setDrawRange(0,0);
orbMat=ptMat.clone(); orbMat.uniforms=THREE.UniformsUtils.clone(ptMat.uniforms);
orbMat.uniforms.uMap.value=tex;
orbMat.uniforms.uScale.value=560;
const opts=new THREE.Points(orbGeo,orbMat); opts.frustumCulled=false; scene.add(opts);
initParticles();
}
/* ------------------------------ chunk management ------------------------- */
function makeMesh(pos,col,mat,cast,recv){
if(!pos.length) return null;
const g=new THREE.BufferGeometry();
g.setAttribute('position',new THREE.BufferAttribute(pos,3));
g.setAttribute('color',new THREE.BufferAttribute(col,3));
g.computeVertexNormals();
const m=new THREE.Mesh(g,mat);
m.castShadow=cast; m.receiveShadow=recv;
scene.add(m); chunkMeshes.push(m);
return m;
}
function spawnChunk(i){
if(chunks.has(i)) return;
const d=buildChunkData(i);
const meshes=[ makeMesh(d.wP,d.wC,matWorld,true,false),
makeMesh(d.fP,d.fC,matFloor,false,true),
makeMesh(d.gP,d.gC,matGlow,false,false) ].filter(Boolean);
chunks.set(i,{data:d,meshes});
}
function dropChunk(i){
const c=chunks.get(i); if(!c) return;
c.meshes.forEach(m=>{ scene.remove(m); const k=chunkMeshes.indexOf(m); if(k>=0) chunkMeshes.splice(k,1); m.geometry.dispose(); });
chunks.delete(i);
}
function resetChunks(){
[...chunks.keys()].forEach(dropChunk);
for(let i=0;i<=AHEAD;i++) spawnChunk(i);
}
function streamChunks(budget){
const ci=Math.floor(P.z/CH_LEN);
for(let i=Math.max(0,ci-BEHIND);i<=ci+AHEAD;i++){
if(!chunks.has(i)){ if(budget<=0) return; spawnChunk(i); budget--; }
}
for(const k of chunks.keys()) if(k<ci-BEHIND-1||k>ci+AHEAD+1) dropChunk(k);
}
/* --------------------------- post-processing ---------------------------- */
const VS_PASS=`varying vec2 vUv; void main(){ vUv=uv; gl_Position=vec4(position.xy,0.0,1.0); }`;
function initPost(){
matBright=new THREE.ShaderMaterial({uniforms:{tDiffuse:{value:null},uDir:{value:new THREE.Vector2(1,0)},
uTexel:{value:new THREE.Vector2()},uThresh:{value:0.74},uThrOn:{value:1}},
vertexShader:VS_PASS,
fragmentShader:`uniform sampler2D tDiffuse;uniform vec2 uDir,uTexel;uniform float uThresh,uThrOn;
varying vec2 vUv;
vec3 th(vec3 c){float l=max(max(c.r,c.g),c.b);float k=smoothstep(uThresh,uThresh+0.42,l);return mix(c,c*k,uThrOn);}
void main(){vec3 s=th(texture2D(tDiffuse,vUv).rgb);float w=1.0;
for(int i=1;i<=4;i++){float fi=float(i);vec2 o=uDir*uTexel*(fi*1.35);float d=1.0/(1.0+fi*0.85);
s+=th(texture2D(tDiffuse,vUv+o).rgb)*d; s+=th(texture2D(tDiffuse,vUv-o).rgb)*d; w+=2.0*d;}
gl_FragColor=vec4(s/w,1.0);}`});
matBlur=matBright.clone(); matBlur.uniforms=THREE.UniformsUtils.clone(matBright.uniforms);
matBlur.uniforms.uThrOn.value=0;
matGrade=new THREE.ShaderMaterial({uniforms:{
tScene:{value:null},tBloom:{value:null},tPrev:{value:null},
uBloom:{value:0.85},uFeedback:{value:0.0},uRadial:{value:0.0},uCA:{value:1.0},
uVig:{value:0.42},uGrain:{value:0.032},uTime:{value:0},uFlash:{value:0},uFlashCol:{value:new THREE.Color(0,0,0)}},
vertexShader:VS_PASS,
fragmentShader:`uniform sampler2D tScene,tBloom,tPrev;
uniform float uBloom,uFeedback,uRadial,uCA,uVig,uGrain,uTime,uFlash; uniform vec3 uFlashCol;
varying vec2 vUv;
void main(){ vec2 uv=vUv; vec2 d=uv-0.5; float r2=dot(d,d);
float ca=uCA*(0.0012+r2*0.0075);
vec3 col;
col.r=texture2D(tScene,uv-d*ca).r;
col.g=texture2D(tScene,uv).g;
col.b=texture2D(tScene,uv+d*ca).b;
float amt=uRadial*smoothstep(0.004,0.22,r2);
vec3 acc=col; float w=1.0;
for(int i=1;i<=5;i++){ float f=float(i)*0.22;
vec2 o=d*amt*f;
acc+=texture2D(tScene,uv-o).rgb; acc+=texture2D(tScene,uv+o*0.4).rgb; w+=2.0; }
col=acc/w;
vec3 pv=texture2D(tPrev,uv).rgb;
col=mix(col,pv,uFeedback);
col+=texture2D(tBloom,uv).rgb*uBloom;
col*=1.06;
col=pow(max(col,vec3(0.0)),vec3(0.90));
col=(col-0.5)*1.07+0.5;
float l=dot(col,vec3(0.299,0.587,0.114));
col=mix(vec3(l),col,1.16);
col*=1.0-uVig*smoothstep(0.05,0.44,r2);
col=mix(col,uFlashCol,uFlash);
float g=fract(sin(dot(uv+vec2(uTime*1.7,uTime*0.9),vec2(12.9898,78.233)))*43758.5453);
col+=(g-0.5)*uGrain;
gl_FragColor=vec4(clamp(col,0.0,1.0),1.0); }`});
matScreen=new THREE.ShaderMaterial({uniforms:{tDiffuse:{value:null}},vertexShader:VS_PASS,
fragmentShader:`uniform sampler2D tDiffuse; varying vec2 vUv;
void main(){ gl_FragColor=vec4(texture2D(tDiffuse,vUv).rgb,1.0); }`});
fsScene=new THREE.Scene();
fsCam=new THREE.OrthographicCamera(-1,1,1,-1,0,1);
fsQuad=new THREE.Mesh(new THREE.PlaneGeometry(2,2),matScreen);
fsQuad.frustumCulled=false; fsScene.add(fsQuad);
}
function makeRT(w,h,depth){
return new THREE.WebGLRenderTarget(Math.max(2,w),Math.max(2,h),{
minFilter:THREE.LinearFilter,magFilter:THREE.LinearFilter,format:THREE.RGBAFormat,
type:THREE.UnsignedByteType,depthBuffer:!!depth,stencilBuffer:false,generateMipmaps:false});
}
function resize(){
const w=innerWidth,h=innerHeight;
const basePr=Math.min(window.devicePixelRatio||1,1.5);
const pr=Math.max(0.55,basePr*QSCALE);
renderer.setPixelRatio(pr); renderer.setSize(w,h,false);
camera.aspect=w/h; camera.updateProjectionMatrix();
const bw=Math.max(2,Math.floor(w*pr)), bh=Math.max(2,Math.floor(h*pr));
const SS=basePr<1.5?1.3:1.0;
let sw=Math.min(Math.floor(bw*SS),2200);
let sh=Math.max(2,Math.round(sw*h/w));
sceneW=sw; sceneH=sh;
bloomW=Math.max(2,Math.ceil(sw/4)); bloomH=Math.max(2,Math.ceil(sh/4));
[rtScene,rtPrev,rtCur].forEach(rt=>rt&&rt.setSize(bw,bh));
rtScene.setSize(sw,sh);
[rtB1,rtB2].forEach(rt=>rt&&rt.setSize(bloomW,bloomH));
}
function initRTs(){
const w=2,h=2;
rtScene=makeRT(w,h,true); rtPrev=makeRT(w,h,false); rtCur=makeRT(w,h,false);
rtB1=makeRT(w,h,false); rtB2=makeRT(w,h,false);
}
function pass(mat,target){
fsQuad.material=mat;
renderer.setRenderTarget(target||null);
renderer.render(fsScene,fsCam);
}
function renderFrame(){
/* 1. scene */
renderer.setRenderTarget(rtScene);
renderer.render(scene,camera);
/* 2. bloom (downsample + threshold + 4 blur passes) */
matBright.uniforms.tDiffuse.value=rtScene.texture;
matBright.uniforms.uDir.value.set(1,0);
matBright.uniforms.uTexel.value.set(1/bloomW,1/bloomH);
pass(matBright,rtB1);
matBlur.uniforms.uDir.value.set(0,1); matBlur.uniforms.uTexel.value.set(1/bloomW,1/bloomH);
matBlur.uniforms.tDiffuse.value=rtB1.texture; pass(matBlur,rtB2);
matBlur.uniforms.tDiffuse.value=rtB2.texture; matBlur.uniforms.uDir.value.set(1,0); pass(matBlur,rtB1);
matBlur.uniforms.tDiffuse.value=rtB1.texture; matBlur.uniforms.uDir.value.set(0,1); pass(matBlur,rtB2);
/* 3. grade + motion blur feedback */
const g=matGrade.uniforms;
g.tScene.value=rtScene.texture; g.tBloom.value=rtB2.texture; g.tPrev.value=rtPrev.texture;
g.uTime.value=S.time;
g.uFeedback.value = S.fbHold>0 ? 0 : clamp(0.10+(S.speed/Math.max(60,S.vMax))*0.17+(S.boosting?0.11:0),0,0.36);
g.uRadial.value = clamp((S.speed/Math.max(60,S.vMax))*0.030+(S.boosting?0.035:0),0,0.075);
g.uCA.value = 1.0+(S.boosting?3.2:0)+ (S.speed/Math.max(60,S.vMax))*0.8;
g.uFlash.value = S.flash;
g.uFlashCol.value.setRGB(S.flashCol[0],S.flashCol[1],S.flashCol[2]);
pass(matGrade,rtCur);
/* 4. to screen */
matScreen.uniforms.tDiffuse.value=rtCur.texture;
pass(matScreen,null);
/* swap feedback buffers */
const t=rtPrev; rtPrev=rtCur; rtCur=t;
}
</script>
<script>
"use strict";
/* ============================== GAME LOGIC =============================== */
let camPos=null, camLook=null, _v=null, _dir=null, _c1=null, _c2=null;
const PAL={};
/* ------------------------------- helpers -------------------------------- */
function project(x,y,z){
if(!_v||!camera) return null;
_v.set(x,y,z); _v.project(camera);
if(_v.z>1) return null;
return { x:(_v.x*0.5+0.5)*innerWidth, y:(-_v.y*0.5+0.5)*innerHeight };
}
function autoPilot(){
const look=P.z+80+S.speed*0.30;
const tx=corrX(look), ty=corrY(look);
return [ clamp((tx-P.x)*0.024-P.vx*0.050,-1,1),
clamp((ty-P.y)*0.032-P.vy*0.050,-1,1) ];
}
function pushOut(o){
const cx=clamp(P.x,o.x0,o.x1), cy=clamp(P.y,o.y0,o.y1), cz=clamp(P.z,o.z0,o.z1);
let nx=P.x-cx, ny=P.y-cy, nz=P.z-cz;
const l=Math.sqrt(nx*nx+ny*ny+nz*nz);
if(l>1e-4){ P.x=cx+nx/l*P_R; P.y=cy+ny/l*P_R; P.z=cz+nz/l*P_R; return; }
const p1=P.x-o.x0, p2=o.x1-P.x, q1=P.y-o.y0, q2=o.y1-P.y;
const m=Math.min(p1,p2,q1,q2);
if(m===p1) P.x=o.x0-P_R; else if(m===p2) P.x=o.x1+P_R;
else if(m===q1) P.y=o.y0-P_R; else P.y=o.y1+P_R;
}
/* --------------------------------- hit ---------------------------------- */
function hit(amt,x,y,z,r,g,b){
spawnSparks(x,y,z,26,r,g,b,34);
S.shake=Math.max(S.shake,1);
S.invuln=0.75;
if(S.mode!=='playing'){ S.shake=Math.max(S.shake,0.45); return; }
S.speed*=0.55; S.mult=1; S.nmTimer=3; S.energy-=amt; S.flashHit=0.65;
AU.thud(); HUD.say('IMPACT','warn',650);
if(S.energy<=0){ S.energy=0; gameOver(); }
}
function nearMiss(label,gap){
S.mult=Math.min(9,S.mult+0.30);
S.nmTimer=0; S.nmCount++;
const pts=Math.round((130+S.speed*0.55)*S.mult);
S.score+=pts;
S.energy=Math.min(100,S.energy+1.6);
S.turbo=Math.min(100,S.turbo+7);
S.shake=Math.min(1,S.shake+0.30);
HUD.pulseMult();
const sp=project(P.x,P.y,P.z);
if(sp) HUD.pop('+'+pts, sp.x+(Math.random()*70-35), sp.y);
if(gap<2.4&&sp) HUD.pop(label, sp.x, sp.y-36, 'nm');
AU.whoosh(1+clamp(0.6-gap*0.10,0,0.6));
}
/* ------------------------------ collision -------------------------------- */
function collide(){
if(S.invuln>0) return;
const t=terrainAt(P.z), rel=P.x-t.x;
if(Math.abs(rel)+P_R>t.hw){
const s=rel>=0?1:-1;
P.x=t.x+s*(t.hw-P_R); P.vx=-P.vx*0.2;
hit(21,P.x,P.y,P.z,1,0.25,0.10); return;
}
if(P.y<P_R+0.2){
P.y=P_R+0.2; P.vy=Math.abs(P.vy)*0.4; S.speed*=0.985;
hit(9,P.x,1.2,P.z,1,0.6,0.2); return;
}
const cy=ceilY(P.z);
if(P.y>cy){ P.y=cy; P.vy=Math.min(P.vy,-2); S.turb=0.35; }
const ci=Math.floor(P.z/CH_LEN);
for(let i=ci-1;i<=ci+1;i++){
const c=chunks.get(i); if(!c) continue;
for(const o of c.data.obstacles){
if(o.z1<P.z-8||o.z0>P.z+8) continue;
if(boxDist(P.x,P.y,P.z,o)<P_R){ pushOut(o); hit(21,P.x,P.y,P.z,1,0.4,0.15); return; }
}
}
}
function wallScan(){
const t=terrainAt(P.z), rel=P.x-t.x;
const gap=t.hw-Math.abs(rel)-P_R;
const side=rel<0?0:1;
if(gap<NEAR_WALL){ if(S.armed[side]){ S.armed[side]=false; nearMiss(gap<3.0?'SKIM':'NEAR MISS',gap); } }
else if(gap>NEAR_WALL*1.7) S.armed[side]=true;
}
function obsScan(){
const ci=Math.floor(P.z/CH_LEN);
for(let i=ci-1;i<=ci+1;i++){
const c=chunks.get(i); if(!c) continue;
for(const o of c.data.obstacles){
if(o.nm||o.z1<P.z-8||o.z0>P.z+10) continue;
const d=boxDist(P.x,P.y,P.z,o);
if(d>0&&d<NEAR_OBS){ o.nm=true; nearMiss('NEAR MISS',d); }
}
}
}
function orbsUpdate(playing){
const ci=Math.floor(P.z/CH_LEN);
for(let i=ci-1;i<=ci+2;i++){
const c=chunks.get(i); if(!c) continue;
for(const o of c.data.orbs){
if(!playing||o.taken||o.z<P.z-25) continue;
const dx=o.x-P.x, dy=o.y-P.y, dz=o.z-P.z;
if(dx*dx+dy*dy+dz*dz>=ORB_R*ORB_R) continue;
o.taken=true;
const turboKind=o.kind==='turbo';
if(turboKind){ S.turbo=Math.min(100,S.turbo+34); S.energy=Math.min(100,S.energy+7);
if(playing) HUD.say('TURBO CHARGE','good',800); }
else { S.energy=Math.min(100,S.energy+8); S.turbo=Math.min(100,S.turbo+5); }
S.score+=Math.round(70*S.mult);
S.mult=Math.min(9,S.mult+0.10);
spawnSparks(o.x,o.y,o.z,16,turboKind?0.4:1.0,turboKind?0.95:0.7,turboKind?1.0:0.35,30);
AU.ping(o.kind); HUD.pulseMult();
const sp=project(o.x,o.y,o.z);
if(sp) HUD.pop('+'+Math.round(70*S.mult),sp.x,sp.y);
}
}
}
function updateOrbPoints(){
const at=orbGeo.attributes, pos=at.position.array, col=at.aColor.array,
siz=at.aSize.array, alp=at.aAlpha.array, N=siz.length;
let n=0;
const ci=Math.floor(P.z/CH_LEN);
for(let i=ci-1;i<=ci+AHEAD;i++){
const c=chunks.get(i); if(!c) continue;
for(const o of c.data.orbs){
if(o.taken||o.z<P.z-30||o.z>P.z+680||n>=N) continue;
pos[n*3]=o.x; pos[n*3+1]=o.y; pos[n*3+2]=o.z;
const C=o.kind==='turbo'?COL.orbTurbo:COL.orb;
col[n*3]=C[0]; col[n*3+1]=C[1]; col[n*3+2]=C[2];
siz[n]=4.4+Math.sin(S.time*4.2+o.ph)*0.7;
alp[n]=0.95;
n++;
}
}
orbGeo.setDrawRange(0,n);
at.position.needsUpdate=true; at.aColor.needsUpdate=true;
at.aSize.needsUpdate=true; at.aAlpha.needsUpdate=true;
}
/* -------------------------------- flight --------------------------------- */
function normalFlight(dt){
readInput();
let inB=IN.bank, inP=IN.pitch;
if(S.auto){ const a=autoPilot(); inB=a[0]; inP=a[1]; }
const playing=S.mode==='playing';
S.bankIn=sm(S.bankIn,inB,playing?8:5,dt);
S.pitchIn=sm(S.pitchIn,inP,playing?7:4.5,dt);
S.vMax=96+54*dOf(P.z);
const wantBoost=playing&&!S.auto&&IN.boost&&S.turbo>0.5;
if(wantBoost&&!S.boosting){ AU.sweep(true); S.shake=Math.min(1,S.shake+0.35); HUD.say('TURBO','good',900); }
if(S.boosting&&!wantBoost) AU.sweep(false);
S.boosting=wantBoost;
if(S.boosting) S.turbo-=34*dt;
else S.turbo+=(S.speed>S.vMax*0.93?3.6:-1.7)*dt;
S.turbo=clamp(S.turbo,0,100);
let target=S.vMax;
target*=1+0.22*clamp(-S.pitchIn,0,1);
target*=1-0.32*clamp(S.pitchIn,0,1);
if(S.boosting) target*=1.78;
S.speed=sm(S.speed,target,S.speed<target?1.25:2.4,dt);
S.maxSpeed=Math.max(S.maxSpeed,S.speed);
P.vx=sm(P.vx,S.bankIn*S.speed*0.62,2.7,dt);
P.x+=P.vx*dt;
P.vy=sm(P.vy,S.pitchIn*S.speed*0.50-7.0,3.2,dt);
P.y+=P.vy*dt;
P.z+=S.speed*dt;
S.yaw=sm(S.yaw,Math.atan2(P.vx,Math.max(35,S.speed*0.8)),3.6,dt);
S.bankVis=sm(S.bankVis,clamp(S.bankIn*0.95+P.vx/Math.max(45,S.speed)*0.5,-1.2,1.2),6,dt);
S.pitchVis=sm(S.pitchVis,-S.pitchIn*0.5-clamp(P.vy/Math.max(45,S.speed),-0.5,0.5)*0.35,7,dt);
}
function overUpdate(dt){
S.speed=Math.max(12,S.speed-dt*52);
P.vx=sm(P.vx,0,1.2,dt);
P.x+=P.vx*dt; P.z+=S.speed*dt;
P.vy-=26*dt; P.y+=P.vy*dt;
if(P.y<2){ P.y=2; P.vy=Math.abs(P.vy)*0.3; }
S.yaw+=dt*0.7; S.bankVis+=dt*2.6; S.pitchVis=Math.sin(S.time*3.4)*0.4;
}
function scoring(dt){
S.energy-=(4.0+6.5*clamp(S.speed/S.vMax,0,1.5))*dt;
if(S.turb>0){ S.energy-=7*dt; S.shake=Math.max(S.shake,0.5); }
S.score+=S.speed*dt*0.11*S.mult;
S.nmTimer+=dt;
if(S.nmTimer>2.6) S.mult=Math.max(1,S.mult-0.6*dt);
if(S.energy<=0){ S.energy=0; gameOver(); }
}
function timers(dt){
S.shake=Math.max(0,S.shake-dt*2.0);
S.flashHit=Math.max(0,S.flashHit-dt*2.2);
S.invuln-=dt;
S.turb=Math.max(0,S.turb-dt);
S.flash=Math.max(0,S.flash-dt*2.6);
S.fbHold-=dt;
}
function trail(dt){
const sn=clamp(S.speed/Math.max(60,S.vMax),0,1.5);
const sy=Math.sin(S.yaw), cz=Math.cos(S.yaw);
const ex=P.x-sy*5.2, ey=P.y-0.1, ez=P.z-cz*5.2;
const n=Math.floor(1+sn*4+(S.boosting?5:0));
for(let i=0;i<n;i++){
const j=0.35+Math.random()*0.65, spd=S.speed*0.10*j;
if(S.boosting) spawnPart(ex,ey,ez,
-sy*spd*0.6+(Math.random()-0.5)*10,(Math.random()-0.5)*9,-cz*spd*0.6-S.speed*0.05+(Math.random()-0.5)*10,
0.28+Math.random()*0.4,0.95+Math.random()*1.1,0.45,0.9,1.0,1.0,1.0);
else spawnPart(ex,ey,ez,
-sy*spd*0.5+(Math.random()-0.5)*7,(Math.random()-0.5)*6,-cz*spd*0.5-S.speed*0.04+(Math.random()-0.5)*7,
0.30+Math.random()*0.45,0.7+Math.random()*0.9,1.0,0.55,0.22,1.0,1.0);
}
}
/* ------------------------------- atmosphere ------------------------------ */
function initPalette(){
PAL.topA=new THREE.Color(0x301d5c); PAL.topB=new THREE.Color(0x09061e);
PAL.midA=new THREE.Color(0x8d3f7d); PAL.midB=new THREE.Color(0x2a1450);
PAL.botA=new THREE.Color(0xff8f4c); PAL.botB=new THREE.Color(0x99334e);
PAL.glowA=new THREE.Color(0xffdca6); PAL.glowB=new THREE.Color(0xff9d8e);
PAL.fogA=new THREE.Color(0xff9a63); PAL.fogB=new THREE.Color(0x7a2f55);
PAL.sunA=new THREE.Color(0xffc490); PAL.sunB=new THREE.Color(0x8fa0e6);
}
function applyAtmosphere(d){
skyMat.uniforms.uTop.value.copy(PAL.topA).lerp(PAL.topB,d);
skyMat.uniforms.uMid.value.copy(PAL.midA).lerp(PAL.midB,d);
skyMat.uniforms.uBot.value.copy(PAL.botA).lerp(PAL.botB,d);
skyMat.uniforms.uGlow.value.copy(PAL.glowA).lerp(PAL.glowB,d);
scene.fog.color.copy(PAL.fogA).lerp(PAL.fogB,d);
scene.fog.near=lerp(155,112,d); scene.fog.far=lerp(880,600,d);
ptMat.uniforms.uFogCol.value.copy(scene.fog.color);
orbMat.uniforms.uFogCol.value.copy(scene.fog.color);
ptMat.uniforms.uFogNear.value=scene.fog.near; ptMat.uniforms.uFogFar.value=scene.fog.far;
orbMat.uniforms.uFogNear.value=scene.fog.near; orbMat.uniforms.uFogFar.value=scene.fog.far;
sunLight.color.copy(PAL.sunA).lerp(PAL.sunB,d);
sunLight.intensity=lerp(1.42,0.86,d);
}
/* --------------------------------- camera -------------------------------- */
function updateCamera(dt,snap){
const sn=clamp(S.speed/Math.max(60,S.vMax),0,1.5);
const back=15.5+sn*4.5+(S.boosting?3.5:0);
const sy=Math.sin(S.yaw), cz=Math.cos(S.yaw);
const tz=P.z-cz*back;
const t=terrainAt(tz);
let tx=P.x-sy*back;
tx=clamp(tx,t.x-t.hw+4.5,t.x+t.hw+4.5);
const ty=clamp(P.y+4.6-S.pitchVis*2.2,1.4,ceilY(tz)+2.5);
if(snap) camPos.set(tx,ty,tz);
else camPos.set(sm(camPos.x,tx,9,dt),sm(camPos.y,ty,9,dt),sm(camPos.z,tz,11,dt));
camLook.set(sm(camLook.x,P.x+sy*20,7,dt),
sm(camLook.y,P.y+3.6-S.pitchVis*7,7,dt),
sm(camLook.z,P.z+cz*20,9,dt));
camera.position.copy(camPos);
camera.lookAt(camLook);
camera.rotateZ(-S.bankVis*0.20);
if(S.shake>0.001){
const a=S.shake*S.shake*1.7+0.015*S.shake;
camera.position.x+=(Math.random()-0.5)*a*1.7;
camera.position.y+=(Math.random()-0.5)*a*1.5;
camera.position.z+=(Math.random()-0.5)*a*1.3;
camera.rotateZ((Math.random()-0.5)*a*0.11);
camera.rotateX((Math.random()-0.5)*a*0.07);
}
const fov=66+sn*10+(S.boosting?17:0);
const nf=sm(camera.fov,fov,4.5,dt);
if(Math.abs(nf-camera.fov)>0.005){ camera.fov=nf; camera.updateProjectionMatrix(); }
const sc=sceneH*0.5/Math.tan(camera.fov*Math.PI/360);
ptMat.uniforms.uScale.value=sc;
orbMat.uniforms.uScale.value=sc*1.05;
sky.position.copy(camera.position);
ground.position.set(P.x,-1.4,P.z);
sunLight.position.set(P.x+_dir.x*300,P.y+_dir.y*300+45,P.z+_dir.z*300);
sunLight.target.position.set(P.x,P.y,P.z);
sunLight.target.updateMatrixWorld();
sunSprite.position.set(camera.position.x+_dir.x*2600,
camera.position.y+_dir.y*2600,
camera.position.z+_dir.z*2600);
}
function craftUpdate(dt){
const sn=clamp(S.speed/Math.max(60,S.vMax),0,1.5);
craft.position.set(P.x,P.y,P.z);
craft.rotation.set(S.pitchVis,S.yaw,-S.bankVis);
const es=2.6+sn*2.4+(S.boosting?4.5:0)+Math.sin(S.time*26)*0.25;
engineSprite.scale.set(es,es,1);
engineSprite.material.opacity=S.mode==='over'?0.3:0.72;
engineLight.intensity=S.mode==='over'?0.3:1.1+sn*1.7+(S.boosting?3.2:0);
}
/* ------------------------------- run control ----------------------------- */
function resetRun(newSeed){
SEED=(newSeed>>>0)||1;
clearParticles();
resetChunks();
P.x=corrX(0); P.y=corrY(0); P.z=0; P.vx=0; P.vy=0;
S.score=0; S.mult=1; S.nmTimer=0; S.nmCount=0;
S.energy=100; S.turbo=0; S.boosting=false;
S.speed=46; S.maxSpeed=0; S.vMax=96;
S.bankIn=0; S.pitchIn=0; S.bankVis=0; S.pitchVis=0; S.yaw=0;
S.shake=0; S.flashHit=0; S.invuln=0; S.turb=0;
S.armed=[true,true]; S.fbHold=0.4; S.overT=0;
S.flash=1; S.flashCol=[0,0,0];
craft.position.set(P.x,P.y,P.z);
camPos.set(corrX(-30),corrY(-30)+4.6,-30);
camLook.set(P.x,P.y+2,P.z+40);
updateCamera(0.016,true);
applyAtmosphere(0);
}
function startGame(){
AU.init();
resetRun((Math.random()*1e9)>>>0);
S.mode='playing'; S.auto=false;
$('ov').classList.remove('show');
$('ov').dataset.mode='menu';
$('hud').classList.add('on');
S.flash=1; S.flashCol=[1,1,1];
HUD.say('RUN LIVE','',1100);
const h=$('hint');
h.textContent='ORBS REFUEL · CLOSE PASSES MULTIPLY';
h.classList.add('on');
clearTimeout(startGame._t);
startGame._t=setTimeout(()=>h.classList.remove('on'),4600);
}
function gameOver(){
if(S.mode!=='playing') return;
S.mode='over'; S.overT=0; S.auto=false; S.energy=0;
S.flash=1; S.flashCol=[0.6,0.07,0.13]; S.shake=1.2;
spawnSparks(P.x,P.y,P.z,90,1.0,0.55,0.25,62);
spawnSparks(P.x,P.y,P.z,55,0.5,0.8,1.0,46);
AU.sweep(false); AU.thud();
S.best=Math.max(S.best,Math.round(S.score));
try{ localStorage.setItem('heliosbest_v2',String(S.best)); }catch(e){}
$('finalScore').textContent=fmt(S.score);
$('finalSpeed').textContent=Math.round(S.maxSpeed*3)+' km/h';
$('finalDist').textContent=(P.z>=1000?(P.z/1000).toFixed(2)+' km':Math.round(P.z)+' m');
$('finalNM').textContent=S.nmCount;
$('overBest').textContent=fmt(S.best);
$('menuBest').textContent=fmt(S.best);
setTimeout(()=>{ if(S.mode==='over'){ $('ov').dataset.mode='over'; $('ov').classList.add('show'); } },950);
}
function toMenu(){
S.mode='menu'; S.auto=true;
$('ov').classList.remove('show');
resetRun((Math.random()*1e9)>>>0);
}
function togglePause(){
if(S.mode==='playing'){
S.mode='pause'; $('ov').dataset.mode='pause'; $('ov').classList.add('show');
AU.update(0,0,false);
} else if(S.mode==='pause'){
S.mode='playing'; $('ov').classList.remove('show'); lastT=performance.now();
}
}
/* --------------------------------- update -------------------------------- */
function update(dt){
S.time+=dt;
if(S.mode==='pause'){ updateCamera(dt,false); craftUpdate(dt); return; }
if(S.mode==='over') overUpdate(dt); else normalFlight(dt);
streamChunks(3);
collide();
if(S.mode==='playing'){ wallScan(); obsScan(); scoring(dt); }
orbsUpdate(S.mode==='playing');
updateParticles(dt);
trail(dt);
updateOrbPoints();
timers(dt);
applyAtmosphere(dOf(P.z));
craftUpdate(dt);
updateCamera(dt,false);
}
/* ---------------------------------- loop --------------------------------- */
let lastT=0, booted=false;
let _ftAcc=0, _ftN=0;
function loop(now){
requestAnimationFrame(loop);
let dt=(now-lastT)/1000; lastT=now;
if(!(dt>0)) dt=0.016;
/* adaptive resolution: keep the frame budget on slower GPUs */
_ftAcc+=dt; _ftN++;
if(_ftN>=50){
const avg=_ftAcc/_ftN; _ftAcc=0; _ftN=0;
let nq=QSCALE;
if(avg>0.030) nq=Math.max(0.55,QSCALE-0.18);
else if(avg<0.0155&&QSCALE<1) nq=Math.min(1,QSCALE+0.10);
if(Math.abs(nq-QSCALE)>0.01){ QSCALE=nq; resize(); }
}
dt=Math.min(dt,0.05);
update(dt);
HUD.update();
AU.update(clamp(S.speed/Math.max(60,S.vMax),0,1.3),S.boosting,S.mode!=='pause');
renderFrame();
}
/* ---------------------------------- boot --------------------------------- */
function boot(){
if(booted) return; booted=true;
canvas=$('view');
renderer=new THREE.WebGLRenderer({canvas:canvas,antialias:false,alpha:false,
powerPreference:'high-performance'});
renderer.setClearColor(0x07040f,1);
renderer.shadowMap.enabled=true;
renderer.shadowMap.type=THREE.PCFSoftShadowMap;
renderer.autoClear=true;
initScene();
initPost();
initRTs();
initPalette();
camPos=new THREE.Vector3(); camLook=new THREE.Vector3();
_v=new THREE.Vector3(); _dir=new THREE.Vector3(-0.16,0.28,0.95).normalize();
_c1=new THREE.Color(); _c2=new THREE.Color();
HUD.init(); initInput();
try{ S.best=parseInt(localStorage.getItem('heliosbest_v2')||'0',10)||0; }catch(e){ S.best=0; }
$('menuBest').textContent=fmt(S.best);
$('best').textContent=fmt(S.best);
resize();
window.addEventListener('resize',resize);
resetRun((Math.random()*1e9)>>>0);
S.mode='menu'; S.auto=true;
$('startBtn').addEventListener('click',startGame);
$('retryBtn').addEventListener('click',startGame);
$('resumeBtn').addEventListener('click',togglePause);
$('quitBtn').addEventListener('click',toMenu);
$('snd').addEventListener('click',()=>{ AU.init(); const m=!AU.muted; AU.setMuted(m);
$('snd').classList.toggle('off',m); });
document.addEventListener('visibilitychange',()=>{
if(document.hidden&&S.mode==='playing') togglePause();
});
$('hud').classList.add('on');
lastT=performance.now();
requestAnimationFrame(loop);
}
/* test hooks (pure logic, no WebGL required) */
window.__T={ buildChunkData, terrainAt, corrX, corrY, ceilY, cX, halfW, wallH, boxDist,
boxInside, NEAR_WALL, NEAR_OBS, CH_LEN, S, P, chunks,
chunkCount:()=>chunks.size, meshCount:()=>chunkMeshes.length,
setSeed:(s)=>{ SEED=s>>>0||1; } };
loadThree(0).then(boot).catch(()=>{ $('fatal').classList.add('on'); });
</script>
</body>
</html>
<!-- agent-meta {"model":"xiaomi/mimo-v2.6-flash","provider":"openrouter","persona":"full","sessionId":"e9fec70b-3bf5-402b-bcd6-8ee2a9b1b1d1","tokensIn":9381682,"tokensOut":153089,"tokensTotal":9534771,"cost":0.15562258880000002,"turns":41,"toolCalls":65,"failedToolCalls":3,"timestamp":"2026-09-27T16:35:52.549Z"} -->