SOURCE / PINNED RELEASE
Made of little things.
blockfall
- Release
- a6bb863994d9…
- Author-recorded commit
- e6ab5899ddac…
- License
- Check the original source
- Author’s source reference
- nostr://npub1dmmh0futyszl4445mv4xrygkgsgj8vpu79yx3qyy9qxmz4pemtzqlqmc87/wss%3A%2F%2Fgit.napplet.soy%2F/n-afafae55ef1
The hash-verified playable HTML, which may contain bundled or minified code. This view does not execute it.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#050d0c" />
<meta
name="description"
content="Steer a thrusting rocket into stacked block towers and bring the skyline down."
/>
<title>Blockfall</title>
<script type="module" crossorigin>var Dt=Object.defineProperty;var Wt=(n,t,e)=>t in n?Dt(n,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):n[t]=e;var d=(n,t,e)=>Wt(n,typeof t!="symbol"?t+"":t,e);function Nt(){const n=new Map;function t(s,a){if(n.has(s))throw new Error(`NAP domain "${s}" is already registered`);n.set(s,a)}function e(s){const a=s.type.indexOf(".");if(a<=0)return!1;const o=s.type.slice(0,a),r=n.get(o);return r?(r(s),!0):!1}function i(){return Array.from(n.keys())}return{registerNap:t,dispatch:e,getRegisteredDomains:i}}var _t=Nt(),zt=_t.registerNap,gt="storage";zt(gt,n=>{});function Gt(){const n=window;if(!n.napplet)throw new Error("window.napplet is unavailable -- runtime did not inject the napplet namespace");return n.napplet}function x(n){const e=Gt()[n];if(!e)throw new Error(`window.napplet.${String(n)} is unavailable -- runtime did not inject this domain`);return e}var xt={getItem(n){return x("storage").getItem(n)},setItem(n,t){return x("storage").setItem(n,t)},removeItem(n){return x("storage").removeItem(n)},keys(){return x("storage").keys()},instance:{getItem(n){return x("storage").instance.getItem(n)},setItem(n,t){return x("storage").instance.setItem(n,t)},removeItem(n){return x("storage").instance.removeItem(n)},keys(){return x("storage").instance.keys()}}},V={get(){return x("config").get()},subscribe(n){return x("config").subscribe(n)},openSettings(n){x("config").openSettings(n)},registerSchema(n,t){return x("config").registerSchema(n,t)},onSchemaError(n){return x("config").onSchemaError(n)},get schema(){return x("config").schema}};function Ft(n,t){return!!(n!=null&&n[t])}function kt(n){return Ft(window.napplet,n)}const m=1120,Z=600,jt=430,f=548,k=82,$t=f-26,lt=9,ht=100,ct=8,ft=270,dt=700,W=388,ut=2.9,Kt=21,Yt=900,pt=320,yt="#3f8a70",J="#ffb457",O="#ffe0a3",vt="#ff5f4a";function w(n,t,e){return n<t?t:n>e?e:n}function Q(n){let t=n;return()=>{t+=1831565813;let e=t;return e=Math.imul(e^e>>>15,e|1),e^=e+Math.imul(e^e>>>7,e|61),((e^e>>>14)>>>0)/4294967296}}function Xt(n,t){return Math.min(n.x+n.w/2,t.x+t.w/2)-Math.max(n.x-n.w/2,t.x-t.w/2)}class Ut{constructor(t,e){d(this,"canvas");d(this,"ctx");d(this,"callbacks");d(this,"blocks",[]);d(this,"rocket",null);d(this,"particles",[]);d(this,"shocks",[]);d(this,"trail",[]);d(this,"floaters",[]);d(this,"stars",[]);d(this,"skyline",[]);d(this,"phase","ready");d(this,"level",1);d(this,"score",0);d(this,"rockets",ct);d(this,"fuel",ht);d(this,"time",0);d(this,"reloadTimer",0);d(this,"clearTimer",0);d(this,"supportTimer",0);d(this,"trailTimer",0);d(this,"shake",0);d(this,"flash",0);d(this,"effects",!0);d(this,"guide",!0);d(this,"left",!1);d(this,"right",!1);d(this,"thrustHeld",!1);d(this,"disposed",!1);d(this,"cssW",0);d(this,"cssH",0);d(this,"dpr",1);d(this,"resizeObserver",null);d(this,"raf",0);d(this,"last",0);d(this,"statsTimer",0);d(this,"loop",t=>{if(this.disposed)return;const e=w((t-this.last)/1e3,.001,.05);this.last=t,this.update(e),this.draw(),this.statsTimer+=e,this.statsTimer>.08&&(this.statsTimer=0,this.callbacks.onStats(this.stats())),this.raf=requestAnimationFrame(this.loop)});this.canvas=t;const i=t.getContext("2d");if(!i)throw new Error("Canvas 2D is unavailable");this.ctx=i,this.callbacks=e,typeof ResizeObserver<"u"&&(this.resizeObserver=new ResizeObserver(()=>this.resize()),this.resizeObserver.observe(t)),this.resize(),this.seedStars(),this.startLevel(1,!1),this.callbacks.onStats(this.stats()),this.callbacks.onBanner({title:"SECTOR 01",sub:"Aim the rail · bring the skyline down",tone:"info"}),this.last=performance.now(),this.raf=requestAnimationFrame(this.loop)}setOptions(t){this.effects=t.effects,this.guide=t.guide}setSteer(t,e){this.left=t,this.right=e}pressPrimary(){this.phase==="ready"?(this.launch(),this.thrustHeld=!0):this.phase==="flying"&&(this.thrustHeld=!0)}releasePrimary(){this.thrustHeld=!1}restart(){this.score=0,this.level=1,this.startLevel(1,!0)}dispose(){var t;this.disposed=!0,cancelAnimationFrame(this.raf),(t=this.resizeObserver)==null||t.disconnect()}stats(){return{score:this.score,level:this.level,rockets:this.rockets,fuel:Math.max(0,Math.round(this.fuel)),phase:this.phase}}resize(){const t=this.canvas.clientWidth,e=this.canvas.clientHeight,i=Math.min(window.devicePixelRatio||1,2);t===this.cssW&&e===this.cssH&&i===this.dpr||(this.cssW=t,this.cssH=e,this.dpr=i,t>0&&e>0&&(this.canvas.width=Math.round(t*i),this.canvas.height=Math.round(e*i)))}seedStars(){const t=Q(9127);this.stars=[];for(let e=0;e<110;e+=1)this.stars.push({x:t()*m,y:-640+t()*1060,size:.6+t()*1.6,alpha:.1+t()*.45})}startLevel(t,e){this.level=t,this.phase="ready",this.rockets=ct,this.blocks=this.buildLevel(t),this.particles=[],this.shocks=[],this.trail=[],this.floaters=[],this.deploy(),this.buildSkyline(t),e&&this.callbacks.onBanner({title:`SECTOR ${String(t).padStart(2,"0")}`,sub:`${this.rockets} rockets · no survivors`,tone:"info"})}buildLevel(t){const e=Q(t*977+41),i=[],s=Math.min(5,3+Math.ceil(t/2)),a=["scaffold","skybridge","stack","ziggurat","scaffold","stack","ziggurat"],o=m-28;let r=322,h=(t-1)%a.length;for(let l=0;l<s;l+=1){let c=a[h%a.length];if(h+=1,r+this.structureWidth(c)>o&&(c=["scaffold","stack"].find(v=>r+this.structureWidth(v)<=o)??"",!c))break;let u=0;c==="scaffold"?u=this.addScaffold(i,r,t,e):c==="skybridge"?u=this.addSkybridge(i,r,t,e):c==="ziggurat"?u=this.addZiggurat(i,r,t,e):u=this.addBrickStack(i,r,t,e),r+=u+12+Math.floor(e()*10)}if(i.length===0)for(let l=0;l<4;l+=1)this.addBlock(i,540+l*44,f,44,24,e);return i}structureWidth(t){return t==="scaffold"?162:t==="skybridge"?222:t==="ziggurat"?192:122}addScaffold(t,e,i,s){const a=136+Math.floor(s()*3)*12,o=22,r=46+Math.floor(s()*3)*5,h=16,l=Math.min(4,2+Math.floor(i/2));let c=f;for(let u=0;u<l;u+=1){this.addBlock(t,e,c,o,r,s),this.addBlock(t,e+a-o,c,o,r,s),this.addBlock(t,e,c-r,a,h,s);const y=c-r-h,v=(a-o*2-12)/2;this.addBlock(t,e+o+4,y,v,22,s),this.addBlock(t,e+a-o-4-v,y,v,22,s),c=y}return this.addBlock(t,e+a/2-11,c-22,22,18,s),a}addSkybridge(t,e,i,s){const a=196+Math.floor(s()*3)*12,o=3+Math.min(3,Math.floor(i/2))+(s()<.4?1:0),r=84,h=42,l=22,c=f-o*l;for(let y=0;y<o;y+=1)for(let v=0;v<2;v+=1)this.addBlock(t,e+v*h,f-y*l,h,l,s),this.addBlock(t,e+a-r+v*h,f-y*l,h,l,s);this.addBlock(t,e,c,a,16,s);const u=c-16;return this.addBlock(t,e+a/2-40,u,32,24,s),this.addBlock(t,e+a/2-4,u,32,24,s),this.addBlock(t,e+a/2-9,u-24,18,16,s),a}addZiggurat(t,e,i,s){const a=4+(i>=3&&s()<.6?1:0),o=38,r=22,h=a*o;let l=f;for(let c=0;c<a;c+=1){const u=a-c,y=e+(h-u*o)/2;for(let v=0;v<u;v+=1)this.addBlock(t,y+v*o,l,o,r,s);l-=r}return this.addBlock(t,e+h/2-9,l,18,18,s),h}addBrickStack(t,e,i,s){const a=2+(s()<.55?1:0),o=4+Math.min(4,Math.floor(i/2))+Math.floor(s()*2),r=40,h=22,l=a*r;for(let u=0;u<o;u+=1)for(let y=0;y<a;y+=1)this.addBlock(t,e+y*r,f-u*h,r,h,s);const c=f-o*h;return this.addBlock(t,e,c,l,14,s),this.addBlock(t,e+2,c-14,22,18,s),this.addBlock(t,e+l-24,c-14,22,18,s),l}addBlock(t,e,i,s,a,o){const r=o()<.14?1.2:.8,h=s*a;t.push({x:e+s/2,y:i-a/2,w:s,h:a,angle:0,vx:0,vy:0,av:0,hp:r,maxHp:r,awake:!1,alive:!0,flash:0,tone:Math.floor(o()*3),mass:h,invMass:0,inertia:h*(s*s+a*a)/12,invInertia:0,restTimer:0,supported:!1})}wake(t){!t.alive||t.awake||(t.awake=!0,t.invMass=1/t.mass,t.invInertia=1/t.inertia,t.restTimer=0)}sleep(t){t.awake=!1,t.vx=0,t.vy=0,t.av=0,t.invMass=0,t.invInertia=0,t.restTimer=0}corners(t){const e=Math.cos(t.angle),i=Math.sin(t.angle),s=t.w/2,a=t.h/2;return[{x:t.x+s*e-a*i,y:t.y+s*i+a*e},{x:t.x-s*e-a*i,y:t.y-s*i+a*e},{x:t.x-s*e+a*i,y:t.y-s*i-a*e},{x:t.x+s*e+a*i,y:t.y+s*i-a*e}]}pointInBlock(t,e,i){const s=Math.cos(-i.angle),a=Math.sin(-i.angle),o=t-i.x,r=e-i.y,h=o*s-r*a,l=o*a+r*s;return Math.abs(h)<i.w/2&&Math.abs(l)<i.h/2}obbContact(t,e){const i=[];for(const l of[t,e]){const c=Math.cos(l.angle),u=Math.sin(l.angle);i.push({x:c,y:u},{x:-u,y:c})}let s=Number.POSITIVE_INFINITY,a=0,o=0;const r=t.x-e.x,h=t.y-e.y;for(const l of i){const c=this.projectionRadius(t,l.x,l.y),u=this.projectionRadius(e,l.x,l.y),y=Math.abs(r*l.x+h*l.y),v=c+u-y;if(v<=0)return null;v<s&&(s=v,a=l.x,o=l.y)}return r*a+h*o<0&&(a=-a,o=-o),{nx:a,ny:o,depth:s}}projectionRadius(t,e,i){const s=Math.cos(t.angle),a=Math.sin(t.angle),o=s,r=a,h=-a,l=s;return Math.abs(e*o+i*r)*(t.w/2)+Math.abs(e*h+i*l)*(t.h/2)}contactPoints(t,e){const i=[],s=(a,o)=>{for(const r of i)if(Math.hypot(r.x-a,r.y-o)<1.2)return;i.push({x:a,y:o})};for(const a of this.corners(t))this.pointInBlock(a.x,a.y,e)&&s(a.x,a.y);for(const a of this.corners(e))this.pointInBlock(a.x,a.y,t)&&s(a.x,a.y);if(i.length===0){let a=Number.POSITIVE_INFINITY,o=this.corners(t)[0],r=this.corners(e)[0];for(const h of this.corners(t))for(const l of this.corners(e)){const c=Math.hypot(h.x-l.x,h.y-l.y);c<a&&(a=c,o=h,r=l)}s((o.x+r.x)/2,(o.y+r.y)/2)}return i.slice(0,2)}buildSkyline(t){const e=Q(t*3301+7);this.skyline=[];let i=-20;for(;i<m+40;){const s=34+e()*86,a=26+e()*104;this.skyline.push({x:i,w:s,h:a}),i+=s+5+e()*14}}deploy(){this.rocket={x:k,y:$t,vx:0,vy:0,angle:-.62},this.phase="ready",this.fuel=ht,this.trail=[],this.thrustHeld=!1,this.trailTimer=0}launch(){const t=this.rocket;!t||this.phase!=="ready"||(t.vx=Math.cos(t.angle)*W,t.vy=Math.sin(t.angle)*W,this.rockets=Math.max(0,this.rockets-1),this.phase="flying",this.trail=[],this.shake=Math.max(this.shake,2),this.spawnBurst(t.x,t.y,8,80,"dust"),this.callbacks.onStats(this.stats()))}update(t){if(this.time+=t,this.shake=Math.max(0,this.shake-t*26),this.flash=Math.max(0,this.flash-t*2.6),this.updateParticles(t),this.updateBlocks(t),this.phase==="ready"){const e=this.rocket;if(e){const i=(this.right?1:0)-(this.left?1:0);e.angle+=i*ut*t}}else this.phase==="flying"?this.updateFlight(t):this.phase==="reloading"?(this.reloadTimer-=t,this.reloadTimer<=0&&(this.blocks.every(e=>!e.alive)?this.winLevel():this.rockets<=0?this.gameOver():this.deploy())):this.phase==="clear"&&(this.clearTimer-=t,this.clearTimer<=0&&this.startLevel(this.level+1,!0));this.supportTimer+=t,this.supportTimer>.07&&(this.supportTimer=0,this.checkSupport()),(this.phase==="flying"||this.phase==="ready"||this.phase==="reloading")&&this.blocks.length>0&&this.blocks.every(e=>!e.alive)&&this.winLevel();for(const e of this.blocks)e.flash=Math.max(0,e.flash-t*3.5)}updateFlight(t){const e=this.rocket;if(!e){this.finishSortie();return}const i=(this.right?1:0)-(this.left?1:0);e.angle+=i*ut*t,this.thrustHeld&&this.fuel>0&&(e.vx+=Math.cos(e.angle)*dt*t,e.vy+=Math.sin(e.angle)*dt*t,this.fuel=Math.max(0,this.fuel-Kt*t),this.effects&&this.spawnExhaust(e)),e.vy+=ft*t;const s=Math.pow(.9995,t*60);e.vx*=s,e.vy*=s;const a=Math.hypot(e.vx,e.vy);if(a>920&&(e.vx=e.vx/a*920,e.vy=e.vy/a*920),e.x+=e.vx*t,e.y+=e.vy*t,this.trailTimer-=t,this.trailTimer<=0&&this.effects&&(this.trail.push({x:e.x,y:e.y,life:1}),this.trail.length>60&&this.trail.shift(),this.trailTimer=.028),this.collideBlocks(e),!!this.rocket){if(e.y+lt>=f){this.explode(e.x,f-5,1.5);return}(e.x>m+140||e.x<-180||e.y<-900)&&(this.rocket=null,this.finishSortie())}}collideBlocks(t){for(const e of this.blocks){if(!e.alive)continue;const i=Math.cos(e.angle),s=Math.sin(e.angle),a=t.x-e.x,o=t.y-e.y,r=a*i+o*s,h=-a*s+o*i,l=e.w/2,c=e.h/2,u=w(r,-l,l),y=w(h,-c,c),v=r-u,B=h-y;if(Math.hypot(v,B)>=lt)continue;const j=e.x+u*i-y*s,$=e.y+u*s+y*i;this.explode(j,$,1.85);return}}updateBlocks(t){const e=this.blocks;let i=0;for(const s of e)!s.alive||!s.awake||(i+=1,s.vy+=Yt*t,s.x+=s.vx*t,s.y+=s.vy*t,s.angle+=s.av*t,s.vx*=Math.pow(.999,t*60),s.vy*=Math.pow(.9995,t*60),s.av*=Math.pow(.985,t*60),s.av=w(s.av,-9,9),s.supported=!1);if(i!==0){for(let s=0;s<6;s+=1){const a=s===0;for(const o of e)!o.alive||!o.awake||this.resolveGround(o,a);for(let o=0;o<e.length;o+=1){const r=e[o];if(!(!r.alive||!r.awake))for(let h=0;h<e.length;h+=1){if(o===h)continue;const l=e[h];l.alive&&(l.awake&&h<o||this.resolvePair(r,l,a))}}}for(const s of e){if(!s.alive||!s.awake)continue;const a=Math.hypot(s.vx,s.vy);s.supported&&a<11&&Math.abs(s.av)<.24?(s.restTimer+=t,s.restTimer>.5&&this.sleep(s)):s.restTimer=0}}}resolveGround(t,e){const s=this.corners(t).filter(o=>o.y>f);if(s.length===0)return;let a=0;for(const o of s)a=Math.max(a,o.y-f);t.y-=a,t.supported=!0;for(const o of s)this.applyContact(t,null,o.x,f,0,-1,.1,e)}resolvePair(t,e,i){if(!t.alive||!e.alive)return;const s=this.obbContact(t,e);if(!s)return;const{nx:a,ny:o,depth:r}=s,h=(t.vx-e.vx)*a+(t.vy-e.vy)*o;!e.awake&&h<-90&&this.wake(e);const l=t.invMass+e.invMass;if(l<=0)return;const c=Math.min(r,9)*.55/l;t.x+=a*c*t.invMass,t.y+=o*c*t.invMass,e.awake&&(e.x-=a*c*e.invMass,e.y-=o*c*e.invMass);for(const u of this.contactPoints(t,e))this.applyContact(t,e,u.x,u.y,a,o,.03,i)}applyContact(t,e,i,s,a,o,r,h){if(!t.alive)return;const l=e!==null&&e.awake&&e.alive,c=l?e.invMass:0,u=l?e.invInertia:0,y=i-t.x,v=s-t.y,B=e?i-e.x:0,E=e?s-e.y:0,j=t.vx-t.av*v,$=t.vy+t.av*y,It=l?e.vx-e.av*E:0,At=l?e.vy+e.av*B:0,Lt=j-It,Et=$-At,K=Lt*a+Et*o;if(K>0)return;const D=-K,Y=y*o-v*a,X=B*o-E*a,nt=t.invMass+c+Y*Y*t.invInertia+X*X*u;if(nt<=0)return;const b=-(1+(D>120?r:0))*K/nt;t.vx+=b*a*t.invMass,t.vy+=b*o*t.invMass,t.av+=Y*b*t.invInertia,l&&(e.vx-=b*a*e.invMass,e.vy-=b*o*e.invMass,e.av-=X*b*e.invInertia);const C=-o,H=a,Ct=t.vx-t.av*v-(l?e.vx-e.av*E:0),Ht=t.vy+t.av*y-(l?e.vy+e.av*B:0),Ot=Ct*C+Ht*H,U=y*H-v*C,q=B*H-E*C,ot=t.invMass+c+U*U*t.invInertia+q*q*u;if(ot>0){const rt=.62*Math.abs(b),P=w(-Ot/ot,-rt,rt);t.vx+=P*C*t.invMass,t.vy+=P*H*t.invMass,t.av+=U*P*t.invInertia,l&&(e.vx-=P*C*e.invMass,e.vy-=P*H*e.invMass,e.av-=q*P*e.invInertia)}o<-.25&&(t.supported=!0),l&&o>.25&&(e.supported=!0),h&&D>300&&(this.damageBlock(t,(D-300)*.0016,i,s,0),e&&e.alive&&this.damageBlock(e,(D-300)*.001,i,s,0))}checkSupport(){for(const t of this.blocks){if(!t.alive||t.awake||t.y+t.h/2>=f-2)continue;const e=t.y+t.h/2;let i=!1;for(const s of this.blocks){if(s===t||!s.alive)continue;const a=s.y-s.h/2;if(Math.abs(a-e)<3&&Xt(t,s)>5){i=!0;break}}i||(this.wake(t),t.av=(Math.random()-.5)*.35,this.effects&&this.particles.length<pt-4&&this.spawnBurst(t.x,t.y+t.h/2,2,26,"dust"))}}damageArea(t,e,i,s,a){for(const o of this.blocks){if(!o.alive)continue;const r=Math.hypot(o.x-t,o.y-e);if(r>i+Math.max(o.w,o.h)*.45)continue;const h=w(1-r/(i*1.5),.22,1);this.damageBlock(o,s*h,t,e,a*h)}}damageBlock(t,e,i,s,a){if(t.alive){if(t.hp-=e,t.flash=1,this.wake(t),a>0){const o=t.x-i,r=t.y-s,h=Math.hypot(o,r)||1,l=w(900/t.mass,.35,1.5);t.vx+=o/h*a*.55*l,t.vy+=r/h*a*.55*l-a*.04,t.av+=(Math.random()-.5)*a*.0035*l}t.hp<=0&&this.destroyBlock(t)}}destroyBlock(t){if(t.alive){t.alive=!1,this.score+=100;for(const e of this.blocks){if(!e.alive||e===t)continue;const i=e.x-t.x,s=e.y-t.y;Math.hypot(i,s)<96&&this.wake(e)}this.effects&&this.spawnBurst(t.x,t.y,7,150,"shard"),this.floaters.length<18&&this.floaters.push({x:t.x,y:t.y,life:.85,text:"+100"})}}explode(t,e,i){this.rocket=null,this.shake=Math.max(this.shake,18*Math.min(i,2)),this.flash=Math.max(this.flash,Math.min(1,.48*i)),this.effects&&(this.shocks.push({x:t,y:e,life:.65,maxLife:.65,maxRadius:120+70*i}),this.shocks.push({x:t,y:e,life:.34,maxLife:.34,maxRadius:55+40*i}),this.spawnBurst(t,e,Math.min(46,Math.round(24*i)),330*i,"spark"),this.spawnBurst(t,e,Math.min(26,Math.round(15*i)),165*i,"dust"),this.spawnBurst(t,e,Math.min(18,Math.round(10*i)),260*i,"shard")),this.damageArea(t,e,100+50*i,.62*i,360*i),this.finishSortie()}finishSortie(){if(this.phase==="flying"){if(this.thrustHeld=!1,this.blocks.length>0&&this.blocks.every(t=>!t.alive)){this.winLevel();return}this.phase="reloading",this.reloadTimer=this.rockets<=0?2.6:1}}winLevel(){if(this.phase==="clear"||this.phase==="over")return;const t=500+this.rockets*150;this.score+=t,this.phase="clear",this.clearTimer=2.4,this.rocket=null,this.callbacks.onStats(this.stats()),this.callbacks.onBanner({title:"SECTOR CLEAR",sub:`+${t} bonus · next sector inbound`,tone:"win"})}gameOver(){this.phase="over",this.rocket=null,this.callbacks.onStats(this.stats()),this.callbacks.onBanner({title:"RANGE CLOSED",sub:`Out of rockets · ${this.score} points`,tone:"lose"})}spawnExhaust(t){const e=t.angle+Math.PI,i=(Math.random()-.5)*.5,s=90+Math.random()*120;this.particles.push({x:t.x+Math.cos(e)*10,y:t.y+Math.sin(e)*10,vx:t.vx*.35+Math.cos(e+i)*s,vy:t.vy*.35+Math.sin(e+i)*s,life:.28,maxLife:.28,size:2+Math.random()*3,kind:"dust",tone:Math.random()<.3?O:J})}spawnBurst(t,e,i,s,a){for(let o=0;o<i&&!(this.particles.length>pt);o+=1){const r=Math.random()*Math.PI*2,h=s*(.3+Math.random()*.8);this.particles.push({x:t,y:e,vx:Math.cos(r)*h,vy:Math.sin(r)*h-(a==="dust"?40:20),life:.4+Math.random()*.7,maxLife:1.1,size:a==="shard"?2+Math.random()*4:1.5+Math.random()*2.5,kind:a,tone:a==="spark"?Math.random()<.4?O:vt:a==="shard"?Math.random()<.5?yt:"#8fb7a4":"#6d8c7d"})}}updateParticles(t){for(const e of this.particles)e.life-=t,e.vy+=(e.kind==="dust"?120:620)*t,e.x+=e.vx*t,e.y+=e.vy*t,e.vx*=Math.pow(.985,t*60),e.y>f&&(e.y=f,e.vy*=-.25,e.vx*=.8);this.particles=this.particles.filter(e=>e.life>0);for(const e of this.shocks)e.life-=t;this.shocks=this.shocks.filter(e=>e.life>0);for(const e of this.trail)e.life-=t*.9;this.trail=this.trail.filter(e=>e.life>0);for(const e of this.floaters)e.life-=t,e.y-=t*26;this.floaters=this.floaters.filter(e=>e.life>0)}draw(){if(this.cssW<2||this.cssH<2)return;const{ctx:t}=this,e=Math.min(this.cssW/m,this.cssH/jt),i=(this.cssW-m*e)/2,s=this.cssH-Z*e,a=-s/e;t.setTransform(1,0,0,1,0,0),t.fillStyle="#03080a",t.fillRect(0,0,this.canvas.width,this.canvas.height),t.setTransform(this.dpr*e,0,0,this.dpr*e,this.dpr*i,this.dpr*s);const o=this.shake>.1?(Math.random()-.5)*this.shake:0,r=this.shake>.1?(Math.random()-.5)*this.shake:0;t.translate(o,r),this.drawSky(a),this.drawSkyline(),this.drawGround(),this.guide&&this.drawGuide(),this.drawPad(),this.drawBlocks(),this.drawTrail(),this.rocket&&this.drawRocket(this.rocket),this.drawParticles(),this.drawShocks(),this.drawFloaters(),this.flash>.01&&(t.setTransform(1,0,0,1,0,0),t.fillStyle=`rgba(255,214,150,${(this.flash*.22).toFixed(3)})`,t.fillRect(0,0,this.canvas.width,this.canvas.height))}drawSky(t){const{ctx:e}=this,i=f+20,s=e.createLinearGradient(0,Math.min(t,-200),0,i);s.addColorStop(0,"#040b0f"),s.addColorStop(.62,"#081a1a"),s.addColorStop(1,"#0d2a23"),e.fillStyle=s,e.fillRect(-200,t-40,m+400,i-t+60),e.fillStyle="#9ce9c6";for(const c of this.stars)c.y<t||c.y>f-60||(e.globalAlpha=c.alpha,e.fillRect(c.x,c.y,c.size,c.size));e.globalAlpha=1,e.strokeStyle="rgba(125,243,184,0.05)",e.lineWidth=1;const a=0;for(let c=a;c<=m;c+=50)e.beginPath(),e.moveTo(c,t),e.lineTo(c,f),e.stroke();const o=Math.floor(t/50)*50;for(let c=o;c<=f;c+=50)e.globalAlpha=c%200===0?.75:.45,e.beginPath(),e.moveTo(0,c),e.lineTo(m,c),e.stroke();e.globalAlpha=1;const r=816,h=128;e.strokeStyle="rgba(125,243,184,0.09)",e.lineWidth=1.5,e.beginPath(),e.arc(r,h,48,0,Math.PI*2),e.stroke(),e.beginPath(),e.moveTo(r-58,h),e.lineTo(r+58,h),e.moveTo(r,h-58),e.lineTo(r,h+58),e.stroke();const l=e.createRadialGradient(220,300,10,220,300,460);l.addColorStop(0,"rgba(125,243,184,0.05)"),l.addColorStop(1,"rgba(125,243,184,0)"),e.fillStyle=l,e.fillRect(0,t,m,f-t)}drawSkyline(){const{ctx:t}=this;t.fillStyle="#081614";for(const e of this.skyline)t.fillRect(e.x,f-e.h,e.w,e.h);t.fillStyle="rgba(125,243,184,0.16)";for(const e of this.skyline)e.h<52||t.fillRect(e.x,f-e.h,e.w,1);t.strokeStyle="rgba(125,243,184,0.12)",t.lineWidth=1,t.beginPath(),t.moveTo(0,f-.5),t.lineTo(m,f-.5),t.stroke()}drawGround(){const{ctx:t}=this,e=t.createLinearGradient(0,f,0,Z);e.addColorStop(0,"#0b1f1a"),e.addColorStop(1,"#050d0c"),t.fillStyle=e,t.fillRect(0,f,m,Z-f+80),t.strokeStyle="rgba(125,243,184,0.55)",t.lineWidth=1.5,t.beginPath(),t.moveTo(0,f),t.lineTo(m,f),t.stroke(),t.fillStyle="rgba(255,180,87,0.45)";for(let i=8;i<m;i+=44)t.fillRect(i,f+7,18,2.5);t.strokeStyle="rgba(125,243,184,0.13)",t.lineWidth=1;for(let i=120;i<m;i+=120)t.beginPath(),t.moveTo(i,f+1),t.lineTo(i-12,f+20),t.stroke()}drawGuide(){const t=this.rocket;if(this.phase!=="ready"||!t)return;const{ctx:e}=this;let i=t.x,s=t.y,a=Math.cos(t.angle)*W,o=Math.sin(t.angle)*W;const r=.06;for(let h=0;h<70&&(i+=a*r,s+=o*r,o+=ft*r,!(s>f||i>m+40||i<-40));h+=1)if(h%3===0){const l=.5*(1-h/70);e.fillStyle=`rgba(255,180,87,${l.toFixed(3)})`,e.beginPath(),e.arc(i,s,1.6,0,Math.PI*2),e.fill()}}drawPad(){const{ctx:t}=this;t.fillStyle="#102720",t.fillRect(k-24,f-6,48,6),t.strokeStyle=yt,t.lineWidth=1.5,t.beginPath(),t.moveTo(k-20,f-6),t.lineTo(k-20,f-14),t.lineTo(k+20,f-14),t.lineTo(k+20,f-6),t.stroke(),t.fillStyle="rgba(255,180,87,0.5)",t.fillRect(k-16,f-12,8,2),t.fillRect(k-4,f-12,8,2),t.fillRect(k+8,f-12,8,2),t.strokeStyle="rgba(125,243,184,0.3)",t.beginPath(),t.moveTo(k+10,f-18),t.lineTo(k+26,f-18),t.stroke()}drawBlocks(){const{ctx:t}=this;for(const e of this.blocks){if(!e.alive)continue;const i=1-e.hp/e.maxHp,s=e.w,a=e.h;t.save(),t.translate(e.x,e.y),t.rotate(e.angle),t.fillStyle=e.tone===0?"#143129":e.tone===1?"#183a30":"#112a25",t.fillRect(-s/2,-a/2,s,a),t.strokeStyle=i>.55?"rgba(255,95,74,0.65)":"rgba(125,243,184,0.38)",t.lineWidth=1,t.strokeRect(-s/2+.75,-a/2+.75,s-1.5,a-1.5),t.fillStyle="rgba(125,243,184,0.22)",t.fillRect(-s/2+2,-a/2+2,s-4,1),s>26&&(t.fillStyle="rgba(8,20,17,0.6)",t.fillRect(-s/2+4,a*.02,s-8,1),t.fillStyle="rgba(125,243,184,0.2)",t.fillRect(-s/2+5,a*.02+2,2,2),t.fillRect(s/2-8,a*.02+2,2,2)),i>.28&&(t.strokeStyle="rgba(255,95,74,0.5)",t.lineWidth=1,t.beginPath(),t.moveTo(-s*.28,-a/2+2),t.lineTo(-s*.12,-a*.05),t.lineTo(-s*.2,a/2-2),t.stroke()),i>.6&&(t.beginPath(),t.moveTo(s*.2,-a/2+1),t.lineTo(s*.08,a*.1),t.lineTo(s*.28,a/2-2),t.stroke()),e.flash>0&&(t.fillStyle=`rgba(255,224,163,${(e.flash*.22).toFixed(3)})`,t.fillRect(-s/2,-a/2,s,a)),t.restore()}}drawTrail(){if(this.trail.length<2)return;const{ctx:t}=this;t.strokeStyle="rgba(255,180,87,0.16)",t.lineWidth=2,t.beginPath();const e=this.trail[0];t.moveTo(e.x,e.y);for(const i of this.trail)t.lineTo(i.x,i.y);t.stroke();for(const i of this.trail)t.fillStyle=`rgba(255,196,120,${(i.life*.25).toFixed(3)})`,t.beginPath(),t.arc(i.x,i.y,1.8,0,Math.PI*2),t.fill()}drawRocket(t){const{ctx:e}=this;if(e.save(),e.translate(t.x,t.y),e.rotate(t.angle),this.phase==="flying"&&this.thrustHeld&&this.fuel>0){const i=14+Math.random()*12+Math.sin(this.time*40)*3,s=e.createLinearGradient(-8,0,-8-i,0);s.addColorStop(0,"#fff2c9"),s.addColorStop(.4,J),s.addColorStop(1,"rgba(255,95,74,0)"),e.fillStyle=s,e.beginPath(),e.moveTo(-8,-4.5),e.lineTo(-8-i,0),e.lineTo(-8,4.5),e.closePath(),e.fill()}e.fillStyle="#23372f",e.beginPath(),e.moveTo(-8,-4),e.lineTo(-13,-11),e.lineTo(-6,-7),e.closePath(),e.fill(),e.beginPath(),e.moveTo(-8,4),e.lineTo(-13,11),e.lineTo(-6,7),e.closePath(),e.fill(),e.fillStyle=O,e.beginPath(),e.moveTo(14,0),e.lineTo(2,6.4),e.lineTo(-7,6),e.lineTo(-9,0),e.lineTo(-7,-6),e.lineTo(2,-6.4),e.closePath(),e.fill(),e.strokeStyle="#8a4c12",e.lineWidth=1,e.stroke(),e.fillStyle="#2b6f7c",e.beginPath(),e.arc(3,0,2.1,0,Math.PI*2),e.fill(),e.globalAlpha=.35+.15*Math.sin(this.time*8),e.strokeStyle=J,e.lineWidth=1,e.beginPath(),e.arc(0,0,15,0,Math.PI*2),e.stroke(),e.globalAlpha=1,e.restore()}drawParticles(){const{ctx:t}=this;for(const e of this.particles){const i=w(e.life/Math.max(e.maxLife,.001),0,1);t.globalAlpha=i,e.kind==="spark"?(t.strokeStyle=e.tone,t.lineWidth=1.4,t.beginPath(),t.moveTo(e.x,e.y),t.lineTo(e.x-e.vx*.02,e.y-e.vy*.02),t.stroke()):e.kind==="shard"?(t.fillStyle=e.tone,t.fillRect(e.x-e.size/2,e.y-e.size/2,e.size,e.size*.7)):(t.fillStyle=e.tone,t.beginPath(),t.arc(e.x,e.y,e.size,0,Math.PI*2),t.fill())}t.globalAlpha=1}drawShocks(){const{ctx:t}=this;for(const e of this.shocks){const i=1-e.life/e.maxLife;t.globalAlpha=(1-i)*.7,t.strokeStyle=i<.4?O:vt,t.lineWidth=2*(1-i)+.5,t.beginPath(),t.arc(e.x,e.y,e.maxRadius*i,0,Math.PI*2),t.stroke()}t.globalAlpha=1}drawFloaters(){const{ctx:t}=this;t.font='600 12px "SFMono-Regular", ui-monospace, monospace',t.textAlign="center";for(const e of this.floaters)t.globalAlpha=w(e.life/.85,0,1),t.fillStyle=O,t.fillText(e.text,e.x,e.y);t.globalAlpha=1,t.textAlign="left"}}const qt="http://json-schema.org/draft-07/schema#",Vt=1,Zt="object",Jt=!1,Qt={effects:{type:"boolean",title:"Impact effects",description:"Show debris, sparks, shockwaves and launch exhaust. Turn off for a quieter simulation.",default:!0,"x-napplet-section":"Presentation","x-napplet-order":1},trajectoryGuide:{type:"boolean",title:"Trajectory guide",description:"Show the ballistic arc while the rocket waits on the launch rail.",default:!0,"x-napplet-section":"Presentation","x-napplet-order":2}},te={$schema:qt,$version:Vt,type:Zt,additionalProperties:Jt,properties:Qt},N={effects:!0,trajectoryGuide:!0},tt=new Set;function ee(n){return tt.add(n),n({...N}),()=>{tt.delete(n)}}function se(){const n={...N};for(const t of tt)t(n)}kt("config")&&(async()=>{V.schema||await V.registerSchema(te);const n=V.subscribe(t=>{typeof t.effects=="boolean"&&(N.effects=t.effects),typeof t.trajectoryGuide=="boolean"&&(N.trajectoryGuide=t.trajectoryGuide),se()});window.addEventListener("pagehide",()=>n.close(),{once:!0})})().catch(()=>{});const Mt="blockfall.best-score",wt=8,p={canvas:g("#scene"),score:g("#score"),level:g("#level"),best:g("#best"),fuel:g("#fuelFill"),ammo:g("#ammo"),ammoCount:g("#ammoCount"),hint:g("#hint"),thrustButton:g("#thrustBtn"),thrustLabel:g("#thrustLabel"),leftButton:g("#leftBtn"),rightButton:g("#rightBtn"),restartButton:g("#restartBtn"),banner:g("#banner"),bannerTitle:g("#bannerTitle"),bannerSub:g("#bannerSub")};function g(n){const t=document.querySelector(n);if(!t)throw new Error(`Missing required element: ${n}`);return t}function et(n,t){return String(Math.max(0,Math.floor(n))).padStart(t,"0")}const ie={ready:"Aim the rail · launch when locked",flying:"Aim for supports · collapse the towers",reloading:"Loading next rocket",clear:"Sector cleared · standby",over:"Range closed · reopen to retry"},St=[];for(let n=0;n<wt;n+=1){const t=document.createElement("span");t.className="pip",p.ammo.append(t),St.push(t)}let S=0,st=0,_=null,z=!1,G=!1,R=!1,I=!1,F=!1,A=!1,mt=0;const Tt=kt(gt);function it(){p.best.textContent=et(S,6)}function bt(n){for(const[t,e]of St.entries())e.classList.toggle("spent",t>=n);p.ammoCount.textContent=`×${n}`,p.ammo.setAttribute("aria-label",`${n} rockets remaining`)}function ae(n){p.score.textContent=et(n.score,6),p.level.textContent=et(n.level,2),p.fuel.style.width=`${n.fuel}%`,p.fuel.dataset.low=n.fuel<25?"true":"false",p.hint.textContent=ie[n.phase],p.thrustLabel.textContent=n.phase==="flying"?"Thrust":"Launch",bt(n.rockets);const t=n.phase==="ready"||n.phase==="flying";p.leftButton.disabled=!t,p.rightButton.disabled=!t,p.thrustButton.disabled=!t,p.restartButton.hidden=n.phase!=="over",n.score>S&&(S=n.score,it()),n.phase!==_&&((n.phase==="over"||n.phase==="clear")&&Bt(),_=n.phase)}function ne(n){p.banner.hidden=!1,p.banner.dataset.tone=n.tone,p.bannerTitle.textContent=n.title,p.bannerSub.textContent=n.sub,p.banner.classList.remove("is-in"),p.banner.offsetWidth,p.banner.classList.add("is-in"),window.clearTimeout(mt),n.tone!=="lose"&&(mt=window.setTimeout(()=>{p.banner.hidden=!0},2500))}async function oe(){if(Tt)try{const n=await xt.getItem(Mt),t=Number(n);Number.isFinite(t)&&t>0&&(S=Math.floor(t),st=S,it())}catch{}}function Bt(){if(!(!Tt||S<=st)){st=S;try{xt.setItem(Mt,String(S)).catch(()=>{})}catch{}}}const T=new Ut(p.canvas,{onStats:ae,onBanner:ne});ee(n=>{T.setOptions({effects:n.effects,guide:n.trajectoryGuide})});function M(){T.setSteer(z||R,G||I)}function L(){F||A?T.pressPrimary():T.releasePrimary()}function Pt(){z=!1,G=!1,F=!1,R=!1,I=!1,A=!1,M(),L()}window.addEventListener("keydown",n=>{const t=n.target;if(!(t instanceof HTMLElement&&(t.tagName==="INPUT"||t.tagName==="TEXTAREA"||t.tagName==="SELECT")))switch(n.code){case"ArrowLeft":case"KeyA":n.preventDefault(),z=!0,M();break;case"ArrowRight":case"KeyD":n.preventDefault(),G=!0,M();break;case"ArrowUp":case"KeyW":case"Space":if(n.preventDefault(),n.repeat)break;_==="over"?T.restart():(F=!0,L());break;case"KeyR":_==="over"&&T.restart();break}});window.addEventListener("keyup",n=>{switch(n.code){case"ArrowLeft":case"KeyA":z=!1,M();break;case"ArrowRight":case"KeyD":G=!1,M();break;case"ArrowUp":case"KeyW":case"Space":F=!1,L();break}});window.addEventListener("blur",Pt);p.canvas.addEventListener("contextmenu",n=>n.preventDefault());function at(n,t,e){n.addEventListener("pointerdown",s=>{s.preventDefault(),n.classList.add("is-held");try{n.setPointerCapture(s.pointerId)}catch{}t()});const i=s=>{n.classList.remove("is-held"),n.hasPointerCapture(s.pointerId)&&n.releasePointerCapture(s.pointerId),e()};n.addEventListener("pointerup",i),n.addEventListener("pointercancel",i),n.addEventListener("lostpointercapture",()=>{n.classList.remove("is-held"),e()})}at(p.leftButton,()=>{R=!0,M()},()=>{R=!1,M()});at(p.rightButton,()=>{I=!0,M()},()=>{I=!1,M()});at(p.thrustButton,()=>{A=!0,L()},()=>{A=!1,L()});p.restartButton.addEventListener("click",()=>{Pt(),T.restart()});function Rt(){!R&&!I&&!A||(R=!1,I=!1,A=!1,M(),L())}window.addEventListener("pointerup",Rt);window.addEventListener("pointercancel",Rt);it();bt(wt);oe();window.addEventListener("pagehide",()=>{Bt(),T.dispose()},{once:!0});
</script>
<style>:root{--void: #03080a;--panel: #071310;--panel-2: #0a1a15;--line: rgba(125, 243, 184, .22);--line-strong: rgba(142, 255, 205, .5);--ink: #cdf6e2;--muted: #6f9c88;--phosphor: #7df3b8;--amber: #ffb457;--amber-hot: #ffe0a3;--hazard: #ff5f4a;--ui: "SFMono-Regular", ui-monospace, "Cascadia Mono", Menlo, Consolas, monospace;--radius: 3px;color-scheme:dark;font:12px/1.35 var(--ui)}*{box-sizing:border-box}:where(html,body,body *){-webkit-user-select:none;user-select:none}html,body,#app{height:100%;margin:0;background:var(--void);color:var(--ink)}body{overflow:hidden}.app{container-type:inline-size;display:grid;grid-template-rows:auto minmax(0,1fr) auto;gap:6px;padding:8px;overflow:hidden;background:radial-gradient(120% 90% at 50% 110%,rgba(22,66,52,.38),transparent 62%),var(--void)}.hud{display:grid;grid-template-columns:1fr auto 1fr;align-items:center;gap:10px;min-height:22px;padding:0 2px}.hud-cell{display:flex;align-items:baseline;gap:8px;min-width:0}.hud-right{justify-content:flex-end}.hud-key{color:var(--muted);font-size:9px;letter-spacing:.18em;text-transform:uppercase;white-space:nowrap}.hud-value{color:var(--phosphor);font-size:16px;font-weight:700;letter-spacing:.08em;font-variant-numeric:tabular-nums;text-shadow:0 0 12px rgba(125,243,184,.35)}.best{color:var(--muted);font-size:8px;letter-spacing:.14em;text-transform:uppercase;white-space:nowrap}.best b{color:var(--amber);font-weight:700;font-variant-numeric:tabular-nums}.hud-brand{display:flex;align-items:center;gap:8px;color:var(--amber);font-size:11px;font-weight:700;letter-spacing:.26em;text-transform:uppercase;white-space:nowrap}.hud-brand em{color:var(--muted);font-size:8px;font-style:normal;font-weight:400;letter-spacing:.16em}.brand-mark{width:7px;height:7px;border-radius:50%;background:var(--hazard);box-shadow:0 0 10px var(--hazard);animation:beacon 2.4s ease-in-out infinite}.stage{position:relative;min-height:0;overflow:hidden;border:1px solid var(--line);border-radius:var(--radius);background:#050d0f;box-shadow:inset 0 0 60px #000000b3}.stage canvas{display:block;width:100%;height:100%;touch-action:none}.stage:before{content:"";position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;pointer-events:none;background:radial-gradient(ellipse at 50% 45%,transparent 48%,rgba(0,0,0,.55) 100%),linear-gradient(90deg,rgba(0,0,0,.35),transparent 12%,transparent 88%,rgba(0,0,0,.35))}.stage:after{content:"";position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;pointer-events:none;opacity:.3;background:repeating-linear-gradient(0deg,rgba(0,0,0,.32) 0,rgba(0,0,0,.32) 1px,transparent 1px,transparent 3px)}.banner{position:absolute;top:20%;left:50%;z-index:2;min-width:min(340px,82%);padding:10px 20px 11px;border-top:1px solid var(--phosphor);border-bottom:1px solid var(--phosphor);background:#030c0ae0;text-align:center;pointer-events:none;transform:translate(-50%);opacity:0}.banner.is-in{animation:banner-in .36s ease-out forwards}.banner[data-tone=win]{border-color:var(--amber)}.banner[data-tone=lose]{border-color:var(--hazard)}.banner p{margin:0}#bannerTitle{color:var(--phosphor);font-size:clamp(14px,3.1vw,22px);font-weight:700;letter-spacing:.26em;text-transform:uppercase}.banner[data-tone=win] #bannerTitle{color:var(--amber-hot)}.banner[data-tone=lose] #bannerTitle{color:var(--hazard)}#bannerSub{margin-top:4px;color:var(--muted);font-size:9px;letter-spacing:.14em;text-transform:uppercase}.restart{position:absolute;bottom:10%;left:50%;z-index:3;padding:9px 20px;transform:translate(-50%);border:1px solid var(--amber);border-radius:var(--radius);background:#120b03eb;color:var(--amber-hot);font:700 11px/1 var(--ui);letter-spacing:.22em;text-transform:uppercase;cursor:pointer}.restart:hover{background:var(--amber);color:#1b0f00}.restart:focus-visible{outline:2px solid var(--phosphor);outline-offset:2px}.console{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:0 2px;min-height:44px}.readout{display:flex;align-items:center;gap:14px;flex:1;min-width:0}.fuel{display:grid;gap:4px;flex:0 1 132px;min-width:58px}.fuel-track{position:relative;height:6px;overflow:hidden;border:1px solid var(--line);background:#04100c}.fuel-track span{display:block;width:100%;height:100%;background:linear-gradient(90deg,#2f9f74,var(--phosphor));transition:width .12s linear}.fuel-track span[data-low=true]{background:linear-gradient(90deg,#a23825,var(--hazard))}.ammo{display:grid;gap:4px}.ammo-row{display:flex;align-items:center;gap:6px}.ammo-count{color:var(--amber);font-size:11px;font-weight:700;letter-spacing:.08em;font-variant-numeric:tabular-nums;white-space:nowrap}.ammo-pips{display:flex;gap:3px;min-height:7px}@container (max-width: 620px){.ammo-pips{display:none}}.pip{width:9px;height:7px;background:var(--amber);clip-path:polygon(0 100%,45% 0,100% 100%);transition:opacity .2s ease,background .2s ease}.pip.spent{background:#27453a;opacity:.55}.hint{margin:0;overflow:hidden;color:var(--muted);font-size:9px;letter-spacing:.12em;text-transform:uppercase;white-space:nowrap;text-overflow:ellipsis}.controls{display:flex;align-items:stretch;gap:6px}.controls button{border:1px solid var(--line-strong);border-radius:var(--radius);background:linear-gradient(180deg,#10241d,#081511);color:var(--ink);font:700 11px/1 var(--ui);letter-spacing:.18em;text-transform:uppercase;cursor:pointer;touch-action:none;-webkit-tap-highlight-color:transparent}.pad{width:44px;min-height:44px;color:var(--phosphor);font-size:13px}.thrust{width:94px;min-height:44px;border-color:var(--amber-hot);background:linear-gradient(180deg,var(--amber-hot),var(--amber));color:#1b0f00}.controls button:active,.controls button.is-held{transform:translateY(1px);filter:brightness(1.08)}.controls button:hover:not(:disabled){border-color:var(--phosphor)}.controls button:disabled{opacity:.3;cursor:default;transform:none}.controls button:focus-visible{outline:2px solid var(--phosphor);outline-offset:2px}@container (max-width: 460px){.hud-brand em,.hint{display:none}.console,.readout{gap:8px}.fuel{flex-basis:84px;min-width:50px}}@container (max-width: 340px){.best{display:none}.hud-brand{font-size:9px;letter-spacing:.16em}.hud-value{font-size:13px}.pad{width:40px;min-height:40px}.thrust{width:82px;min-height:40px;letter-spacing:.12em}#bannerTitle{letter-spacing:.18em}}@container (max-width: 300px){.ammo,.hud-brand{display:none}.fuel{flex-basis:52px}.pad{width:30px;min-height:32px}.thrust{width:60px;min-height:32px;font-size:9px;letter-spacing:.08em}.console{min-height:32px}.hud-value{font-size:12px}}@container (max-width: 240px){.hud-key,.fuel .hud-key{display:none}.fuel{flex-basis:40px}}@media(max-height:320px){.app{gap:4px;padding:5px}.hud{min-height:18px}.hud-value{font-size:14px}.console,.controls button{min-height:36px}.pad{width:38px}.thrust{width:82px}.hint,.ammo .hud-key{display:none}.ammo-pips{min-height:6px}.banner{padding:6px 14px 7px}}@media(max-height:220px){.app{gap:3px;padding:4px}.hud-key{font-size:8px}.hud-brand em,.best,.hint,.ammo .hud-key{display:none}.hud-value{font-size:12px}.controls button{min-height:30px;font-size:10px}.pad{width:32px}.thrust{width:62px}.fuel{flex-basis:68px}.fuel-track{height:4px}.console{min-height:30px}#bannerTitle{font-size:12px}#bannerSub{font-size:8px}}@keyframes beacon{0%,to{opacity:1}50%{opacity:.3}}@keyframes banner-in{0%{opacity:0;transform:translate(-50%,-6px)}to{opacity:1;transform:translate(-50%)}}@media(prefers-reduced-motion:reduce){.brand-mark{animation:none}.banner.is-in{animation:none;opacity:1}.fuel-track span,.pip{transition:none}}[hidden]{display:none!important}
</style>
<meta name="napplet-type" content="my-napplet">
<meta name="napplet-config-schema" content="{"$schema":"http://json-schema.org/draft-07/schema#","$version":1,"type":"object","additionalProperties":false,"properties":{"effects":{"type":"boolean","title":"Impact effects","description":"Show debris, sparks, shockwaves and launch exhaust. Turn off for a quieter simulation.","default":true,"x-napplet-section":"Presentation","x-napplet-order":1},"trajectoryGuide":{"type":"boolean","title":"Trajectory guide","description":"Show the ballistic arc while the rocket waits on the launch rail.","default":true,"x-napplet-section":"Presentation","x-napplet-order":2}}}">
</head>
<body>
<!--
A napplet is framed by its runtime, so this surface is the game itself:
a status rail, the demolition range, and the console controls.
-->
<main id="app" class="app">
<header class="hud" aria-label="Mission status">
<div class="hud-cell">
<span class="hud-key">Score</span>
<span id="score" class="hud-value">000000</span>
</div>
<div class="hud-brand">
<span class="brand-mark" aria-hidden="true"></span>
<span>Blockfall</span>
<em>demolition range</em>
</div>
<div class="hud-cell hud-right">
<span class="hud-key">Sector</span>
<span id="level" class="hud-value">01</span>
<span class="best">best <b id="best">000000</b></span>
</div>
</header>
<section id="stage" class="stage" aria-label="Demolition range">
<canvas id="scene"></canvas>
<div id="banner" class="banner" aria-live="polite" hidden>
<p id="bannerTitle"></p>
<p id="bannerSub"></p>
</div>
<button id="restartBtn" class="restart" type="button" hidden>Re-open range</button>
</section>
<footer class="console" aria-label="Launch console">
<div class="readout">
<div class="fuel">
<span class="hud-key">Fuel</span>
<div class="fuel-track"><span id="fuelFill"></span></div>
</div>
<div class="ammo">
<span class="hud-key">Rockets</span>
<div class="ammo-row">
<div id="ammo" class="ammo-pips"></div>
<span id="ammoCount" class="ammo-count">×8</span>
</div>
</div>
<p id="hint" class="hint">Aim the rail · launch when locked</p>
</div>
<div class="controls" role="group" aria-label="Rocket controls">
<button id="leftBtn" class="pad" type="button" aria-label="Rotate left">
<span aria-hidden="true">◀</span>
</button>
<button id="thrustBtn" class="thrust" type="button">
<span id="thrustLabel">Launch</span>
</button>
<button id="rightBtn" class="pad" type="button" aria-label="Rotate right">
<span aria-hidden="true">▶</span>
</button>
</div>
</footer>
</main>
</body>
</html>
