/* ============ cloth (verlet cape) ============ */ function makeCape(sc,color){ const W=(cfg.quality>1)?8:6,H=(cfg.quality>1)?10:8,SP=.3; const N=W*H; const pos=new Float32Array(N*3),old=new Float32Array(N*3); const geo=new THREE.PlaneGeometry(SP*(W-1),SP*(H-1),W-1,H-1); const mesh=new THREE.Mesh(geo,new THREE.MeshLambertMaterial({color:color||0x181826,side:THREE.DoubleSide})); mesh.frustumCulled=false;sc.add(mesh); const cons=[]; for(let y=0;y0&&i%4===1&&!last){ const ant=new THREE.Mesh(new THREE.CylinderGeometry(.06,.06,4,6),M(0x4a4f62)); ant.position.set(xo+w/2-.8,y+2,z-d/2);sc.add(ant); const pm=plusMesh();pm.position.set(xo+w/2-.8,y+4.4,z-d/2);sc.add(pm); r.plus={m:pm,taken:false}; } z-=d+rnd(7,11); y+=rnd(-2.2,.8); xo=clamp(xo+rnd(-4,4),-8,8); } const last=roofs[roofs.length-1]; // spotlight goal const spot=new THREE.Mesh(new THREE.CylinderGeometry(.8,3.4,26,12,1,true), new THREE.MeshBasicMaterial({color:0x7de8ff,transparent:true,opacity:.16,blending:THREE.AdditiveBlending,side:THREE.DoubleSide})); spot.position.set(last.x,last.y+13,last.z);sc.add(spot); const spotP=plusMesh(0x7de8ff);spotP.scale.set(2,2,2); spotP.position.set(last.x,last.y+2.4,last.z);sc.add(spotP); // rain let rain=null,rainPos=null; if(cfg.quality>0){ const n=cfg.quality>1?520:280; rainPos=new Float32Array(n*3); for(let i=0;iru.r.y-1.4){ p.y=ru.r.y;p.vy=0;p.grounded=true;p.lastRoof=ru.i; if(ru.i===roofs.length-1){finishLevel();return;} } if(p.grounded&&In.jp){p.vy=12;sfx('jump');} // fall if(p.y iron flight) ============ */ function makeL6(){ const sc=new THREE.Scene(); sc.background=new THREE.Color(0x241a4d); sc.fog=new THREE.Fog(0x241a4d,60,220); lights(sc,0xb8a8ff,0x2a1830,1); const sun=new THREE.PointLight(0xff8a3a,.8,300);sun.position.set(-40,30,-160);sc.add(sun); const cam=new THREE.PerspectiveCamera(62,innerWidth/innerHeight,.1,400); // clouds for(let i=0;i<26;i++){ const c=new THREE.Mesh(new THREE.SphereGeometry(rnd(3,8),8,6), new THREE.MeshLambertMaterial({color:0x4a3a7a,transparent:true,opacity:.7})); c.scale.y=.4; c.position.set(rnd(-50,50),rnd(-6,26),rnd(-500,20));sc.add(c); } const ground=new THREE.Mesh(new THREE.PlaneGeometry(400,900),M(0x1a1238)); ground.rotation.x=-Math.PI/2;ground.position.set(0,-14,-260);sc.add(ground); // rings const rings=[]; for(let i=0;i<12;i++){ const m=new THREE.Mesh(new THREE.TorusGeometry(2.6,.32,10,24),M(0xffd23e,0x8a6a00)); const x=Math.sin(i*1.3)*9,y=9+Math.cos(i*.9)*5,z=-46-i*36; m.position.set(x,y,z);sc.add(m); rings.push({m,x,y,z,done:false}); } // rider (broom seeker) const rider=new THREE.Group(); const broom=new THREE.Mesh(new THREE.CylinderGeometry(.09,.14,3,8),M(0x7a4a20)); broom.rotation.x=Math.PI/2;rider.add(broom); const bristle=new THREE.Mesh(new THREE.ConeGeometry(.4,1,8),M(0xb08030)); bristle.rotation.x=-Math.PI/2;bristle.position.z=1.8;rider.add(bristle); const rb=BX(.6,.9,.5,0x3a2a5a);rb.position.y=.7;rider.add(rb); const rh=new THREE.Mesh(new THREE.SphereGeometry(.32,10,8),M(0xf0c090));rh.position.y=1.4;rider.add(rh); const scarf=BX(.24,.14,1.6,0xc8a030);scarf.position.set(0,1.15,1.1);rider.add(scarf); const glasses=BX(.5,.1,.1,0x101018);glasses.position.set(0,1.45,-.28);rider.add(glasses); // armor (iron form) const armor=new THREE.Group(); const at=BX(.9,1.2,.6,0xb02030,0x300808);at.position.y=.6;armor.add(at); const ah=new THREE.Mesh(new THREE.SphereGeometry(.36,10,8),M(0xc8a030,0x403008));ah.position.y=1.5;armor.add(ah); const av=BX(.3,.14,.06,0x7de8ff,0x7de8ff);av.position.set(0,1.5,-.34);armor.add(av); const arc=new THREE.Mesh(new THREE.CylinderGeometry(.14,.14,.08,10), new THREE.MeshBasicMaterial({color:0x7de8ff})); arc.rotation.x=Math.PI/2;arc.position.set(0,.75,-.34);armor.add(arc); const hand=BX(.22,.22,.22,0xc8a030,0x403008);hand.position.set(.55,.5,-.4);armor.add(hand); armor.visible=false; const PL=new THREE.Group();PL.add(rider);PL.add(armor);sc.add(PL); // rivals const rivals=[]; for(let i=0;i<2;i++){ const g=new THREE.Group(); const b=BX(.6,.8,.5,i?0x2a7a3a:0x7a2a2a);b.position.y=.5;g.add(b); const br=new THREE.Mesh(new THREE.CylinderGeometry(.08,.12,2.6,8),M(0x5a3a18)); br.rotation.x=Math.PI/2;g.add(br); sc.add(g);rivals.push({g,ph:i*2.4}); } // drones (phase 2) const drones=[]; let gate=null; const fx=FX(sc); const beam=new THREE.Mesh(new THREE.CylinderGeometry(.08,.08,1,6), new THREE.MeshBasicMaterial({color:0x7de8ff,transparent:true,opacity:.9})); beam.visible=false;sc.add(beam); let beamT=0,fireCd=0; const p={x:0,y:9,z:20,phase:1,ringIdx:0,misses:0}; function spawnDrones(){ for(let i=0;i<6;i++){ const m=new THREE.Mesh(new THREE.OctahedronGeometry(1.1),M(0xd82a3a,0x500a10)); const bx=rnd(-12,12),by=rnd(4,18),bz=p.z-70-i*28; m.position.set(bx,by,bz);sc.add(m); drones.push({m,bx,by,bz,ph:rnd(TAU),alive:true}); } } function transform(){ p.phase=2;p.tz=p.z; rider.visible=false;armor.visible=true; flashWhite();sfx('power'); pop('\u00b1 OVERRIDE: SUIT ONLINE','#7de8ff',true); pop('J / X = REPULSOR','#ffd23e'); $('cross').classList.remove('hidden'); spawnDrones(); for(let i=0;ir.z-3){ r.done=true; if(Math.abs(p.x-r.x)<2.4&&Math.abs(p.y-r.y)<2.4){ addScore(30);addTime(1);sfx('ring'); r.m.material.emissive.setHex(0x3a8a3a); r.m.material.color.setHex(0x59ff8a); fx.burst(r.m.position,0xffd23e,14,6); }else{ p.misses++; if(p.misses%2===1)pop('un rival a prins inelul!','#d8309a'); } } } // rivals weave ahead for(let i=0;ip.z+6)d.bz=p.z-rnd(60,120); } if(fireCd>0)fireCd-=dt; if(In.ap&&fireCd<=0){ fireCd=.35; // nearest drone in front cone let best=null,bd=1e9; for(let i=0;i70)continue; if(Math.abs(d.m.position.x-p.x)>11||Math.abs(d.m.position.y-p.y)>9)continue; if(dz0){beamT-=dt;if(beamT<=0)beam.visible=false;} if(left===0&&!gate){ gate=new THREE.Group(); const gr=new THREE.Mesh(new THREE.TorusGeometry(7,.8,10,28),M(0x7de8ff,0x1a4a5a)); gate.add(gr); const gp=plusMesh(0xffffff);gp.scale.set(3,3,3);gate.add(gp); gate.position.set(0,10,p.z-150);sc.add(gate); pop('FINAL GATE \u2192','#ffd23e',true); } if(gate){ gate.rotation.z=tt; if(p.z{}); PL.position.set(p.x,p.y,p.z); PL.rotation.z=-In.x*.5; PL.rotation.x=In.y*.35; if(p.phase===2)armor.rotation.x=-.9; cam.position.set(p.x*.85,p.y+2.6,p.z+11); cam.lookAt(p.x,p.y,p.z-22); shakeCam(cam); fx.update(dt); } return{scene:sc,cam,update}; } /* ============ LEVEL METADATA ============ */ LEVELS.push( {name:'ZONE 01 \u00b7 VELOCITY',sub:'the blue blur protocol', story:'ALEX \u2014 semnalul \u00b1 te-a tras in prima falie. Cineva foarte rapid si foarte albastru ti-a lasat adidasii lui. Tine viteza sus, sari peste tepi, ruleaza (JOS) pe pante \u2014 si daca prinzi destula viteza inainte de bucla mare... vezi tu. Inele = scor. VERDE + = timp. ROSU \u2212 = fereste-te.', ctrl:'auto-run \u00b7 SPACE / A sari \u00b7 JOS = roll',make:makeL1}, {name:'ZONE 02 \u00b7 PIPEWORKS',sub:'memo de la instalator', story:'Warp gresit, Alex. Un instalator celebru a lasat un bilet pe teava: "Iesirea e la dreapta. Mereu e." Loveste blocurile ? de jos \u2014 unele ascund timp. Calca pe ce misca. Nu atinge ce misca.', ctrl:'\u2190\u2192 mergi \u00b7 SPACE sari (tine apasat = mai sus)',make:makeL2}, {name:'ZONE 03 \u00b7 DARK FATHER',sub:'crimson saber duel', story:'Respiratia grea nu minte. "Scorul tau e puternic." PAREAZA pe directia atacului (\u2190 / \u2191 / \u2192). Cand sageata e ROSIE, fereste-te (\u2193). Dupa destule parari perfecte se deschide fereastra: STRIKE.', ctrl:'\u2190\u2191\u2192 parry \u00b7 \u2193 dodge \u00b7 J / X = strike',make:makeL3}, {name:'ZONE 04 \u00b7 BUCURESTI RUSH',sub:'traficul nu doarme niciodata', story:'Falia are forma unui oras cunoscut. Alex, ai de prins B \u2192 C \u2192 D \u2192 E inainte sa te ajunga din urma grupul de colegi antipatici. Pe jos? Skate? Bicicleta? Statia de BUS te teleporteaza prin oras. Tata cu masina e rapid... daca nu e TRAFIC. Fiecare alegere costa sau salveaza secunde.', ctrl:'stick / sageti = miscare \u00b7 stai pe statia BUS ca sa sari prin oras',make:makeL4}, {name:'ZONE 05 \u00b7 NIGHTFALL',sub:'detectivul cu pelerina', story:'Ploaie. Sodiu. Acoperisuri. Cineva cu urechi ascutite ti-a imprumutat pelerina, Alex \u2014 tine SARITURA apasata in aer si vantul te duce peste goluri. Nu te uita in jos. (Ba uita-te. E superb.)', ctrl:'\u2190\u2192 directie \u00b7 SPACE sari \u00b7 tine SPACE in aer = GLIDE',make:makeL5}, {name:'ZONE 06 \u00b7 SEEKER PROTOCOL',sub:'ascensiunea finala', story:'Ultimul realm. Prinde inelele de aur inaintea rivalilor, Alex. Si daca la un moment dat cerul se rescrie si simti ceva metalic... ai incredere in semnal. Un om de fier zicea candva: sometimes you gotta run before you can walk.', ctrl:'stick = zbor \u00b7 J / X = ... vei vedea',make:makeL6} ); /* ============ FINALE ============ */ const fxc=$('fx'); const fctx=fxc.getContext('2d'); let fin=null; function calcTier(){ const perf=G.score+Math.max(0,G.time)*9-G.continues*350+D()*500; if(perf>=2700)return 'S'; if(perf>=2100)return 'A'; if(perf>=1500)return 'B'; if(perf>=900)return 'C'; return 'D'; } function makeCode(tier){ const h=(((G.score+13)*2654435761)>>>0).toString(36).toUpperCase().padStart(6,'0').slice(0,4); return '\u00b116-'+tier+'-'+h; } function rrect(x,y,w,h,r){ fctx.beginPath(); fctx.moveTo(x+r,y); fctx.arcTo(x+w,y,x+w,y+h,r); fctx.arcTo(x+w,y+h,x,y+h,r); fctx.arcTo(x,y+h,x,y,r); fctx.arcTo(x,y,x+w,y,r); fctx.closePath(); } const CARD_G=['\u00b1','\u25ce','?','\u2020','\u25b2','\u2b21','\u2605']; const CARD_C=['#7de8ff','#2456e8','#e23333','#c22a3a','#ff8b3d','#4a5a8a','#c8a030']; function spawnCard(i){ fin.cards.push({ x:rnd(60,Math.max(120,innerWidth-60)),y:-50, vx:rnd(2,4.6)*(Math.random()<.5?-1:1),vy:rnd(0,1.5), g:CARD_G[i%CARD_G.length],c:CARD_C[i%CARD_C.length] }); } function finale(){ G.state='end';stopMusic();sfx('win'); hideCues(); $('hud').classList.add('hidden'); fxc.width=innerWidth;fxc.height=innerHeight; $('fx').classList.remove('hidden'); fctx.fillStyle='#05060a';fctx.fillRect(0,0,fxc.width,fxc.height); const tier=calcTier(),code=makeCode(tier); fin={t:0,conf:[],cards:[],burstT:0,tier,code}; for(let i=0;i<7;i++)spawnCard(i); setTimeout(()=>sfx('power'),650); setTimeout(()=>sfx('win'),1400); setTimeout(()=>{ const t=Math.max(0,G.time); $('estats').innerHTML= 'SCORE '+String(G.score).padStart(5,'0')+ ' \u00b7 TIMP RAMAS '+(t/60|0)+':'+String(Math.floor(t%60)).padStart(2,'0')+ ' \u00b7 RANK '+fin.tier+''+ (G.continues?' \u00b7 continues: '+G.continues:''); $('ecode').textContent=fin.code; $('endp').classList.remove('hidden'); },2600); } function stopFinale(){fin=null;} function finaleTick(dt){ if(!fin)return; fin.t+=dt; const w=fxc.width,h=fxc.height,k=dt*60; fctx.fillStyle='rgba(5,6,10,0.055)'; fctx.fillRect(0,0,w,h); // confetti bursts fin.burstT-=dt; if(fin.burstT<=0&&fin.t<10){ fin.burstT=.5; for(let i=0;i<46;i++){ fin.conf.push({x:rnd(w),y:h+10,vx:rnd(-2.6,2.6),vy:rnd(-13,-7), c:'hsl('+(rnd(360)|0)+' 90% 62%)',r:rnd(TAU),vr:rnd(-4,4),s:rnd(3,7)}); } } for(let i=fin.conf.length-1;i>=0;i--){ const c=fin.conf[i]; c.vy+=.16*k;c.x+=c.vx*k;c.y+=c.vy*k;c.r+=c.vr*dt; fctx.save(); fctx.translate(c.x,c.y);fctx.rotate(c.r); fctx.fillStyle=c.c; fctx.fillRect(-c.s/2,-c.s/4,c.s,c.s/2); fctx.restore(); if(c.y>h+40)fin.conf.splice(i,1); } // solitaire cascade cards for(let i=0;ih-84){c.y=h-84;c.vy*=-0.87;} if(c.x<-70||c.x>w+70){ c.x=rnd(60,w-60);c.y=-60;c.vy=rnd(0,1.5); c.vx=rnd(2,4.6)*(Math.random()<.5?-1:1); } rrect(c.x,c.y,56,78,7); fctx.fillStyle='#efece2';fctx.fill(); fctx.lineWidth=3;fctx.strokeStyle=c.c;fctx.stroke(); fctx.fillStyle=c.c; fctx.font='bold 32px "Courier New",monospace'; fctx.textAlign='center';fctx.textBaseline='middle'; fctx.fillText(c.g,c.x+28,c.y+40); } } /* ============ MENU SCENE ============ */ let menuScene=null,menuCam=null; const menuBits=[]; function makeMenu(){ menuScene=new THREE.Scene(); menuScene.background=new THREE.Color(0x07070d); menuScene.fog=new THREE.Fog(0x07070d,8,55); lights(menuScene,0x8fb0ff,0x1a1020,.9); for(let i=0;i<42;i++){ const m=Math.random()<.55?plusMesh(0x2c3f66):minusMesh(0x5c2333); m.position.set(rnd(-26,26),rnd(-15,15),rnd(-45,-5)); m.rotation.set(rnd(TAU),rnd(TAU),rnd(TAU)); m.userData.r=rnd(.2,.9); menuScene.add(m);menuBits.push(m); } menuCam=new THREE.PerspectiveCamera(60,innerWidth/innerHeight,.1,200); menuCam.position.set(0,0,10); } function menuUpdate(dt){ for(let i=0;i{initAC();startGame();}); $('again').addEventListener('click',()=>{ stopFinale(); $('endp').classList.add('hidden'); $('fx').classList.add('hidden'); $('hud').classList.add('hidden'); G.state='menu'; $('menu').classList.remove('hidden'); }); $('diff').addEventListener('input',e=>{ cfg.diff=e.target.value/100; $('diffLbl').textContent=['CASUAL','STEADY','SHARP','NIGHTMARE'][Math.min(3,(cfg.diff*4)|0)]; }); $('vol').addEventListener('input',e=>{ cfg.vol=e.target.value/100; $('volLbl').textContent=e.target.value; setVol(); }); document.querySelectorAll('.qb').forEach(b=>{ b.addEventListener('click',()=>{ document.querySelectorAll('.qb').forEach(x=>x.classList.remove('on')); b.classList.add('on'); cfg.quality=+b.dataset.q; applyQuality(); }); }); /* ============ MAIN LOOP ============ */ let lastT=performance.now(); function frame(t){ requestAnimationFrame(frame); let dt=Math.min(.05,(t-lastT)/1000); lastT=t; pollInput();musicTick(); if(G.freeze>0){G.freeze-=dt;dt=0;} if(G.state==='menu'){ menuUpdate(dt); renderer.render(menuScene,menuCam); if(In.jp)startGame(); return; } if(G.state==='end'){finaleTick(dt);return;} if(!L)return; if(G.state==='between'){ renderer.render(L.scene,L.cam); return; } if(G.state==='inter'){ if((In.jp||In.ap)&&performance.now()-interLock>600)beginLevel(); renderer.render(L.scene,L.cam); return; } if(G.state==='dead'){ if(In.jp&&performance.now()-interLock>600)doContinue(); renderer.render(L.scene,L.cam); return; } if(G.state==='pause'){ renderer.render(L.scene,L.cam); return; } if(G.state==='play'){ G.time-=dt; if(G.time<=0){G.time=0;updateHUD();timeOver();return;} L.update(dt); updateHUD(); if(G.state==='play'||G.state==='between')renderer.render(L.scene,L.cam); } } addEventListener('resize',()=>{ if(renderer)renderer.setSize(innerWidth,innerHeight); const cams=[menuCam,L&&L.cam].filter(Boolean); for(let i=0;i