SOURCE / PINNED RELEASE
Made of little things.
Rail Game
- Release
- 68ae2f014903…
- Author-recorded commit
- bd5a281ba5d3…
- 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.
<!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 It=Object.defineProperty;var Tt=(e,t,s)=>t in e?It(e,t,{enumerable:!0,configurable:!0,writable:!0,value:s}):e[t]=s;var c=(e,t,s)=>Tt(e,typeof t!="symbol"?t+"":t,s);function At(){const e=new Map;function t(o,r){if(e.has(o))throw new Error(`NAP domain "${o}" is already registered`);e.set(o,r)}function s(o){const r=o.type.indexOf(".");if(r<=0)return!1;const l=o.type.slice(0,r),a=e.get(l);return a?(a(o),!0):!1}function i(){return Array.from(e.keys())}return{registerNap:t,dispatch:s,getRegisteredDomains:i}}var Rt=At(),qt=Rt.registerNap,jt="theme";function ut(){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 Pt(){return ut().get()}function Lt(e){return ut().onChanged(e)}qt(jt,e=>{});function Dt(){const e=window;if(!e.napplet)throw new Error("window.napplet is unavailable -- runtime did not inject the napplet namespace");return e.napplet}function f(e){const s=Dt()[e];if(!s)throw new Error(`window.napplet.${String(e)} is unavailable -- runtime did not inject this domain`);return s}var pt={getItem(e){return f("storage").getItem(e)},setItem(e,t){return f("storage").setItem(e,t)},removeItem(e){return f("storage").removeItem(e)},keys(){return f("storage").keys()},instance:{getItem(e){return f("storage").instance.getItem(e)},setItem(e,t){return f("storage").instance.setItem(e,t)},removeItem(e){return f("storage").instance.removeItem(e)},keys(){return f("storage").instance.keys()}}},$t={createSession(e){return f("media").createSession(e)},updateSession(e,t){f("media").updateSession(e,t)},destroySession(e){f("media").destroySession(e)},reportState(e,t){f("media").reportState(e,t)},reportCapabilities(e,t){f("media").reportCapabilities(e,t)},sendCommand(e,t,s){f("media").sendCommand(e,t,s)},onCommand(e,t){return f("media").onCommand(e,t)},onState(e,t){return f("media").onState(e,t)},onCapabilities(e,t){return f("media").onCapabilities(e,t)},onControls(e,t){return f("media").onControls(e,t)}},J={get(){return f("config").get()},subscribe(e){return f("config").subscribe(e)},openSettings(e){f("config").openSettings(e)},registerSchema(e,t){return f("config").registerSchema(e,t)},onSchemaError(e){return f("config").onSchemaError(e)},get schema(){return f("config").schema}};function Nt(e,t){return!!(e!=null&&e[t])}function U(e){return Nt(window.napplet,e)}class Bt{constructor(t,s,i={}){c(this,"context");c(this,"output");c(this,"session","");c(this,"pending",!1);c(this,"blocked",!1);c(this,"disposed",!1);c(this,"muted",!1);c(this,"volume",1);c(this,"stopped",!1);c(this,"listener");this.media=t,this.title=s,this.options=i}activate(){var t,s;if(!this.media||this.disposed||this.blocked)return!1;try{return this.context||(this.context=((s=(t=this.options).createContext)==null?void 0:s.call(t))??new AudioContext,this.output=this.context.createGain(),this.output.gain.value=0,this.output.connect(this.context.destination),this.context.onstatechange=()=>this.report()),!this.session&&!this.pending&&(this.pending=!0,this.register()),this.context.state==="suspended"&&this.resume(),this.audible}catch{return this.dispose(),!1}}get audible(){var t;return!!this.session&&!this.disposed&&!this.blocked&&!this.muted&&((t=this.context)==null?void 0:t.state)==="running"}setMuted(t){this.muted=t,this.applyGain(),this.report()}applyGain(){this.output&&(this.output.gain.value=this.audible?this.volume:0)}report(){var t,s;if(this.applyGain(),(s=(t=this.options).onChange)==null||s.call(t),!(!this.session||this.disposed))try{this.media.reportState(this.session,{status:this.audible?"playing":this.stopped?"stopped":"paused",volume:this.muted?0:this.volume})}catch{this.dispose()}}async resume(){var t;try{await((t=this.context)==null?void 0:t.resume()),this.report()}catch{this.applyGain()}}async register(){try{const t=await this.media.createSession({owner:"napplet",metadata:{title:this.title,mediaType:"audio"},capabilities:["play","pause","stop","volume"]});if(this.disposed){t.sessionId&&this.media.destroySession(t.sessionId);return}if(t.error||!t.sessionId||t.owner!=="napplet"){t.sessionId&&this.media.destroySession(t.sessionId),this.dispose();return}this.session=t.sessionId,this.listener=this.media.onCommand(this.session,(s,i)=>{var o;this.disposed||(s==="pause"||s==="stop"?(this.blocked=!0,this.stopped=s==="stop",this.applyGain(),(o=this.context)==null||o.suspend().then(()=>this.report()).catch(()=>{})):s==="play"?(this.blocked=!1,this.stopped=!1,this.resume()):s==="volume"&&typeof i=="number"&&Number.isFinite(i)&&(this.volume=Math.max(0,Math.min(1,i))),this.report())}),this.report()}catch{this.dispose()}finally{this.pending=!1}}dispose(){var t;if(!this.disposed){this.disposed=!0,this.applyGain();try{(t=this.listener)==null||t.close()}catch{}if(this.context&&(this.context.onstatechange=null,this.context.close().catch(()=>{})),this.session)try{this.media.destroySession(this.session)}catch{}this.session=""}}}const Ht="http://json-schema.org/draft-07/schema#",Gt=1,Ot="object",_t=!1,Wt={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}},Ft={$schema:Ht,$version:Gt,type:Ot,additionalProperties:_t,properties:Wt};function Kt(e){let t,s=!1;return U("config")&&(async()=>{J.schema||await J.registerSchema(Ft),!s&&(t=J.subscribe(i=>e({soundEnabled:i.soundEnabled===!0,reducedMotion:i.reducedMotion===!0})))})().catch(()=>{}),()=>{s=!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}],B=12,rt=250,Ut=.22,Vt=.09,H=(e,t,s)=>Math.min(s,Math.max(t,e));class Xt{constructor(t=0){c(this,"phase","ready");c(this,"stage",0);c(this,"distance",0);c(this,"elapsed",0);c(this,"x",0);c(this,"squad",B);c(this,"peakSquad",B);c(this,"damage",1);c(this,"rapid",0);c(this,"shield",!1);c(this,"score",0);c(this,"kills",0);c(this,"invincible",0);c(this,"entities",[]);c(this,"bullets",[]);c(this,"particles",[]);c(this,"events",[]);c(this,"reducedMotion",!1);c(this,"shotTimer",0);c(this,"bossTimer",1.8);c(this,"nextId",1);this.prepare(t)}prepare(t){this.stage=H(Math.floor(t),0,x.length-1),this.phase="ready",this.distance=0,this.elapsed=0,this.x=0,this.squad=B,this.peakSquad=B,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,s,i,o=0,r=1){this.entities.push({id:this.nextId++,kind:t,x:s*2,z:i,hp:r,maxHp:r,value:o,width:t==="boss"?2:t==="enemy"?.3:1.65,hit:0,alive:!0})}wave(t,s,i,o=6+Math.min(3,Math.floor(this.stage/2))){for(let r=0;r<o;r++)for(let l=-2;l<=2;l++)this.add("enemy",t+l*.17,s+r*1.7,0,i)}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 s=0;s<4+t;s++){const i=108+s*180,o=(s+t)%3-1,r=(s+t+1)%3-1,l=(s+t+2)%3-1,a=7+t*3+s*2;this.add("recruit",o,i,24+t*3+s*2),this.add("multiply",r,i,s%3===2?3:2),this.wave(o,i+24,a),this.wave(r,i+30,a),this.add("barrier",l,i+52),this.add("damage",r,i+65),this.add("recruit",o,i+65,30+t*4),this.wave(l,i+88,a+3),this.wave(r,i+94,a+2),s%3===1&&this.wave(o,i+88,a+2),this.add("rapid",o,i+121),this.add("shield",l,i+121),this.add("barrier",r,i+141),s%3===2&&this.add("barrier",l,i+141),this.wave(o,i+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 s=Math.min(14,Math.ceil(Math.sqrt(this.squad*1.6))),i=Math.floor(t/s),o=Math.min(s,this.squad-i*s);return{x:(t%s-(o-1)/2)*.1,z:1.4-i*.32}}emit(t,s,i){this.events.push({text:t,tone:s,sound:i})}burst(t,s,i,o=14){if(!this.reducedMotion)for(let r=0;r<o&&this.particles.length<420;r++){const l=r*2.39996;this.particles.push({x:t,z:s,y:.5,vx:Math.cos(l)*1.8,vz:Math.sin(l)*2,vy:2+r%3,life:.5+r%4*.12,color:i})}}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 s=Math.min(t,this.squad);this.squad-=s,this.emit(`−${s} 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,s=0,i){if(this.phase!=="playing")return;t=H(t,0,.05),this.elapsed+=t,this.invincible=Math.max(0,this.invincible-t),this.rapid=Math.max(0,this.rapid-t),i!==void 0?this.x+=H(i-this.x,-7*t,7*t):this.x+=s*4.6*t,this.x=H(this.x,-2.25,2.25);const o=Math.min(this.definition.speed*t,this.definition.length-this.distance);this.distance+=o;for(const a of this.entities)if(a.alive&&(a.z-=o,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?Vt:Ut;for(let a=0;a<this.squad;a++){const p=this.formation(a);this.bullets.push({x:this.x+p.x,z:p.z+.9,damage:this.damage,hostile:!1,vx:0})}this.events.push({text:"",tone:"neutral",sound:"shot"})}const r=this.boss;if(r&&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,p=(r.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-r.x)/p;this.bullets.push({x:r.x,z:r.z-1,damage:6+this.stage*2,hostile:!0,vx:h})}}const l=this.entities.filter(a=>a.alive&&a.z>=-.65&&a.z<=106&&(a.kind==="enemy"||a.kind==="boss"||a.kind==="barrier")).sort((a,p)=>a.z-p.z);for(const a of this.bullets){const p=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 l){if(h.z-.65>a.z)break;if(h.alive&&!(Math.abs(h.x-a.x)>h.width/2+.07)&&!(h.z+.65<p||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-o,a.y+=a.vy*t,a.vy-=10*t,a.life-=t;this.particles=this.particles.filter(a=>a.life>0)}}const I=(e,t,s)=>`rgb(${e.map((i,o)=>Math.round(i+(t[o]-i)*s)).join(",")})`;class Qt{constructor(t){c(this,"ctx");c(this,"width",1);c(this,"height",1);c(this,"scale",1);c(this,"horizon",1);c(this,"depth",1);c(this,"cameraLeft",1.5);c(this,"light",!1);c(this,"palette",{bg:"#101d25",text:"#e9f2f2",track:"#21333d",edge:"#344b56",grid:"#33434c",scenery:"#1c2b33",top:"#30434b",accent:"#78edbb",fog:"#172831"});this.canvas=t;const s=t.getContext("2d",{alpha:!1});if(!s)throw new Error("Canvas rendering is unavailable.");this.ctx=s}setTheme(t,s,i){const o=document.createElement("canvas");o.width=o.height=1;const r=o.getContext("2d"),l=n=>{r.clearRect(0,0,1,1),r.fillStyle=n,r.fillRect(0,0,1,1);const h=r.getImageData(0,0,1,1).data;return[h[0],h[1],h[2]]},a=l(t),p=l(s);this.light=a[0]*.2126+a[1]*.7152+a[2]*.0722>150,this.palette={bg:t,text:s,accent:i,track:I(a,p,.09),edge:I(a,p,.22),grid:I(a,p,.14),scenery:I(a,p,.055),top:I(a,p,.13),fog:I(a,p,.025)}}resize(){const t=this.canvas.getBoundingClientRect();this.width=t.width,this.height=t.height;const s=Math.min(window.devicePixelRatio||1,2);this.canvas.width=Math.round(this.width*s),this.canvas.height=Math.round(this.height*s),this.ctx.setTransform(s,0,0,s,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,s,i=0){const o=28/(28+Math.max(-10,s));return{x:this.width/2+(t+this.cameraLeft)*this.scale*o-this.cameraLeft*this.scale,y:this.horizon+this.depth*o-i*this.scale*o,s:this.scale*o}}poly(t,s){const i=this.ctx;i.beginPath(),t.forEach((o,r)=>r?i.lineTo(o.x,o.y):i.moveTo(o.x,o.y)),i.closePath(),i.fillStyle=s,i.fill()}ground(t,s,i,o,r){this.poly([this.project(t,i),this.project(s,i),this.project(s,o),this.project(t,o)],r)}box(t,s,i,o,r,l,a=0){const p=this.project(t-i/2,s-o/2,a),n=this.project(t+i/2,s-o/2,a),h=this.project(t-i/2,s-o/2,a+r),m=this.project(t+i/2,s-o/2,a+r),w=this.project(t+i/2,s+o/2,a+r),z=this.project(t-i/2,s+o/2,a+r);t>-this.cameraLeft?this.poly([p,this.project(t-i/2,s+o/2,a),z,h],l[1]):this.poly([n,this.project(t+i/2,s+o/2,a),w,m],l[1]),this.poly([p,n,m,h],l[0]),this.poly([h,m,w,z],l[2])}label(t,s,i,o,r){this.ctx.fillStyle=r,this.ctx.font=`700 ${o}px ui-monospace, "SFMono-Regular", Consolas, monospace`,this.ctx.textAlign="center",this.ctx.textBaseline="middle",this.ctx.fillText(t,s,i)}shadow(t,s,i){const o=this.project(t,s);this.ctx.fillStyle=this.light?"#152f3920":"#00000038",this.ctx.beginPath(),this.ctx.ellipse(o.x+o.s*.1,o.y+2,i*o.s,.14*o.s,-.1,0,Math.PI*2),this.ctx.fill()}soldier(t,s,i,o,r){const a=r?0:Math.sin(i*17+o*1.8)*.02,p=r?0:Math.sin(i*17+o*1.8)*.05,n=(m,w,z,g,k,Et,Ct=0)=>this.box(t+m*.44,s+w*.44,z*.44,g*.44,k*.44,Et,Ct*.44+a);this.shadow(t,s,.12);const h=["#2589a2","#17647b","#55cbd4"];n(-.07,p,.075,.12,.2,h),n(.07,-p,.075,.12,.2,h),n(0,0,.24,.17,.29,["#52d7cf","#288b9d","#a1f5df"],.18),n(0,0,.21,.2,.19,o===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,s,i){const o=this.ctx,r=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,m=n?1:.7,w=i?0:Math.sin(s*5+t.id)*.025,z=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)*m,z,.22+w),this.box(t.x,t.z,h*.65,.32,(n?.48:.3)*m,z,(n?1.42:.66)+w);const g=this.project(t.x,t.z-.21,(n?1.65:.78)+w);if(o.fillStyle="#522e39",o.fillRect(g.x-h*g.s*.24,g.y,h*g.s*.48,g.s*.09),o.fillStyle="#fff3c1",o.fillRect(g.x-h*g.s*.19,g.y+g.s*.02,h*g.s*.1,g.s*.035),o.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 k=this.project(t.x,t.z,n?2.24:1.1);o.fillStyle=this.palette.bg,o.fillRect(k.x-r.s*h*.6,k.y,r.s*h*1.2,n?5:3),o.fillStyle="#fb8e7f",o.fillRect(k.x-r.s*h*.6,k.y,r.s*h*1.2*Math.max(0,t.hp/t.maxHp),n?5:3),n&&t.z<65&&this.label("WARDEN",k.x,k.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 l=t.kind==="recruit"||t.kind==="multiply",a=t.kind==="recruit"?"#77dadd":t.kind==="multiply"?"#a0edac":t.kind==="damage"?"#ffd179":t.kind==="rapid"?"#c3a1ef":"#88bdff",p=t.kind==="recruit"?"#244e5a":t.kind==="multiply"?"#335347":t.kind==="damage"?"#65523d":t.kind==="rapid"?"#4a3d65":"#304c6c";if(l){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,p,a]);this.box(t.x,t.z,1.7,.17,.63,[p,p,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,r.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,r.s*.17)),a)}}else{const n=i?.25:.25+Math.sin(s*3+t.id)*.1;this.box(t.x,t.z,.72,.6,.68,[p,p,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,r.s*.48,a),t.z<55){const m=this.project(t.x,t.z,1.24);this.label(t.kind==="damage"?"DAMAGE":t.kind==="rapid"?"RAPID":"SHIELD",m.x,m.y,Math.max(9,Math.min(12,r.s*.23)),a)}}}draw(t,s){const i=this.ctx,o=this.width,r=this.height,l=this.palette;i.fillStyle=l.bg,i.fillRect(0,0,o,r);const a=i.createLinearGradient(0,0,0,r);a.addColorStop(0,l.bg),a.addColorStop(.45,l.fog),a.addColorStop(1,l.bg),i.fillStyle=a,i.fillRect(0,0,o,r);for(let n=15;n>=0;n--){const h=((n*13+200-t.distance*.65)%208+208)%208-12;for(const m of[-1,1])this.box(m*(4.2+n%4*1.7),h,.8+n%3*.8,2.5+n%3,.15+n%4*.34,[l.scenery,l.scenery,l.top])}this.ground(-3.13,3.13,-10,180,l.edge),this.ground(-3,3,-10,180,l.track);for(let n=160-t.distance%8;n>-10;n-=8)this.ground(-3,3,n,n+.07,l.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,l.edge);for(const h of[-3.04,3.04])this.ground(h-.025,h+.025,n,n+2,l.accent)}for(const n of[-2.96,2.96])this.ground(n-.017,n+.017,-10,180,l.accent);if(t.definition.length-t.distance<100){const n=t.definition.length-t.distance+28;for(const m of[-2.9,2.9])this.box(m,n,.15,.4,3,[l.edge,l.scenery,l.top]);this.box(0,n,5.95,.4,.3,[l.edge,l.scenery,l.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),l.text)}for(const n of t.entities.filter(h=>h.alive&&h.z<130&&h.z>-2).sort((h,m)=>m.z-h.z))this.entity(n,s,t.reducedMotion);for(const n of t.bullets){const h=this.project(n.x,n.z,n.hostile?.46:.25),m=this.project(n.x,n.z-(n.hostile?-1.1:.9),n.hostile?.46:.25);i.strokeStyle=n.hostile?"#ff786b":n.damage>1?"#ffd67c":"#bcffe1",i.lineWidth=Math.max(n.hostile?3:1,h.s*(n.hostile?.11:.02)),i.lineCap="round",i.beginPath(),i.moveTo(m.x,m.y),i.lineTo(h.x,h.y),i.stroke()}if(t.shield){const n=this.project(t.x,-1,.3);i.fillStyle="#88bdff12",i.strokeStyle="#88bdff",i.lineWidth=1.5,i.beginPath(),i.ellipse(n.x,n.y,n.s*.78,n.s*.62,0,0,Math.PI*2),i.fill(),i.stroke()}i.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,s,n,t.reducedMotion||t.phase!=="playing")}if(i.globalAlpha=1,t.squad>0&&o>=480&&r>=500){const n=this.project(t.x,-5.1);i.fillStyle=this.light?"#ffffffd0":"#101d25d0",i.beginPath(),i.roundRect(n.x-22,n.y-11,44,22,4),i.fill(),this.label(String(t.squad).padStart(2,"0"),n.x,n.y,13,l.text)}for(const n of t.particles){const h=this.project(n.x,n.z,Math.max(0,n.y));i.globalAlpha=Math.min(1,n.life*2),i.fillStyle=n.color,i.fillRect(h.x,h.y,Math.max(2,h.s*.06),Math.max(2,h.s*.06))}i.globalAlpha=1;const p=i.createLinearGradient(0,this.horizon,0,this.horizon+r*.14);p.addColorStop(0,l.bg),p.addColorStop(1,this.light?"#ffffff00":"#101d2500"),i.fillStyle=p,i.fillRect(0,0,o,this.horizon+r*.14)}}function ft(e){const t={unlocked:0,best:x.map(()=>0)};if(!e)return t;try{const s=JSON.parse(e);if(!s||typeof s!="object")return t;const i=s;return{unlocked:typeof i.unlocked=="number"&&Number.isInteger(i.unlocked)?Math.max(0,Math.min(x.length-1,i.unlocked)):0,best:x.map((o,r)=>Array.isArray(i.best)&&Number.isSafeInteger(i.best[r])&&i.best[r]>=0?Math.min(9999999,i.best[r]):0)}}catch{return t}}function Jt(e,t){return{unlocked:Math.max(e.unlocked,t.unlocked),best:x.map((s,i)=>Math.max(e.best[i],t.best[i]))}}function Yt(e,t,s,i){return{unlocked:i?Math.max(e.unlocked,Math.min(x.length-1,t+1)):e.unlocked,best:e.best.map((o,r)=>r===t?Math.max(o,s):o)}}const A=new Bt(U("media")?$t:void 0,"Rail Game"),u=e=>document.getElementById(e),V=u("app"),y=u("game"),Y=u("primaryButton"),mt=u("secondaryButton"),G=u("pauseButton"),O=u("soundButton"),d=new Xt,M=new Qt(y),gt=new AbortController,b={signal:gt.signal};let S=ft(null),E=new Set;const T=new Map;let R,bt=0,xt=0,C,et=0,_=0,P=0,ht=0,W=0,Z="",tt=!1,v=!1,L,dt=0,yt=!1;const vt=matchMedia("(prefers-reduced-motion: reduce)");let F,St=!1,lt=Promise.resolve();const wt=U("storage");async function st(e){let t=0;try{return await Promise.race([e,new Promise((s,i)=>{t=window.setTimeout(()=>i(new Error("Timed out")),5e3)})])}finally{clearTimeout(t)}}function D(e){u("saveStatus").textContent=e}const Zt=wt?st(Promise.resolve().then(()=>pt.getItem("rail-progress-v1"))).then(e=>{S=Jt(S,ft(e)),D("Progress saved on this device"),Q()}).catch(()=>D("Save unavailable · records stay in this session")):Promise.resolve();function te(){wt&&(D("Saving progress…"),lt=lt.then(async()=>{await Zt,await st(pt.setItem("rail-progress-v1",JSON.stringify(S))),D("Progress saved on this device")}).catch(()=>D("Could not save · records stay in this session")))}function ct(e){if(St)return;const{background:t,text:s,primary:i}=e.colors,o=document.documentElement;o.style.setProperty("--bg",t),o.style.setProperty("--fg",s),o.style.setProperty("--primary",i);const r=document.createElement("canvas").getContext("2d");r.fillStyle=i,r.fillRect(0,0,1,1);const l=r.getImageData(0,0,1,1).data;o.style.setProperty("--button-fg",l[0]*.299+l[1]*.587+l[2]*.114>150?"#102a24":"#ffffff"),r.fillStyle=t,r.fillRect(0,0,1,1);const a=r.getImageData(0,0,1,1).data;o.style.colorScheme=a[0]*.299+a[1]*.587+a[2]*.114>150?"light":"dark",M.setTheme(t,s,i)}if(U("theme"))try{F=Lt(ct),st(Pt()).then(ct).catch(()=>{})}catch{}function it(){A.setMuted(!v),O.setAttribute("aria-label",v?"Mute sound":"Enable sound"),O.title=v?"Mute sound":"Enable sound",O.setAttribute("aria-pressed",String(v)),u("soundOff").hidden=v}function X(){v&&(A.activate(),L=A.context)}function kt(e){if(!v||!L||!A.audible)return;const t=L.currentTime;if(e==="shot"&&t-dt<.13)return;e==="shot"&&(dt=t);const s=L.createOscillator(),i=L.createGain(),o={shot:160,pickup:620,hit:95,kill:230,win:780},r=e==="shot"?.045:e==="win"?.5:.15;s.type=e==="shot"||e==="hit"?"triangle":"sine",s.frequency.setValueAtTime(o[e],t),s.frequency.exponentialRampToValueAtTime(e==="pickup"||e==="win"?o[e]*1.5:50,t+r),i.gain.setValueAtTime(e==="shot"?.018:.075,t),i.gain.exponentialRampToValueAtTime(.001,t+r),s.connect(i),i.connect(A.output),s.start(),s.stop(t+r),s.onended=()=>{s.disconnect(),i.disconnect()}}function nt(){d.reducedMotion=vt.matches||yt}const ee=Kt(e=>{v=e.soundEnabled,yt=e.reducedMotion,it(),nt()});vt.addEventListener("change",nt,b);nt();function Q(){u("stagePicker").replaceChildren(...x.map((e,t)=>{const s=document.createElement("button");return s.type="button",s.textContent=String(t+1).padStart(2,"0"),s.disabled=t>S.unlocked,s.title=`${e.name}${S.best[t]?` · Best ${S.best[t]}`:""}`,s.setAttribute("aria-label",`Stage ${t+1}: ${e.name}${t>S.unlocked?" (locked)":""}`),s.setAttribute("aria-pressed",String(d.stage===t)),s.addEventListener("click",()=>K(t),b),s}))}function j(){E.clear(),T.clear(),C=void 0,R=void 0,u("leftButton").dataset.held="false",u("rightButton").dataset.held="false"}function K(e){j(),d.prepare(e),tt=!1,Z="",u("toast").dataset.visible="false",W=0,Q(),at(),N()}function $(){innerWidth<240||innerHeight<280||(j(),X(),d.start(),P=0,_=performance.now(),y.focus({preventScroll:!0}),N())}function q(){j(),d.pause(),N()}function N(){if(Z===d.phase)return;Z=d.phase,V.dataset.phase=d.phase;const e=d.phase==="playing";if(u("overlay").hidden=e,G.disabled=!e&&d.phase!=="paused",G.textContent=d.phase==="paused"?"▷":"Ⅱ",G.setAttribute("aria-label",d.phase==="paused"?"Resume game":"Pause game"),u("leftButton").disabled=!e,u("rightButton").disabled=!e,e)return;u("legend").hidden=d.phase!=="ready",u("stagePicker").hidden=d.phase==="paused",u("resultStats").hidden=d.phase==="ready"||d.phase==="paused",mt.hidden=d.phase!=="paused",u("panelLabel").textContent=`STAGE ${String(d.stage+1).padStart(2,"0")} · ${d.definition.length} METERS`;const t=d.phase==="ready"?["Build your squad.",d.stage===0?"Collect soldiers. Hit the multipliers. Keep moving and take down the warden.":d.definition.subtitle,"Start run"]:d.phase==="paused"?["Take a breather.","Your squad is right where you left it.","Resume run"]:d.phase==="lost"?["Squad down.","Steer around striped barriers. Grow your squad before facing the warden.","Try again"]:d.stage===x.length-1?["All lines cleared.",`${x.length} wardens down. Take another run and beat your record.`,"Play again"]:["Warden down.",`${x[d.stage+1].name} is unlocked. A fresh squad awaits.`,"Next stage"];u("panelTitle").textContent=t[0],u("panelText").textContent=t[1],Y.replaceChildren(document.createTextNode(t[2]));const s=document.createElement("span");s.textContent="↗",s.setAttribute("aria-hidden","true"),Y.append(s),(d.phase==="won"||d.phase==="lost")&&(u("resultStats").replaceChildren(...[["SCORE",d.score],["BEST SQUAD",d.peakSquad],["ELIMINATED",d.kills]].map(([i,o])=>{const r=document.createElement("span"),l=document.createElement("strong");return r.textContent=String(i),l.textContent=String(o),r.append(l),r})),tt||(tt=!0,j(),S=Yt(S,d.stage,d.score,d.phase==="won"),te(),Q()))}function at(){u("stageNumber").textContent=`${String(d.stage+1).padStart(2,"0")} / ${String(x.length).padStart(2,"0")}`,u("stageName").textContent=d.definition.name,u("squad").textContent=String(d.squad).padStart(2,"0"),u("damage").textContent=`×${d.damage}`,u("score").textContent=String(d.score).padStart(4,"0"),u("progress").style.width=`${d.progress*100}%`,u("routeLabel").textContent=d.progress>=1?"TAKE DOWN THE WARDEN":`${Math.ceil(d.definition.length-d.distance)} m TO WARDEN`,u("powerStatus").textContent=[d.rapid>0?`RAPID ${Math.ceil(d.rapid)}s`:"",d.shield?"◇ SHIELD":""].filter(Boolean).join(" / ");const e=d.boss;u("bossHud").hidden=!e||d.progress<1,e&&(u("bossHealth").style.width=`${Math.max(0,e.hp/e.maxHp*100)}%`),y.setAttribute("aria-label",`Rail track. Squad ${d.squad}. Position ${d.x<-.8?"left":d.x>.8?"right":"center"}. Use A/D, arrows or drag to move. Shooting is automatic.`)}Y.addEventListener("click",()=>{d.phase==="won"?K(d.stage<x.length-1?d.stage+1:0):d.phase==="lost"&&K(d.stage),$()},b);mt.addEventListener("click",()=>{K(d.stage),$()},b);G.addEventListener("click",()=>d.phase==="paused"?$():q(),b);O.addEventListener("click",()=>{v=!v,it(),X(),v&&kt("pickup")},b);V.addEventListener("keydown",e=>{if(e.altKey||e.ctrlKey||e.metaKey)return;const t=e.key.toLowerCase();["arrowleft","arrowright","a","d"].includes(t)&&d.phase==="playing"&&(e.preventDefault(),E.add(t),C=void 0),["p","escape"].includes(t)&&!e.repeat&&(e.preventDefault(),d.phase==="playing"?q():d.phase==="paused"&&$()),t===" "&&e.target===y&&!e.repeat&&(e.preventDefault(),d.phase==="playing"?q():(d.phase==="paused"||d.phase==="ready")&&$())},b);V.addEventListener("keyup",e=>{E.delete(e.key.toLowerCase())},b);for(const[e,t]of[["leftButton",-1],["rightButton",1]]){const s=u(e);s.addEventListener("pointerdown",o=>{d.phase==="playing"&&(o.preventDefault(),s.setPointerCapture(o.pointerId),T.set(o.pointerId,t),C=void 0,s.dataset.held="true",y.focus({preventScroll:!0}),X())},b);const i=o=>{T.delete(o.pointerId),s.dataset.held=String([...T.values()].includes(t))};s.addEventListener("pointerup",i,b),s.addEventListener("pointercancel",i,b),s.addEventListener("lostpointercapture",i,b)}y.addEventListener("pointerdown",e=>{d.phase!=="playing"||R!==void 0||(e.preventDefault(),y.focus({preventScroll:!0}),y.setPointerCapture(e.pointerId),R=e.pointerId,bt=M.worldX(e.clientX),xt=d.x,C=d.x,X())},b);y.addEventListener("pointermove",e=>{e.pointerId===R&&(C=xt+M.worldX(e.clientX)-bt)},b);const ot=e=>{e.pointerId===R&&(R=void 0,C=void 0)};y.addEventListener("pointerup",ot,b);y.addEventListener("pointercancel",ot,b);y.addEventListener("lostpointercapture",ot,b);window.addEventListener("blur",q,b);document.addEventListener("visibilitychange",()=>{document.hidden&&q()},b);const Mt=new ResizeObserver(()=>{M.resize(),j(),(innerWidth<240||innerHeight<280)&&q()});Mt.observe(V);function zt(e){const t=_?Math.min((e-_)/1e3,.1):0;if(_=e,!document.hidden&&M.width>0&&M.height>0){if(d.phase==="playing")for(P+=t;P>=1/60;){const s=E.has("a")||E.has("arrowleft")||[...T.values()].includes(-1),i=E.has("d")||E.has("arrowright")||[...T.values()].includes(1);d.update(1/60,Number(i)-Number(s),C),P-=1/60}else P=0;for(const s of d.events.splice(0))kt(s.sound),s.text&&(u("toast").textContent=s.text,u("toast").dataset.tone=s.tone,u("toast").dataset.visible="true",W=e+1700);W&&e>W&&(u("toast").dataset.visible="false"),M.draw(d,d.elapsed),e-ht>80&&(at(),ht=e),N()}et=requestAnimationFrame(zt)}window.addEventListener("pagehide",()=>{St=!0,d.pause(),j(),cancelAnimationFrame(et),Mt.disconnect(),gt.abort(),F==null||F.close(),ee(),A.dispose()},{once:!0});M.resize();Q();at();N();it();et=requestAnimationFrame(zt);
</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="{"$schema":"http://json-schema.org/draft-07/schema#","$version":1,"type":"object","additionalProperties":false,"properties":{"soundEnabled":{"type":"boolean","title":"Sound effects","description":"Play synthesized shot and pickup sounds after a game interaction.","default":false,"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":false,"x-napplet-section":"Game","x-napplet-order":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>
