Back to Stick Brawl
SOURCE / PINNED RELEASE

Made of little things.

Stick Brawl

Release
732bb70ea856…
Author-recorded commit
918970bb749a…
License
Check the original source
Author’s source reference
nostr://npub1ny7l44q0au5xkv3mjgt6zks5f0e2ydefslq4nhh2ve2xypgc2mqqat8qx2/wss%3A%2F%2Fgit.napplet.soy%2F/n-554b329db93

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" />
    <title>Stick Brawl</title>
    <script type="module" crossorigin>var jt=Object.defineProperty;var qt=(e,t,o)=>t in e?jt(e,t,{enumerable:!0,configurable:!0,writable:!0,value:o}):e[t]=o;var h=(e,t,o)=>qt(e,typeof t!="symbol"?t+"":t,o);var Vt="config";function Zt(){const e=window;if(!e.napplet)throw new Error("window.napplet is unavailable -- runtime did not inject the napplet namespace");return e.napplet}function x(e){const o=Zt()[e];if(!o)throw new Error(`window.napplet.${String(e)} is unavailable -- runtime did not inject this domain`);return o}var tt={get(){return x("config").get()},subscribe(e){return x("config").subscribe(e)},openSettings(e){x("config").openSettings(e)},registerSchema(e,t){return x("config").registerSchema(e,t)},onSchemaError(e){return x("config").onSchemaError(e)},get schema(){return x("config").schema}};function Jt(e,t){return!!(e!=null&&e[t])}function Qt(e){return Jt(window.napplet,e)}const te="http://json-schema.org/draft-07/schema#",ee=1,ne="object",oe=!1,ie={touchButtons:{type:"boolean",title:"Always show touch buttons",description:"Show the on-screen move, jump, roll and attack buttons even when a keyboard is available.",default:!1,"x-napplet-section":"Game","x-napplet-order":1}},ae={$schema:te,$version:ee,type:ne,additionalProperties:oe,properties:ie};function le(e){(async()=>{tt.schema||await tt.registerSchema(ae);const t=tt.subscribe(o=>e(o));window.addEventListener("pagehide",()=>t.close(),{once:!0})})().catch(()=>{})}const se=["lean","armFu","armFl","armBu","armBl","legFu","legFl","legBu","legBl"],rt=8.5,ht=8.5,Tt=13,ct=7,ft=7,V=4.5;function re(e,t,o,n){const i=1-Math.exp(-o*n);for(const a of se)e[a]+=(t[a]-e[a])*i}function Gt(e){const t=Math.sin(e*2.5)*.04;return{lean:.08+t,armFu:.35+t,armFl:1.9,armBu:.1-t,armBl:1.6,legFu:.22,legFl:0,legBu:-.2,legBl:-.05}}function et(e){const t=Math.sin(e),o=Math.cos(e),n=Math.max(0,o)*1.3+.2,i=Math.max(0,-o)*1.3+.2;return{lean:.28,armFu:-t*.9+.2,armFl:-t*.9+1.6,armBu:t*.9+.2,armBl:t*.9+1.6,legFu:t*.85+.1,legFl:t*.85+.1-n,legBu:-t*.85+.1,legBl:-t*.85+.1-i}}function he(e){const t=e<0;return{lean:.15,armFu:t?2.4:1.4,armFl:t?2.8:2,armBu:t?-1.2:-.9,armBl:t?-.6:-.3,legFu:1.1,legFl:-.1,legBu:.3,legBl:-1}}function ce(e,t=.25){return e<t?{lean:-.05,armFu:-.3,armFl:1.2,armBu:.6,armBl:2,legFu:.45,legFl:.1,legBu:-.4,legBl:-.2}:{lean:.3,armFu:1.62,armFl:1.6,armBu:-.4,armBl:.9,legFu:.5,legFl:.15,legBu:-.45,legBl:-.2}}function fe(e){return e<.25?{lean:-.1,armFu:.8,armFl:2.2,armBu:-.5,armBl:.6,legFu:1.6,legFl:-.5,legBu:-.05,legBl:-.05}:{lean:-.45,armFu:.9,armFl:2,armBu:-1,armBl:-.2,legFu:1.7,legFl:1.6,legBu:-.1,legBl:-.05}}function ue(){return{lean:-1.25,armFu:-1.6,armFl:-1.3,armBu:-2,armBl:-1.6,legFu:1.45,legFl:1.6,legBu:1.25,legBl:1.5}}function me(){return{lean:-.55,armFu:-1.2,armFl:-.6,armBu:-1.6,armBl:-.9,legFu:1.35,legFl:1.45,legBu:.55,legBl:-.95}}function de(){return{lean:.9,armFu:1.3,armFl:2.3,armBu:1,armBl:2.2,legFu:2,legFl:-.4,legBu:1.7,legBl:-.6}}function ye(){return{lean:-.08,armFu:1.1,armFl:3,armBu:.9,armBl:2.8,legFu:.4,legFl:.15,legBu:-.4,legBl:-.2}}function pe(){return{lean:.02,armFu:-.12,armFl:.3,armBu:-.2,armBl:.3,legFu:.3,legFl:.05,legBu:-.3,legBl:-.1}}function ge(){return{lean:.05,armFu:.55,armFl:1.45,armBu:-.3,armBl:.4,legFu:.35,legFl:.1,legBu:-.35,legBl:-.15}}function be(e){const t={legFu:.55,legFl:.15,legBu:-.5,legBl:-.25};return e==="grab"?{lean:0,armFu:3.58,armFl:3.38,armBu:.2,armBl:1.2,...t}:e==="raise"?{lean:-.12,armFu:2.95,armFl:3.75,armBu:-.4,armBl:.6,...t}:e==="slash"?{lean:.35,armFu:1.25,armFl:1,armBu:-.8,armBl:.2,...t,legFu:.75}:{lean:.25,armFu:.95,armFl:.8,armBu:-.6,armBl:.3,...t}}function we(){return{lean:-.5,armFu:1,armFl:1.8,armBu:-1.4,armBl:-.8,legFu:.5,legFl:0,legBu:-.3,legBl:-.6}}function Se(){return{lean:0,armFu:2.6,armFl:2.9,armBu:2.4,armBl:2.1,legFu:.1,legFl:0,legBu:-.15,legBl:.1}}function q(e,t=6){const n={x:0,y:-Math.max(Mt(e.legFu,e.legFl),Mt(e.legBu,e.legBl),t)},i={x:n.x+Math.sin(e.lean)*Tt,y:n.y-Math.cos(e.lean)*Tt},a={x:i.x+Math.sin(e.lean)*(V+1.5),y:i.y-Math.cos(e.lean)*(V+1.5)},l=Z(i,e.armFu,ct),s=Z(l,e.armFl,ft);return{hip:n,shoulder:i,head:a,elbowF:l,handF:s}}function Z(e,t,o){return{x:e.x+Math.sin(t)*o,y:e.y+Math.cos(t)*o}}function Mt(e,t){return rt*Math.cos(e)+ht*Math.cos(t)}function ke(e,t,o,n,i,a){var g,vt;e.save(),e.translate(t,o),e.scale(n*(a.size??1),a.size??1),e.globalAlpha=a.alpha??1,a.topple&&e.rotate(a.topple);const l=q(i,a.minHip),{hip:s,shoulder:r,head:c}=l;a.spin&&(e.translate(s.x,s.y),e.rotate(a.spin),e.translate(-s.x,-s.y));const m=a.color,f=ve(m);e.lineCap="round",e.lineJoin="round",e.lineWidth=2.6,(g=a.gear)==null||g.call(a,e,l,"behind"),e.strokeStyle=f,K(e,r,i.armBu,ct,i.armBl,ft),K(e,s,i.legBu,rt,i.legBl,ht),e.strokeStyle=m,e.beginPath(),e.moveTo(s.x,s.y),e.lineTo(r.x,r.y),e.stroke(),K(e,s,i.legFu,rt,i.legFl,ht),K(e,r,i.armFu,ct,i.armFl,ft),e.fillStyle=m,e.beginPath(),e.arc(c.x,c.y,V,0,Math.PI*2),e.fill(),(vt=a.gear)==null||vt.call(a,e,l,"front"),a.flash&&a.flash>0&&(e.globalAlpha=(a.alpha??1)*Math.min(1,a.flash),e.strokeStyle="#ffffff",e.fillStyle="#ffffff",e.beginPath(),e.arc(c.x,c.y,V,0,Math.PI*2),e.fill(),e.beginPath(),e.moveTo(s.x,s.y),e.lineTo(r.x,r.y),e.stroke()),e.restore()}function K(e,t,o,n,i,a){const l=Z(t,o,n),s=Z(l,i,a);e.beginPath(),e.moveTo(t.x,t.y),e.lineTo(l.x,l.y),e.lineTo(s.x,s.y),e.stroke()}function ve(e){const t=e.match(/^hsl\((\d+(?:\.\d+)?),\s*(\d+)%,\s*(\d+)%\)$/);if(t)return`hsl(${t[1]}, ${t[2]}%, ${Math.round(Number(t[3])*.68)}%)`;const o=e.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i);if(o){const[n,i,a]=[o[1],o[2],o[3]].map(l=>Math.round(parseInt(l,16)*.68));return`rgb(${n}, ${i}, ${a})`}return e}function Dt(e,t){const{elbowF:o,handF:n}=t;e.translate(n.x,n.y),e.rotate(Math.atan2(n.y-o.y,n.x-o.x))}function Te(e){const{elbowF:t,handF:o}=e,n=Math.atan2(o.y-t.y,o.x-t.x);return{x:o.x+Math.cos(n)*10+Math.sin(n)*2.6,y:o.y+Math.sin(n)*10-Math.cos(n)*2.6}}function Me(e){const{elbowF:t,handF:o}=e,n=Math.atan2(o.y-t.y,o.x-t.x);return{x:o.x+Math.cos(n)*25,y:o.y+Math.sin(n)*25}}function Fe(e,t,o){e.save(),Dt(e,t),e.fillStyle="#6b4a2f",e.beginPath(),e.moveTo(-1.6,-2.2),e.lineTo(1.4,-2.2),e.lineTo(.2,3.6),e.lineTo(-2.6,3),e.closePath(),e.fill(),e.strokeStyle="#4d535b",e.lineWidth=.7,e.beginPath(),e.arc(2.4,-1.4,1.3,0,Math.PI),e.stroke(),e.fillStyle="#6c737c",e.fillRect(.6,-4.2,4,2.8),e.fillStyle="#9aa3ad",e.fillRect(4.4,-3.7,6,1.8),o&&(e.fillStyle="#ffe27a",e.beginPath(),e.moveTo(10.4,-5),e.lineTo(17,-2.8),e.lineTo(10.4,-.6),e.closePath(),e.fill()),e.restore()}function Pe(e,t,o){const{hip:n}=t;e.save(),e.translate(n.x+1.5,n.y+1),e.rotate(.25),o&&(e.fillStyle="#6b4a2f",e.fillRect(-1.2,-3.4,2.6,3.6)),e.fillStyle="#4a3423",e.fillRect(-1.8,0,3.6,6.5),e.restore()}function Xt(e){e.fillStyle="#5a3d27",e.fillRect(-4.5,-1,6,2),e.fillStyle="#c9a227",e.beginPath(),e.arc(-4.8,0,1.3,0,Math.PI*2),e.fill(),e.fillRect(1.3,-3.2,1.6,6.4),e.fillStyle="#cfd6de",e.beginPath(),e.moveTo(2.9,-1.3),e.lineTo(22.5,-1.1),e.lineTo(25,0),e.lineTo(22.5,1.1),e.lineTo(2.9,1.3),e.closePath(),e.fill(),e.fillStyle="#ffffff",e.fillRect(3.5,-1.1,18.5,.6)}function Re(e,t){e.save(),Dt(e,t),Xt(e),e.restore()}function Be(e,t){const{shoulder:o,hip:n}=t,i=Math.hypot(n.x-o.x,n.y-o.y)||1,a={x:(n.x-o.x)/i,y:(n.y-o.y)/i},l={x:-a.y,y:a.x};e.save(),e.translate(o.x+l.x*3-a.x*3,o.y+l.y*3-a.y*3),e.rotate(Math.atan2(a.y+l.y*.55,a.x+l.x*.55)),e.translate(2.5,0),Xt(e),e.restore()}const _=5,gt=4,w=360,k=gt*w,bt=160,I=10,O=64,H=6,Ae=190,y=44,d=_*bt+60,C=d-_*bt,N=3,wt=2,B=420,b=wt*B,It=200,W=12,T=40,Ct=220,u=k+y+Ct+T,ut=d-N*It,nt=34,L=-y-200,D=u+b+T+200;function v(e){return d-e*bt}function S(e){return d-e*It}function Ee(){const e=[],t=[],o=[],n=[],i=[];e.push({x:L-300,y:d,w:D-L+600,h:200,kind:"ground",style:"street"}),n.push({y:d,x0:L,x1:-y,place:"Street"}),n.push({y:d,x0:-y,x1:k+y,place:"Apartments · Floor 1"}),n.push({y:d,x0:k+y,x1:u,place:"Street"}),n.push({y:d,x0:u,x1:u+b,place:"Warehouse · Floor 1"}),n.push({y:d,x0:u+b,x1:D,place:"Street"}),i.push({x:L+20,y:d,facing:1}),i.push({x:D-20,y:d,facing:-1});for(let s=1;s<=_;s++)e.push({x:0,y:v(s),w:k,h:I,kind:"slab",style:"apartments"});for(let s=1;s<_;s++){const r=v(s);e.push({x:-y,y:r,w:y,h:I,kind:"balcony",style:"apartments"}),e.push({x:k,y:r,w:y,h:I,kind:"balcony",style:"apartments"}),n.push({y:r,x0:-y,x1:k+y,place:`Apartments · Floor ${s+1}`}),e.push({x:-y,y:r-22,w:3,h:22,kind:"rail",style:"apartments"}),i.push({x:-y+14,y:r,facing:1})}e.push({x:k+y-3,y:v(_-1)-22,w:3,h:22,kind:"rail",style:"apartments"});for(let s=0;s<_;s++){const r=v(s),c=v(s+1)+I;for(let m=0;m<=gt;m++){const f=m*w-H/2;e.push({x:f,y:c,w:H,h:r-O-c,kind:"wall",style:"apartments"});const g={x:f,y:r-O,w:H,h:O,kind:"door",style:"apartments",broken:!1};e.push(g),t.push(g)}}for(let s=0;s<_-1;s++){const r=s%2===0,c=r?1:2,m=r?1:-1,f=r?c*w+70:(c+1)*w-70;o.push({bottomX:f,topX:f+m*Ae,bottomY:v(s),topY:v(s+1),dir:m,style:"wood"})}const a=k+y-6;o.push({bottomX:a,topX:u-T+6,bottomY:v(1),topY:S(1),dir:1,style:"fire-escape"}),o.push({bottomX:a,topX:u-T+6,bottomY:v(2),topY:S(2),dir:1,style:"fire-escape"}),o.push({bottomX:a,topX:u+10,bottomY:v(3),topY:ut,dir:1,style:"fire-escape"});for(let s=1;s<=N;s++)e.push({x:u,y:S(s),w:b,h:W,kind:"slab",style:"warehouse"});n.push({y:ut,x0:u,x1:u+b,place:"Warehouse roof"});for(let s=1;s<N;s++){const r=S(s);e.push({x:u-T,y:r,w:T,h:W,kind:"balcony",style:"warehouse"}),e.push({x:u+b,y:r,w:T,h:W,kind:"balcony",style:"warehouse"}),e.push({x:u+b+T-3,y:r-22,w:3,h:22,kind:"rail",style:"warehouse"}),n.push({y:r,x0:u-T,x1:u+b+T,place:`Warehouse · Floor ${s+1}`}),i.push({x:u+b+T-14,y:r,facing:-1})}for(let s=0;s<N;s++){const r=S(s),c=S(s+1)+W;for(let m=0;m<=wt;m++){const f=u+m*B-H/2;e.push({x:f,y:c,w:H,h:r-O-c,kind:"wall",style:"warehouse"});const g={x:f,y:r-O,w:H,h:O,kind:"door",style:"warehouse",broken:!1};e.push(g),t.push(g)}}o.push({bottomX:u+100,topX:u+330,bottomY:S(0),topY:S(1),dir:1,style:"metal"}),o.push({bottomX:u+b-100,topX:u+b-330,bottomY:S(1),topY:S(2),dir:-1,style:"metal"});const l=[[0,470,0],[0,504,0],[0,487,1],[0,640,0],[1,120,0],[1,600,0],[1,600,1],[2,250,0],[2,640,0]];for(const[s,r,c]of l)e.push({x:u+r,y:S(s)-nt*(c+1),w:nt,h:nt,kind:"crate",style:"warehouse"});return{solids:e,doors:t,stairs:o,surfaces:n,spawns:i,routes:new Map}}function _e(e){for(const t of e.doors)t.broken=!1}function G(e,t,o){for(const n of e.surfaces)if(Math.abs(n.y-o)<3&&t>=n.x0&&t<=n.x1)return n;return null}function Le(e,t,o){if(t===o)return null;let n=e.routes.get(t);return n||e.routes.set(t,n=new Map),n.has(o)||n.set(o,Ye(e,t,o)),n.get(o)}function Ye(e,t,o){const n=new Map,i=[t],a=new Set([t]);for(;i.length>0;){const l=i.shift();for(const[s,r]of xe(e,l))if(!a.has(s)){if(a.add(s),n.set(s,l===t?r:n.get(l)),s===o)return n.get(s);i.push(s)}}return null}function xe(e,t){const o=[];for(const n of e.surfaces)n===t||n.y!==t.y||(n.x0===t.x1&&o.push([n,{walkTo:n.x0+4}]),n.x1===t.x0&&o.push([n,{walkTo:n.x1-4}]));for(const n of e.stairs){const i=G(e,n.bottomX,n.bottomY),a=G(e,n.topX,n.topY);i===t&&a&&o.push([a,{stairs:n,up:!0}]),a===t&&i&&o.push([i,{stairs:n,up:!1}])}return o}function $(e){const t=Math.sin(e*12.9898)*43758.5453;return t-Math.floor(t)}const Ft=["#2d3147","#3a2f45","#2c3b40","#3d3530","#30344a","#3b3045","#2b3a36","#41352d"];function Oe(e,t,o,n){const i=e.createLinearGradient(0,0,0,o);i.addColorStop(0,"#070816"),i.addColorStop(1,"#221d45"),e.fillStyle=i,e.fillRect(0,0,t,o),e.fillStyle="#d9dcff";for(let a=0;a<40;a++){const l=($(a*3.1)*900-n*.05)%t;e.globalAlpha=.3+$(a*7.7)*.7,e.fillRect((l+t)%t,$(a*5.3)*o*.8,1,1)}e.globalAlpha=1,e.fillStyle="#f4f1d0",e.beginPath(),e.arc(t*.8-n*.03,50,14,0,Math.PI*2),e.fill()}function He(e,t,o,n){const i=(a,l,s,r)=>a+s>o.x&&a<o.x+o.w&&l+r>o.y&&l<o.y+o.h;e.fillStyle="#141429";for(let a=-10;a<50;a++){const l=a*70,s=d-120-$(a*9.1)*260;i(l,s,66,d-s)&&e.fillRect(l,s,66,d-s)}Ge(e,i),De(e,i),Xe(e,i,n);for(const a of t.stairs)Ie(e,a,i);for(const a of t.solids)i(a.x,a.y-24,a.w,a.h+24)&&Ce(e,a);We(e,n),ze(e,i)}function Ge(e,t){for(const o of[L+90,k+y+Ct/2,u+b+150]){if(!t(o-60,d-110,120,110))continue;e.fillStyle="#3d4152",e.fillRect(o-1.5,d-90,3,90),e.fillRect(o-1.5,d-90,12,3),e.fillStyle="#ffd98a",e.fillRect(o+6,d-88,6,3);const n=e.createRadialGradient(o+9,d-86,2,o+9,d-86,80);n.addColorStop(0,"rgba(255, 217, 138, 0.22)"),n.addColorStop(1,"rgba(255, 217, 138, 0)"),e.fillStyle=n,e.fillRect(o-75,d-170,170,170)}}function De(e,t){for(let o=0;o<_;o++){const n=v(o),i=v(o+1)+I;for(let a=0;a<gt;a++){const l=a*w;if(!t(l,i,w,n-i))continue;e.fillStyle=Ft[(o*3+a)%Ft.length],e.fillRect(l,i,w,n-i),e.fillStyle="rgba(0, 0, 0, 0.18)",e.fillRect(l,n-40,w,40),e.fillStyle="rgba(255, 255, 255, 0.08)",e.fillRect(l,n-42,w,2);const s=l+((o+a)%2===0?w*.68:w*.18);e.fillStyle="rgba(255, 255, 255, 0.12)",e.fillRect(s-3,i+22,56,50),e.fillStyle="#0b0d22",e.fillRect(s,i+25,50,44),e.fillStyle="rgba(255, 255, 255, 0.12)",e.fillRect(s+23,i+25,4,44);const r=l+w/2;e.fillStyle="#555b77",e.fillRect(r-.5,i,1,10),e.fillStyle="#ffe9a8",e.beginPath(),e.arc(r,i+13,4,Math.PI,0),e.fill();const c=e.createRadialGradient(r,i+13,2,r,i+13,110);c.addColorStop(0,"rgba(255, 233, 168, 0.14)"),c.addColorStop(1,"rgba(255, 233, 168, 0)"),e.fillStyle=c,e.fillRect(l,i,w,n-i)}}}function Xe(e,t,o){for(let n=0;n<N;n++){const i=S(n),a=S(n+1)+W;for(let l=0;l<wt;l++){const s=u+l*B;if(!t(s,a,B,i-a))continue;e.fillStyle="#4a2622",e.fillRect(s,a,B,i-a),e.fillStyle="#3a1c19";for(let c=a+8,m=0;c<i;c+=8,m++){e.fillRect(s,c,B,1);for(let f=s+m%2*9;f<s+B;f+=18)e.fillRect(f,c-8,1,8)}const r=s+(l===0?230:70);e.fillStyle="#2b2f36",e.fillRect(r-4,a+20,128,96);for(let c=0;c<4;c++)for(let m=0;m<6;m++){const f=$(n*31+l*7+c*6+m)>.8;e.fillStyle=f?"#2f3a5c":"#10132a",e.fillRect(r+m*20,a+24+c*22,18,20)}e.fillStyle="#2a1715",e.fillRect(s,i-18,B,18);for(let c=s;c<s+B;c+=16)e.fillStyle="#c9a227",e.beginPath(),e.moveTo(c,i-18),e.lineTo(c+8,i-18),e.lineTo(c+2,i),e.lineTo(c-6,i),e.closePath(),e.fill();for(const c of[s+110,s+310]){const m=c===u+310&&n===1&&Math.sin(o*23)>.6?.3:1;e.fillStyle="#6b6f78",e.fillRect(c-.5,a,1,34),e.fillStyle="#8f949e",e.beginPath(),e.moveTo(c-9,a+42),e.lineTo(c+9,a+42),e.lineTo(c+3,a+34),e.lineTo(c-3,a+34),e.closePath(),e.fill(),e.fillStyle=`rgba(255, 240, 200, ${.09*m})`,e.beginPath(),e.moveTo(c-8,a+42),e.lineTo(c+8,a+42),e.lineTo(c+70,i),e.lineTo(c-70,i),e.closePath(),e.fill()}}}}function Ie(e,t,o){const n=Math.min(t.bottomX,t.topX);if(!o(n-10,t.topY-30,Math.abs(t.topX-t.bottomX)+20,t.bottomY-t.topY+30))return;const i=Math.max(6,Math.round((t.bottomY-t.topY)/11)),a=(t.topX-t.bottomX)/i,l=(t.bottomY-t.topY)/i;if(t.style==="wood"){e.fillStyle="#4a3829",e.beginPath(),e.moveTo(t.bottomX,t.bottomY);for(let s=0;s<i;s++)e.lineTo(t.bottomX+a*s,t.bottomY-l*(s+1)),e.lineTo(t.bottomX+a*(s+1),t.bottomY-l*(s+1));e.lineTo(t.topX,t.bottomY),e.closePath(),e.fill()}else e.strokeStyle=t.style==="metal"?"#59606b":"#3f454f",e.lineWidth=3,e.beginPath(),e.moveTo(t.bottomX,t.bottomY+2),e.lineTo(t.topX,t.topY+2),e.stroke();e.fillStyle=t.style==="wood"?"#7a5c43":t.style==="metal"?"#9aa1ab":"#6f7682";for(let s=0;s<i;s++){const r=t.bottomX+a*s;e.fillRect(Math.min(r,r+a),t.bottomY-l*(s+1),Math.abs(a),2)}e.strokeStyle=t.style==="wood"?"#9a8a7a":"#c9a227",e.lineWidth=1.2,e.beginPath(),e.moveTo(t.bottomX,t.bottomY-26),e.lineTo(t.topX,t.topY-26),e.stroke();for(let s=0;s<=i;s+=3){const r=t.bottomX+a*s,c=t.bottomY-l*s;e.beginPath(),e.moveTo(r,c),e.lineTo(r,c-26),e.stroke()}}function Ce(e,t){const o=t.style==="warehouse";if(t.kind==="ground"){e.fillStyle="#26272e",e.fillRect(t.x,t.y,t.w,t.h),e.fillStyle="#3a3c46",e.fillRect(t.x,t.y,t.w,3),e.fillStyle="#6e5644",e.fillRect(-y,t.y,k+y*2,3),e.fillStyle="#7d8088",e.fillRect(u,t.y,b,3),e.fillStyle="#8a8a7a";for(let n=L;n<D;n+=40)e.fillRect(n,t.y+14,18,2)}else if(t.kind==="slab"||t.kind==="balcony")if(e.fillStyle=o?"#2e3138":"#3c3a44",e.fillRect(t.x,t.y,t.w,t.h),o){e.fillStyle="#7d8088",e.fillRect(t.x,t.y,t.w,3),e.fillStyle="#4c5058";for(let n=t.x;n<t.x+t.w;n+=6)e.fillRect(n,t.y+3,1,t.h-3)}else e.fillStyle=t.kind==="slab"?"#6e5644":"#5d6270",e.fillRect(t.x,t.y,t.w,3);else if(t.kind==="rail"){e.strokeStyle=o?"#c9a227":"#7d8494",e.lineWidth=1.2;const n=t.x<0?1:-1,i=t.x+t.w/2,a=(o?T:y)*n;e.beginPath(),e.moveTo(i,t.y+t.h),e.lineTo(i,t.y),e.lineTo(i+a,t.y),e.stroke()}else if(t.kind==="wall")e.fillStyle=o?"#5b2f29":"#5a5f73",e.fillRect(t.x,t.y,t.w,t.h);else if(t.kind==="crate")e.fillStyle="#8a6a3f",e.fillRect(t.x,t.y,t.w,t.h),e.strokeStyle="#5e4527",e.lineWidth=2,e.strokeRect(t.x+1,t.y+1,t.w-2,t.h-2),e.beginPath(),e.moveTo(t.x+2,t.y+2),e.lineTo(t.x+t.w-2,t.y+t.h-2),e.moveTo(t.x+t.w-2,t.y+2),e.lineTo(t.x+2,t.y+t.h-2),e.stroke();else if(t.broken)e.fillStyle=o?"#6c727c":"#6b4a2f",e.beginPath(),e.moveTo(t.x,t.y),e.lineTo(t.x+t.w,t.y),e.lineTo(t.x+t.w*.6,t.y+9),e.lineTo(t.x+t.w*.2,t.y+5),e.closePath(),e.fill(),e.beginPath(),e.moveTo(t.x,t.y+t.h),e.lineTo(t.x+t.w,t.y+t.h),e.lineTo(t.x+t.w*.7,t.y+t.h-7),e.lineTo(t.x+t.w*.3,t.y+t.h-11),e.closePath(),e.fill();else if(o){e.fillStyle="#7b818b",e.fillRect(t.x,t.y,t.w,t.h),e.fillStyle="#5d636c",e.fillRect(t.x+1,t.y+3,t.w-2,t.h-6);for(let n=t.y+26;n<t.y+38;n+=4)e.fillStyle=n/4%2<1?"#c9a227":"#1d1d1d",e.fillRect(t.x,n,t.w,4)}else e.fillStyle="#8a5f3c",e.fillRect(t.x,t.y,t.w,t.h),e.fillStyle="#6b4a2f",e.fillRect(t.x+1,t.y+6,t.w-2,20),e.fillRect(t.x+1,t.y+34,t.w-2,24),e.fillStyle="#e0c060",e.fillRect(t.x+t.w/2-1,t.y+31,2,2)}function We(e,t){e.fillStyle="#4b4f63",e.fillRect(0,C-8,4,8),e.fillRect(k-4,C-8,4,8),e.fillStyle=Math.sin(t*3)>0?"#ff4d4d":"#5a1a1a",e.fillRect(k/2-1.5,C-22,3,3),e.fillStyle="#4b4f63",e.fillRect(k/2-.5,C-19,1,5)}function ze(e,t){const o=u+b-180,n=ut;if(t(o-10,n-120,100,120)){e.strokeStyle="#3a3d45",e.lineWidth=2,e.beginPath(),e.moveTo(o+10,n),e.lineTo(o+18,n-50),e.moveTo(o+70,n),e.lineTo(o+62,n-50),e.moveTo(o+12,n-12),e.lineTo(o+66,n-40),e.moveTo(o+68,n-12),e.lineTo(o+14,n-40),e.stroke(),e.fillStyle="#5a4128",e.fillRect(o+12,n-98,56,48),e.fillStyle="#4a3420";for(let i=0;i<4;i++)e.fillRect(o+12,n-92+i*12,56,2);e.fillStyle="#3a2a1a",e.beginPath(),e.moveTo(o+8,n-98),e.lineTo(o+40,n-118),e.lineTo(o+72,n-98),e.closePath(),e.fill()}}const P=480,A=270,j=5,Pt=36,ot=150,Ne=14,$e=1500,Rt=150,Ue=250,Ke=.42,Bt=.14,je=1.35,qe=110,Ve=1.4,Ze=16,mt=100,Je=2,At="#3ddc5a",Qe=2.6,tn=3,Y=1/120,en=500,Et={length:.28,activeFrom:.07,activeTo:.16,reach:26,top:36,bottom:18,knockback:140},it={length:.42,activeFrom:.1,activeTo:.26,reach:32,top:30,bottom:4,knockback:950},at={length:3,activeFrom:.04,activeTo:3,reach:26,top:22,bottom:-6,knockback:1300},nn=300,_t={length:.3,activeFrom:.14,activeTo:.22,reach:30,top:48,bottom:22,knockback:170},Lt={length:0,activeFrom:0,activeTo:0,reach:0,top:0,bottom:0,knockback:120},on=3,an=1.35,ln=.25,sn=.7,rn=1.5,hn=1.5,cn=10,lt=120,fn={length:.45,activeFrom:.24,activeTo:.34,reach:19,top:26,bottom:12,knockback:60,damage:.5},un=.42,st=.12,Yt=.3,R={length:.55,activeFrom:.2,activeTo:.34,reach:40,top:54,bottom:0,knockback:260,damage:1.5},xt=1100,mn=25,dn={length:.5,activeFrom:.28,activeTo:.38,reach:24,top:36,bottom:18,knockback:90};class Ot{constructor(t,o,n,i){h(this,"vx",0);h(this,"vy",0);h(this,"onGround",!0);h(this,"facing",1);h(this,"hp");h(this,"action","none");h(this,"attack",null);h(this,"actionTime",0);h(this,"actionLength",0);h(this,"hitAlready",new Set);h(this,"pose",Gt(0));h(this,"runPhase",0);h(this,"flash",0);h(this,"deadTime",0);h(this,"climb",null);h(this,"stuck",!1);h(this,"cooldown",.8);h(this,"attackSpeed",1);h(this,"blocking",!1);h(this,"slide",null);h(this,"speed",100);h(this,"range",20);h(this,"size",1);h(this,"punch",dn);h(this,"fired",!1);h(this,"trail",[]);this.x=t,this.y=o,this.color=n,this.isPlayer=i,this.hp=i?mt:Je}start(t,o,n=null){this.action=t,this.actionTime=0,this.actionLength=o,this.attack=n,this.attackSpeed=1,this.hitAlready.clear()}get alive(){return this.action!=="dead"}}class yn{constructor(t){h(this,"held",{left:!1,right:!1,down:!1,block:!1});h(this,"paused",!0);h(this,"ctx");h(this,"scale",1);h(this,"building",Ee());h(this,"camera",{x:0,y:0});h(this,"lastPlayerSurface",null);h(this,"downPressedAt",null);h(this,"downPressedVx",0);h(this,"player");h(this,"enemies",[]);h(this,"drops",[]);h(this,"stains",[]);h(this,"sparks",[]);h(this,"bullets",[]);h(this,"popups",[]);h(this,"splinters",[]);h(this,"presses",[]);h(this,"lastAttackPress",-10);h(this,"spawnTimer",1);h(this,"runtTimer",2);h(this,"gangTimer",lt);h(this,"knockouts",0);h(this,"headshots",0);h(this,"defeatTimer",0);h(this,"time",0);h(this,"lastFrame",0);h(this,"leftover",0);this.canvas=t;const o=t.getContext("2d");if(!o)throw new Error("Canvas 2D is not available");this.ctx=o,this.restart()}restart(){this.player=new Ot(w/2,d,At,!0),_e(this.building),this.lastPlayerSurface=null,this.downPressedAt=null,this.snapCamera(),this.enemies=[],this.drops=[],this.stains=[],this.sparks=[],this.bullets=[],this.popups=[],this.splinters=[],this.presses=[],this.knockouts=0,this.headshots=0,this.spawnTimer=1,this.runtTimer=2,this.gangTimer=lt,this.defeatTimer=0}press(t){this.paused||this.presses.push(t)}resize(t,o){const n=window.devicePixelRatio||1;this.canvas.width=Math.max(1,Math.round(t*n)),this.canvas.height=Math.max(1,Math.round(o*n)),this.scale=this.canvas.width/P,this.draw()}start(){const t=o=>{const n=this.lastFrame?Math.min((o-this.lastFrame)/1e3,.1):0;if(this.lastFrame=o,!this.paused)for(this.leftover+=n;this.leftover>=Y;)this.update(Y),this.leftover-=Y;this.draw(),requestAnimationFrame(t)};requestAnimationFrame(t)}update(t){this.time+=t,this.defeatTimer>0?(this.defeatTimer-=t,this.defeatTimer<=0&&this.restart()):(this.controlPlayer(),this.spawnEnemies(t)),this.presses=[];for(const o of this.enemies)this.thinkEnemy(o,t);for(const o of[this.player,...this.enemies])this.moveFighter(o,t);this.enemies=this.enemies.filter(o=>!(o.action==="dead"&&o.deadTime>2.5)),this.updateBullets(t),this.updateDrops(t)}controlPlayer(){const t=this.player;if(t.alive){for(const o of this.presses){const n=t.action==="none"||(t.action==="punch"||t.action==="kick")&&t.attack!==null&&t.actionTime>t.attack.activeTo;if(o==="attack"){const i=this.time-this.lastAttackPress,a=Math.max(1,Math.min(2.2,.3/i));n&&(t.start("punch",Et.length,Et),t.attackSpeed=a),this.lastAttackPress=this.time}else if(o==="kick")!t.onGround&&(t.action==="none"||t.action==="punch")?(t.start("dropkick",at.length,at),t.vy=Math.max(t.vy,-120)):n&&t.start("kick",it.length,it);else if(o==="jump"){const i=this.stairsHere(t,"bottom");i&&t.action==="none"?this.startClimb(t,i,!0):t.onGround&&t.action==="none"&&(t.vy=-500,t.onGround=!1)}else if(o==="shoot")t.action==="shoot"&&t.fired?(t.actionTime=st-.04,t.fired=!1):t.action!=="roll"&&t.action!=="hurt"&&t.action!=="sword"&&t.action!=="shoot"&&(t.start("shoot",un),t.fired=!1);else if(o==="sword")t.action!=="roll"&&t.action!=="hurt"&&t.action!=="sword"&&(t.start("sword",R.length,R),t.trail=[]);else if(o==="roll"){const i=this.stairsHere(t,"top");i&&t.action==="none"?this.startClimb(t,i,!1):(this.downPressedAt=this.time,this.downPressedVx=t.vx)}}if(this.rollOrSlide(t),t.blocking=this.held.block&&t.onGround&&t.action==="none"&&!t.slide,t.slide){if(t.slide.speed=Math.max(0,t.slide.speed-(qe+t.slide.speed*Ve)*Y),t.vx=t.slide.dir*t.slide.speed,X(t.action)){const o=(this.held.right?1:0)-(this.held.left?1:0);o!==0&&(t.facing=o)}Math.random()<.5&&this.sparks.push({x:t.x-t.slide.dir*4,y:t.y-1,vx:-t.slide.dir*(20+Math.random()*40),vy:-10-Math.random()*40,life:.2+Math.random()*.2,dust:!0})}else if(t.blocking)t.vx=F(t.vx,0,1400*Y);else if(t.action==="none"){const o=(this.held.right?1:0)-(this.held.left?1:0),n=t.onGround?1400:900;t.vx=F(t.vx,o*Rt,n*Y),o!==0&&(t.facing=o)}else if(X(t.action)){const o=(this.held.right?1:0)-(this.held.left?1:0),n=t.onGround?1400:900;t.vx=F(t.vx,o*Rt,n*Y),o!==0&&(t.facing=o)}}}spawnEnemies(t){if(this.runtTimer-=t,this.runtTimer<=0&&(this.runtTimer=hn,this.spawnEnemy("runt")),this.spawnTimer-=t,this.spawnTimer<=0&&(this.spawnTimer=Qe,this.spawnEnemy(Math.random()<ln?"brute":"normal")),this.gangTimer-=t,this.gangTimer<=0){this.gangTimer=lt;const o=this.randomSpawn();for(let n=0;n<cn;n++)this.spawnEnemy("normal",o,n*2)}}randomSpawn(){return this.building.spawns[Math.floor(Math.random()*this.building.spawns.length)]}spawnEnemy(t,o=this.randomSpawn(),n=0){const i=new Ot(o.x+o.facing*n,o.y,pn(),!1);i.facing=o.facing,i.speed=70+Math.random()*45,i.range=18+Math.random()*5,t==="runt"?(i.size=sn,i.hp=rn,i.punch=fn,i.range=13+Math.random()*4):t==="brute"&&(i.size=an,i.hp=on,i.punch=_t,i.speed=32+Math.random()*12,i.range=24+Math.random()*4),i.cooldown=.1+Math.random()*.25,this.enemies.push(i)}thinkEnemy(t,o){if(!t.alive){t.deadTime+=o;return}if(t.cooldown-=o,t.action!=="none")return;const n=this.player;if(!n.alive||this.defeatTimer>0){t.onGround&&(t.vx=F(t.vx,0,800*o));return}if(!t.onGround){t.vx=F(t.vx,t.facing*t.speed,600*o);return}const i=G(this.building,t.x,t.y),a=this.playerSurface;let l=n.x,s=i!==null&&i===a;if(i&&a&&!s){const f=Le(this.building,i,a);if(f&&"stairs"in f){if(l=f.up?f.stairs.bottomX:f.stairs.topX,Math.abs(t.x-l)<6){this.startClimb(t,f.stairs,f.up);return}}else f?l=f.walkTo:s=!0}const r=l-t.x;if(t.facing=r>=0?1:-1,this.doorAhead(t)){t.vx=F(t.vx,0,900*o),t.cooldown<=0&&(t.start("punch",t.punch.length,t.punch),t.cooldown=t.punch.length+.2);return}if(this.lowObstacleAhead(t)){t.vy=-500,t.vx=t.facing*t.speed,t.onGround=!1;return}const m=Math.abs(r);if(!s||m>t.range){const f=this.enemies.some(g=>g!==t&&g.alive&&Math.sign(g.x-t.x)===t.facing&&Math.abs(g.x-t.x)<15&&Math.abs(n.x-g.x)<m);t.vx=F(t.vx,f&&s?0:t.facing*t.speed,900*o)}else if(t.vx=F(t.vx,0,900*o),t.cooldown<=0&&Math.abs(n.y-t.y)<30){t.start("punch",t.punch.length,t.punch);const f=t.punch===_t?.05+Math.random()*.15:.15+Math.random()*.45;t.cooldown=t.punch.length+f}}moveFighter(t,o){if(t.flash=Math.max(0,t.flash-o*5),t.action==="climb"&&t.climb){this.moveOnStairs(t,o),this.animate(t,o);return}if(t.stuck){this.animate(t,o);return}t.action!=="none"&&t.action!=="dead"&&(t.actionTime+=o*(t.action==="punch"||t.action==="kick"?t.attackSpeed:1),t.action==="shoot"&&!t.fired&&t.actionTime>=st&&(t.fired=!0,this.fire(t)),t.action==="roll"?t.vx=t.facing*Ue:t.action==="dropkick"?(t.vx=t.facing*nn,t.onGround&&(t.action="none",t.vx*=.3)):t.onGround&&!(t.isPlayer&&X(t.action))&&(t.vx=F(t.vx,0,700*o)),t.attack&&t.actionTime>=t.attack.activeFrom&&t.actionTime<=t.attack.activeTo&&this.checkHits(t,t.attack),t.actionTime>=t.actionLength&&(t.action="none")),t.action==="dead"&&t.onGround&&(t.vx=F(t.vx,0,600*o)),t.vy=Math.min(t.vy+$e*o,900),this.moveX(t,t.vx*o),this.moveY(t,t.vy*o),t.x=Math.max(L+6,Math.min(D-6,t.x)),this.animate(t,o)}solidsFor(t){return this.building.solids.filter(o=>!o.broken&&this.overlaps(t,o))}overlaps(t,o){const n=j*t.size;return t.x+n>o.x&&t.x-n<o.x+o.w&&t.y>o.y&&t.y-Pt*t.size<o.y+o.h}moveX(t,o){t.x+=o;const n=j*t.size;for(const i of this.solidsFor(t))o>0?t.x=i.x-n:o<0&&(t.x=i.x+i.w+n),t.action!=="dead"?t.vx=0:t.vx=-t.vx*.3}moveY(t,o){t.y+=o,t.onGround=!1;for(const n of this.solidsFor(t))o>0?(t.y=n.y,t.onGround=!0):o<0&&(t.y=n.y+n.h+Pt*t.size),t.vy=0}stairsHere(t,o){if(!t.onGround)return null;for(const n of this.building.stairs){const i=o==="bottom"?n.bottomX:n.topX,a=o==="bottom"?n.bottomY:n.topY;if(Math.abs(t.x-i)<Ne&&Math.abs(t.y-a)<2)return n}return null}startClimb(t,o,n){t.start("climb",99),t.climb={stairs:o,up:n,along:n?0:1},t.vx=0,t.vy=0}moveOnStairs(t,o){const n=t.climb,i=n.stairs,a=Math.hypot(i.topX-i.bottomX,i.topY-i.bottomY);n.along+=(n.up?1:-1)*ot*o/a;const l=Math.max(0,Math.min(1,n.along));t.x=i.bottomX+(i.topX-i.bottomX)*l,t.y=i.bottomY+(i.topY-i.bottomY)*l,t.facing=n.up?i.dir:-i.dir,t.vx=t.facing*ot*.75,t.onGround=!0,(n.along<=0||n.along>=1)&&(t.action="none",t.climb=null,t.x+=t.facing*8,t.vx=0)}rollOrSlide(t){if(this.downPressedAt!==null){const o=this.time-this.downPressedAt;if(!this.held.down)o<Bt&&t.onGround&&t.action==="none"&&(t.slide=null,t.start("roll",Ke)),this.downPressedAt=null;else if(o>=Bt){this.downPressedAt=null;const n=t.action==="roll"||t.action==="climb"||t.action==="dropkick"||t.action==="hurt";if(t.onGround&&!n){const i=Math.abs(t.vx)>Math.abs(this.downPressedVx)?t.vx:this.downPressedVx;t.slide={dir:Math.sign(i)||t.facing,speed:Math.abs(i)*je}}}}if(t.slide){const o=t.slide.speed<12,n=t.action==="roll"||t.action==="climb"||t.action==="dropkick"||t.action==="hurt"||t.action==="dead";(o||n||!this.held.down||!t.onGround)&&(t.slide=null)}}lowObstacleAhead(t){const o=j*t.size,n=t.x+t.facing*(o+4);return this.building.solids.some(i=>i.kind==="crate"&&n>i.x&&n<i.x+i.w&&i.y<t.y&&i.y>t.y-75)}get playerSurface(){const t=this.player;if(t.climb){const o=t.climb.stairs;this.lastPlayerSurface=t.climb.up?G(this.building,o.topX,o.topY):G(this.building,o.bottomX,o.bottomY)}else this.lastPlayerSurface=G(this.building,t.x,t.y)??this.lastPlayerSurface;return this.lastPlayerSurface}doorAhead(t){for(const o of this.building.doors){if(o.broken||Math.abs(o.y+o.h-t.y)>2)continue;const n=(o.x+o.w/2-t.x)*t.facing;if(n>0&&n<j*t.size+10)return o}return null}breakDoor(t,o){t.broken=!0;for(let n=0;n<18;n++)this.splinters.push({x:t.x+t.w/2,y:t.y+Math.random()*t.h,vx:o*(40+Math.random()*180)+(Math.random()-.5)*60,vy:-60-Math.random()*160,spin:(Math.random()-.5)*20,angle:Math.random()*Math.PI,life:1.2+Math.random()*.6});this.stains=this.stains.filter(n=>!(n.x>t.x-2&&n.x<t.x+t.w+2&&n.y>t.y&&n.y<t.y+t.h))}checkHits(t,o){const n=t.y-o.top,i=t.y-o.bottom;for(const l of this.building.doors){if(l.broken)continue;const s=(l.x+l.w/2-t.x)*t.facing;s<-4||s>o.reach+4||i<l.y||n>l.y+l.h||this.breakDoor(l,t.facing)}const a=t.isPlayer?this.enemies:[this.player];for(const l of a){if(!l.alive||l.action==="roll"||t.hitAlready.has(l))continue;const s=(l.x-t.x)*t.facing;if(s<-4||s>o.reach+6*l.size)continue;const r=l.slide?Ze:36*l.size;i<l.y-r||n>l.y||(t.hitAlready.add(l),l.blocking&&Math.sign(t.x-l.x)===l.facing?this.blocked(l,t):this.hurt(l,t,o))}}hurt(t,o,n){const i=n.damage??1;t.hp-=i,t.flash=1;const a=o.facing,l=t.hp<=0;this.splat(t,a,(l?80:16)*t.size*i,l?2.2:1),l&&t.onGround&&this.addStain(t.x+a*6,t.y+1,16,3,t.color),t.isPlayer||(t.facing=-a);const s=t.action==="climb";if(t.hp<=0){t.start("dead",0),t.stuck=s,t.climb=null,t.deadTime=0,t.vx=a*n.knockback*.8/t.size,t.vy=-200,t.isPlayer?this.defeatTimer=tn:this.knockouts+=1;return}if(s)return;const r=n===it||n===at;t.start("hurt",t.isPlayer?.12:r?.6:.25),t.vx=a*n.knockback/t.size,t.vy=r?-320:-70}fire(t){const o=(Math.random()-.5)*mn*Math.PI/180,n=Te(q(t.pose)),i=t.x+t.facing*n.x,a=t.y+n.y;this.bullets.push({x:i,y:a,vx:Math.cos(o)*xt*t.facing,vy:Math.sin(o)*xt,life:1});for(let l=0;l<6;l++)this.sparks.push({x:i+t.facing*4,y:a,vx:t.facing*(60+Math.random()*120),vy:(Math.random()-.5)*120,life:.08+Math.random()*.08})}updateBullets(t){for(const o of this.bullets){o.life-=t,o.x+=o.vx*t,o.y+=o.vy*t;const n=this.enemies.find(a=>{if(!a.alive)return!1;const l=q(a.pose).head,s=a.x+a.facing*l.x*a.size,r=a.y+l.y*a.size;return Math.hypot(o.x-s,o.y-r)<5.5*a.size}),i=this.enemies.find(a=>a.alive&&Math.abs(o.x-a.x)<6*a.size&&o.y>a.y-30*a.size&&o.y<a.y);if(n)this.hurt(n,this.player,{...Lt,damage:n.hp,knockback:220}),this.popups.push({x:n.x,y:n.y-46*n.size,text:"HEADSHOT!",life:1}),this.headshots+=1,o.life=0;else if(i)this.hurt(i,this.player,Lt),o.life=0;else if(this.solidAt(o.x,o.y)){const a=this.solidAt(o.x,o.y),l=o.x,s=o.y;a.kind==="door"?this.breakDoor(a,Math.sign(o.vx)):this.addStain(l,s,1.4,1.4,"#0b0c14");for(let r=0;r<4;r++)this.sparks.push({x:l,y:s,vx:(Math.random()-.5)*140,vy:-40-Math.random()*100,life:.15+Math.random()*.1});o.life=0}}this.bullets=this.bullets.filter(o=>o.life>0);for(const o of this.splinters){o.life-=t,o.vy+=700*t,o.x+=o.vx*t,o.y+=o.vy*t,o.angle+=o.spin*t;const n=this.solidAt(o.x,o.y);n&&n.kind!=="door"&&(o.y=n.y,o.vx*=.5,o.vy=0,o.spin=0)}this.splinters=this.splinters.filter(o=>o.life>0);for(const o of this.popups)o.life-=t,o.y-=18*t;this.popups=this.popups.filter(o=>o.life>0)}blocked(t,o){t.vx=o.facing*60;for(let n=0;n<8;n++)this.sparks.push({x:t.x+t.facing*8,y:t.y-30+(Math.random()-.5)*6,vx:t.facing*(20+Math.random()*90)*-1+(Math.random()-.5)*60,vy:-30-Math.random()*120,life:.25+Math.random()*.15})}splat(t,o,n,i=1){for(let a=0;a<n;a++)this.drops.push({x:t.x+(Math.random()-.5)*6,y:t.y-22-Math.random()*10,vx:(o*(50+Math.random()*220)+(Math.random()-.5)*80)*(i>1?1.4:1),vy:(-40-Math.random()*240)*(i>1?1.3:1),size:(1+Math.random()*1.8)*(i>1?.7+Math.random()*i*.6:1),color:t.color,wallAt:Math.random()<.3?.08+Math.random()*.3:null,age:0})}updateDrops(t){for(const n of this.sparks)n.life-=t,n.vy+=500*t,n.x+=n.vx*t,n.y+=n.vy*t;this.sparks=this.sparks.filter(n=>n.life>0);const o=[];for(const n of this.drops)if(n.age+=t,n.vy+=900*t,n.x+=n.vx*t,n.y+=n.vy*t,n.wallAt!==null&&n.age>=n.wallAt)this.addStain(n.x,n.y,n.size*1.3,n.size*1.3,n.color);else if(this.solidAt(n.x,n.y)){const i=this.solidAt(n.x,n.y);n.y-n.vy*t<=i.y+1?this.addStain(n.x,i.y+1,n.size*2.6,n.size*.7,n.color):this.addStain(n.vx>0?i.x-1:i.x+i.w+1,n.y,n.size*.8,n.size*2,n.color)}else n.age>4||o.push(n);this.drops=o}solidAt(t,o){for(const n of this.building.solids)if(!n.broken&&t>=n.x&&t<=n.x+n.w&&o>=n.y&&o<=n.y+n.h)return n;return null}addStain(t,o,n,i,a){this.stains.push({x:t,y:o,rx:n,ry:i,color:a}),this.stains.length>en&&this.stains.shift()}animate(t,o){let n,i=14;const a=t.actionLength>0?Math.min(1,t.actionTime/t.actionLength):0;if(t.action==="dead")n=Se(),i=10;else if(t.action==="roll")n=de(),i=30;else if(t.action==="punch"){const l=t.attack?t.attack.activeFrom/t.attack.length:.25;n=ce(a,l),i=t.isPlayer?34:22}else if(t.action==="shoot")n=t.actionTime>=.06&&t.actionTime<Yt?ge():pe(),i=45;else if(t.action==="sword"){const l=t.actionTime;n=be(l<.1?"grab":l<R.activeFrom?"raise":l<.45?l<R.activeTo?"slash":"follow":"grab"),i=l>=R.activeFrom&&l<R.activeTo?55:35}else t.action==="dropkick"?(n=ue(),i=30):t.action==="kick"?(n=fe(a),i=30):t.action==="climb"?(t.runPhase+=o*ot*.06,n=et(t.runPhase),i=22):t.blocking?(n=ye(),i=30):t.action==="hurt"?(n=we(),i=24):t.onGround?Math.abs(t.vx)>15?(t.runPhase+=o*Math.abs(t.vx)*.075,n=et(t.runPhase),i=22):(n=Gt(this.time+t.speed),i=10):(n=he(t.vy),i=12);if(t.slide){const l=me();n=X(t.action)?{...n,lean:l.lean,legFu:l.legFu,legFl:l.legFl,legBu:l.legBu,legBl:l.legBl}:l,i=Math.max(i,30)}else if(X(t.action)&&t.action!=="kick"&&t.onGround&&Math.abs(t.vx)>15){t.runPhase+=o*Math.abs(t.vx)*.075;const l=et(t.runPhase);n={...n,legFu:l.legFu,legFl:l.legFl,legBu:l.legBu,legBl:l.legBl}}if(re(t.pose,n,i,o),t.action==="sword"&&t.actionTime>=R.activeFrom-.03&&t.actionTime<R.activeTo+.05){const l=Me(q(t.pose));t.trail.push({x:t.x+t.facing*l.x*t.size,y:t.y+l.y*t.size}),t.trail.length>8&&t.trail.shift()}else t.trail.length>0&&t.trail.shift()}draw(){const t=this.ctx;this.followCamera(),t.setTransform(this.scale,0,0,this.scale,0,0),Oe(t,P,A,this.camera.x),t.save(),t.translate(-Math.round(this.camera.x*4)/4,-Math.round(this.camera.y*4)/4),He(t,this.building,{x:this.camera.x,y:this.camera.y,w:P,h:A},this.time);for(const n of this.stains)t.fillStyle=n.color,t.globalAlpha=.85,t.beginPath(),t.ellipse(n.x,n.y,n.rx,n.ry,0,0,Math.PI*2),t.fill();t.globalAlpha=1;const o=[...this.enemies.filter(n=>!n.alive),...this.enemies.filter(n=>n.alive),this.player];for(const n of o)this.drawFighter(n);for(const n of this.drops)t.fillStyle=n.color,t.beginPath(),t.arc(n.x,n.y,n.size,0,Math.PI*2),t.fill();t.strokeStyle="#fff2b0",t.lineWidth=1.2;for(const n of this.bullets)t.beginPath(),t.moveTo(n.x,n.y),t.lineTo(n.x-n.vx*.012,n.y-n.vy*.012),t.stroke();for(const n of this.sparks)t.globalAlpha=Math.min(1,n.life*4)*(n.dust?.5:1),t.fillStyle=n.dust?"#9a9aa6":"#fff6c8",t.fillRect(n.x-1,n.y-1,2,2);t.globalAlpha=1,t.fillStyle="#8a5f3c";for(const n of this.splinters)t.save(),t.globalAlpha=Math.min(1,n.life*2),t.translate(n.x,n.y),t.rotate(n.angle),t.fillRect(-3,-.8,6,1.6),t.restore();t.globalAlpha=1,this.drawStairsHint();for(const n of this.popups)t.globalAlpha=Math.min(1,n.life*2),t.textAlign="center",t.font="bold 11px system-ui, sans-serif",t.fillStyle="#12131c",t.fillText(n.text,n.x+1,n.y+1),t.fillStyle="#ffd23f",t.fillText(n.text,n.x,n.y);t.globalAlpha=1,t.restore(),this.drawHud()}drawStairsHint(){const t=this.player;if(t.action!=="none"||!t.alive)return;const o=this.stairsHere(t,"bottom"),n=this.stairsHere(t,"top");if(!o&&!n)return;const i=this.ctx,a=Math.sin(this.time*6)*1.5;i.fillStyle="#ffd23f",i.font="bold 12px system-ui, sans-serif",i.textAlign="center",i.fillText(o?"↑":"↓",t.x,t.y-46+a)}cameraGoal(){const t=this.player;return{x:Math.max(L-20,Math.min(D+20-P,t.x-P/2)),y:Math.max(C-60,Math.min(d+30-A,t.y-A*.62))}}snapCamera(){this.camera=this.cameraGoal()}followCamera(){const t=this.cameraGoal();this.camera.x+=(t.x-this.camera.x)*.12,this.camera.y+=(t.y-this.camera.y)*.12}drawFighter(t){const o=t.actionLength>0?Math.min(1,t.actionTime/t.actionLength):0,n=t.action==="roll"?o*Math.PI*2:0,i=t.action==="dead"?-Math.min(1,t.deadTime*3.5)*(Math.PI/2):0,a=t.action==="dead"&&!t.isPlayer?Math.max(0,Math.min(1,2.5-t.deadTime)):1,l=t.action==="roll"?15:6;ke(this.ctx,t.x,t.y,t.facing,t.pose,{color:t.color,spin:n,minHip:l,topple:i,alpha:a,flash:t.flash,size:t.size,gear:t.isPlayer?(s,r,c)=>this.drawGear(t,s,r,c):void 0}),t.isPlayer&&this.drawSwordTrail(t)}drawGear(t,o,n,i){const a=t.actionTime,l=t.action==="shoot"&&a>=.05&&a<Yt+.08,s=t.action==="sword"&&a>=.07&&a<R.length-.04;if(i==="behind"){s||Be(o,n);return}Pe(o,n,!l),l&&Fe(o,n,t.fired&&a<st+.06),s&&Re(o,n)}drawSwordTrail(t){if(t.action!=="sword"||t.trail.length<2)return;const o=this.ctx;o.lineCap="round";for(let n=1;n<t.trail.length;n++)o.strokeStyle=`rgba(230, 240, 255, ${n/t.trail.length*.6})`,o.lineWidth=1+n/t.trail.length*3,o.beginPath(),o.moveTo(t.trail[n-1].x,t.trail[n-1].y),o.lineTo(t.trail[n].x,t.trail[n].y),o.stroke()}drawHud(){var l;const t=this.ctx,o=this.player;t.fillStyle="rgba(0, 0, 0, 0.45)",t.fillRect(10,10,124,14);const n=Math.max(0,o.hp),i=n/mt;t.fillStyle=i>.3?At:"#ff5a4f",t.fillRect(12,12,120*i,10),t.fillStyle="#ffffff",t.font="bold 9px system-ui, sans-serif",t.textAlign="left",t.fillText(`HP ${Number.isInteger(n)?n:n.toFixed(1)} / ${mt}`,16,20.5),t.textAlign="left",t.font="bold 12px system-ui, sans-serif",t.fillText(`KOs: ${this.knockouts}`,146,21.5),t.fillStyle="#ffd23f",t.fillText(`Headshots: ${this.headshots}`,216,21.5),t.fillStyle="#ffffff";const a=o.climb?o.climb.stairs.style==="fire-escape"?"Fire escape":"Stairs":((l=this.playerSurface)==null?void 0:l.place)??"";t.font="bold 10px system-ui, sans-serif",t.fillText(a,312,21),this.defeatTimer>0&&(t.fillStyle="rgba(10, 8, 16, 0.75)",t.fillRect(0,A/2-32,P,64),t.textAlign="center",t.fillStyle="#ff5a4f",t.font="bold 24px system-ui, sans-serif",t.fillText("DEFEATED!",P/2,A/2-4),t.fillStyle="#ffffff",t.font="12px system-ui, sans-serif",t.fillText(`You knocked out ${this.knockouts} stickmen with ${this.headshots} headshots. Starting again...`,P/2,A/2+18))}}function X(e){return e==="punch"||e==="kick"||e==="shoot"||e==="sword"}function F(e,t,o){return e<t?Math.min(t,e+o):Math.max(t,e-o)}function pn(){let e=Math.floor(Math.random()*260);e>=70&&(e+=100);const t=70+Math.floor(Math.random()*20),o=52+Math.floor(Math.random()*12);return`hsl(${e}, ${t}%, ${o}%)`}const gn=M("#app"),z=M("#stage"),Ht=M("#screen"),J=M("#game"),U=M("#menu"),St=M("#menuButton"),dt=M("#playButton"),Wt=M("#restartButton"),yt=M("#touch"),bn=M("#tooSmall");function M(e){const t=document.querySelector(e);if(!t)throw new Error(`Missing required element: ${e}`);return t}const p=new yn(J),wn=matchMedia("(pointer: coarse)").matches&&!matchMedia("(any-pointer: fine)").matches;let zt=!1;function Nt(){const e=wn||zt;yt.hidden=!e,gn.classList.toggle("touch-mode",e),$t()}function E(e,t,o){const n=M(e),i=()=>{t&&(p.held[t]=!1),n.classList.remove("held")};n.addEventListener("pointerdown",a=>{a.preventDefault(),n.setPointerCapture(a.pointerId),t&&(p.held[t]=!0),o&&p.press(o),n.classList.add("held")}),n.addEventListener("pointerup",i),n.addEventListener("pointercancel",i),n.addEventListener("lostpointercapture",i),n.addEventListener("contextmenu",a=>a.preventDefault())}E("#touchLeft","left",null);E("#touchRight","right",null);E("#touchRoll","down","roll");E("#touchJump",null,"jump");E("#touchAttack",null,"attack");E("#touchBlock","block",null);E("#touchShoot",null,"shoot");E("#touchSword",null,"sword");E("#touchKick",null,"kick");function $t(){const e=z.clientWidth,t=z.clientHeight-(yt.hidden?0:yt.offsetHeight+8),o=Math.max(.1,Math.min(e/P,t/A)),n=Math.floor(P*o),i=Math.floor(A*o);Ht.style.width=`${n}px`,Ht.style.height=`${i}px`,p.resize(n,i),bn.hidden=z.clientWidth>=240&&z.clientHeight>=170}new ResizeObserver($t).observe(z);let pt=!1;function Q(){p.paused=!0,Ut(),U.hidden=!1,St.hidden=!0,dt.textContent=pt?"Keep fighting":"Fight!",Wt.hidden=!pt,dt.focus()}function kt(){pt=!0,U.hidden=!0,St.hidden=!1,p.paused=!1,J.focus()}dt.addEventListener("click",kt);Wt.addEventListener("click",()=>{p.restart(),kt()});St.addEventListener("click",Q);function Ut(){p.held.left=!1,p.held.right=!1,p.held.down=!1,p.held.block=!1}const Kt={ArrowLeft:"left",ArrowRight:"right",ArrowDown:"down",ControlLeft:"block",ControlRight:"block"},Sn={ArrowUp:"jump",Space:"attack",KeyZ:"kick",ArrowDown:"roll",AltLeft:"shoot",ShiftLeft:"sword"};window.addEventListener("keydown",e=>{if(e.code==="Escape"){e.preventDefault(),U.hidden?Q():kt();return}if(!U.hidden)return;const t=Kt[e.code],o=Sn[e.code];!t&&!o||(e.preventDefault(),t&&(p.held[t]=!0),o&&!e.repeat&&p.press(o))});window.addEventListener("keyup",e=>{e.code==="AltLeft"&&e.preventDefault();const t=Kt[e.code];t&&(p.held[t]=!1)});window.addEventListener("blur",Ut);document.addEventListener("visibilitychange",()=>{document.hidden&&U.hidden&&Q()});J.addEventListener("pointerdown",()=>J.focus());Qt(Vt)&&le(e=>{zt=e.touchButtons===!0,Nt()});Nt();Q();p.start();
