Update index.html
Browse files- index.html +224 -0
index.html
CHANGED
|
@@ -160,3 +160,227 @@ for(let z=40; z<=1600; ){
|
|
| 160 |
}
|
| 161 |
|
| 162 |
// ---------- Paper glider ----------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
}
|
| 161 |
|
| 162 |
// ---------- Paper glider ----------
|
| 163 |
+
function makeGlider(){
|
| 164 |
+
const g=new THREE.Group();
|
| 165 |
+
const paper=new THREE.MeshLambertMaterial({color:0xf4f6f8, side:THREE.DoubleSide});
|
| 166 |
+
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);
|
| 167 |
+
const body=new THREE.Mesh(new THREE.ExtrudeGeometry(bodyS,{depth:.03,bevelEnabled:false}),paper);
|
| 168 |
+
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);};
|
| 169 |
+
const L=wing(-1), Rg=wing(1); L.position.y=.01; Rg.position.y=.015; L.rotation.x=.18; Rg.rotation.x=.18;
|
| 170 |
+
const crease=new THREE.Line(new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(0,.03,0),new THREE.Vector3(0,.03,1.05)]),
|
| 171 |
+
new THREE.LineBasicMaterial({color:0xdedede}));
|
| 172 |
+
g.add(body,L,Rg,crease); g.rotation.order="ZXY"; g.rotation.x=-Math.PI/2; return g;
|
| 173 |
+
}
|
| 174 |
+
const glider=makeGlider(); scene.add(glider);
|
| 175 |
+
|
| 176 |
+
// ---------- State & physics ----------
|
| 177 |
+
let state='aim'; // aim | fly | end
|
| 178 |
+
let power=0, maxPower=16, charging=false;
|
| 179 |
+
let stam=100, maxStam=100, combo=1, comboTimer=0, comboWindow=2.6;
|
| 180 |
+
let dist=0, score=0, best= Number(localStorage.getItem('zen_sky_best')||0);
|
| 181 |
+
const vel=new THREE.Vector3(0,0,0); const forward={cur:0};
|
| 182 |
+
const gravity=5.2, drag=.013;
|
| 183 |
+
let pitch=0, bank=0;
|
| 184 |
+
let slowMo=1.0; // 1 normal, <1 in bubbles
|
| 185 |
+
let showBubbles=false;
|
| 186 |
+
|
| 187 |
+
// UI refs
|
| 188 |
+
const $=id=>document.getElementById(id);
|
| 189 |
+
const elDist=$('dist'), elScore=$('score'), elPow=$('pBar'), elStam=$('sBar'), center=$('center');
|
| 190 |
+
const headline=$('headline'), details=$('details'), badges=$('badges');
|
| 191 |
+
$('btnRestart').onclick=()=>reset(); $('btnScreenshot').onclick=()=>photo();
|
| 192 |
+
|
| 193 |
+
function setBar(el,v){ el.style.width = `${THREE.MathUtils.clamp(v,0,100)}%`; }
|
| 194 |
+
|
| 195 |
+
// Controls
|
| 196 |
+
let key={L:false,R:false,U:false,D:false,B:false}, tL=false,tR=false;
|
| 197 |
+
addEventListener('keydown',e=>{
|
| 198 |
+
if(e.code==='Space'){ if(state==='aim'){charging=true;e.preventDefault();} else if(state==='end'){reset();}}
|
| 199 |
+
if(e.code==='ArrowLeft') key.L=true;
|
| 200 |
+
if(e.code==='ArrowRight') key.R=true;
|
| 201 |
+
if(e.code==='ArrowUp') key.U=true;
|
| 202 |
+
if(e.code==='ArrowDown') key.D=true;
|
| 203 |
+
if(e.code==='ShiftLeft'||e.code==='ShiftRight') key.B=true;
|
| 204 |
+
if(e.code==='KeyT') showBubbles = !showBubbles;
|
| 205 |
+
if(e.code==='KeyP') photo();
|
| 206 |
+
if(e.code==='Escape') reset();
|
| 207 |
+
if(e.code==='KeyR'){ // reroll seed
|
| 208 |
+
const newSeed=Math.floor(Math.random()*1e9).toString(36);
|
| 209 |
+
location.search='?seed='+newSeed;
|
| 210 |
+
}
|
| 211 |
+
});
|
| 212 |
+
addEventListener('keyup',e=>{
|
| 213 |
+
if(e.code==='Space' && state==='aim'){ launch(); e.preventDefault(); }
|
| 214 |
+
if(e.code==='ArrowLeft') key.L=false;
|
| 215 |
+
if(e.code==='ArrowRight') key.R=false;
|
| 216 |
+
if(e.code==='ArrowUp') key.U=false;
|
| 217 |
+
if(e.code==='ArrowDown') key.D=false;
|
| 218 |
+
if(e.code==='ShiftLeft'||e.code==='ShiftRight') key.B=false;
|
| 219 |
+
});
|
| 220 |
+
$('touchL').addEventListener('touchstart',()=>tL=true,{passive:true});
|
| 221 |
+
$('touchL').addEventListener('touchend',()=>tL=false,{passive:true});
|
| 222 |
+
$('touchR').addEventListener('touchstart',()=>tR=true,{passive:true});
|
| 223 |
+
$('touchR').addEventListener('touchend',()=>tR=false,{passive:true});
|
| 224 |
+
addEventListener('mousedown',()=>{ if(state==='aim')charging=true; });
|
| 225 |
+
addEventListener('mouseup',()=>{ if(state==='aim'&&charging)launch(); });
|
| 226 |
+
|
| 227 |
+
function reset(){
|
| 228 |
+
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;
|
| 229 |
+
glider.position.set(0, 2.5, 0); glider.rotation.set(-Math.PI/2,0,0);
|
| 230 |
+
camera.position.set(0, glider.position.y+4, glider.position.z-10);
|
| 231 |
+
center.style.display='none'; badges.innerHTML='';
|
| 232 |
+
}
|
| 233 |
+
function launch(){
|
| 234 |
+
forward.cur = THREE.MathUtils.mapLinear(power,0,maxPower,9,28);
|
| 235 |
+
vel.y = Math.sin(Math.PI/4)*(power*.68);
|
| 236 |
+
state='fly'; power=0; charging=false;
|
| 237 |
+
}
|
| 238 |
+
reset();
|
| 239 |
+
|
| 240 |
+
// ---------- Helpers ----------
|
| 241 |
+
const clock=new THREE.Clock();
|
| 242 |
+
function photo(){
|
| 243 |
+
const was= center.style.display;
|
| 244 |
+
center.style.display='none';
|
| 245 |
+
const d=renderer.domElement.toDataURL('image/png');
|
| 246 |
+
const a=document.createElement('a'); a.href=d; a.download=`zen-skylines-${Date.now()}.png`; a.click();
|
| 247 |
+
center.style.display=was;
|
| 248 |
+
}
|
| 249 |
+
function endRun(reason, earned=[]){
|
| 250 |
+
state='end';
|
| 251 |
+
if(score>best){ best=score; localStorage.setItem('zen_sky_best', String(Math.floor(best))); }
|
| 252 |
+
headline.textContent = reason||'Run Ended';
|
| 253 |
+
details.textContent = `Distance: ${dist.toFixed(1)} m • Score: ${Math.floor(score)} • Combo ×${combo} • Best ${Math.floor(best)}`;
|
| 254 |
+
badges.innerHTML = earned.map(t=>`<span class="badge">${t}</span>`).join(' ');
|
| 255 |
+
center.style.display='grid';
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
// ---------- Gameplay loop ----------
|
| 259 |
+
function animate(){
|
| 260 |
+
requestAnimationFrame(animate);
|
| 261 |
+
const rawDt = Math.min(.033, clock.getDelta());
|
| 262 |
+
const dt = rawDt * slowMo;
|
| 263 |
+
|
| 264 |
+
// sky parallax
|
| 265 |
+
skyTex.rotation = Math.min(1, dist/1600) * Math.PI * .42; skyTex.needsUpdate=true;
|
| 266 |
+
|
| 267 |
+
// UI
|
| 268 |
+
setBar(elPow, state==='aim' ? (power/maxPower)*100 : 0);
|
| 269 |
+
setBar(elStam, (stam/maxStam)*100);
|
| 270 |
+
elDist.textContent = `Distance: ${dist.toFixed(1)} m`;
|
| 271 |
+
elScore.textContent = `Score: ${Math.floor(score)} | Combo ×${combo} | Best ${Math.floor(best)}`;
|
| 272 |
+
|
| 273 |
+
// aim charging
|
| 274 |
+
if(state==='aim' && charging){ power=Math.min(maxPower, power + 26*rawDt); }
|
| 275 |
+
|
| 276 |
+
if(state==='fly'){
|
| 277 |
+
// flow-field wind force at glider
|
| 278 |
+
const f = flowVector(glider.position.x, glider.position.y, glider.position.z, performance.now()/1000);
|
| 279 |
+
// steering
|
| 280 |
+
const steerL = key.L||tL, steerR = key.R||tR;
|
| 281 |
+
const bankT = (steerL?-1:0)+(steerR?1:0); bank = THREE.MathUtils.lerp(bank, bankT*Math.PI/5, .1);
|
| 282 |
+
const pitchT = (key.U?.35:0) + (key.D?-.35:0); pitch = THREE.MathUtils.lerp(pitch, pitchT, .12);
|
| 283 |
+
|
| 284 |
+
// forward speed & drag
|
| 285 |
+
forward.cur *= (1 - drag);
|
| 286 |
+
if((key.B||key.D) && stam>0){ forward.cur += 8*dt; vel.y += 5.5*dt; stam -= 18*dt; } else { stam += 4*dt; }
|
| 287 |
+
stam = THREE.MathUtils.clamp(stam, 0, maxStam);
|
| 288 |
+
|
| 289 |
+
// lift vs gravity
|
| 290 |
+
const lift=(Math.sin(pitch)*(forward.cur+6))*0.95 + f.y*0.8;
|
| 291 |
+
vel.y += (lift - gravity) * dt;
|
| 292 |
+
|
| 293 |
+
// lateral control + wind push
|
| 294 |
+
const lateral = (steerR?-1:0)+(steerL?1:0);
|
| 295 |
+
glider.position.x += (lateral*3.6 + f.x*0.9) * dt;
|
| 296 |
+
|
| 297 |
+
// forward + wind bias
|
| 298 |
+
glider.position.z += (forward.cur + 10 + f.z*1.3) * dt;
|
| 299 |
+
|
| 300 |
+
// vertical integrate
|
| 301 |
+
glider.position.y += vel.y * dt;
|
| 302 |
+
// clamp vertical play space
|
| 303 |
+
if(glider.position.y<0.5){ glider.position.y=0.5; vel.y=Math.max(0,vel.y); }
|
| 304 |
+
if(glider.position.y>18){ glider.position.y=18; vel.y=Math.min(0,vel.y); }
|
| 305 |
+
|
| 306 |
+
// orientation
|
| 307 |
+
glider.rotation.x = -Math.PI/2 + (-pitch*.6);
|
| 308 |
+
glider.rotation.z = bank*.8;
|
| 309 |
+
|
| 310 |
+
// move flow ribbons
|
| 311 |
+
for(const rb of ribbons){
|
| 312 |
+
rb.position.y += Math.sin((performance.now()+rb.position.z*40)/7000)*0.02;
|
| 313 |
+
rb.position.x += Math.sin((performance.now()+rb.position.z*55)/6000)*0.02;
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
// walls motion & collisions (graze bonus if within 0.6..1.2 of surface)
|
| 317 |
+
let earned=[];
|
| 318 |
+
for(const w of walls){
|
| 319 |
+
w.position.x += w.userData.vx*dt;
|
| 320 |
+
w.position.y += w.userData.vy*dt;
|
| 321 |
+
if(w.position.x<-12||w.position.x>12) w.userData.vx*=-1;
|
| 322 |
+
if(w.position.y<0.5||w.position.y>8) w.userData.vy*=-1;
|
| 323 |
+
|
| 324 |
+
// AABB approx
|
| 325 |
+
const dx=Math.abs(glider.position.x - w.position.x);
|
| 326 |
+
const dy=Math.abs(glider.position.y - w.position.y);
|
| 327 |
+
const dz=Math.abs(glider.position.z - w.position.z);
|
| 328 |
+
const hx=w.geometry.parameters.width/2, hy=w.geometry.parameters.height/2, hz=0.7;
|
| 329 |
+
if(dx<hx && dy<hy && dz<hz){
|
| 330 |
+
return endRun('Storm wall impact', ['Risk-taker']);
|
| 331 |
+
}
|
| 332 |
+
// graze
|
| 333 |
+
if(dz<hz*1.25){
|
| 334 |
+
const gx = Math.abs(dx-hx), gy=Math.abs(dy-hy);
|
| 335 |
+
if((gx<1.0||gy<1.0) && rand()<0.02){ score += 6*combo; earned.push('Graze +6'); }
|
| 336 |
+
}
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
// rings
|
| 340 |
+
for(let i=rings.length-1;i>=0;i--){
|
| 341 |
+
const t=rings[i]; const d=glider.position.distanceTo(t.position);
|
| 342 |
+
if(d<1.55){ // collect
|
| 343 |
+
scene.remove(t); rings.splice(i,1);
|
| 344 |
+
stam = Math.min(maxStam, stam+34); combo = Math.min(12, combo+1); comboTimer = comboWindow; score += 160*combo;
|
| 345 |
+
earned.push('Ring +160×'+combo);
|
| 346 |
+
} else {
|
| 347 |
+
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);
|
| 348 |
+
}
|
| 349 |
+
}
|
| 350 |
+
|
| 351 |
+
// slow-time bubbles (affect slowMo and scoring drip)
|
| 352 |
+
let inBubble=false;
|
| 353 |
+
for(const b of bubbles){
|
| 354 |
+
const d=glider.position.distanceTo(b.position);
|
| 355 |
+
if(d < b.userData.r){ inBubble=true; score += 4*combo*dt; }
|
| 356 |
+
}
|
| 357 |
+
slowMo = THREE.MathUtils.lerp(slowMo, inBubble ? 0.6 : 1.0, 0.15);
|
| 358 |
+
|
| 359 |
+
// combo decay
|
| 360 |
+
comboTimer -= dt; if(comboTimer<=0 && combo>1){ combo=Math.max(1, combo-1); comboTimer=.35; }
|
| 361 |
+
|
| 362 |
+
// progress + score drip
|
| 363 |
+
const dz=(forward.cur + 10 + f.z*1.3) * dt;
|
| 364 |
+
dist += dz; score += dz*(.9*combo);
|
| 365 |
+
|
| 366 |
+
// out-of-corridor fail
|
| 367 |
+
if(Math.abs(glider.position.x)>14) return endRun('You left the jetstream corridor', []);
|
| 368 |
+
if(glider.position.z>1650) return endRun('Finish line reached!', ['Course Clear']);
|
| 369 |
+
}
|
| 370 |
+
|
| 371 |
+
// camera chase
|
| 372 |
+
const camTarget=new THREE.Vector3(glider.position.x, glider.position.y+3.4, glider.position.z-10.5);
|
| 373 |
+
camera.position.lerp(camTarget,.12); camera.lookAt(glider.position);
|
| 374 |
+
|
| 375 |
+
// bubble view toggle visuals
|
| 376 |
+
for(const b of bubbles){ b.material.opacity = showBubbles? .28 : .18; }
|
| 377 |
+
|
| 378 |
+
renderer.render(scene,camera);
|
| 379 |
+
}
|
| 380 |
+
animate();
|
| 381 |
+
|
| 382 |
+
// resize
|
| 383 |
+
addEventListener('resize',()=>{ camera.aspect=innerWidth/innerHeight; camera.updateProjectionMatrix(); renderer.setSize(innerWidth,innerHeight); });
|
| 384 |
+
</script>
|
| 385 |
+
</body>
|
| 386 |
+
</html>
|