Back to Crossfire
SOURCE / PINNED RELEASE

Made of little things.

Crossfire

Release
9614abe48623
Author-recorded commit
6f6c8299e2fb…
License
Check the original source
Author’s source reference
nostr://npub1n8ga89w8h6tvwamxusfyzexw8gjy84yxu9rxgnmk955cxtml4ujswzxydd/wss%3A%2F%2Fgit.napplet.soy%2F/n-347ab685aef

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,viewport-fit=cover"><title>Crossfire</title>  <script type="module" crossorigin>var ht=Object.defineProperty;var ut=(e,t,r)=>t in e?ht(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r;var y=(e,t,r)=>ut(e,typeof t!="symbol"?t+"":t,r);function yt(){const e=new Map;function t(a,i){if(e.has(a))throw new Error(`NAP domain "${a}" is already registered`);e.set(a,i)}function r(a){const i=a.type.indexOf(".");if(i<=0)return!1;const o=a.type.slice(0,i),c=e.get(o);return c?(c(a),!0):!1}function n(){return Array.from(e.keys())}return{registerNap:t,dispatch:r,getRegisteredDomains:n}}var mt=yt(),pt=mt.registerNap,gt="theme";function Xe(){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 wt(){return Xe().get()}function vt(e){return Xe().onChanged(e)}pt(gt,e=>{});function xt(){const e=window;if(!e.napplet)throw new Error("window.napplet is unavailable -- runtime did not inject the napplet namespace");return e.napplet}function h(e){const r=xt()[e];if(!r)throw new Error(`window.napplet.${String(e)} is unavailable -- runtime did not inject this domain`);return r}var Ae={getPublicKey(){return h("identity").getPublicKey()},onChanged(e){return h("identity").onChanged(e)},getRelays(){return h("identity").getRelays()},getProfile(){return h("identity").getProfile()},getFollows(){return h("identity").getFollows()},getList(e){return h("identity").getList(e)},getZaps(){return h("identity").getZaps()},getMutes(){return h("identity").getMutes()},getBlocked(){return h("identity").getBlocked()},getBadges(){return h("identity").getBadges()}},bt={discover(e){return h("cvm").discover(e)},request(e,t,r){return h("cvm").request(e,t,r)},listTools(e,t){return h("cvm").listTools(e,t)},callTool(e,t,r,n){return h("cvm").callTool(e,t,r,n)},listResources(e,t){return h("cvm").listResources(e,t)},readResource(e,t,r){return h("cvm").readResource(e,t,r)},close(e){return h("cvm").close(e)},onEvent(e){return h("cvm").onEvent(e)},registry:{list(e){return h("cvm").registry.list(e)},has(e,t){return h("cvm").registry.has(e,t)},describe(e,t){return h("cvm").registry.describe(e,t)},call(e,t,r,n){return h("cvm").registry.call(e,t,r,n)}}},z={open(e){return h("webrtc").open(e)},send(e,t){return h("webrtc").send(e,t)},close(e,t){return h("webrtc").close(e,t)},onEvent(e){return h("webrtc").onEvent(e)}};function Mt(e,t){return!!(e!=null&&e[t])}function Ze(e){return Mt(window.napplet,e)}const kt=1,At="naddr1qvzqqqyf8ypzpxw36w2u005kcamkdeqjg9jvuw3yg02gdc2xv38hvtffsvhhlte9qqxkutfnxsmkzc3k8q6kzetx2p9awd",Ct={pubkey:"60517f64548279854ee9a122531e4d1a413e3dc956e2d0a450217420623ca688",relays:["wss://relay.napplet.soy/"]},Et=[],Te={version:kt,napplet:At,provider:Ct,boards:Et},C=960,E=640,Qe=[[210,130,90,160],[660,350,90,160],[400,280,160,80],[660,130,90,65],[210,445,90,65]],K=()=>({x:0,y:0,a:0,fire:!1}),me=[[100,100],[860,540],[860,100],[100,540],[480,100],[480,540],[100,320],[860,320]],Ke=["#91edbc","#ff927d","#9fc4ff","#edafff","#ffe28a","#76e3ed","#ffadd0","#c4ccff"];function qe(e,t){return{id:e,spawn:0,active:!0,x:me[t][0],y:me[t][1],a:0,hp:100,score:0,dead:0,shield:1.5,cooldown:0}}function Ve(e=["you","bot"]){return{p:e.map(qe),b:[],winner:-1}}const $e=(e,t,r)=>Math.max(t,Math.min(r,e));function G(e,t,r){return e<r||t<r||e>C-r||t>E-r||Qe.some(([n,a,i,o])=>e>n-r&&e<n+i+r&&t>a-r&&t<a+o+r)}function pe(e,t,r){if(e.dead||!e.active)return;const n=Math.max(1,Math.hypot(t.x,t.y)),a=$e(e.x+t.x/n*220*r,16,C-16),i=$e(e.y+t.y/n*220*r,16,E-16);G(a,e.y,15)||(e.x=a),G(e.x,i,15)||(e.y=i)}let Nt=0;function It(e,t,r){e.winner>=0||(e.p.forEach((n,a)=>{if(!n.active)return;if(n.shield=Math.max(0,n.shield-r),n.cooldown-=r,n.dead>0){n.dead=Math.max(0,n.dead-r),n.dead||(n.spawn++,n.hp=100,n.x=me[a][0],n.y=me[a][1],n.shield=1.5);return}const i=t[a]||K();pe(n,i,r),n.a=i.a,i.fire&&n.cooldown<=0&&(n.cooldown=.18,n.shield=0,e.b.push({id:++Nt,x:n.x+Math.cos(n.a)*22,y:n.y+Math.sin(n.a)*22,a:n.a,owner:a,life:1.2}))}),e.b=e.b.filter(n=>{if(n.life-=r,n.x+=Math.cos(n.a)*620*r,n.y+=Math.sin(n.a)*620*r,n.life<=0||G(n.x,n.y,3))return!1;const a=e.p.find((i,o)=>o!==n.owner&&i.active&&!i.dead&&!i.shield&&Math.hypot(i.x-n.x,i.y-n.y)<=18);return a?(a.hp-=25,a.hp<=0&&(a.dead=2,e.p[n.owner].score++,e.p[n.owner].score>=7&&(e.winner=n.owner)),!1):!0}))}function St(e){if(!e||typeof e!="object")return!1;const t=e;return Number.isFinite(t.x)&&Math.abs(t.x)<=1&&Number.isFinite(t.y)&&Math.abs(t.y)<=1&&Number.isFinite(t.a)&&Math.abs(t.a)<=10&&typeof t.fire=="boolean"}function Pt(e){var r;if(!e||typeof e!="object")return!1;const t=e;return Number.isInteger(t.winner)&&t.winner>=-1&&t.winner<((r=t.p)==null?void 0:r.length)&&Array.isArray(t.p)&&t.p.length>=1&&t.p.length<=8&&t.p.every(n=>n&&typeof n=="object")&&new Set(t.p.map(n=>n.id)).size===t.p.length&&t.p.every(n=>n&&typeof n.id=="string"&&n.id.length<=64&&(n.name===void 0||typeof n.name=="string"&&n.name.length<=32)&&Number.isSafeInteger(n.spawn)&&n.spawn>=0&&typeof n.active=="boolean"&&[n.x,n.y,n.a,n.hp,n.score,n.dead,n.shield,n.cooldown].every(Number.isFinite)&&n.x>=0&&n.x<=C&&n.y>=0&&n.y<=E&&n.hp>=0&&n.hp<=100&&Number.isInteger(n.score)&&n.score>=0&&n.score<=7&&n.dead>=0&&n.dead<=2&&n.shield>=0&&n.shield<=1.5)&&Array.isArray(t.b)&&t.b.length<=64&&t.b.every(n=>n&&[n.x,n.y,n.a,n.life].every(Number.isFinite)&&Number.isInteger(n.owner)&&n.owner>=0&&n.owner<t.p.length&&Math.abs(n.x)<=C+100&&Math.abs(n.y)<=E+100&&n.life<=1.2)}const F=1/60;function Rt(e){return Array.isArray(e)&&e.length<=12&&e.every(t=>Array.isArray(t)&&t.length===5&&Number.isSafeInteger(t[0])&&t[0]>=0&&St({x:t[1],y:t[2],a:t[3],fire:t[4]===1})&&(t[4]===0||t[4]===1))}class Tt{constructor(){y(this,"pending",[]);y(this,"local");y(this,"fx",[]);y(this,"n",0);y(this,"sent",0);y(this,"ack",-1);y(this,"offset",{x:0,y:0})}reset(){this.pending=[],this.local=void 0,this.fx=[],this.n=0,this.sent=0,this.ack=-1,this.offset={x:0,y:0}}reconcile(t,r){if(r<this.ack)return;this.ack=r;const n=this.local,a=!n||n.spawn!==t.spawn||!!n.dead!=!!t.dead;this.pending=this.pending.filter(i=>i.n>r),a&&(this.pending=[],this.fx=[],this.offset={x:0,y:0}),this.local={...t};for(const i of this.pending)pe(this.local,i.u,F);if(n&&!a){const i=n.x-this.local.x,o=n.y-this.local.y;Math.hypot(i,o)<80?(this.offset.x+=i,this.offset.y+=o):this.offset={x:0,y:0},this.local.cooldown=n.cooldown}}advance(t,r){const n=this.local;if(!n||(n.a=t.a,this.pending.length>=120))return;const a=++this.n;this.pending.push({n:a,u:{...t}}),r<0&&pe(n,t,F),n.cooldown=Math.max(0,n.cooldown-F),r<0&&!n.dead&&t.fire&&n.cooldown<=0&&(n.cooldown=.18,this.fx.push({id:a,x:n.x+Math.cos(t.a)*22,y:n.y+Math.sin(t.a)*22,a:t.a,owner:0,life:1.2}))}render(t,r){const n=Math.exp(-t/.06);return this.offset.x*=n,this.offset.y*=n,this.fx=this.fx.filter(a=>(a.life-=t,a.x+=Math.cos(a.a)*620*t,a.y+=Math.sin(a.a)*620*t,a.life>0&&!G(a.x,a.y,3))),this.local?{...this.local,x:this.local.x+this.offset.x,y:this.local.y+this.offset.y,a:r}:void 0}batch(){return this.pending.filter(t=>t.n>this.sent).slice(0,12).map(t=>[t.n,t.u.x,t.u.y,t.u.a,Number(t.u.fire)])}}const We=(e,t,r)=>e+(t-e)*r,qt=(e,t,r)=>e+Math.atan2(Math.sin(t-e),Math.cos(t-e))*r;class Ft{constructor(){y(this,"frames",[]);y(this,"clock",0);y(this,"latest",0);y(this,"ready",!1)}reset(){this.frames=[],this.clock=0,this.latest=0,this.ready=!1}push(t,r){t<=this.latest&&this.ready||(this.latest=t,this.frames.push({t,world:structuredClone(r)}),this.frames.length>20&&this.frames.shift(),this.ready||(this.clock=t-100,this.ready=!0))}render(t){if(!this.frames.length)return;const r=this.latest-100;this.clock+=t*1e3*Math.max(.9,Math.min(1.1,1+(r-this.clock)/1e3)),this.clock=Math.min(this.clock,this.latest+100);let n=this.frames[0],a=this.frames.at(-1);for(let f=1;f<this.frames.length;f++){if(this.frames[f].t>=this.clock){n=this.frames[f-1],a=this.frames[f];break}n=this.frames[f]}const i=this.frames.at(-1);this.clock>i.t&&this.frames.length>1&&(n=this.frames.at(-2),a=i);const o=Math.max(1,a.t-n.t),c=Math.max(0,Math.min(1+100/o,(this.clock-n.t)/o)),l=structuredClone(a.world);l.p=l.p.map(f=>{const A=n.world.p.find(dt=>dt.id===f.id);if(!A||A.spawn!==f.spawn||A.dead||f.dead||Math.hypot(f.x-A.x,f.y-A.y)>80)return f;const De=We(A.x,f.x,c),Be=We(A.y,f.y,c);return{...f,...c>1&&G(De,Be,15)?{}:{x:De,y:Be},a:qt(A.a,f.a,Math.min(1,c))}});const u=Math.max(-.1,Math.min(.1,(this.clock-a.t)/1e3));return l.b=l.b.map(f=>({...f,x:f.x+Math.cos(f.a)*620*u,y:f.y+Math.sin(f.a)*620*u,life:f.life-u})).filter(f=>f.life>0&&!G(f.x,f.y,3)),l}}const S=e=>Math.round(e*100)/100,Me=e=>[S(e.x),S(e.y),S(e.a),e.hp,e.score,S(e.dead),S(e.shield),S(Math.max(-1,e.cooldown)),Number(e.active),e.spawn],jt=e=>[e.id,S(e.x),S(e.y),S(e.a),e.owner,S(e.life)];class Lt{constructor(){y(this,"previous");y(this,"n",0);y(this,"lastFull",-1/0)}reset(){this.previous=void 0,this.n=0,this.lastFull=-1/0}encode(t,r,n,a=!1){const i=this.previous,o=a||!i||r-this.lastFull>=1e3||t.p.some((l,u)=>{var f,A;return l.id!==((f=i.p[u])==null?void 0:f.id)||l.name!==((A=i.p[u])==null?void 0:A.name)})||t.p.length!==i.p.length;o&&(this.lastFull=r);const c={type:"state",n:++this.n,base:o?-1:this.n-1,t:r,full:o,p:[],b:[],gone:[],winner:t.winner,ack:n};return o&&(c.roster=t.p.map(l=>[l.id,l.name||"Guest"])),t.p.forEach((l,u)=>{(o||JSON.stringify(Me(l))!==JSON.stringify(Me(i.p[u])))&&c.p.push([u,...Me(l)])}),c.b=t.b.filter(l=>o||!i.b.some(u=>u.id===l.id)).map(jt),o||(c.gone=i.b.filter(l=>!t.b.some(u=>u.id===l.id)).map(l=>l.id)),this.previous=structuredClone(t),c}}class _t{constructor(){y(this,"world");y(this,"n",-1);y(this,"t",0)}reset(){this.world=void 0,this.n=-1,this.t=0}decode(t){if(!t||typeof t!="object")return;const r=t;if(r.type!=="state"||!Number.isSafeInteger(r.n)||r.n<=this.n||!Number.isFinite(r.t)||r.t<0||!Array.isArray(r.ack)||r.ack.length>8||r.ack.some(i=>!Number.isSafeInteger(i)||i<0)||!Array.isArray(r.p)||r.p.length>8||!Array.isArray(r.b)||r.b.length>64||!Array.isArray(r.gone)||r.gone.length>64||!r.full&&(!this.world||r.base!==this.n))return;let n;if(r.full){if(!Array.isArray(r.roster)||r.roster.length<1||r.roster.length>8||r.roster.some(i=>!Array.isArray(i)||i.length!==2||typeof i[0]!="string"||i[0].length>64||typeof i[1]!="string"||i[1].length>32)||r.p.length!==r.roster.length)return;n={p:r.roster.map(([i,o])=>({id:i,name:o})),b:[],winner:r.winner}}else{n=structuredClone(this.world);const i=Math.min(2,Math.max(0,(r.t-this.t)/1e3));n.b.forEach(o=>{o.x+=Math.cos(o.a)*620*i,o.y+=Math.sin(o.a)*620*i,o.life-=i}),n.b=n.b.filter(o=>o.life>0&&!r.gone.includes(o.id)),n.winner=r.winner}const a=new Set;for(const i of r.p){if(!Array.isArray(i)||i.length!==11||!i.every(Number.isFinite)||!Number.isInteger(i[0])||i[0]<0||i[0]>=n.p.length||a.has(i[0])||!Number.isInteger(i[10])||i[10]<0||i[9]!==0&&i[9]!==1)return;a.add(i[0]),Object.assign(n.p[i[0]],{x:i[1],y:i[2],a:i[3],hp:i[4],score:i[5],dead:i[6],shield:i[7],cooldown:i[8],active:!!i[9],spawn:i[10]})}for(const i of r.b){if(!Array.isArray(i)||i.length!==6||!i.every(Number.isFinite)||!Number.isSafeInteger(i[0])||i[0]<0)return;n.b.push({id:i[0],x:i[1],y:i[2],a:i[3],owner:i[4],life:i[5]})}if(!(r.ack.length!==n.p.length||!Pt(n)))return this.world=n,this.n=r.n,this.t=r.t,{world:n,ack:r.ack,t:r.t}}}const v=e=>document.querySelector(e),g=v("canvas"),s=g.getContext("2d"),Oe=v("#status"),et=v("#menu"),ge=v("#find"),ve=v("#leave"),Ce=v("#notice");let d=Ve(),m=0,N=!1,R=!1,p=!1,Y=!1,x="",J="",w=0,W="",D="",re=0,I=[K(),K()],Z,fe,ce=!1,O=-1,ze=0,Q=0,He=performance.now(),de=0,Ee="#101b22",_="#e6f1ed",b=1,Ne=0,Ie=0;const M=new Set;let ie={x:0,y:0},xe=0,be=!1,we=!1;const Fe="crossfire-8-v5",$=new Tt,je=new Ft,tt=new Lt,Se=new _t;let Le=0,ee=!0,Pe=0,P;const j=new Map,U=new Map,X=new Map;let se="Guest",he,Ge=0;const H=(e,t=32)=>typeof e=="string"?e.replace(/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g,"").trim().slice(0,t):"";async function Ye(){const e=++Ge;let t="Guest";try{const r=await Ae.getPublicKey();if(r){t=r.slice(0,8)+"…";try{const n=await Ae.getProfile(),a=n;t=H(n==null?void 0:n.displayName)||H(a==null?void 0:a.display_name)||H(n==null?void 0:n.name)||t}catch{}}}catch{}e===Ge&&(se=t,v("#player-name").textContent=t+" (You)")}function Je(e){const t=d.p[e];return(e===m?se:R?"Target":t.name||"Guest "+(e+1))+(e===m?" (You)":"")}let L=new Set,te=new Map,V=new Map,nt=0,ke=!1,ue=0,Re="",k=!1,ye=!1,rt=0;const q=v("#rooms"),it=v("#refresh"),ne=new Set;function oe(e){Oe.textContent=e,Oe.title=e}async function B(e,t={}){var n;const r=await bt.callTool(Te.provider,e,t);if(r.isError)throw Error(((n=r.content)==null?void 0:n.filter(a=>a.type==="text").map(a=>a.text).join(" "))||"Matchmaking unavailable");if(!r.structuredContent)throw Error("Matchmaking returned no result");return r.structuredContent}function _e(){M.clear(),ie={x:0,y:0},be=!1,we=!1}async function T(e="Ready to play"){w++,ue++,Re="",ne.clear(),j.clear(),U.clear(),X.clear(),N=!1,Y=!1,_e(),document.body.classList.remove("playing"),et.hidden=!1,ve.hidden=!0,ge.disabled=!1,ge.textContent="Host game",k=!1,Ce.hidden=!0,ae(),Z==null||Z.close(),Z=void 0,clearInterval(nt);const t=x,r=J;x="",J="",oe(e),await Promise.allSettled([t?z.close(t,"Left arena"):Promise.resolve(),r?B("soy_room_leave",{room:r}):Promise.resolve()])}function ot(e){R=e,d=Ve(e?["you","bot"]:[W]),m=0,I=Array.from({length:8},K),O=-1,de=0,Le=0,$.reset(),je.reset(),tt.reset(),Se.reset(),ee=!0,Pe=0,P=void 0,Q=0,N=!0,document.body.classList.add("playing"),et.hidden=!0,ve.hidden=!1,g.focus(),oe(e?"Practice · moving target":"Arena open · waiting for players")}function at(e){if(!Array.isArray(e.peers))throw Error("Invalid room membership");if(L=new Set(e.peers.filter(t=>typeof t=="string")),!L.has(W))throw Error("Room membership expired · find another match");if(!L.has(D))throw Error("Host left · find another arena");if(p){for(const t of j.keys())L.has(t)||(j.delete(t),U.delete(t),X.delete(t),V.delete(t),te.delete(t));for(const t of d.p)L.has(t.id)||(t.active=!1);for(const t of L)if(!d.p.some(r=>r.id===t&&r.active)){let r=d.p.findIndex(n=>!n.active);if(r<0&&(r=d.p.length),r>=8)continue;d.p[r]=qe(t,r),I[r]=K(),j.set(t,[]),U.set(t,0),X.set(t,12),d.b=d.b.filter(n=>n.owner!==r)}}}async function Dt(e){if(!(ke||e!==w||!J)){ke=!0;try{const t=await B("soy_room_status",{room:J});e===w&&(at(t),await st(e))}catch(t){e===w&&T(t instanceof Error?t.message:"Room unavailable")}finally{ke=!1}}}async function st(e){const t=D;if(t===Re&&x)return;Re=t;const r=++ue,n=x;if(x="",Y=!1,ne.clear(),n&&await z.close(n,"Arena membership changed").catch(()=>{}),e!==w||r!==ue)return;const a=await z.open({scope:{type:"room",room:J,...p?{}:{peers:[D]}},channel:"game",protocol:Fe});if(e!==w||r!==ue){await z.close(a.session.id,"Cancelled");return}x=a.session.id,re=performance.now()}function ae(){v("#game-name").toggleAttribute("disabled",k),ge.disabled=k,it.disabled=k||ye,v("#practice").toggleAttribute("disabled",k),q.querySelectorAll("button").forEach(e=>e.disabled=k||e.dataset.full==="true")}async function le(){if(N||k||ye)return;const e=w;ye=!0,ae(),q.children.length||(q.textContent="Loading games…");try{const t=await B("soy_room_list",{napplet:Te.napplet,protocol:Fe});if(e!==w||N||k)return;if(!Array.isArray(t.rooms))throw Error("Invalid room list");q.replaceChildren();for(const r of t.rooms){if(!r||typeof r!="object")continue;const n=r;if(typeof n.room!="string"||typeof n.name!="string"||!/^Crossfire [a-f0-9]{16}(?: \| .{1,48})?$/.test(n.name)||!Array.isArray(n.peers)||!n.peers.some(l=>typeof l=="string"&&l.startsWith(String(n.name).slice(10,26))))continue;const a=document.createElement("div");a.className="room-row";const i=document.createElement("span");i.textContent=H(n.name.slice(29),48)||"Arena "+n.room.slice(-6).toUpperCase(),i.title=i.textContent;const o=document.createElement("small");o.textContent=n.peers.length+"/8";const c=document.createElement("button");c.textContent=n.peers.length>=8?"Full":"Join",c.dataset.full=String(n.peers.length>=8),c.disabled=n.peers.length>=8,c.setAttribute("aria-label",c.textContent+" "+i.textContent),c.onclick=()=>void lt(n.room),a.append(i,o,c),q.append(a)}q.children.length||(q.textContent="No games yet. Host one to get started.")}catch(t){e===w&&!N&&!k&&(q.textContent=t instanceof Error?t.message:"Could not load games. Refresh to retry")}finally{ye=!1,ae()}}async function lt(e){if(k)return;await T();const t=++w;k=!0,ae(),ve.hidden=!1,oe(e?"Joining arena…":"Creating arena…");try{const r=await B("soy_session");if(t!==w)return;if(typeof r.actor!="string")throw Error("Session unavailable");W=r.actor;const n=e?await B("soy_room_join",{room:e}):await B("soy_room_create",{napplet:Te.napplet,protocol:Fe,name:"Crossfire "+W.slice(0,16)+" | "+(H(v("#game-name").value,48)||se+"’s arena"),capacity:8,listed:!0});if(typeof n.room!="string")throw Error("Invalid arena response");if(t!==w){await B("soy_room_leave",{room:n.room});return}J=n.room;const a=Array.isArray(n.peers)?n.peers:[],i=String(n.name).slice(10,26);if(D=String(a.find(o=>typeof o=="string"&&o.startsWith(i))||""),!D)throw Error("Arena host unavailable");if(p=D===W,ot(!1),at(n),re=performance.now(),te.clear(),V.clear(),Z=z.onEvent(o=>{if(!(t!==w||o.sessionId!==x)){if(o.type==="peer"&&(o.state==="joined"&&(ne.add(o.pubkey),Y=!0,ee=!0),o.state==="left"&&(ne.delete(o.pubkey),Y=ne.size>0,p))){j.delete(o.pubkey),U.delete(o.pubkey),X.delete(o.pubkey),V.delete(o.pubkey),te.delete(o.pubkey);const c=d.p.findIndex(l=>l.id===o.pubkey);c>=0&&(I[c]=K())}if(o.type==="closed"&&T(o.reason||"Connection closed · try again"),o.type==="message"&&L.has(o.from)&&o.payload&&typeof o.payload=="object"){const c=o.payload;if(!p&&o.from===D){const l=Se.decode(c);if(l){const u=l.world.p.findIndex(f=>f.id===W&&f.active);u>=0&&(d=l.world,m=u,O=Se.n,re=performance.now(),$.reconcile(d.p[u],l.ack[u]??0),je.push(l.t,d))}}if(p&&c.type==="input"&&Rt(c.commands)){const l=d.p.findIndex(f=>f.id===o.from&&f.active);if(l<0)return;typeof c.name=="string"&&(d.p[l].name=H(c.name)||"Guest");const u=j.get(o.from)??[];for(const f of c.commands)f[0]<=(V.get(o.from)??0)||u.length>=120||(V.set(o.from,f[0]),u.push({n:f[0],u:{x:f[1],y:f[2],a:f[3],fire:f[4]===1}}));j.set(o.from,u),te.set(o.from,performance.now())}}}}),await st(t),t!==w)return;nt=window.setInterval(()=>void Dt(t),2e3),oe(p?"Arena open · friends can join from the lobby":"Connecting to arena…")}catch(r){t===w&&(await T(r instanceof Error?r.message:"Could not join · refresh and retry"),le())}finally{t===w&&(k=!1,ae())}}ge.onclick=()=>void lt();it.onclick=()=>void le();ve.onclick=()=>void T().then(()=>le());v("#practice").onclick=()=>{T().then(()=>{m=0,p=!0,ot(!0)})};g.addEventListener("keydown",e=>{["KeyW","KeyA","KeyS","KeyD","ArrowUp","ArrowDown","ArrowLeft","ArrowRight","Space"].includes(e.code)&&(e.preventDefault(),M.add(e.code))});g.addEventListener("keyup",e=>{M.delete(e.code)});window.addEventListener("blur",_e);document.addEventListener("visibilitychange",()=>{document.hidden&&_e()});function ct(e){const t=g.getBoundingClientRect(),r=(P==null?void 0:P.p[m])||d.p[m];xe=Math.atan2((e.clientY-t.top-Ie)/b-r.y,(e.clientX-t.left-Ne)/b-r.x)}g.onpointermove=e=>{e.pointerType!=="touch"&&ct(e)};g.onpointerdown=e=>{e.pointerType!=="touch"&&(g.focus(),g.setPointerCapture(e.pointerId),ct(e),be=!0)};g.onpointerup=g.onpointercancel=()=>{be=!1};g.oncontextmenu=e=>e.preventDefault();for(const e of["move","aim"]){const t=v("#"+e);let r=-1;const n=a=>{const i=t.getBoundingClientRect(),o=(a.clientX-i.left-i.width/2)/30,c=(a.clientY-i.top-i.height/2)/30,l=Math.max(1,Math.hypot(o,c));t.querySelector("i").style.transform=`translate(${o/l*24}px,${c/l*24}px)`,e==="move"?ie={x:o/l,y:c/l}:(xe=Math.atan2(c,o),we=Math.hypot(o,c)>.2)};t.onpointerdown=a=>{r===-1&&(r=a.pointerId,t.setPointerCapture(r),n(a))},t.onpointermove=a=>{a.pointerId===r&&n(a)},t.onpointerup=t.onpointercancel=a=>{a.pointerId===r&&(r=-1,t.querySelector("i").style.transform="",e==="move"?ie={x:0,y:0}:we=!1)}}async function Bt(){if(ce||!x||!Y)return;ce=!0;const e=x;let t,r=[];if(p)t=tt.encode(d,Le*F*1e3,d.p.map(n=>U.get(n.id)??0),ee),ee=!1;else{if(r=$.batch(),!r.length){ce=!1;return}const n=performance.now()-Pe>1e3;t={type:"input",commands:r,...n?{name:se}:{}},n&&(Pe=performance.now())}try{await z.send(e,t),x===e&&r.length&&($.sent=r.at(-1)[0])}catch(n){ee=!0;const a=n instanceof Error?n.message:"retry";x===e&&!/backpressure|connecting/i.test(a)&&T("Connection interrupted: "+a)}finally{ce=!1}}function Kt(e){if(P=!p&&!R&&N?je.render(e):void 0,P){const o=$.render(e,xe);o&&(P.p[m]=o),P.b=P.b.filter(c=>c.owner!==m).concat($.fx.map(c=>({...c,owner:m})))}const t=P||d,r=g.getBoundingClientRect(),n=Math.min(devicePixelRatio||1,2);(g.width!==Math.round(r.width*n)||g.height!==Math.round(r.height*n))&&(g.width=Math.round(r.width*n),g.height=Math.round(r.height*n)),s.setTransform(n,0,0,n,0,0),s.fillStyle=Ee,s.fillRect(0,0,r.width,r.height),b=Math.max(r.width/C,r.height/E),N||(b=Math.min(r.width/C,r.height/E));const a=t.p[m];Ne=N?Math.min(0,Math.max(r.width-C*b,r.width/2-a.x*b)):(r.width-C*b)/2,Ie=N?Math.min(0,Math.max(r.height-E*b,r.height/2-a.y*b)):(r.height-E*b)/2,s.translate(Ne,Ie),s.scale(b,b),s.strokeStyle=_,s.globalAlpha=.065,s.lineWidth=1;for(let o=0;o<=C;o+=40)s.beginPath(),s.moveTo(o,0),s.lineTo(o,E),s.stroke();for(let o=0;o<=E;o+=40)s.beginPath(),s.moveTo(0,o),s.lineTo(C,o),s.stroke();s.globalAlpha=.5,s.strokeRect(2,2,C-4,E-4);for(const[o,c,l,u]of Qe)s.globalAlpha=.09,s.fillStyle=_,s.fillRect(o,c,l,u),s.globalAlpha=.35,s.strokeRect(o,c,l,u),s.globalAlpha=.16,s.fillRect(o,c,l,5);s.globalAlpha=1,t.b.forEach(o=>{s.strokeStyle=Ke[o.owner],s.lineWidth=4,s.beginPath(),s.moveTo(o.x-Math.cos(o.a)*10,o.y-Math.sin(o.a)*10),s.lineTo(o.x,o.y),s.stroke()}),t.p.forEach((o,c)=>{if(o.dead||!o.active)return;s.save(),s.translate(o.x,o.y);const l=Ke[c];o.shield&&(s.strokeStyle=l,s.globalAlpha=.45,s.lineWidth=2,s.beginPath(),s.arc(0,0,23,0,Math.PI*2),s.stroke(),s.globalAlpha=1),s.rotate(o.a),s.fillStyle=l,s.fillRect(4,-5,24,10),s.beginPath(),s.arc(0,0,14,0,Math.PI*2),s.fill(),s.strokeStyle=_,s.lineWidth=1.5,s.stroke(),s.fillStyle="#18303a",s.beginPath(),s.arc(3,0,6,0,Math.PI*2),s.fill(),s.rotate(-o.a),s.fillStyle=_,s.globalAlpha=.25,s.fillRect(-19,-30,38,5),s.globalAlpha=1,s.fillStyle=l,s.fillRect(-18,-29,36*o.hp/100,3),s.font="bold 9px monospace",s.textAlign="center",s.fillStyle=_,s.fillText(Je(c),0,-37,150),s.restore()}),v("#health").innerHTML=`${a.hp} <small>HP</small>`,v("#score").textContent=N?`${d.p.filter(o=>o.active).length}/8 · ${d.p[m].score}/7 KOs`:"FIRST TO 7";const i=d.winner>=0?(d.winner===m?"You win!":`${Je(d.winner)} wins!`)+" · Next round shortly":a.dead?`Respawning in ${Math.ceil(a.dead)}…`:"";Ce.textContent=i,Ce.hidden=!i}function ft(e){const t=Math.min((e-He)/1e3,.05);if(He=e,N){if(I[m]={x:Math.max(-1,Math.min(1,ie.x+Number(M.has("KeyD")||M.has("ArrowRight"))-Number(M.has("KeyA")||M.has("ArrowLeft")))),y:Math.max(-1,Math.min(1,ie.y+Number(M.has("KeyS")||M.has("ArrowDown"))-Number(M.has("KeyW")||M.has("ArrowUp")))),a:xe,fire:be||we||M.has("Space")},R){const r=d.p[1],n=d.p[0];I[1]={x:Math.sin(e/900)*.55,y:Math.cos(e/1200)*.55,a:Math.atan2(n.y-r.y,n.x-r.x),fire:Math.sin(e/1400)>.25}}for(p&&d.winner>=0&&(Q||(Q=e),e-Q>5e3&&(d.p=d.p.map((r,n)=>({...qe(r.id,n),name:r.name,spawn:r.spawn+1,active:r.active})),d.b=[],d.winner=-1,Q=0)),de+=t;de>=F;)p?(d.p[m].name=se,R||d.p.forEach((r,n)=>{if(n===m||!r.active)return;let a=Math.min(12,(X.get(r.id)??0)+1);const i=j.get(r.id)||[];let o=!1;for(let c=0;c<4&&a>=1&&i.length;c++,a--){const l=i.shift();d.winner<0&&pe(r,l.u,F),I[n]={...l.u,x:0,y:0},o||(o=l.u.fire),U.set(r.id,l.n)}o&&(I[n].fire=!0),X.set(r.id,a)}),It(d,I,F),Le++):O>=0&&$.advance(I[m],d.winner),de-=F;if(!R&&p&&d.p.forEach((r,n)=>{n!==m&&e-(te.get(r.id)??0)>500&&(I[n]=K())}),!R&&e-ze>=50&&(ze=e,Bt()),!R&&!p&&x&&e-re>(O<0?6e4:45e3)&&T("Peer stopped responding · rejoin"),!R&&x){const r=d.p.filter(n=>n.active).length;oe(!p&&O>=0&&e-re>2e3?"Reconnecting to arena…":r>1&&Y&&(p||O>=0)?`Live · ${r} players · first to 7`:p?"Arena open · waiting for players":"Connecting to arena…")}}Kt(t),requestAnimationFrame(ft)}function Ue(e){Ee=e.colors.background,_=e.colors.text;const t=document.documentElement.style;t.setProperty("--bg",Ee),t.setProperty("--fg",_),t.setProperty("--primary",e.colors.primary)}if(Ze("identity")){Ye();try{he=Ae.onChanged(()=>void Ye())}catch{}}if(Ze("theme"))try{wt().then(Ue).catch(()=>{}),fe=vt(Ue)}catch{}rt=window.setInterval(()=>{document.hidden||le()},5e3);le();window.addEventListener("pagehide",()=>{clearInterval(rt),T(),fe==null||fe.close(),he==null||he.close()});requestAnimationFrame(ft);
</script>
  <style>:root{--bg:#101b22;--fg:#e6f1ed;--primary:#96f5bd;--line:color-mix(in srgb,var(--fg) 14%,var(--bg));--muted:color-mix(in srgb,var(--fg) 58%,var(--bg));font:13px/1.4 ui-monospace,SFMono-Regular,Consolas,monospace;color-scheme:dark light}*{box-sizing:border-box}html,body,#app{height:100%;margin:0;background:var(--bg);color:var(--fg)}body{-webkit-user-select:none;user-select:none;overflow:hidden}#app{display:grid;grid-template-rows:40px minmax(0,1fr);container-type:inline-size}.toolbar{display:flex;align-items:center;gap:8px;padding:4px 12px;border-bottom:1px solid var(--line)}.signal{width:6px;height:6px;border-radius:50%;background:var(--primary)}#status{flex:1;min-width:0;white-space:nowrap;text-overflow:ellipsis;overflow:hidden;font-size:11px}#score{font-weight:700;letter-spacing:1px;font-size:11px;white-space:nowrap}button{font:inherit;min-height:30px;border:1px solid var(--line);background:var(--bg);color:var(--fg);border-radius:5px;padding:4px 12px;cursor:pointer}button:hover{border-color:var(--primary)}button:focus-visible,canvas:focus-visible{outline:2px solid var(--primary);outline-offset:-2px}button:disabled{opacity:.5;cursor:wait}#arena{position:relative;min-height:0;overflow:hidden}canvas{width:100%;height:100%;display:block;touch-action:none}#menu{position:absolute;top:0;right:0;bottom:0;left:0;display:grid;place-items:center;background:color-mix(in srgb,var(--bg) 70%,transparent)}#menu[hidden],[hidden]{display:none!important}.menu-copy{padding:12px;text-align:center;display:grid;gap:8px;width:min(280px,100%)}.crosshair{font-size:42px;color:var(--primary);line-height:1}p{margin:4px 0 12px}.menu-copy p{font-size:16px;line-height:1.6}.menu-copy strong{font-weight:500;color:var(--muted)}.primary{background:var(--primary);color:var(--bg);font-weight:750;display:flex;justify-content:space-between;align-items:center;min-height:40px}.menu-copy small{color:var(--muted);font-size:10px;margin-top:8px;line-height:1.8}#notice{position:absolute;top:20%;left:8px;right:8px;text-align:center;font-size:20px;font-weight:700;pointer-events:none;text-shadow:0 2px 5px var(--bg)}#health{position:absolute;bottom:16px;left:16px;font-size:28px;font-weight:800;pointer-events:none}#health small{font-size:11px;color:var(--muted)}#hint{position:absolute;bottom:20px;right:16px;font-size:10px;color:var(--muted)}.sticks{display:none;position:absolute;bottom:32px;inset-inline:16px;justify-content:space-between;pointer-events:none}.stick{width:88px;height:88px;border-radius:50%;border:1px solid var(--line);background:color-mix(in srgb,var(--bg) 65%,transparent);position:relative;touch-action:none;pointer-events:auto}.stick i{position:absolute;width:34px;height:34px;left:26px;top:26px;border-radius:50%;border:1px solid var(--primary);background:color-mix(in srgb,var(--primary) 25%,transparent)}.stick span{position:absolute;top:92px;left:0;right:0;text-align:center;font-size:9px;color:var(--muted)}.too-small,body.playing #menu{display:none}body:not(.playing) #health,body:not(.playing) #hint{display:none}@media(pointer:coarse){body.playing .sticks{display:flex}#health{top:8px;bottom:auto;font-size:22px}#hint{display:none}button{min-height:44px}}@media(max-width:479px){.toolbar{padding-inline:8px}#score{font-size:10px}.menu-copy p{font-size:14px}}@media(max-width:279px),(max-height:239px){body.playing .too-small{display:grid;position:absolute;top:0;right:0;bottom:0;left:0;place-content:center;text-align:center;background:var(--bg);padding:8px;z-index:5}.crosshair,.menu-copy small,.menu-copy p,#score{display:none}.menu-copy{gap:4px;padding:8px}#app{grid-template-rows:32px minmax(0,1fr)}}@media(min-width:900px){#health{font-size:36px}#hint{font-size:12px}}#app{min-width:0;overflow:hidden}.toolbar,#arena{min-width:0}.toolbar button{flex-shrink:0}canvas{min-width:0}#health{font-variant-numeric:tabular-nums}#menu{display:flex;flex-direction:column;align-items:stretch;justify-content:start;background:var(--bg);padding:8px;gap:8px;overflow:hidden}.lobby-actions{display:flex;gap:4px;flex-wrap:wrap}.lobby-actions .primary{min-height:30px}#rooms{flex:1;min-height:0;overflow:auto;display:grid;grid-template-columns:1fr;align-content:start;gap:4px;color:var(--muted)}.room-row{display:flex;gap:8px;align-items:center;padding:8px 0;border-bottom:1px solid var(--line);color:var(--fg);min-width:0}.room-row>span{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.room-row small{color:var(--muted)}.room-row button{flex-shrink:0}.lobby-help{color:var(--muted);font-size:10px}.lobby-actions #practice{margin-left:auto}@media(min-width:900px){#rooms{grid-template-columns:repeat(auto-fit,minmax(280px,1fr));column-gap:24px}}@media(max-width:239px),(max-height:239px){.lobby-help{display:none}.lobby-actions #practice{font-size:10px;padding-inline:4px}.lobby-actions button{padding-inline:6px}.lobby-actions{flex-wrap:nowrap}#menu{gap:4px;padding:4px}}@media(pointer:coarse){.lobby-actions .primary{min-height:44px}}.host-name{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.host-name label{color:var(--muted)}.host-name input{font:inherit;color:var(--fg);background:var(--bg);border:1px solid var(--line);border-radius:5px;padding:6px 8px;min-width:0;flex:1;-webkit-user-select:text;user-select:text}.host-name input:focus{outline:1px solid var(--primary)}#player-name{font-size:11px;color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:40%}@media(max-width:479px){#player-name{display:none}}@media(max-width:239px),(max-height:239px){.host-name label{display:none}.host-name input{padding:3px 6px}}
</style>
  <meta name="napplet-type" content="crossfire">
  <meta name="napplet-requires" content="cvm,webrtc">
  <meta name="napplet-config-schema" content="{&quot;$version&quot;:1,&quot;type&quot;:&quot;object&quot;,&quot;additionalProperties&quot;:false,&quot;properties&quot;:{}}">
</head>
<body><div id="app"><div class="toolbar"><span class="signal"></span><output id="status" aria-live="polite">Ready to play</output><span id="score">FIRST TO 7</span><button id="leave" hidden>Leave</button></div><main id="arena"><canvas aria-label="Top-down arena. WASD or arrow keys to move, mouse to aim, hold click to fire." tabindex="0"></canvas><div id="menu"><div class="host-name"><label for="game-name">Game name</label><input id="game-name" maxlength="48" placeholder="Your arena name" autocomplete="off"><span id="player-name">Guest (You)</span></div><div class="lobby-actions"><button id="find" class="primary">Host game</button><button id="refresh" aria-label="Refresh games"></button><button id="practice">Practice solo</button></div><div id="rooms" aria-label="Public games"></div><small class="lobby-help">Up to 8 players · First to 7 eliminations<br>WASD / arrows · aim &amp; hold click · touch sticks on mobile</small></div><div id="notice" hidden></div><div class="sticks"><div id="move" class="stick" aria-label="Drag to move"><i></i><span>MOVE</span></div><div id="aim" class="stick" aria-label="Drag to aim and shoot"><i></i><span>AIM / FIRE</span></div></div><p class="too-small">Enlarge to at least 280 × 240 to play.</p><div id="health">100 <small>HP</small></div><div id="hint">USE COVER · KEEP MOVING</div></main></div></body></html>