Glider / index.html
ZENLLC's picture
Update index.html
dfd73c1 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>ZEN SkyLines — Jetstream Rally</title>
<style>
:root{
--hud:#eaf6ff;--dim:#9fb6c9;--glass:rgba(255,255,255,.06);--glass2:rgba(255,255,255,.12);
--ok:#22d39a;--warn:#ffb020;--danger:#ff5577;--acc:#00e5ff;
}
html,body{margin:0;height:100%;background:#05070b;color:var(--hud);font-family:Inter,system-ui,Segoe UI,Roboto,Arial,sans-serif;overflow:hidden}
#hud{position:fixed;inset:0;pointer-events:none}
.pill{position:absolute;background:var(--glass);backdrop-filter:blur(8px);border:1px solid rgba(255,255,255,.08);border-radius:999px;padding:8px 12px;font-size:12px;letter-spacing:.3px}
#dist{top:12px;left:12px}
#score{top:12px;right:12px}
#seed{top:44px;left:12px;opacity:.9}
#gauges{position:absolute;bottom:16px;left:50%;transform:translateX(-50%);width:min(560px,92vw);display:grid;gap:10px}
.bar{height:14px;border-radius:999px;background:rgba(255,255,255,.08);overflow:hidden;border:1px solid rgba(255,255,255,.1)}
.fill{height:100%;width:0%}
.fill.power{background:linear-gradient(90deg,#ffe08a,#ff9d00)}
.fill.stam{background:linear-gradient(90deg,#8bffe1,#00e5ff)}
#hint{position:absolute;bottom:64px;right:12px;opacity:.88;font-size:12px;background:var(--glass);border:1px solid rgba(255,255,255,.08);padding:8px 10px;border-radius:10px}
#center{position:absolute;inset:0;display:none;place-items:center;text-align:center;pointer-events:none;font-weight:600;letter-spacing:.3px;text-shadow:0 6px 28px rgba(0,0,0,.55)}
#center .card{background:var(--glass2);border:1px solid rgba(255,255,255,.12);border-radius:16px;padding:18px 20px;display:inline-grid;gap:8px;min-width:280px;backdrop-filter:blur(10px)}
#btnRestart,#btnScreenshot{pointer-events:auto;cursor:pointer;margin-top:8px;border-radius:10px;border:0;padding:10px 14px;background:#0ea5e9;color:#fff;font-weight:700;box-shadow:0 8px 18px rgba(14,165,233,.35)}
#btnScreenshot{background:#22d39a;margin-left:8px}
#touchL,#touchR{position:fixed;top:0;bottom:0;width:50vw;pointer-events:auto}
#touchL{left:0}#touchR{right:0}
@media (pointer:fine){#touchL,#touchR{display:none}}
.badge{display:inline-block;border:1px solid rgba(255,255,255,.12);border-radius:999px;padding:4px 8px;font-size:11px;background:var(--glass);margin-top:4px}
.kicker{font-size:11px;color:var(--dim)}
</style>
</head>
<body>
<div id="hud">
<div id="dist" class="pill">Distance: 0 m</div>
<div id="score" class="pill">Score: 0 | Combo ×1 | Best 0</div>
<div id="seed" class="pill">Seed: <span id="seedVal"></span> <span class="kicker">[R to reroll]</span></div>
<div id="gauges">
<div class="bar"><div id="pBar" class="fill power"></div></div>
<div class="bar"><div id="sBar" class="fill stam"></div></div>
</div>
<div id="hint">
Space: charge → release to throw • ←/→ bank • ↑/↓ pitch • <b>Shift</b> Burst • <b>T</b> time bubble view • <b>P</b> photo • <b>ESC</b> restart
</div>
<div id="center">
<div class="card">
<div id="headline">Run Ended</div>
<div id="details" style="font-size:12px;color:var(--dim)"></div>
<div>
<button id="btnRestart">Restart</button>
<button id="btnScreenshot">Photo</button>
</div>
<div id="badges" style="margin-top:6px"></div>
</div>
</div>
<div id="touchL"></div><div id="touchR"></div>
</div>
<script src="https://cdn.jsdelivr.net/npm/three@0.152.2/build/three.min.js"></script>
<script>
/* ZEN SkyLines — Jetstream Rally (Static, no audio)
Mechanics: flow-field wind, moving storm walls (graze bonus), slow-time bubbles, ring chains, stamina burst, shareable seed, local best.
*/
// ---------- RNG with shareable seed ----------
let seed = (new URLSearchParams(location.search).get('seed')) || Math.floor(Math.random()*1e9).toString(36);
document.getElementById('seedVal').textContent = seed;
function sfc32FromSeed(s){
// hash seed to 4 ints
function xmur3(str){for(var i=1779033703^str.length,j=0;j<str.length;j++)i=Math.imul(i^str.charCodeAt(j),3432918353),i=i<<13|i>>>19;return function(){i=Math.imul(i^i>>>16,2246822507);i=Math.imul(i^i>>>13,3266489909);return (i^i>>>16)>>>0}}
const x = xmur3(s); let a=x(), b=x(), c=x(), d=x();
return function(){a|=0; b|=0; c|=0; d|=0; var t=(a+b|0)+d|0; d=d+1|0; a=b^b>>>9; b=c+(c<<3)|0; c=(c<<21|c>>>11); c=c+t|0; return (t>>>0)/4294967296;}
}
const rand = sfc32FromSeed(seed);
const R = (min,max)=>min+(max-min)*rand();
// ---------- Scene ----------
const scene = new THREE.Scene();
// gradient sky
const skyCanvas=document.createElement('canvas'); skyCanvas.width=1; skyCanvas.height=256;
const skyCtx=skyCanvas.getContext('2d'); const grad=skyCtx.createLinearGradient(0,256,0,0);
grad.addColorStop(0,'#0b1220'); grad.addColorStop(.5,'#0e1c38'); grad.addColorStop(1,'#091019');
skyCtx.fillStyle=grad; skyCtx.fillRect(0,0,1,256);
const skyTex=new THREE.CanvasTexture(skyCanvas); skyTex.center.set(.5,.5); scene.background=skyTex;
// stars
{
const N=1100, pos=new Float32Array(N*3);
for(let i=0;i<N;i++){ const r=700, th=rand()*Math.PI*2, ph=rand()*Math.PI*.55;
pos[3*i]=r*Math.sin(ph)*Math.cos(th);
pos[3*i+1]=r*Math.cos(ph)+100;
pos[3*i+2]=r*Math.sin(ph)*Math.sin(th);
}
const g=new THREE.BufferGeometry(); g.setAttribute('position',new THREE.BufferAttribute(pos,3));
scene.add(new THREE.Points(g,new THREE.PointsMaterial({size:1,sizeAttenuation:false, color:0xffffff,opacity:.85,transparent:true})));
}
const camera = new THREE.PerspectiveCamera(75, innerWidth/innerHeight, 0.1, 3000);
const renderer = new THREE.WebGLRenderer({antialias:true});
renderer.setSize(innerWidth, innerHeight); renderer.shadowMap.enabled=true;
document.body.appendChild(renderer.domElement);
const hemi=new THREE.HemisphereLight(0x6ba7ff,0x0a0a0a,.7); scene.add(hemi);
const sun=new THREE.DirectionalLight(0xfff2cc,1.1); sun.position.set(-120,180,-60); sun.castShadow=true; sun.shadow.mapSize.set(1024,1024); scene.add(sun);
// ---------- Minimal ground grid for horizon ----------
const groundGeom=new THREE.PlaneGeometry(4000,4000,40,40);
const groundMat=new THREE.MeshBasicMaterial({color:0x0d121e, wireframe:true, wireframeLinewidth:1, opacity:.25, transparent:true});
const ground=new THREE.Mesh(groundGeom,groundMat); ground.rotation.x=-Math.PI/2; ground.position.y=-6; scene.add(ground);
// ---------- Flow-field (jetstreams) ----------
function flowVector(x,y,z,time){
// Smooth layered curls for wind; returns {x,y,z}
const s = 0.015, t = time*0.25;
const nx = Math.sin(s*z + 1.3*Math.cos(s*y+t)) + 0.6*Math.sin(s*0.7*z+2.1);
const ny = 0.5*Math.sin(s*x + 0.8*Math.cos(s*z+t*0.7));
const nz = Math.sin(s*x + 1.1*Math.cos(s*y+t*1.1)) + 0.5*Math.sin(s*0.6*x+1.7);
return {x:nx, y:ny, z:1.8 + nz}; // bias forward (z)
}
// visual ribbons to show flow lanes
const ribbons=[];
function makeRibbon(pathZ){
const geo=new THREE.CylinderGeometry(0.15,0.15,6,6,1,true);
const mat=new THREE.MeshBasicMaterial({color:0x00e5ff, transparent:true, opacity:.3, side:THREE.DoubleSide});
const cyl=new THREE.Mesh(geo,mat);
cyl.position.set(R(-8,8), R(-1,3), pathZ);
scene.add(cyl); ribbons.push(cyl);
}
// seed a field of flow hints
for(let z=20; z<=1600; z+=20){ if(rand()<0.6) makeRibbon(z + R(-4,4)); }
// ---------- Obstacles: moving storm walls + time bubbles + rings ----------
const walls=[]; const bubbles=[]; const rings=[];
function makeWall(z){
const w= R(8,14), h= R(6,10);
const box=new THREE.Mesh(new THREE.BoxGeometry(w,h,1.4), new THREE.MeshBasicMaterial({color:0xff5577, transparent:true, opacity:.25}));
box.position.set(R(-10,10), R(0.5,4.5), z);
box.userData = {vx: R(-0.6,0.6), vy: R(-0.2,0.2)};
scene.add(box); walls.push(box);
}
function makeBubble(z){
const r=R(1.6,2.4);
const s=new THREE.Mesh(new THREE.SphereGeometry(r, 20, 16), new THREE.MeshBasicMaterial({color:0x22d39a, transparent:true, opacity:.18}));
s.position.set(R(-9,9), R(1.0,5.0), z);
s.userData = {r}; scene.add(s); bubbles.push(s);
}
function makeRing(z){
const t=new THREE.Mesh(new THREE.TorusGeometry(1.6,.12,8,24),
new THREE.MeshBasicMaterial({color:0x00e5ff,transparent:true,opacity:.9}));
t.position.set(R(-8,8), R(1.2,4.8), z);
scene.add(t); rings.push(t);
}
for(let z=40; z<=1600; ){
if(rand()<0.6) makeRing(z);
if(rand()<0.45) makeBubble(z+R(4,10));
if(rand()<0.55) makeWall(z+R(8,14));
z += 26 + Math.floor(rand()*14);
}
// ---------- Paper glider ----------
function makeGlider(){
const g=new THREE.Group();
const paper=new THREE.MeshLambertMaterial({color:0xf4f6f8, side:THREE.DoubleSide});
const bodyS=new THREE.Shape(); bodyS.moveTo(0,0); bodyS.lineTo(-.22,.55); bodyS.lineTo(-.42,1.05); bodyS.lineTo(.42,1.05); bodyS.lineTo(.22,.55); bodyS.lineTo(0,0);
const body=new THREE.Mesh(new THREE.ExtrudeGeometry(bodyS,{depth:.03,bevelEnabled:false}),paper);
const wing=(d)=>{const s=new THREE.Shape(); s.moveTo(0,.2); s.lineTo(d*.32,.85); s.lineTo(d*1.0,.52); s.lineTo(0,.2); return new THREE.Mesh(new THREE.ExtrudeGeometry(s,{depth:.02,bevelEnabled:false}),paper);};
const L=wing(-1), Rg=wing(1); L.position.y=.01; Rg.position.y=.015; L.rotation.x=.18; Rg.rotation.x=.18;
const crease=new THREE.Line(new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(0,.03,0),new THREE.Vector3(0,.03,1.05)]),
new THREE.LineBasicMaterial({color:0xdedede}));
g.add(body,L,Rg,crease); g.rotation.order="ZXY"; g.rotation.x=-Math.PI/2; return g;
}
const glider=makeGlider(); scene.add(glider);
// ---------- State & physics ----------
let state='aim'; // aim | fly | end
let power=0, maxPower=16, charging=false;
let stam=100, maxStam=100, combo=1, comboTimer=0, comboWindow=2.6;
let dist=0, score=0, best= Number(localStorage.getItem('zen_sky_best')||0);
const vel=new THREE.Vector3(0,0,0); const forward={cur:0};
const gravity=5.2, drag=.013;
let pitch=0, bank=0;
let slowMo=1.0; // 1 normal, <1 in bubbles
let showBubbles=false;
// UI refs
const $=id=>document.getElementById(id);
const elDist=$('dist'), elScore=$('score'), elPow=$('pBar'), elStam=$('sBar'), center=$('center');
const headline=$('headline'), details=$('details'), badges=$('badges');
$('btnRestart').onclick=()=>reset(); $('btnScreenshot').onclick=()=>photo();
function setBar(el,v){ el.style.width = `${THREE.MathUtils.clamp(v,0,100)}%`; }
// Controls
let key={L:false,R:false,U:false,D:false,B:false}, tL=false,tR=false;
addEventListener('keydown',e=>{
if(e.code==='Space'){ if(state==='aim'){charging=true;e.preventDefault();} else if(state==='end'){reset();}}
if(e.code==='ArrowLeft') key.L=true;
if(e.code==='ArrowRight') key.R=true;
if(e.code==='ArrowUp') key.U=true;
if(e.code==='ArrowDown') key.D=true;
if(e.code==='ShiftLeft'||e.code==='ShiftRight') key.B=true;
if(e.code==='KeyT') showBubbles = !showBubbles;
if(e.code==='KeyP') photo();
if(e.code==='Escape') reset();
if(e.code==='KeyR'){ // reroll seed
const newSeed=Math.floor(Math.random()*1e9).toString(36);
location.search='?seed='+newSeed;
}
});
addEventListener('keyup',e=>{
if(e.code==='Space' && state==='aim'){ launch(); e.preventDefault(); }
if(e.code==='ArrowLeft') key.L=false;
if(e.code==='ArrowRight') key.R=false;
if(e.code==='ArrowUp') key.U=false;
if(e.code==='ArrowDown') key.D=false;
if(e.code==='ShiftLeft'||e.code==='ShiftRight') key.B=false;
});
$('touchL').addEventListener('touchstart',()=>tL=true,{passive:true});
$('touchL').addEventListener('touchend',()=>tL=false,{passive:true});
$('touchR').addEventListener('touchstart',()=>tR=true,{passive:true});
$('touchR').addEventListener('touchend',()=>tR=false,{passive:true});
addEventListener('mousedown',()=>{ if(state==='aim')charging=true; });
addEventListener('mouseup',()=>{ if(state==='aim'&&charging)launch(); });
function reset(){
state='aim'; power=0; charging=false; stam=maxStam; combo=1; comboTimer=0; dist=0; score=0; forward.cur=0; vel.set(0,0,0); pitch=0; bank=0; slowMo=1.0;
glider.position.set(0, 2.5, 0); glider.rotation.set(-Math.PI/2,0,0);
camera.position.set(0, glider.position.y+4, glider.position.z-10);
center.style.display='none'; badges.innerHTML='';
}
function launch(){
forward.cur = THREE.MathUtils.mapLinear(power,0,maxPower,9,28);
vel.y = Math.sin(Math.PI/4)*(power*.68);
state='fly'; power=0; charging=false;
}
reset();
// ---------- Helpers ----------
const clock=new THREE.Clock();
function photo(){
const was= center.style.display;
center.style.display='none';
const d=renderer.domElement.toDataURL('image/png');
const a=document.createElement('a'); a.href=d; a.download=`zen-skylines-${Date.now()}.png`; a.click();
center.style.display=was;
}
function endRun(reason, earned=[]){
state='end';
if(score>best){ best=score; localStorage.setItem('zen_sky_best', String(Math.floor(best))); }
headline.textContent = reason||'Run Ended';
details.textContent = `Distance: ${dist.toFixed(1)} m • Score: ${Math.floor(score)} • Combo ×${combo} • Best ${Math.floor(best)}`;
badges.innerHTML = earned.map(t=>`<span class="badge">${t}</span>`).join(' ');
center.style.display='grid';
}
// ---------- Gameplay loop ----------
function animate(){
requestAnimationFrame(animate);
const rawDt = Math.min(.033, clock.getDelta());
const dt = rawDt * slowMo;
// sky parallax
skyTex.rotation = Math.min(1, dist/1600) * Math.PI * .42; skyTex.needsUpdate=true;
// UI
setBar(elPow, state==='aim' ? (power/maxPower)*100 : 0);
setBar(elStam, (stam/maxStam)*100);
elDist.textContent = `Distance: ${dist.toFixed(1)} m`;
elScore.textContent = `Score: ${Math.floor(score)} | Combo ×${combo} | Best ${Math.floor(best)}`;
// aim charging
if(state==='aim' && charging){ power=Math.min(maxPower, power + 26*rawDt); }
if(state==='fly'){
// flow-field wind force at glider
const f = flowVector(glider.position.x, glider.position.y, glider.position.z, performance.now()/1000);
// steering
const steerL = key.L||tL, steerR = key.R||tR;
const bankT = (steerL?-1:0)+(steerR?1:0); bank = THREE.MathUtils.lerp(bank, bankT*Math.PI/5, .1);
const pitchT = (key.U?.35:0) + (key.D?-.35:0); pitch = THREE.MathUtils.lerp(pitch, pitchT, .12);
// forward speed & drag
forward.cur *= (1 - drag);
if((key.B||key.D) && stam>0){ forward.cur += 8*dt; vel.y += 5.5*dt; stam -= 18*dt; } else { stam += 4*dt; }
stam = THREE.MathUtils.clamp(stam, 0, maxStam);
// lift vs gravity
const lift=(Math.sin(pitch)*(forward.cur+6))*0.95 + f.y*0.8;
vel.y += (lift - gravity) * dt;
// lateral control + wind push
const lateral = (steerR?-1:0)+(steerL?1:0);
glider.position.x += (lateral*3.6 + f.x*0.9) * dt;
// forward + wind bias
glider.position.z += (forward.cur + 10 + f.z*1.3) * dt;
// vertical integrate
glider.position.y += vel.y * dt;
// clamp vertical play space
if(glider.position.y<0.5){ glider.position.y=0.5; vel.y=Math.max(0,vel.y); }
if(glider.position.y>18){ glider.position.y=18; vel.y=Math.min(0,vel.y); }
// orientation
glider.rotation.x = -Math.PI/2 + (-pitch*.6);
glider.rotation.z = bank*.8;
// move flow ribbons
for(const rb of ribbons){
rb.position.y += Math.sin((performance.now()+rb.position.z*40)/7000)*0.02;
rb.position.x += Math.sin((performance.now()+rb.position.z*55)/6000)*0.02;
}
// walls motion & collisions (graze bonus if within 0.6..1.2 of surface)
let earned=[];
for(const w of walls){
w.position.x += w.userData.vx*dt;
w.position.y += w.userData.vy*dt;
if(w.position.x<-12||w.position.x>12) w.userData.vx*=-1;
if(w.position.y<0.5||w.position.y>8) w.userData.vy*=-1;
// AABB approx
const dx=Math.abs(glider.position.x - w.position.x);
const dy=Math.abs(glider.position.y - w.position.y);
const dz=Math.abs(glider.position.z - w.position.z);
const hx=w.geometry.parameters.width/2, hy=w.geometry.parameters.height/2, hz=0.7;
if(dx<hx && dy<hy && dz<hz){
return endRun('Storm wall impact', ['Risk-taker']);
}
// graze
if(dz<hz*1.25){
const gx = Math.abs(dx-hx), gy=Math.abs(dy-hy);
if((gx<1.0||gy<1.0) && rand()<0.02){ score += 6*combo; earned.push('Graze +6'); }
}
}
// rings
for(let i=rings.length-1;i>=0;i--){
const t=rings[i]; const d=glider.position.distanceTo(t.position);
if(d<1.55){ // collect
scene.remove(t); rings.splice(i,1);
stam = Math.min(maxStam, stam+34); combo = Math.min(12, combo+1); comboTimer = comboWindow; score += 160*combo;
earned.push('Ring +160×'+combo);
} else {
t.rotation.x += .6*dt; t.rotation.z += .4*dt; const s=1+.08*Math.sin(performance.now()/260+i); t.scale.set(s,s,s);
}
}
// slow-time bubbles (affect slowMo and scoring drip)
let inBubble=false;
for(const b of bubbles){
const d=glider.position.distanceTo(b.position);
if(d < b.userData.r){ inBubble=true; score += 4*combo*dt; }
}
slowMo = THREE.MathUtils.lerp(slowMo, inBubble ? 0.6 : 1.0, 0.15);
// combo decay
comboTimer -= dt; if(comboTimer<=0 && combo>1){ combo=Math.max(1, combo-1); comboTimer=.35; }
// progress + score drip
const dz=(forward.cur + 10 + f.z*1.3) * dt;
dist += dz; score += dz*(.9*combo);
// out-of-corridor fail
if(Math.abs(glider.position.x)>14) return endRun('You left the jetstream corridor', []);
if(glider.position.z>1650) return endRun('Finish line reached!', ['Course Clear']);
}
// camera chase
const camTarget=new THREE.Vector3(glider.position.x, glider.position.y+3.4, glider.position.z-10.5);
camera.position.lerp(camTarget,.12); camera.lookAt(glider.position);
// bubble view toggle visuals
for(const b of bubbles){ b.material.opacity = showBubbles? .28 : .18; }
renderer.render(scene,camera);
}
animate();
// resize
addEventListener('resize',()=>{ camera.aspect=innerWidth/innerHeight; camera.updateProjectionMatrix(); renderer.setSize(innerWidth,innerHeight); });
</script>
</body>
</html>