</script>
    <style>:root{--ink: #12131c;--dusk: #2d3147;--sun: #3ddc5a;--cream: #f1f3ff;--warn: #ff5a4f;--green: #3ddc5a;color-scheme:dark;font:14px/1.4 system-ui,-apple-system,Segoe UI,sans-serif}*{box-sizing:border-box}:where(html,body,body *){-webkit-user-select:none;user-select:none;-webkit-touch-callout:none}html,body,#app{height:100%;margin:0}body{background:var(--ink);color:var(--cream);overflow:hidden}.stage{position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;width:100%;height:100%}.screen{position:relative;flex:none}.game{display:block;width:100%;height:100%;outline:none;touch-action:none}button{font:inherit;cursor:pointer}.menu-button{position:absolute;top:6px;right:6px;padding:4px 10px;border:2px solid var(--ink);border-radius:6px;background:var(--cream);color:var(--ink);font-weight:700;font-size:12px}.menu{position:absolute;top:0;right:0;bottom:0;left:0;display:grid;place-items:center;padding:8px;background:#1d1724b8;overflow:auto}.menu[hidden]{display:none}.menu-card{width:min(400px,100%);max-height:100%;overflow:auto;padding:14px 18px;border:3px solid var(--ink);border-radius:10px;background:var(--cream);color:var(--ink);box-shadow:0 6px 0 var(--ink)}.menu-title{font-size:26px;font-weight:900;letter-spacing:2px;text-align:center;font-style:italic}.green{padding:0 4px;border-radius:3px;background:var(--ink);color:var(--green)}.menu-result{margin-top:4px;text-align:center;font-weight:700;font-size:16px;color:#b0441e}.menu-result[hidden]{display:none}.how p{margin:8px 0}.keys{margin:8px 0;padding:0;list-style:none;font-size:13px}.keys li{margin:4px 0}kbd{display:inline-block;min-width:20px;padding:0 4px;border:1.5px solid var(--ink);border-bottom-width:3px;border-radius:4px;background:#fff;font:700 11px/16px system-ui,sans-serif;text-align:center}.menu-actions{display:flex;gap:8px;justify-content:center;margin-top:10px}.menu-actions button{min-width:100px;min-height:36px;padding:6px 14px;border:2px solid var(--ink);border-radius:8px;background:#fff;color:var(--ink);font-weight:800}.menu-actions button.primary{background:var(--green)}.menu-actions button[hidden]{display:none}.menu-actions button:focus-visible,.menu-button:focus-visible{outline:3px solid var(--sun);outline-offset:2px}.touch-mode .keys,.touch-keys{display:none}.touch-mode .touch-keys{display:block}.touch{display:flex;justify-content:space-between;align-items:center;width:100%;max-width:900px;padding:0 8px 8px}.touch[hidden]{display:none}.touch-group{display:flex;gap:10px}.touch button{width:64px;height:64px;border:3px solid var(--cream);border-radius:50%;background:#fff4dc26;color:var(--cream);font-size:22px;font-weight:900;touch-action:none}.touch button{width:50px;height:50px;font-size:18px}.touch-group{gap:8px}.touch-grid{display:grid;grid-template-columns:repeat(4,auto);gap:6px;align-items:center;justify-items:center}.touch button.touch-attack{width:50px;height:50px;font-size:16px;border-color:var(--warn);color:var(--warn)}.touch button.held{background:#fff4dc66}.too-small{position:absolute;top:0;right:0;bottom:0;left:0;display:grid;place-items:center;padding:8px;text-align:center;background:var(--ink);color:var(--warn);font-weight:700}.too-small[hidden]{display:none}
</style>
    <meta name="napplet-type" content="stick-brawl">
    <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;touchButtons&quot;:{&quot;type&quot;:&quot;boolean&quot;,&quot;title&quot;:&quot;Always show touch buttons&quot;,&quot;description&quot;:&quot;Show the on-screen move, jump, roll and attack buttons even when a keyboard is available.&quot;,&quot;default&quot;:false,&quot;x-napplet-section&quot;:&quot;Game&quot;,&quot;x-napplet-order&quot;:1}}}">
  </head>
  <body>
    <main id="app" class="app">
      <div id="stage" class="stage">
        <div id="screen" class="screen">
          <canvas
            id="game"
            class="game"
            tabindex="0"
            aria-label="Stick Brawl. You are the green stickman. Fight off the enemy stickmen."
          ></canvas>
          <button id="menuButton" class="menu-button" type="button" title="Menu (Esc)" hidden>☰ Menu</button>
        </div>

        <div id="touch" class="touch" hidden>
          <div class="touch-group">
            <button id="touchLeft" type="button" aria-label="Move left">◀</button>
            <button id="touchRight" type="button" aria-label="Move right">▶</button>
          </div>
          <div class="touch-grid">
            <button id="touchBlock" type="button" aria-label="Block">🛡</button>
            <button id="touchShoot" type="button" aria-label="Shoot">🔫</button>
            <button id="touchSword" type="button" aria-label="Sword">⚔</button>
            <button id="touchKick" type="button" aria-label="Kick">🦶</button>
            <span></span>
            <button id="touchRoll" type="button" aria-label="Roll">▼</button>
            <button id="touchJump" type="button" aria-label="Jump">▲</button>
            <button id="touchAttack" class="touch-attack" type="button" aria-label="Attack">HIT</button>
          </div>
        </div>

        <div id="menu" class="menu" role="dialog" aria-modal="true" aria-labelledby="menuTitle">
          <div class="menu-card">
            <div id="menuTitle" class="menu-title">STICK BRAWL</div>
            <div class="how">
              <p>You are the <b class="green">green stickman</b>. Enemy stickmen keep coming to get you, and there is no limit to how many! Knock them out before they swarm you!</p>
              <p>The fight is in a <b>5-floor building</b> with 4 rooms on each floor. Every room has <b>doors</b> that smash with one hit (enemies can smash them too). Enemies climb in from the <b>balconies</b> and the <b>street</b>, and use the <b>stairs</b> to find you.</p>
              <p>A <b>fire escape</b> runs from the apartment balconies across to the old <b>warehouse</b> next door. It has crates to jump over and a roof with a water tower.</p>
              <p>Enemies go down after <b>2 hits</b>. Big <b>Brutes</b> take <b>3 hits</b> and punch really fast! Little <b>Runts</b> go down after <b>1½ hits</b> and only do half a hit, but there are lots of them. And every 2 minutes a big <b>gang of 10</b> stickmen shows up together! You can take <b>100 hits</b> before the fight starts over. Watch out: when an enemy pulls his fist back, he's about to punch!</p>
              <ul class="keys">
                <li><kbd>←</kbd> <kbd>→</kbd> move</li>
                <li><kbd>↑</kbd> jump, or climb up the stairs when you see <b>↑</b></li>
                <li><kbd>Space</kbd> punch (press faster to punch faster!)</li>
                <li><kbd>Z</kbd> kick, or <b>drop-kick</b> if you're in the air</li>
                <li>hold <kbd>Ctrl</kbd> block punches from the front</li>
                <li><kbd>Left Alt</kbd> quick-draw your revolver and shoot (one shot per press). Shoot an enemy in the <b>head</b> to knock him out at once!</li>
                <li><kbd>Left Shift</kbd> pull the sword off your back and swing it (worth 1½ hits!)</li>
                <li>tap <kbd>↓</kbd> roll (you can't be hit while rolling), or go down the stairs when you see <b>↓</b></li>
                <li>hold <kbd>↓</kbd> slide along the floor (the faster you run, the farther you go). Punches go over your head, and you can attack while sliding!</li>
                <li><kbd>Esc</kbd> open this menu</li>
              </ul>
              <p class="touch-keys">Use <b>◀ ▶</b> to move, <b>▲</b> to jump or climb stairs, tap <b>▼</b> to roll or go down stairs (hold it to slide), hold <b>🛡</b> to block, <b>🔫</b> to shoot, <b>⚔</b> to swing your sword, <b>HIT</b> to punch and <b>🦶</b> to kick (in the air it's a drop-kick).</p>
            </div>
            <div class="menu-actions">
              <button id="playButton" class="primary" type="button">Fight!</button>
              <button id="restartButton" type="button" hidden>Start over</button>
            </div>
          </div>
        </div>

        <div id="tooSmall" class="too-small" hidden>Make the window bigger to fight!</div>
      </div>
    </main>
  </body>
</html>