Back to Rail Game
SOURCE / PINNED RELEASE

Made of little things.

Rail Game

Release
ccdd5b913413…
Author-recorded commit
ddf0122b6acc…
License
Check the original source
Author’s source reference
nostr://npub1n8ga89w8h6tvwamxusfyzexw8gjy84yxu9rxgnmk955cxtml4ujswzxydd/wss%3A%2F%2Fgit.napplet.soy%2F/n-91ec90c6fdf

The hash-verified playable HTML, which may contain bundled or minified code. This view does not execute it.

index.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
    <meta name="theme-color" content="#101d25" />
    <title>Rail Game</title>
    <script type="module" crossorigin>var Tt=Object.defineProperty;var It=(e,t,i)=>t in e?Tt(e,t,{enumerable:!0,configurable:!0,writable:!0,value:i}):e[t]=i;var p=(e,t,i)=>It(e,typeof t!="symbol"?t+"":t,i);function At(){const e=new Map;function t(r,o){if(e.has(r))throw new Error(`NAP domain "${r}" is already registered`);e.set(r,o)}function i(r){const o=r.type.indexOf(".");if(o<=0)return!1;const d=r.type.slice(0,o),a=e.get(d);return a?(a(r),!0):!1}function s(){return Array.from(e.keys())}return{registerNap:t,dispatch:i,getRegisteredDomains:s}}var Rt=At(),Ct=Rt.registerNap,qt="theme";function ct(){var t;const e=window;if(!((t=e.napplet)!=null&&t.theme))throw new Error("window.napplet.theme is unavailable -- runtime did not inject this domain");return e.napplet.theme}function jt(){return ct().get()}function Pt(e){return ct().onChanged(e)}Ct(qt,e=>{});function Lt(){const e=window;if(!e.napplet)throw new Error("window.napplet is unavailable -- runtime did not inject the napplet namespace");return e.napplet}function b(e){const i=Lt()[e];if(!i)throw new Error(`window.napplet.${String(e)} is unavailable -- runtime did not inject this domain`);return i}var ut={getItem(e){return b("storage").getItem(e)},setItem(e,t){return b("storage").setItem(e,t)},removeItem(e){return b("storage").removeItem(e)},keys(){return b("storage").keys()},instance:{getItem(e){return b("storage").instance.getItem(e)},setItem(e,t){return b("storage").instance.setItem(e,t)},removeItem(e){return b("storage").instance.removeItem(e)},keys(){return b("storage").instance.keys()}}},Q={get(){return b("config").get()},subscribe(e){return b("config").subscribe(e)},openSettings(e){b("config").openSettings(e)},registerSchema(e,t){return b("config").registerSchema(e,t)},onSchemaError(e){return b("config").onSchemaError(e)},get schema(){return b("config").schema}};function Dt(e,t){return!!(e!=null&&e[t])}function tt(e){return Dt(window.napplet,e)}const $t="http://json-schema.org/draft-07/schema#",Nt=1,Bt="object",Ht=!1,Ot={soundEnabled:{type:"boolean",title:"Sound effects",description:"Play synthesized shot and pickup sounds after a game interaction.",default:!1,"x-napplet-section":"Game","x-napplet-order":1},reducedMotion:{type:"boolean",title:"Reduce effects",description:"Disable particles and decorative bobbing. Track movement remains visible.",default:!1,"x-napplet-section":"Game","x-napplet-order":2}},Gt={$schema:$t,$version:Nt,type:Bt,additionalProperties:Ht,properties:Ot};function _t(e){let t,i=!1;return tt("config")&&(async()=>{Q.schema||await Q.registerSchema(Gt),!i&&(t=Q.subscribe(s=>e({soundEnabled:s.soundEnabled===!0,reducedMotion:s.reducedMotion===!0})))})().catch(()=>{}),()=>{i=!0,t==null||t.close()}}const x=[{name:"The causeway",subtitle:"Build an army across four combat sectors.",length:900,speed:10,bossHp:6e3},{name:"Red freight",subtitle:"Break through the freight-yard formations.",length:1080,speed:10.5,bossHp:8e3},{name:"Last platform",subtitle:"The old terminus is only the beginning.",length:1260,speed:11,bossHp:1e4},{name:"Iron crossing",subtitle:"Pick your route through staggered barricades.",length:1440,speed:11.5,bossHp:12500},{name:"Ember viaduct",subtitle:"Hold the line against deeper enemy ranks.",length:1620,speed:12,bossHp:15e3},{name:"Siege express",subtitle:"Multiply your reserves. Keep the guns firing.",length:1800,speed:12.5,bossHp:18e3},{name:"Blackrock pass",subtitle:"A long assault through the heaviest formations.",length:1980,speed:13,bossHp:21e3},{name:"The citadel",subtitle:"Eleven sectors. One final warden. Bring everyone.",length:2160,speed:13.5,bossHp:25e3}],N=12,rt=250,Wt=.22,Ft=.09,B=(e,t,i)=>Math.min(i,Math.max(t,e));class Kt{constructor(t=0){p(this,"phase","ready");p(this,"stage",0);p(this,"distance",0);p(this,"elapsed",0);p(this,"x",0);p(this,"squad",N);p(this,"peakSquad",N);p(this,"damage",1);p(this,"rapid",0);p(this,"shield",!1);p(this,"score",0);p(this,"kills",0);p(this,"invincible",0);p(this,"entities",[]);p(this,"bullets",[]);p(this,"particles",[]);p(this,"events",[]);p(this,"reducedMotion",!1);p(this,"shotTimer",0);p(this,"bossTimer",1.8);p(this,"nextId",1);this.prepare(t)}prepare(t){this.stage=B(Math.floor(t),0,x.length-1),this.phase="ready",this.distance=0,this.elapsed=0,this.x=0,this.squad=N,this.peakSquad=N,this.damage=1,this.rapid=0,this.shield=!1,this.score=0,this.kills=0,this.invincible=0,this.shotTimer=0,this.bossTimer=1.8,this.nextId=1,this.entities=[],this.bullets=[],this.particles=[],this.events=[],this.buildStage()}get definition(){return x[this.stage]}get boss(){return this.entities.find(t=>t.kind==="boss"&&t.alive)}get progress(){return Math.min(1,this.distance/this.definition.length)}start(){(this.phase==="ready"||this.phase==="paused")&&(this.phase="playing")}pause(){this.phase==="playing"&&(this.phase="paused")}add(t,i,s,r=0,o=1){this.entities.push({id:this.nextId++,kind:t,x:i*2,z:s,hp:o,maxHp:o,value:r,width:t==="boss"?2:t==="enemy"?.3:1.65,hit:0,alive:!0})}wave(t,i,s,r=6+Math.min(3,Math.floor(this.stage/2))){for(let o=0;o<r;o++)for(let d=-2;d<=2;d++)this.add("enemy",t+d*.17,i+o*1.7,0,s)}buildStage(){const t=this.stage;this.add("recruit",0,28,24),this.wave(0,50,4+t),this.wave(-1,54,4+t,3),this.wave(1,58,4+t,3),this.add("recruit",-1,68,24),this.add("multiply",1,68,2);for(let i=0;i<4+t;i++){const s=108+i*180,r=(i+t)%3-1,o=(i+t+1)%3-1,d=(i+t+2)%3-1,a=7+t*3+i*2;this.add("recruit",r,s,24+t*3+i*2),this.add("multiply",o,s,i%3===2?3:2),this.wave(r,s+24,a),this.wave(o,s+30,a),this.add("barrier",d,s+52),this.add("damage",o,s+65),this.add("recruit",r,s+65,30+t*4),this.wave(d,s+88,a+3),this.wave(o,s+94,a+2),i%3===1&&this.wave(r,s+88,a+2),this.add("rapid",r,s+121),this.add("shield",d,s+121),this.add("barrier",o,s+141),i%3===2&&this.add("barrier",d,s+141),this.wave(r,s+151,a+4)}this.add("recruit",0,this.definition.length-62,60+t*5),this.add("damage",0,this.definition.length-44),this.add("rapid",0,this.definition.length-26),this.add("boss",0,this.definition.length+20,0,this.definition.bossHp)}formation(t){const i=Math.min(14,Math.ceil(Math.sqrt(this.squad*1.6))),s=Math.floor(t/i),r=Math.min(i,this.squad-s*i);return{x:(t%i-(r-1)/2)*.1,z:1.4-s*.32}}emit(t,i,s){this.events.push({text:t,tone:i,sound:s})}burst(t,i,s,r=14){if(!this.reducedMotion)for(let o=0;o<r&&this.particles.length<420;o++){const d=o*2.39996;this.particles.push({x:t,z:i,y:.5,vx:Math.cos(d)*1.8,vz:Math.sin(d)*2,vy:2+o%3,life:.5+o%4*.12,color:s})}}hurt(t){if(this.invincible>0)return;if(this.invincible=.65,this.shield){this.shield=!1,this.emit("Shield absorbed the hit","neutral","pickup"),this.burst(this.x,0,"#83caff");return}const i=Math.min(t,this.squad);this.squad-=i,this.emit(`−${i} soldiers`,"bad","hit"),this.burst(this.x,0,"#ff897e"),this.squad<=0&&(this.phase="lost")}collect(t){switch(t.kind){case"recruit":this.squad=Math.min(rt,this.squad+t.value),this.emit(`+${t.value} soldiers`,"good","pickup");break;case"multiply":this.squad=Math.min(rt,this.squad*t.value),this.emit(`Squad ×${t.value}`,"good","pickup");break;case"damage":this.damage=Math.min(4,this.damage+1),this.emit(`Damage ×${this.damage}`,"good","pickup");break;case"rapid":this.rapid=10,this.emit("Rapid fire · 10 seconds","good","pickup");break;case"shield":this.shield=!0,this.emit("Shield equipped","good","pickup");break;case"enemy":this.hurt(3+this.stage);return;case"barrier":this.hurt(Math.max(6+this.stage,Math.ceil(this.squad*.3)));return;default:return}this.peakSquad=Math.max(this.peakSquad,this.squad),this.score+=50,this.burst(t.x,0,"#78edbb")}update(t,i=0,s){if(this.phase!=="playing")return;t=B(t,0,.05),this.elapsed+=t,this.invincible=Math.max(0,this.invincible-t),this.rapid=Math.max(0,this.rapid-t),s!==void 0?this.x+=B(s-this.x,-7*t,7*t):this.x+=i*4.6*t,this.x=B(this.x,-2.25,2.25);const r=Math.min(this.definition.speed*t,this.definition.length-this.distance);this.distance+=r;for(const a of this.entities)if(a.alive&&(a.z-=r,a.hit=Math.max(0,a.hit-t),a.kind==="boss"&&this.progress>=1&&(a.x=Math.sin(this.elapsed*.8)*1.65),a.z<=.35&&a.kind!=="boss"&&(Math.abs(a.x-this.x)<a.width/2+.17&&this.collect(a),a.alive=!1,this.phase!=="playing")))return;if(this.shotTimer-=t,this.shotTimer<=0){this.shotTimer+=this.rapid>0?Ft:Wt;for(let a=0;a<this.squad;a++){const u=this.formation(a);this.bullets.push({x:this.x+u.x,z:u.z+.9,damage:this.damage,hostile:!1,vx:0})}this.events.push({text:"",tone:"neutral",sound:"shot"})}const o=this.boss;if(o&&this.progress>=1&&(this.bossTimer-=t,this.bossTimer<=0)){this.bossTimer=Math.max(.9,1.7-this.stage*.12);const a=15+this.stage*2,u=(o.z-1)/a;for(let n=-1-Math.min(3,this.stage);n<=1+Math.min(3,this.stage);n++){const h=(this.x+n*.55-o.x)/u;this.bullets.push({x:o.x,z:o.z-1,damage:6+this.stage*2,hostile:!0,vx:h})}}const d=this.entities.filter(a=>a.alive&&a.z>=-.65&&a.z<=106&&(a.kind==="enemy"||a.kind==="boss"||a.kind==="barrier")).sort((a,u)=>a.z-u.z);for(const a of this.bullets){const u=a.z;if(a.z+=(a.hostile?-(15+this.stage*2):58)*t,a.x+=a.vx*t,a.hostile){if(a.z<=.3&&(Math.abs(a.x-this.x)<.52&&this.hurt(a.damage),a.z=-100,this.squad<=0))return;continue}let n;for(const h of d){if(h.z-.65>a.z)break;if(h.alive&&!(Math.abs(h.x-a.x)>h.width/2+.07)&&!(h.z+.65<u||h.z-.65>a.z)){n=h;break}}if(n&&(a.z=1e3,n.kind!=="barrier"&&!(n.kind==="boss"&&this.progress<1)&&(n.hp-=a.damage,n.hit=.09,n.hp<=0))){if(n.alive=!1,this.kills++,this.score+=n.kind==="boss"?1e3:100,this.burst(n.x,n.z,n.kind==="boss"?"#ffcd72":"#ff897e",n.kind==="boss"?50:8),n.kind==="boss"){this.phase="won",this.score+=this.squad*25,this.emit("Stage cleared","good","win");break}this.events.push({text:"",tone:"neutral",sound:"kill"})}}this.bullets=this.bullets.filter(a=>a.z>-8&&a.z<105&&Math.abs(a.x)<5);for(const a of this.particles)a.x+=a.vx*t,a.z+=a.vz*t-r,a.y+=a.vy*t,a.vy-=10*t,a.life-=t;this.particles=this.particles.filter(a=>a.life>0)}}const A=(e,t,i)=>`rgb(${e.map((s,r)=>Math.round(s+(t[r]-s)*i)).join(",")})`;class Ut{constructor(t){p(this,"ctx");p(this,"width",1);p(this,"height",1);p(this,"scale",1);p(this,"horizon",1);p(this,"depth",1);p(this,"cameraLeft",1.5);p(this,"light",!1);p(this,"palette",{bg:"#101d25",text:"#e9f2f2",track:"#21333d",edge:"#344b56",grid:"#33434c",scenery:"#1c2b33",top:"#30434b",accent:"#78edbb",fog:"#172831"});this.canvas=t;const i=t.getContext("2d",{alpha:!1});if(!i)throw new Error("Canvas rendering is unavailable.");this.ctx=i}setTheme(t,i,s){const r=document.createElement("canvas");r.width=r.height=1;const o=r.getContext("2d"),d=n=>{o.clearRect(0,0,1,1),o.fillStyle=n,o.fillRect(0,0,1,1);const h=o.getImageData(0,0,1,1).data;return[h[0],h[1],h[2]]},a=d(t),u=d(i);this.light=a[0]*.2126+a[1]*.7152+a[2]*.0722>150,this.palette={bg:t,text:i,accent:s,track:A(a,u,.09),edge:A(a,u,.22),grid:A(a,u,.14),scenery:A(a,u,.055),top:A(a,u,.13),fog:A(a,u,.025)}}resize(){const t=this.canvas.getBoundingClientRect();this.width=t.width,this.height=t.height;const i=Math.min(window.devicePixelRatio||1,2);this.canvas.width=Math.round(this.width*i),this.canvas.height=Math.round(this.height*i),this.ctx.setTransform(i,0,0,i,0,0),this.scale=Math.min(this.width*.125,this.height*.23),this.horizon=this.height*.14,this.depth=this.height*.59}worldX(t){return(t-this.canvas.getBoundingClientRect().left-this.width/2)/this.scale}project(t,i,s=0){const r=28/(28+Math.max(-10,i));return{x:this.width/2+(t+this.cameraLeft)*this.scale*r-this.cameraLeft*this.scale,y:this.horizon+this.depth*r-s*this.scale*r,s:this.scale*r}}poly(t,i){const s=this.ctx;s.beginPath(),t.forEach((r,o)=>o?s.lineTo(r.x,r.y):s.moveTo(r.x,r.y)),s.closePath(),s.fillStyle=i,s.fill()}ground(t,i,s,r,o){this.poly([this.project(t,s),this.project(i,s),this.project(i,r),this.project(t,r)],o)}box(t,i,s,r,o,d,a=0){const u=this.project(t-s/2,i-r/2,a),n=this.project(t+s/2,i-r/2,a),h=this.project(t-s/2,i-r/2,a+o),f=this.project(t+s/2,i-r/2,a+o),S=this.project(t+s/2,i+r/2,a+o),E=this.project(t-s/2,i+r/2,a+o);t>-this.cameraLeft?this.poly([u,this.project(t-s/2,i+r/2,a),E,h],d[1]):this.poly([n,this.project(t+s/2,i+r/2,a),S,f],d[1]),this.poly([u,n,f,h],d[0]),this.poly([h,f,S,E],d[2])}label(t,i,s,r,o){this.ctx.fillStyle=o,this.ctx.font=`700 ${r}px ui-monospace, "SFMono-Regular", Consolas, monospace`,this.ctx.textAlign="center",this.ctx.textBaseline="middle",this.ctx.fillText(t,i,s)}shadow(t,i,s){const r=this.project(t,i);this.ctx.fillStyle=this.light?"#152f3920":"#00000038",this.ctx.beginPath(),this.ctx.ellipse(r.x+r.s*.1,r.y+2,s*r.s,.14*r.s,-.1,0,Math.PI*2),this.ctx.fill()}soldier(t,i,s,r,o){const a=o?0:Math.sin(s*17+r*1.8)*.02,u=o?0:Math.sin(s*17+r*1.8)*.05,n=(f,S,E,g,M,zt,Et=0)=>this.box(t+f*.44,i+S*.44,E*.44,g*.44,M*.44,zt,Et*.44+a);this.shadow(t,i,.12);const h=["#2589a2","#17647b","#55cbd4"];n(-.07,u,.075,.12,.2,h),n(.07,-u,.075,.12,.2,h),n(0,0,.24,.17,.29,["#52d7cf","#288b9d","#a1f5df"],.18),n(0,0,.21,.2,.19,r===0?["#f6cf7b","#bf9556","#fff0b3"]:["#8cf1de","#48bfc9","#ccffed"],.49),n(0,-.12,.14,.1,.18,h,.22),n(.14,.18,.075,.44,.08,["#b5d7d5","#4c717b","#e5fff4"],.35)}entity(t,i,s){const r=this.ctx,o=this.project(t.x,t.z);if(this.shadow(t.x,t.z,t.width*.54),t.kind==="enemy"||t.kind==="boss"){const n=t.kind==="boss",h=n?2:.3,f=n?1:.7,S=s?0:Math.sin(i*5+t.id)*.025,E=t.hit>0?["#fff5e4","#ffe0c6","#ffffff"]:["#df6f69","#aa444d","#ffaaa0"];this.box(t.x-h*.26,t.z,h*.22,.4,.28,["#743c49","#492d3d","#ba6669"]),this.box(t.x+h*.26,t.z,h*.22,.4,.28,["#743c49","#492d3d","#ba6669"]),this.box(t.x,t.z,h,n?.85:.4,(n?1.15:.62)*f,E,.22+S),this.box(t.x,t.z,h*.65,.32,(n?.48:.3)*f,E,(n?1.42:.66)+S);const g=this.project(t.x,t.z-.21,(n?1.65:.78)+S);if(r.fillStyle="#522e39",r.fillRect(g.x-h*g.s*.24,g.y,h*g.s*.48,g.s*.09),r.fillStyle="#fff3c1",r.fillRect(g.x-h*g.s*.19,g.y+g.s*.02,h*g.s*.1,g.s*.035),r.fillRect(g.x+h*g.s*.09,g.y+g.s*.02,h*g.s*.1,g.s*.035),n||t.hp<t.maxHp){const M=this.project(t.x,t.z,n?2.24:1.1);r.fillStyle=this.palette.bg,r.fillRect(M.x-o.s*h*.6,M.y,o.s*h*1.2,n?5:3),r.fillStyle="#fb8e7f",r.fillRect(M.x-o.s*h*.6,M.y,o.s*h*1.2*Math.max(0,t.hp/t.maxHp),n?5:3),n&&t.z<65&&this.label("WARDEN",M.x,M.y-10,12,this.palette.text)}return}if(t.kind==="barrier"){this.box(t.x,t.z,1.65,.65,.73,["#5f6570","#343e49","#8b8c91"]);for(let n=0;n<5;n++){const h=t.x-.77+n*.32;this.poly([this.project(h,t.z-.333,.08),this.project(h+.18,t.z-.333,.08),this.project(h+.44,t.z-.333,.65),this.project(h+.26,t.z-.333,.65)],"#ffcf78")}return}const d=t.kind==="recruit"||t.kind==="multiply",a=t.kind==="recruit"?"#77dadd":t.kind==="multiply"?"#a0edac":t.kind==="damage"?"#ffd179":t.kind==="rapid"?"#c3a1ef":"#88bdff",u=t.kind==="recruit"?"#244e5a":t.kind==="multiply"?"#335347":t.kind==="damage"?"#65523d":t.kind==="rapid"?"#4a3d65":"#304c6c";if(d){this.ground(t.x-.84,t.x+.84,t.z-.3,t.z+.3,a+"40"),this.poly([this.project(t.x-.78,t.z,.12),this.project(t.x+.78,t.z,.12),this.project(t.x+.78,t.z,1.7),this.project(t.x-.78,t.z,1.7)],a+(this.light?"36":"18"));for(const h of[-1,1])this.box(t.x+h*.81,t.z,.07,.15,1.75,[a,u,a]);this.box(t.x,t.z,1.7,.17,.63,[u,u,a],1.35);const n=this.project(t.x,t.z-.1,1.68);if(this.label(t.kind==="multiply"?`×${t.value}`:`+${t.value}`,n.x,n.y,Math.min(38,o.s*.62),a),t.z<55){const h=this.project(t.x,t.z,1.02);this.label(t.kind==="multiply"?"MULTIPLY":"RECRUIT",h.x,h.y,Math.max(8,Math.min(10,o.s*.17)),a)}}else{const n=s?.25:.25+Math.sin(i*3+t.id)*.1;this.box(t.x,t.z,.72,.6,.68,[u,u,a],n);const h=this.project(t.x,t.z-.31,n+.36);if(this.label(t.kind==="damage"?"↑":t.kind==="rapid"?"»":"◇",h.x,h.y,o.s*.48,a),t.z<55){const f=this.project(t.x,t.z,1.24);this.label(t.kind==="damage"?"DAMAGE":t.kind==="rapid"?"RAPID":"SHIELD",f.x,f.y,Math.max(9,Math.min(12,o.s*.23)),a)}}}draw(t,i){const s=this.ctx,r=this.width,o=this.height,d=this.palette;s.fillStyle=d.bg,s.fillRect(0,0,r,o);const a=s.createLinearGradient(0,0,0,o);a.addColorStop(0,d.bg),a.addColorStop(.45,d.fog),a.addColorStop(1,d.bg),s.fillStyle=a,s.fillRect(0,0,r,o);for(let n=15;n>=0;n--){const h=((n*13+200-t.distance*.65)%208+208)%208-12;for(const f of[-1,1])this.box(f*(4.2+n%4*1.7),h,.8+n%3*.8,2.5+n%3,.15+n%4*.34,[d.scenery,d.scenery,d.top])}this.ground(-3.13,3.13,-10,180,d.edge),this.ground(-3,3,-10,180,d.track);for(let n=160-t.distance%8;n>-10;n-=8)this.ground(-3,3,n,n+.07,d.grid);for(let n=160-t.distance%10;n>-10;n-=10){for(const h of[-1,1])this.ground(h-.014,h+.014,n,n+3,d.edge);for(const h of[-3.04,3.04])this.ground(h-.025,h+.025,n,n+2,d.accent)}for(const n of[-2.96,2.96])this.ground(n-.017,n+.017,-10,180,d.accent);if(t.definition.length-t.distance<100){const n=t.definition.length-t.distance+28;for(const f of[-2.9,2.9])this.box(f,n,.15,.4,3,[d.edge,d.scenery,d.top]);this.box(0,n,5.95,.4,.3,[d.edge,d.scenery,d.top],2.9);const h=this.project(0,n,3.05);this.label("END OF LINE",h.x,h.y,Math.max(9,h.s*.22),d.text)}for(const n of t.entities.filter(h=>h.alive&&h.z<130&&h.z>-2).sort((h,f)=>f.z-h.z))this.entity(n,i,t.reducedMotion);for(const n of t.bullets){const h=this.project(n.x,n.z,n.hostile?.46:.25),f=this.project(n.x,n.z-(n.hostile?-1.1:.9),n.hostile?.46:.25);s.strokeStyle=n.hostile?"#ff786b":n.damage>1?"#ffd67c":"#bcffe1",s.lineWidth=Math.max(n.hostile?3:1,h.s*(n.hostile?.11:.02)),s.lineCap="round",s.beginPath(),s.moveTo(f.x,f.y),s.lineTo(h.x,h.y),s.stroke()}if(t.shield){const n=this.project(t.x,-1,.3);s.fillStyle="#88bdff12",s.strokeStyle="#88bdff",s.lineWidth=1.5,s.beginPath(),s.ellipse(n.x,n.y,n.s*.78,n.s*.62,0,0,Math.PI*2),s.fill(),s.stroke()}s.globalAlpha=t.invincible>0&&Math.floor(t.elapsed*18)%2===0?.45:1;for(let n=0;n<t.squad;n++){const h=t.formation(n);this.soldier(t.x+h.x,h.z,i,n,t.reducedMotion||t.phase!=="playing")}if(s.globalAlpha=1,t.squad>0&&r>=480&&o>=500){const n=this.project(t.x,-5.1);s.fillStyle=this.light?"#ffffffd0":"#101d25d0",s.beginPath(),s.roundRect(n.x-22,n.y-11,44,22,4),s.fill(),this.label(String(t.squad).padStart(2,"0"),n.x,n.y,13,d.text)}for(const n of t.particles){const h=this.project(n.x,n.z,Math.max(0,n.y));s.globalAlpha=Math.min(1,n.life*2),s.fillStyle=n.color,s.fillRect(h.x,h.y,Math.max(2,h.s*.06),Math.max(2,h.s*.06))}s.globalAlpha=1;const u=s.createLinearGradient(0,this.horizon,0,this.horizon+o*.14);u.addColorStop(0,d.bg),u.addColorStop(1,this.light?"#ffffff00":"#101d2500"),s.fillStyle=u,s.fillRect(0,0,r,this.horizon+o*.14)}}function pt(e){const t={unlocked:0,best:x.map(()=>0)};if(!e)return t;try{const i=JSON.parse(e);if(!i||typeof i!="object")return t;const s=i;return{unlocked:typeof s.unlocked=="number"&&Number.isInteger(s.unlocked)?Math.max(0,Math.min(x.length-1,s.unlocked)):0,best:x.map((r,o)=>Array.isArray(s.best)&&Number.isSafeInteger(s.best[o])&&s.best[o]>=0?Math.min(9999999,s.best[o]):0)}}catch{return t}}function Vt(e,t){return{unlocked:Math.max(e.unlocked,t.unlocked),best:x.map((i,s)=>Math.max(e.best[s],t.best[s]))}}function Xt(e,t,i,s){return{unlocked:s?Math.max(e.unlocked,Math.min(x.length-1,t+1)):e.unlocked,best:e.best.map((r,o)=>o===t?Math.max(r,i):r)}}const c=e=>document.getElementById(e),K=c("app"),y=c("game"),J=c("primaryButton"),ft=c("secondaryButton"),H=c("pauseButton"),O=c("soundButton"),l=new Kt,z=new Ut(y),gt=new AbortController,m={signal:gt.signal};let k=pt(null),T=new Set;const R=new Map;let C,mt=0,bt=0,I,et=0,G=0,P=0,ot=0,_=0,Y="",Z=!1,w=!1,v,ht=0,xt=!1;const yt=matchMedia("(prefers-reduced-motion: reduce)");let W,vt=!1,lt=Promise.resolve();const wt=tt("storage");async function st(e){let t=0;try{return await Promise.race([e,new Promise((i,s)=>{t=window.setTimeout(()=>s(new Error("Timed out")),5e3)})])}finally{clearTimeout(t)}}function L(e){c("saveStatus").textContent=e}const Qt=wt?st(Promise.resolve().then(()=>ut.getItem("rail-progress-v1"))).then(e=>{k=Vt(k,pt(e)),L("Progress saved on this device"),X()}).catch(()=>L("Save unavailable · records stay in this session")):Promise.resolve();function Jt(){wt&&(L("Saving progress…"),lt=lt.then(async()=>{await Qt,await st(ut.setItem("rail-progress-v1",JSON.stringify(k))),L("Progress saved on this device")}).catch(()=>L("Could not save · records stay in this session")))}function dt(e){if(vt)return;const{background:t,text:i,primary:s}=e.colors,r=document.documentElement;r.style.setProperty("--bg",t),r.style.setProperty("--fg",i),r.style.setProperty("--primary",s);const o=document.createElement("canvas").getContext("2d");o.fillStyle=s,o.fillRect(0,0,1,1);const d=o.getImageData(0,0,1,1).data;r.style.setProperty("--button-fg",d[0]*.299+d[1]*.587+d[2]*.114>150?"#102a24":"#ffffff"),o.fillStyle=t,o.fillRect(0,0,1,1);const a=o.getImageData(0,0,1,1).data;r.style.colorScheme=a[0]*.299+a[1]*.587+a[2]*.114>150?"light":"dark",z.setTheme(t,i,s)}if(tt("theme"))try{W=Pt(dt),st(jt()).then(dt).catch(()=>{})}catch{}function U(){O.setAttribute("aria-label",w?"Mute sound":"Enable sound"),O.title=w?"Mute sound":"Enable sound",O.setAttribute("aria-pressed",String(w)),c("soundOff").hidden=w}function V(){if(w)try{v??(v=new AudioContext),v.resume().catch(()=>{})}catch{w=!1,U()}}function kt(e){if(!w||!v||v.state!=="running")return;const t=v.currentTime;if(e==="shot"&&t-ht<.13)return;e==="shot"&&(ht=t);const i=v.createOscillator(),s=v.createGain(),r={shot:160,pickup:620,hit:95,kill:230,win:780},o=e==="shot"?.045:e==="win"?.5:.15;i.type=e==="shot"||e==="hit"?"triangle":"sine",i.frequency.setValueAtTime(r[e],t),i.frequency.exponentialRampToValueAtTime(e==="pickup"||e==="win"?r[e]*1.5:50,t+o),s.gain.setValueAtTime(e==="shot"?.018:.075,t),s.gain.exponentialRampToValueAtTime(.001,t+o),i.connect(s),s.connect(v.destination),i.start(),i.stop(t+o),i.onended=()=>{i.disconnect(),s.disconnect()}}function it(){l.reducedMotion=yt.matches||xt}const Yt=_t(e=>{w=e.soundEnabled,xt=e.reducedMotion,U(),it()});yt.addEventListener("change",it,m);it();function X(){c("stagePicker").replaceChildren(...x.map((e,t)=>{const i=document.createElement("button");return i.type="button",i.textContent=String(t+1).padStart(2,"0"),i.disabled=t>k.unlocked,i.title=`${e.name}${k.best[t]?` · Best ${k.best[t]}`:""}`,i.setAttribute("aria-label",`Stage ${t+1}: ${e.name}${t>k.unlocked?" (locked)":""}`),i.setAttribute("aria-pressed",String(l.stage===t)),i.addEventListener("click",()=>F(t),m),i}))}function j(){T.clear(),R.clear(),I=void 0,C=void 0,c("leftButton").dataset.held="false",c("rightButton").dataset.held="false"}function F(e){j(),l.prepare(e),Z=!1,Y="",c("toast").dataset.visible="false",_=0,X(),nt(),$()}function D(){innerWidth<240||innerHeight<280||(j(),V(),l.start(),P=0,G=performance.now(),y.focus({preventScroll:!0}),$())}function q(){j(),l.pause(),$()}function $(){if(Y===l.phase)return;Y=l.phase,K.dataset.phase=l.phase;const e=l.phase==="playing";if(c("overlay").hidden=e,H.disabled=!e&&l.phase!=="paused",H.textContent=l.phase==="paused"?"▷":"Ⅱ",H.setAttribute("aria-label",l.phase==="paused"?"Resume game":"Pause game"),c("leftButton").disabled=!e,c("rightButton").disabled=!e,e)return;c("legend").hidden=l.phase!=="ready",c("stagePicker").hidden=l.phase==="paused",c("resultStats").hidden=l.phase==="ready"||l.phase==="paused",ft.hidden=l.phase!=="paused",c("panelLabel").textContent=`STAGE ${String(l.stage+1).padStart(2,"0")} · ${l.definition.length} METERS`;const t=l.phase==="ready"?["Build your squad.",l.stage===0?"Collect soldiers. Hit the multipliers. Keep moving and take down the warden.":l.definition.subtitle,"Start run"]:l.phase==="paused"?["Take a breather.","Your squad is right where you left it.","Resume run"]:l.phase==="lost"?["Squad down.","Steer around striped barriers. Grow your squad before facing the warden.","Try again"]:l.stage===x.length-1?["All lines cleared.",`${x.length} wardens down. Take another run and beat your record.`,"Play again"]:["Warden down.",`${x[l.stage+1].name} is unlocked. A fresh squad awaits.`,"Next stage"];c("panelTitle").textContent=t[0],c("panelText").textContent=t[1],J.replaceChildren(document.createTextNode(t[2]));const i=document.createElement("span");i.textContent="↗",i.setAttribute("aria-hidden","true"),J.append(i),(l.phase==="won"||l.phase==="lost")&&(c("resultStats").replaceChildren(...[["SCORE",l.score],["BEST SQUAD",l.peakSquad],["ELIMINATED",l.kills]].map(([s,r])=>{const o=document.createElement("span"),d=document.createElement("strong");return o.textContent=String(s),d.textContent=String(r),o.append(d),o})),Z||(Z=!0,j(),k=Xt(k,l.stage,l.score,l.phase==="won"),Jt(),X()))}function nt(){c("stageNumber").textContent=`${String(l.stage+1).padStart(2,"0")} / ${String(x.length).padStart(2,"0")}`,c("stageName").textContent=l.definition.name,c("squad").textContent=String(l.squad).padStart(2,"0"),c("damage").textContent=`×${l.damage}`,c("score").textContent=String(l.score).padStart(4,"0"),c("progress").style.width=`${l.progress*100}%`,c("routeLabel").textContent=l.progress>=1?"TAKE DOWN THE WARDEN":`${Math.ceil(l.definition.length-l.distance)} m TO WARDEN`,c("powerStatus").textContent=[l.rapid>0?`RAPID ${Math.ceil(l.rapid)}s`:"",l.shield?"◇ SHIELD":""].filter(Boolean).join("   /   ");const e=l.boss;c("bossHud").hidden=!e||l.progress<1,e&&(c("bossHealth").style.width=`${Math.max(0,e.hp/e.maxHp*100)}%`),y.setAttribute("aria-label",`Rail track. Squad ${l.squad}. Position ${l.x<-.8?"left":l.x>.8?"right":"center"}. Use A/D, arrows or drag to move. Shooting is automatic.`)}J.addEventListener("click",()=>{l.phase==="won"?F(l.stage<x.length-1?l.stage+1:0):l.phase==="lost"&&F(l.stage),D()},m);ft.addEventListener("click",()=>{F(l.stage),D()},m);H.addEventListener("click",()=>l.phase==="paused"?D():q(),m);O.addEventListener("click",()=>{w=!w,U(),V(),w&&kt("pickup")},m);K.addEventListener("keydown",e=>{if(e.altKey||e.ctrlKey||e.metaKey)return;const t=e.key.toLowerCase();["arrowleft","arrowright","a","d"].includes(t)&&l.phase==="playing"&&(e.preventDefault(),T.add(t),I=void 0),["p","escape"].includes(t)&&!e.repeat&&(e.preventDefault(),l.phase==="playing"?q():l.phase==="paused"&&D()),t===" "&&e.target===y&&!e.repeat&&(e.preventDefault(),l.phase==="playing"?q():(l.phase==="paused"||l.phase==="ready")&&D())},m);K.addEventListener("keyup",e=>{T.delete(e.key.toLowerCase())},m);for(const[e,t]of[["leftButton",-1],["rightButton",1]]){const i=c(e);i.addEventListener("pointerdown",r=>{l.phase==="playing"&&(r.preventDefault(),i.setPointerCapture(r.pointerId),R.set(r.pointerId,t),I=void 0,i.dataset.held="true",y.focus({preventScroll:!0}),V())},m);const s=r=>{R.delete(r.pointerId),i.dataset.held=String([...R.values()].includes(t))};i.addEventListener("pointerup",s,m),i.addEventListener("pointercancel",s,m),i.addEventListener("lostpointercapture",s,m)}y.addEventListener("pointerdown",e=>{l.phase!=="playing"||C!==void 0||(e.preventDefault(),y.focus({preventScroll:!0}),y.setPointerCapture(e.pointerId),C=e.pointerId,mt=z.worldX(e.clientX),bt=l.x,I=l.x,V())},m);y.addEventListener("pointermove",e=>{e.pointerId===C&&(I=bt+z.worldX(e.clientX)-mt)},m);const at=e=>{e.pointerId===C&&(C=void 0,I=void 0)};y.addEventListener("pointerup",at,m);y.addEventListener("pointercancel",at,m);y.addEventListener("lostpointercapture",at,m);window.addEventListener("blur",q,m);document.addEventListener("visibilitychange",()=>{document.hidden&&q()},m);const St=new ResizeObserver(()=>{z.resize(),j(),(innerWidth<240||innerHeight<280)&&q()});St.observe(K);function Mt(e){const t=G?Math.min((e-G)/1e3,.1):0;if(G=e,!document.hidden&&z.width>0&&z.height>0){if(l.phase==="playing")for(P+=t;P>=1/60;){const i=T.has("a")||T.has("arrowleft")||[...R.values()].includes(-1),s=T.has("d")||T.has("arrowright")||[...R.values()].includes(1);l.update(1/60,Number(s)-Number(i),I),P-=1/60}else P=0;for(const i of l.events.splice(0))kt(i.sound),i.text&&(c("toast").textContent=i.text,c("toast").dataset.tone=i.tone,c("toast").dataset.visible="true",_=e+1700);_&&e>_&&(c("toast").dataset.visible="false"),z.draw(l,l.elapsed),e-ot>80&&(nt(),ot=e),$()}et=requestAnimationFrame(Mt)}window.addEventListener("pagehide",()=>{vt=!0,l.pause(),j(),cancelAnimationFrame(et),St.disconnect(),gt.abort(),W==null||W.close(),Yt(),v==null||v.close().catch(()=>{})},{once:!0});z.resize();X();nt();$();U();et=requestAnimationFrame(Mt);
</script>
    <style>:root{--bg: #101d25;--fg: #e9f2f2;--primary: #78edbb;--surface: color-mix(in srgb, var(--bg) 94%, var(--fg));--border: color-mix(in srgb, var(--fg) 16%, transparent);--muted: color-mix(in srgb, var(--fg) 58%, var(--bg));--panel: color-mix(in srgb, var(--bg) 96%, transparent);--control-h: 44px;color-scheme:dark;font:13px/1.4 Avenir Next,Avenir,Segoe UI,sans-serif}*{box-sizing:border-box}html,body,#app{height:100%;margin:0;background:var(--bg);color:var(--fg)}body{overflow:hidden;user-select:none;-webkit-user-select:none}button{color:inherit;font:inherit;cursor:pointer;-webkit-tap-highlight-color:transparent}button:focus-visible,canvas:focus-visible{outline:2px solid var(--primary);outline-offset:-3px}button:disabled{opacity:.35;cursor:default}button:hover:not(:disabled){filter:brightness(1.12)}[hidden]{display:none!important}#app{container-type:inline-size;overflow:hidden}.game-surface{position:relative;height:100%;overflow:hidden}#game{display:block;width:100%;height:100%;touch-action:none;outline:none}.hud{position:absolute;inset:max(12px,env(safe-area-inset-top)) 16px auto;display:flex;align-items:center;justify-content:space-between;gap:16px;pointer-events:none}.stage-info{display:flex;flex-direction:column;gap:3px}.stage-info>strong{font-size:15px;font-weight:600;letter-spacing:-.3px}.micro{color:var(--muted);font:9px/1.4 ui-monospace,SFMono-Regular,Consolas,monospace;letter-spacing:1.4px}.micro b{color:var(--fg);margin-left:5px;font-weight:500}.stats{display:flex;gap:20px;align-items:center}.stats>div{display:flex;flex-direction:column;gap:2px}.stats strong{font:600 25px/1 ui-monospace,SFMono-Regular,Consolas,monospace;letter-spacing:-1px}.stats>div:first-child strong{color:var(--primary)}.stat-divider{width:1px;height:25px;background:var(--border)}.actions{display:flex;gap:4px;pointer-events:auto}.icon-button{width:44px;height:44px;background:color-mix(in srgb,var(--bg) 70%,transparent);border:1px solid var(--border);border-radius:6px;font-size:20px;position:relative}#soundOff{position:absolute;left:16px;top:8px;font-size:19px}.route{position:absolute;top:76px;left:16px;right:16px;display:flex;gap:12px;align-items:center;pointer-events:none}.route-track{height:3px;flex:1;background:var(--border);position:relative}#progress{height:100%;width:0;background:var(--primary)}.route-track i{position:absolute;top:-2px;width:1px;height:7px;background:var(--muted);left:25%}.route-track i:nth-last-child(2){left:50%}.route-track i:last-child{left:75%}#routeLabel{color:var(--muted);font:9px ui-monospace,monospace;letter-spacing:1px}.power-status{position:absolute;top:100px;left:16px;color:var(--primary);font:10px ui-monospace,monospace;letter-spacing:.7px}.boss-hud{position:absolute;top:101px;left:30%;right:30%;text-align:center;color:#ff897e;font:10px ui-monospace,monospace;letter-spacing:2px}.boss-hud>div{height:5px;background:var(--border);margin-top:8px}.boss-hud i{display:block;background:#ff897e;height:100%}.toast{position:absolute;top:26%;left:12px;right:12px;text-align:center;font:600 14px ui-monospace,monospace;pointer-events:none;opacity:0;transition:opacity .16s;text-shadow:0 2px 8px var(--bg)}.toast[data-visible=true]{opacity:1}.toast[data-tone=good]{color:var(--primary)}.toast[data-tone=bad]{color:#f78b7e}.overlay{position:absolute;top:106px;right:12px;bottom:78px;left:12px;display:grid;grid-template-columns:minmax(0,1fr);place-items:center;pointer-events:none}.briefing{min-width:0;pointer-events:auto;width:min(340px,100%);padding:16px;background:var(--panel);border:1px solid var(--border);border-radius:8px;box-shadow:0 12px 56px #00000024}.briefing-top{display:flex;justify-content:space-between;align-items:center;gap:8px}.live-dot{height:6px;width:6px;border-radius:50%;background:var(--primary)}h2{font-size:28px;font-weight:600;line-height:1.15;letter-spacing:-1.1px;margin:12px 0 8px}.briefing p{color:var(--muted);margin:0 0 16px;font-size:12px;line-height:1.65}.legend{display:flex;gap:12px;font-size:10px;color:var(--muted);margin-bottom:16px}.legend span{display:flex;align-items:center;gap:4px}.legend i{height:6px;width:6px;border-radius:1px}.cyan{background:#77dadd}.green{background:#a0edac}.coral{background:#df6f69}.stage-picker{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:4px;margin-bottom:12px}.stage-picker button{flex:1;height:44px;background:transparent;border:1px solid var(--border);border-radius:4px;font:11px ui-monospace,monospace}.stage-picker button[aria-pressed=true]{background:color-mix(in srgb,var(--primary) 9%,var(--bg));border-color:var(--primary);color:var(--primary)}.primary-button{display:flex;align-items:center;justify-content:space-between;padding:0 12px;width:100%;min-height:44px;background:var(--primary);color:var(--button-fg, #102a24);border:0;border-radius:4px;font-weight:700}.primary-button span{font-size:20px}.secondary-button{width:100%;min-height:36px;border:0;background:transparent;color:var(--muted);margin-top:4px;font-size:11px}.save-status{margin-top:10px;font:9px ui-monospace,monospace;color:var(--muted);text-align:center}.result-stats{display:flex;justify-content:space-between;margin:16px 0;gap:8px;font:10px ui-monospace,monospace;color:var(--muted)}.result-stats strong{display:block;margin-top:4px;font-size:20px;color:var(--fg)}.controls{position:absolute;bottom:max(12px,env(safe-area-inset-bottom));left:16px;right:16px;display:flex;align-items:center;justify-content:space-between;gap:8px;pointer-events:none}.controls>button{pointer-events:auto;touch-action:none;width:68px;height:44px;background:color-mix(in srgb,var(--bg) 82%,transparent);border:1px solid var(--border);border-radius:6px;font-size:22px}.controls>button:active,.controls>button[data-held=true]{border-color:var(--primary);background:color-mix(in srgb,var(--primary) 16%,var(--bg))}.control-hint{display:flex;gap:16px;align-items:center;color:var(--muted);font:10px ui-monospace,monospace}.keyboard-hint{display:inline-flex;gap:4px;align-items:center}kbd{border:1px solid var(--border);border-radius:3px;padding:2px 5px;font:inherit}.auto-fire{display:flex;align-items:center;gap:6px;font-size:9px;letter-spacing:1.4px}.auto-fire i{width:4px;height:4px;border-radius:50%;background:var(--primary)}.touch-hint,.too-small{display:none}@media(max-width:600px){.hud{gap:8px;inset-inline:12px}.stage-info>strong{font-size:12px}.stage-info .micro{font-size:8px;letter-spacing:.5px}.stats{gap:9px}.stats strong{font-size:21px}.stats .micro{font-size:8px;letter-spacing:.8px}.score-stat{display:none!important}.actions{gap:2px}.icon-button{width:36px;border-color:transparent}.route{left:12px;right:12px;top:68px}#routeLabel{font-size:8px;letter-spacing:.5px}.keyboard-hint{display:none}.touch-hint{display:block;font-size:8px;letter-spacing:.7px}.control-hint{flex-direction:column;gap:6px}.controls{left:12px;right:12px}.controls>button{width:64px;height:48px}.power-status{top:88px;font-size:9px}.boss-hud{top:109px;left:22%;right:22%}}@media(pointer:coarse){.keyboard-hint{display:none}.touch-hint{display:block}.icon-button{min-width:44px}.secondary-button{min-height:44px}}@media(max-width:350px){.stage-info>strong{max-width:84px;font-size:11px}.stats{gap:7px}.stats strong{font-size:19px}.stat-divider{display:none}.legend{gap:8px;font-size:9px}.briefing{padding:12px}h2{font-size:25px}}@media(max-height:500px)and (min-width:480px){.hud{top:8px}.route{top:64px}.overlay{top:78px;right:92px;bottom:62px;left:92px}.briefing{width:min(540px,100%);display:grid;grid-template-columns:1fr 1fr;gap:8px 16px;padding:12px}.briefing-top,h2,.briefing p,.legend,.result-stats{grid-column:1}.briefing-top{grid-row:1}h2{grid-row:2;font-size:24px;margin:0}.briefing p{grid-row:3 / 5;margin:0;font-size:10px}.legend{display:none}.stage-picker{grid-column:2;grid-row:1 / 3;margin:0;align-self:end}.primary-button{grid-column:2;grid-row:3}.save-status{grid-column:2;margin:0;grid-row:5;font-size:8px}.secondary-button{grid-column:2;grid-row:4;min-height:28px;margin:0}.result-stats{grid-row:5;margin:0}.result-stats strong{font-size:14px}.power-status{top:77px}.boss-hud{top:80px}.toast{top:30%}}@media(max-height:470px)and (max-width:479px){.overlay{top:83px;right:12px;bottom:66px;left:12px}.briefing{padding:10px}h2{margin:6px 0;font-size:22px}.briefing p{font-size:10px;margin-bottom:8px}.legend{display:none}.stage-picker{margin-bottom:8px}.save-status{margin-top:5px}.result-stats{margin:8px 0}}@media(max-width:239px),(max-height:279px){.game-surface{display:none}.too-small{display:grid;height:100%;place-content:center;padding:8px;text-align:center;color:var(--muted);font-size:12px}}@media(max-height:380px)and (max-width:479px){.overlay{top:77px;right:12px;bottom:66px;left:12px}.briefing{padding:8px}.briefing p,.briefing-top,.save-status,.result-stats{display:none}h2{font-size:18px;margin:0 0 8px}.stage-picker{margin-bottom:8px;grid-template-columns:none;grid-auto-flow:column;grid-auto-columns:44px;overflow-x:auto}.stage-picker button{height:32px;font-size:9px}.secondary-button{min-height:30px}.toast{top:31%;font-size:11px}}@media(max-width:280px){.stage-info>strong{display:none}.stage-info .micro b{display:block;margin-left:0}.hud{gap:4px}.stats{gap:6px}.actions{gap:0}}@media(prefers-reduced-motion:reduce){*,*:before,*:after{transition:none!important}}
</style>
    <meta name="napplet-type" content="rail-game">
    <meta name="napplet-config-schema" content="{&quot;$schema&quot;:&quot;http://json-schema.org/draft-07/schema#&quot;,&quot;$version&quot;:1,&quot;type&quot;:&quot;object&quot;,&quot;additionalProperties&quot;:false,&quot;properties&quot;:{&quot;soundEnabled&quot;:{&quot;type&quot;:&quot;boolean&quot;,&quot;title&quot;:&quot;Sound effects&quot;,&quot;description&quot;:&quot;Play synthesized shot and pickup sounds after a game interaction.&quot;,&quot;default&quot;:false,&quot;x-napplet-section&quot;:&quot;Game&quot;,&quot;x-napplet-order&quot;:1},&quot;reducedMotion&quot;:{&quot;type&quot;:&quot;boolean&quot;,&quot;title&quot;:&quot;Reduce effects&quot;,&quot;description&quot;:&quot;Disable particles and decorative bobbing. Track movement remains visible.&quot;,&quot;default&quot;:false,&quot;x-napplet-section&quot;:&quot;Game&quot;,&quot;x-napplet-order&quot;:2}}}">
  </head>
  <body>
    <main id="app">
      <div class="too-small">Needs 240 × 280 to play.<br />Enlarge this pane.</div>
      <div class="game-surface">
        <canvas id="game" tabindex="0" aria-label="Rail track. Move with A and D or arrow keys, drag, or use the on-screen arrows. Shooting is automatic."></canvas>
        <div class="hud">
          <div class="stage-info"><span class="micro">STAGE <b id="stageNumber">01 / 08</b></span><strong id="stageName">The causeway</strong></div>
          <div class="stats"><div><span class="micro">SQUAD</span><strong id="squad">12</strong></div><span class="stat-divider"></span><div><span class="micro">DAMAGE</span><strong id="damage">×1</strong></div><span class="stat-divider score-stat"></span><div class="score-stat"><span class="micro">SCORE</span><strong id="score">0000</strong></div></div>
          <div class="actions"><button id="soundButton" class="icon-button" aria-label="Enable sound" title="Enable sound">♪<span id="soundOff">╱</span></button><button id="pauseButton" class="icon-button" aria-label="Pause game" title="Pause (P or Escape)" disabled>Ⅱ</button></div>
        </div>
        <div class="route"><div class="route-track"><div id="progress"></div><i></i><i></i><i></i></div><span id="routeLabel">330 m TO WARDEN</span></div>
        <div id="powerStatus" class="power-status"></div>
        <div id="bossHud" class="boss-hud" hidden><span>WARDEN</span><div><i id="bossHealth"></i></div></div>
        <output id="toast" class="toast" aria-live="polite"></output>
        <div id="overlay" class="overlay">
          <div class="briefing">
            <div class="briefing-top"><span id="panelLabel" class="micro">STAGE 01 · 330 METERS</span><span class="live-dot" aria-hidden="true"></span></div>
            <h2 id="panelTitle">Build your squad.</h2>
            <p id="panelText">Collect soldiers. Hit the multipliers.<br />Keep moving and take down the warden.</p>
            <div id="legend" class="legend"><span><i class="cyan"></i>Recruit</span><span><i class="green"></i>Multiply</span><span><i class="coral"></i>Avoid / shoot</span></div>
            <div id="resultStats" class="result-stats" hidden></div>
            <div id="stagePicker" class="stage-picker" aria-label="Choose stage"></div>
            <button id="primaryButton" class="primary-button">Start run <span aria-hidden="true">↗</span></button>
            <button id="secondaryButton" class="secondary-button" hidden>Restart stage</button>
            <div id="saveStatus" class="save-status">Records stay in this session</div>
          </div>
        </div>
        <div class="controls"><button id="leftButton" aria-label="Move left">←</button><div class="control-hint"><span class="keyboard-hint"><kbd>A</kbd><kbd>D</kbd> or <kbd>←</kbd><kbd>→</kbd></span><span class="touch-hint">DRAG TO MOVE</span><span class="auto-fire"><i></i>AUTO FIRE</span></div><button id="rightButton" aria-label="Move right">→</button></div>
      </div>
    </main>
  </body>
</html>