Back to Squad Table
SOURCE / PINNED RELEASE

Made of little things.

Squad Table

Release
d36201f8ed4f…
Author-recorded commit
7b9bb681d6ba…
License
Check the original source
Author’s source reference
nostr://npub1sj2vketepqw9v6fmge5fdzuj7atj9sv6qv2jss05n2z6rl64lcfqumdsnt/wss%3A%2F%2Fgit.napplet.soy%2F/n-c043b0c0f1d

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

index.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
    <title>Squad Table</title>
    <script type="module" crossorigin>var ns=Object.defineProperty;var ss=(t,e,s)=>e in t?ns(t,e,{enumerable:!0,configurable:!0,writable:!0,value:s}):t[e]=s;var M=(t,e,s)=>ss(t,typeof e!="symbol"?e+"":e,s);function os(){const t=new Map;function e(i,o){if(t.has(i))throw new Error(`NAP domain "${i}" is already registered`);t.set(i,o)}function s(i){const o=i.type.indexOf(".");if(o<=0)return!1;const r=i.type.slice(0,o),a=t.get(r);return a?(a(i),!0):!1}function n(){return Array.from(t.keys())}return{registerNap:e,dispatch:s,getRegisteredDomains:n}}var is=os(),rs=is.registerNap,$n="identity";rs($n,t=>{});function as(){const t=window;if(!t.napplet)throw new Error("window.napplet is unavailable -- runtime did not inject the napplet namespace");return t.napplet}function S(t){const s=as()[t];if(!s)throw new Error(`window.napplet.${String(t)} is unavailable -- runtime did not inject this domain`);return s}var Qe={getItem(t){return S("storage").getItem(t)},setItem(t,e){return S("storage").setItem(t,e)},removeItem(t){return S("storage").removeItem(t)},keys(){return S("storage").keys()},instance:{getItem(t){return S("storage").instance.getItem(t)},setItem(t,e){return S("storage").instance.setItem(t,e)},removeItem(t){return S("storage").instance.removeItem(t)},keys(){return S("storage").instance.keys()}}},on={getPublicKey(){return S("identity").getPublicKey()},onChanged(t){return S("identity").onChanged(t)},getRelays(){return S("identity").getRelays()},getProfile(){return S("identity").getProfile()},getFollows(){return S("identity").getFollows()},getList(t){return S("identity").getList(t)},getZaps(){return S("identity").getZaps()},getMutes(){return S("identity").getMutes()},getBlocked(){return S("identity").getBlocked()},getBadges(){return S("identity").getBadges()}},$t={get(){return S("config").get()},subscribe(t){return S("config").subscribe(t)},openSettings(t){S("config").openSettings(t)},registerSchema(t,e){return S("config").registerSchema(t,e)},onSchemaError(t){return S("config").onSchemaError(t)},get schema(){return S("config").schema}},ls={discover(t){return S("cvm").discover(t)},request(t,e,s){return S("cvm").request(t,e,s)},listTools(t,e){return S("cvm").listTools(t,e)},callTool(t,e,s,n){return S("cvm").callTool(t,e,s,n)},listResources(t,e){return S("cvm").listResources(t,e)},readResource(t,e,s){return S("cvm").readResource(t,e,s)},close(t){return S("cvm").close(t)},onEvent(t){return S("cvm").onEvent(t)},registry:{list(t){return S("cvm").registry.list(t)},has(t,e){return S("cvm").registry.has(t,e)},describe(t,e){return S("cvm").registry.describe(t,e)},call(t,e,s,n){return S("cvm").registry.call(t,e,s,n)}}},tt={open(t){return S("webrtc").open(t)},send(t,e){return S("webrtc").send(t,e)},close(t,e){return S("webrtc").close(t,e)},onEvent(t){return S("webrtc").onEvent(t)}};function cs(t,e){return!!(t!=null&&t[e])}function re(t){return cs(window.napplet,t)}const ds="http://json-schema.org/draft-07/schema#",us=2,fs="object",hs=!1,ps={showCoords:{type:"boolean",title:"Show hex coordinates",description:"Print each hex's id (such as K6) and hill level on the map.",default:!0,"x-napplet-section":"Map","x-napplet-order":1},sharePointer:{type:"boolean",title:"Share my pointer",description:"During live play, outline the hex under your pointer for your opponent.",default:!0,"x-napplet-section":"Live play","x-napplet-order":2},autoLive:{type:"boolean",title:"Open live link automatically",description:"Connect directly to your opponent whenever you open an online table. Your host may ask for permission first.",default:!0,"x-napplet-section":"Live play","x-napplet-order":3}},ms={$schema:ds,$version:us,type:fs,additionalProperties:hs,properties:ps},Ne={showCoords:!0,sharePointer:!0,autoLive:!0},vn=new Set;function gs(t){vn.add(t)}re("config")&&(async()=>{$t.schema||await $t.registerSchema(ms);const t=$t.subscribe(e=>{Ne.showCoords=e.showCoords!==!1,Ne.sharePointer=e.sharePointer!==!1,Ne.autoLive=e.autoLive!==!1,vn.forEach(s=>s())});window.addEventListener("pagehide",()=>t.close(),{once:!0})})().catch(()=>{});const le={ge:{id:"ge",name:"German",short:"GE"},us:{id:"us",name:"American",short:"US"},ru:{id:"ru",name:"Russian",short:"RU"},mk:{id:"mk",name:"Markers",short:"MK"}},vt=(t,e)=>e.map(([s,n,i,o])=>({k:`${t}-${s}`,nat:t,type:n,label:i,tag:o})),wt=t=>[{k:`${t}-lmg`,nat:t,type:"weapon",label:"LMG",back:"MALF"},{k:`${t}-mmg`,nat:t,type:"weapon",label:"MMG",back:"MALF"},{k:`${t}-hmg`,nat:t,type:"weapon",label:"HMG",back:"MALF"},{k:`${t}-mtr`,nat:t,type:"weapon",label:"MTR",tag:"light"},{k:`${t}-dc`,nat:t,type:"weapon",label:"DC",tag:"charge"},{k:`${t}-ft`,nat:t,type:"weapon",label:"FT",tag:"flame"},{k:`${t}-gun`,nat:t,type:"gun",label:"AT Gun"},{k:`${t}-tank`,nat:t,type:"vehicle",label:"Tank"},{k:`${t}-ht`,nat:t,type:"vehicle",label:"Halftrack"}],xt=t=>["9-2","9-1","8-1","8-0","7-0"].map(e=>({k:`${t}-ldr${e.replace("-","")}`,nat:t,type:"leader",label:e,tag:"leader",back:"BROKEN"})),wn=[...vt("ge",[["548","squad","5-4-8","assault"],["467","squad","4-6-7","rifle"],["447","squad","4-4-7","second"],["hs","half","2-4-7","half"],["crew","crew","2-2-8","crew"]]),...xt("ge"),{k:"ge-pf",nat:"ge",type:"weapon",label:"PF",tag:"panzerfaust"},...wt("ge"),...vt("us",[["747","squad","7-4-7","elite"],["666","squad","6-6-6","rifle"],["hs","half","3-3-6","half"],["crew","crew","2-2-7","crew"]]),...xt("us"),{k:"us-baz",nat:"us",type:"weapon",label:"BAZ",tag:"bazooka"},...wt("us"),...vt("ru",[["628","squad","6-2-8","guards"],["447","squad","4-4-7","rifle"],["426","squad","4-2-6","conscript"],["hs","half","2-2-7","half"],["crew","crew","2-2-7","crew"]]),...xt("ru"),{k:"ru-atr",nat:"ru",type:"weapon",label:"ATR",tag:"anti-tank"},...wt("ru"),...[["prep","Prep Fire","fire"],["first","First Fire","fire"],["final","Final Fire","fire"],["pin","Pin","state"],["cx","CX","move"],["moved","Moved","move"],["dm","DM","state"],["conceal","?","info"],["melee","Melee","fire"],["smoke","Smoke","terrain"],["blaze","Blaze","terrain"],["rubble","Rubble","terrain"],["wreck","Wreck","terrain"],["wire","Wire","terrain"],["fox","Foxhole","terrain"],["note","Note","info"]].map(([t,e,s])=>({k:`mk-${t}`,nat:"mk",type:s==="terrain"?"fort":"marker",label:e,tone:s,back:t==="smoke"?"Dispersed":t==="first"?"Final Fire":void 0}))],Fe=new Map(wn.map(t=>[t.k,t]));function bs(t){return wn.filter(e=>e.nat===t)}const Le=Math.sqrt(3),T=40;function ys(t){let e=t,s="";do s=String.fromCharCode(65+e%26)+s,e=Math.floor(e/26)-1;while(e>=0);return s}function J(t){return`${ys(t.c)}${t.r}`}function F(t){const e=/^([A-Z]{1,2})(\d{1,2})$/.exec(t);if(!e)return null;let s=0;for(const n of e[1])s=s*26+(n.charCodeAt(0)-64);return{c:s-1,r:Number(e[2])}}function N(t){return{x:T*1.5*t.c+T,y:Le*T*(t.r-1+.5*(t.c&1))+Le/2*T}}function Z(t,e=T){const{x:s,y:n}=N(t);return Array.from({length:6},(i,o)=>{const r=Math.PI/3*o;return[s+e*Math.cos(r),n+e*Math.sin(r)]})}function Ie(t){return(t.c&1?[[0,-1],[0,1],[-1,0],[1,0],[-1,1],[1,1]]:[[0,-1],[0,1],[-1,-1],[1,-1],[-1,0],[1,0]]).map(([n,i])=>({c:t.c+n,r:t.r+i}))}function rn(t){const e=t.c,s=t.r-1-(t.c-(t.c&1))/2;return{q:e,r:s,s:-e-s}}function xn(t,e){const s=rn(t),n=rn(e);return Math.max(Math.abs(s.q-n.q),Math.abs(s.r-n.r),Math.abs(s.s-n.s))}function ks(t,e){const s=-t-e;let n=Math.round(t),i=Math.round(e);const o=Math.round(s),r=Math.abs(n-t),a=Math.abs(i-e),l=Math.abs(o-s);return r>a&&r>l?n=-i-o:a>l&&(i=-n-o),{q:n,r:i,s:-n-i}}function Pt(t,e){const s=t-T,n=e-Le/2*T,i=2/3*s/T,o=(-1/3*s+Le/3*n)/T,r=ks(i,o);return{c:r.q,r:r.r+(r.q-(r.q&1))/2+1}}function Mn(t,e){const s=N(t),n=N(e),i=Math.max(1,xn(t,e)*24),o=new Map,r=.9;for(let a=0;a<=i;a+=1){const l=a/i,d=s.x+(n.x-s.x)*l,u=s.y+(n.y-s.y)*l,h=-(n.y-s.y),p=n.x-s.x,k=Math.hypot(h,p)||1,m=J(Pt(d+h/k*r,u+p/k*r)),b=J(Pt(d-h/k*r,u-p/k*r));for(const y of new Set([m,b]))o.has(y)||o.set(y,{id:y,edge:m!==b});if(m===b){const y=o.get(m);y&&(y.edge=!1)}}return[...o.values()]}function $s(t,e){const s=N(t),n=N(e),i=(s.x+n.x)/2,o=(s.y+n.y)/2,r=n.x-s.x,a=n.y-s.y,l=Math.hypot(r,a),d=T/2,u=-a/l*d,h=r/l*d;return[i-u,o-h,i+u,o+h]}const Oe={open:"Open ground",woods:"Woods",brush:"Brush",grain:"Grain",orchard:"Orchard","wood-bldg":"Wooden building","stone-bldg":"Stone building",marsh:"Marsh",water:"Water"},vs={".":"open",w:"woods",h:"brush",g:"grain",o:"orchard",b:"wood-bldg",s:"stone-bldg",m:"marsh","~":"water"};function ws(t){var r;const e=t.terrain.length,s=t.terrain[0].length,n=new Map,i=new Set(t.roads.flat());for(let a=1;a<=e;a+=1)for(let l=0;l<s;l+=1){const d={c:l,r:a},u=J(d),h=i.has(u),p=t.terrain[a-1][l];n.set(u,{id:u,hex:d,terrain:h?"open":vs[p]??"open",level:Number(((r=t.elevation[a-1])==null?void 0:r[l])??0)||0,road:h})}const o=[...(t.hedges??[]).map(([a,l])=>({a,b:l,kind:"hedge"})),...(t.walls??[]).map(([a,l])=>({a,b:l,kind:"wall"}))];return{id:t.id,name:t.name,cols:s,rows:e,cells:n,roads:t.roads,hexsides:o}}const xs=(t,e,s)=>{const n=F(`${e}${t}`),i=F(`${s}${t}`),o=[];for(let r=n.c;r<=i.c;r+=1)o.push(J({c:r,r:t}));return o},Ms=(t,e,s)=>Array.from({length:s-e+1},(n,i)=>`${t}${e+i}`),se={millbrook:ws({id:"millbrook",name:"Millbrook crossroads",terrain:["www..ggg.....oo...ww..","wwh..ggg..b..oo..www..","wh.........b.....ww..h",".....h...bs.sb........","......................","...gg....bs..sb....oo.","...gg.....b..b....h...","ww.......h......gg...w","www..mm~~.......gg..ww","wwww.mm~~~..h......www"],elevation:["0000000000000000122221","0000000000000000122221","0000000000000000011211","0000000000000000001110","0000000000000000000000","0000000000000000000000","0000000000000000000000","0000000000000000000000","0000000000000000000000","0000000000000000000000"],roads:[xs(5,"A","V"),Ms("L",1,10)],hedges:[["C6","D6"],["C7","D7"],["D5","D6"],["E6","F6"],["P8","Q8"],["P9","Q9"]],walls:[["J4","J5"],["K4","K5"],["M4","M5"],["N4","N5"],["S5","S6"],["T5","T6"]]})},Dt=[{id:"millbrook-dawn",name:"Dawn at Millbrook",map:"millbrook",turns:6,brief:"A reinforced American company pushes east along the valley road. A thin German screen holds the stone houses at the crossroads.",victory:"Americans win at game end if they hold more stone-building hexes than the Germans. Otherwise the Germans win.",first:"a",sides:{a:{nat:"us",name:"American",setup:"Enter on turn 1 along the west edge (column A).",entry:"west",units:[{k:"us-666",at:"pool",count:6},{k:"us-747",at:"pool",count:2},{k:"us-ldr91",at:"pool"},{k:"us-ldr80",at:"pool"},{k:"us-ldr70",at:"pool"},{k:"us-mmg",at:"pool"},{k:"us-lmg",at:"pool",count:2},{k:"us-baz",at:"pool"}]},b:{nat:"ge",name:"German",setup:"Set up first, east of column H. Units start where shown; adjust before turn 1 by agreement.",units:[{k:"ge-467",at:"K4"},{k:"ge-467",at:"N6"},{k:"ge-467",at:"K6"},{k:"ge-447",at:"M4"},{k:"ge-447",at:"L7"},{k:"ge-ldr81",at:"K6"},{k:"ge-ldr80",at:"N6"},{k:"ge-mmg",at:"K6"},{k:"ge-lmg",at:"K4"},{k:"ge-pf",at:"N6"},{k:"mk-conceal",at:"M4"},{k:"mk-conceal",at:"L7"}]}}},{id:"hill-stand",name:"Stand on the High Ground",map:"millbrook",turns:5,brief:"Russian guards storm the wooded hill east of Millbrook, held by German riflemen with machine-gun support.",victory:"Russians win if they control every level-2 hex at game end.",first:"a",sides:{a:{nat:"ru",name:"Russian",setup:"Set up second, west of column P.",units:[{k:"ru-628",at:"O2",count:2},{k:"ru-628",at:"O4",count:2},{k:"ru-447",at:"N6",count:2},{k:"ru-447",at:"N3",count:2},{k:"ru-ldr91",at:"O2"},{k:"ru-ldr80",at:"N6"},{k:"ru-lmg",at:"O4"},{k:"ru-mtr",at:"N3"}]},b:{nat:"ge",name:"German",setup:"Set up first on or east of column R.",units:[{k:"ge-467",at:"S2"},{k:"ge-467",at:"T3"},{k:"ge-447",at:"U2"},{k:"ge-ldr91",at:"S2"},{k:"ge-hmg",at:"S2"},{k:"ge-lmg",at:"T3"},{k:"mk-fox",at:"S2"},{k:"mk-fox",at:"T3"}]}}},{id:"open-table",name:"Open table",map:"millbrook",turns:8,brief:"An empty map. Add counters from the tray to set up your own scenario or try a situation from your rulebook.",victory:"Agree on victory conditions before you start.",first:"a",sides:{a:{nat:"us",name:"American",setup:"Free setup.",units:[]},b:{nat:"ge",name:"German",setup:"Free setup.",units:[]}}}],Ae=new Map(Dt.map(t=>[t.id,t])),fe=["Rally","Prep Fire","Movement","Defensive Fire","Advancing Fire","Rout","Advance","Close Combat"],H={movement:{infantryMF:{squad:4,half:4,crew:4,leader:6},leaderBonusMF:2,doubleTimeMF:1,roadBonusMF:1,upslopeMultiplier:2,minimumMove:!0},terrainMF:{open:1,grain:1.5,orchard:1,brush:2,woods:2,"wood-bldg":2,"stone-bldg":2,marsh:2,water:null},roadMF:1,hexsideMF:{hedge:1,wall:1},routMF:{squad:4,half:4,crew:4,leader:6},stacking:{squadEquivalents:3,leaders:4,weight:{squad:1,half:.5,crew:.5,leader:0}},portage:{capacity:{squad:3,half:2,crew:2,leader:1},weapon:{lmg:1,mmg:4,hmg:5,mtr:3,dc:1,ft:2,pf:0,baz:1,atr:2},penaltyPerPP:1}},Ss=["All costs, tables and unit values come from src/rules/data.ts. The IFT, close combat table, weapon stats and anything marked VERIFY there should be checked against your charts.","Automated: phase sequence, movement, line of sight, Prep/Defensive/Advancing Fire on the IFT, morale and pin checks, casualty reduction, cowering, weapon breakdown, rally, repair and close combat.","Not yet automated: Defensive First Fire during movement, fire groups spanning several hexes, rate of fire, ELR, leader loss checks, wounds, heat of battle, rout requirements, concealment, mortars and other special weapons, vehicles and guns."],R={columns:[1,2,4,6,8,12,16,20,24,30,36],ift:{1:["1KIA","K/1","1MC","NMC","PTC"],2:["1KIA","K/1","2MC","1MC","NMC","PTC"],4:["1KIA","K/2","2MC","1MC","NMC","NMC","PTC"],6:["2KIA","1KIA","K/2","2MC","1MC","NMC","PTC","PTC"],8:["2KIA","1KIA","K/3","3MC","2MC","1MC","NMC","PTC","PTC"],12:["3KIA","2KIA","1KIA","K/3","3MC","2MC","1MC","NMC","PTC"],16:["3KIA","2KIA","1KIA","K/4","4MC","3MC","2MC","1MC","NMC","PTC"],20:["4KIA","3KIA","2KIA","1KIA","K/4","4MC","3MC","2MC","1MC","NMC","PTC"],24:["4KIA","3KIA","2KIA","1KIA","K/4","4MC","4MC","3MC","2MC","1MC","NMC","PTC"],30:["5KIA","4KIA","3KIA","2KIA","1KIA","K/4","4MC","3MC","2MC","1MC","NMC","PTC"],36:["6KIA","5KIA","4KIA","3KIA","2KIA","1KIA","K/4","4MC","3MC","2MC","NMC","PTC"]},tem:{open:0,grain:0,orchard:0,brush:1,woods:1,"wood-bldg":2,"stone-bldg":3,marsh:0,water:0},hexsideTem:{hedge:1,wall:2},hindrance:{grain:1,orchard:1,brush:1},smokeHindrance:2,maxHindrance:6,obstacleHeight:{woods:1,"wood-bldg":1,"stone-bldg":1},longRangeMultiple:2,longRangeFP:.5,pbfFP:2,tpbfFP:3,advancingFireFP:.5,pinnedFP:.5,cxDRM:1,coweringShift:1,inexperienced:["4-2-6"],squadKeepsInherentWithSW:!0,halfSquadKeepsInherentWithSW:!1,weapons:{"ge-lmg":{fp:3,range:8,b:12},"ge-mmg":{fp:5,range:12,b:12},"ge-hmg":{fp:7,range:16,b:12},"us-lmg":{fp:2,range:6,b:11},"us-mmg":{fp:4,range:10,b:11},"us-hmg":{fp:5,range:15,b:11},"ru-lmg":{fp:2,range:6,b:11},"ru-mmg":{fp:4,range:10,b:11},"ru-hmg":{fp:6,range:16,b:11}}},ie={twelveCasualty:!0,brokenFailReduces:!0,rallyNeedsLeader:!0,selfRallyDRM:1,rallyTerrainDRM:-1,desperationDRM:4,rallyTwelveCasualty:!0,repair:1,repairLoss:6},Cs={"5-4-8":"2-3-8","4-6-7":"2-4-7","4-4-7":"2-2-7","7-4-7":"3-3-7","6-6-6":"3-3-6","6-2-8":"3-2-8","4-2-6":"2-2-6"},qt={odds:[{ratio:1/8,label:"1:8",kill:1},{ratio:1/6,label:"1:6",kill:2},{ratio:1/4,label:"1:4",kill:3},{ratio:1/3,label:"1:3",kill:4},{ratio:1/2,label:"1:2",kill:5},{ratio:1,label:"1:1",kill:6},{ratio:3/2,label:"3:2",kill:7},{ratio:2,label:"2:1",kill:8},{ratio:3,label:"3:1",kill:9},{ratio:4,label:"4:1",kill:10},{ratio:6,label:"6:1",kill:11},{ratio:8,label:"8:1",kill:12},{ratio:10,label:"10:1",kill:13}],equalReduces:!0,leaderFP:1};function Sn(){return{spent:new Map,done:new Set,mover:[],offRoad:new Set,doubled:new Set}}function L(t){var s;const e=((s=Fe.get(t.k))==null?void 0:s.type)??"marker";return e==="fort"?"marker":e}function Ts(t){const e=t.k.split("-")[1]??"";return H.portage.weapon[e]??1}function Ye(t){if(t.over)return"none";if(t.turn===0)return"setup";const e=fe[t.phase];return e==="Movement"?"move":e==="Rout"?"rout":e==="Advance"?"advance":"none"}function ce(t){return t.turn===0?"Setup":fe[t.phase]}const an=new WeakMap;function jt(t,e,s){let n=an.get(t);if(!n){n=new Set;for(const i of t.roads)for(let o=1;o<i.length;o+=1)n.add(`${i[o-1]}|${i[o]}`),n.add(`${i[o]}|${i[o-1]}`);an.set(t,n)}return n.has(`${e}|${s}`)}function Ls(t,e,s){return t.hexsides.find(n=>n.a===e&&n.b===s||n.a===s&&n.b===e)}function As(t){return t==="a"?"b":t==="b"?"a":null}function Cn(t,e,s){const n=As(s);return n?(t.stacks.get(e)??[]).some(i=>{const o=t.counters.get(i);return o&&o.side===n&&L(o)!=="marker"}):!1}function Tn(t,e,s){const n=t.scenario.sides[e].entry,i=F(s),o=se[t.scenario.map];return!n||!i?!1:n==="west"?i.c===0:n==="east"?i.c===o.cols-1:n==="north"?i.r===1:i.r===o.rows}function We(t,e,s,n,i){const o=se[t.scenario.map],r=o.cells.get(s);if(!r)return{cost:null,reason:`${s} is not on the map.`};if(i!=="advance"&&Cn(t,s,n))return{cost:null,reason:`${s} holds enemy units. Infantry can only enter them by advancing into Close Combat.`};const a=e?o.cells.get(e):void 0,l=!!(a&&r.road&&a.road&&jt(o,a.id,r.id)),d=H.terrainMF[r.terrain];if(!l&&d===null)return{cost:null,reason:`Infantry cannot enter ${r.terrain} (${s}).`};let u=l?H.roadMF:d;a&&r.level>a.level&&(u*=H.movement.upslopeMultiplier);const h=a&&!l?Ls(o,a.id,r.id):void 0;return h&&(u+=H.hexsideMF[h.kind]),{cost:u}}function Ln(t,e){var d;const s=e.map(u=>t.counters.get(u)).filter(u=>!!u);if(!s.length||s.length!==e.length)return{reasons:["Some of those counters are no longer on the table."]};const n=s[0].at;if(s.some(u=>u.at!==n))return{reasons:["Pick counters from a single hex to move them together."]};const i=s.filter(u=>L(u)!=="marker");if(new Set(i.map(u=>u.side)).size>1)return{reasons:["Move each side’s counters separately."]};const r=((d=i[0])==null?void 0:d.side)??"n",a=i.filter(u=>["squad","half","crew","leader"].includes(L(u))),l=i.filter(u=>L(u)==="weapon");return{ids:e,units:s,infantry:a,weapons:l,at:n,side:r}}function An(t,e,s,n){const i=[],o=ce(t);if(e.side!=="n"&&s!==e.side&&i.push("You can only move your own side’s counters."),n==="setup")return i;if(n==="none")return i.push(`Units don’t move during the ${o} Phase.`),i;e.at.startsWith("elim-")&&i.push("Eliminated units cannot return to the map."),e.units.some(u=>["gun","vehicle"].includes(L(u)))&&i.push("Guns and vehicles are not handled by the movement engine yet."),e.infantry.length||i.push(e.weapons.length?"Support weapons need infantry to carry them.":"Nothing here can move."),(n==="move"||n==="advance")&&e.side!==t.active&&i.push(`Only the ${t.scenario.sides[t.active].name} side moves in the ${o} Phase.`);const r=e.infantry.filter(u=>u.face===1);n==="rout"?e.infantry.some(u=>u.face===0)&&i.push("Only broken units rout in the Rout Phase."):r.length&&i.push("Broken units can’t move now; they move in the Rout Phase."),(n==="move"||n==="advance")&&e.infantry.some(u=>u.pin)&&i.push("Pinned units can’t move or advance."),n==="move"&&e.units.some(u=>u.fired==="prep")&&i.push("Units that fired in the Prep Fire Phase can’t move."),t.over&&i.push("The game is over."),e.at.startsWith("pool-")&&n!=="move"&&i.push(`Units enter the map in the Movement Phase, not the ${o} Phase.`);const a=t.move;return e.ids.filter(u=>a.done.has(u)).length&&i.push("Some of these units already finished moving this phase."),e.ids.filter(u=>(a.spent.get(u)??0)>0&&!a.mover.includes(u)).length&&i.push("These units stopped when another unit started moving."),i}function En(t,e,s,n){const i=t.move,o=H.movement,r=e.infantry.some(b=>L(b)==="leader"&&b.face===0),a=e.infantry.filter(b=>L(b)!=="leader"),l=e.infantry.reduce((b,y)=>b+H.portage.capacity[L(y)],0),d=e.weapons.reduce((b,y)=>b+Ts(y),0),u=Math.max(0,d-l)*H.portage.penaltyPerPP;let h=1/0,p=1/0;for(const b of e.infantry){const y=L(b);let w=s==="rout"?H.routMF[y]:H.movement.infantryMF[y];s==="move"&&(r&&a.length&&(w=Math.max(w,H.movement.infantryMF.squad+o.leaderBonusMF)),(n||i.doubled.has(b.id))&&(w+=o.doubleTimeMF)),w=Math.max(0,w-u),p=Math.min(p,w),h=Math.min(h,w-(i.spent.get(b.id)??0))}Number.isFinite(h)||(h=0);const k=e.infantry.every(b=>(i.spent.get(b.id)??0)===0),m=s==="move"&&e.infantry.every(b=>!i.offRoad.has(b.id));return{allowance:Number.isFinite(p)?p:0,left:Math.max(0,h),minimumMove:o.minimumMove&&k,roadBonus:m}}function Ft(t,e,s){if(s.includes("-"))return[];const n=new Set(e.ids),o=[...(t.stacks.get(s)??[]).map(d=>t.counters.get(d)).filter(d=>d&&!n.has(d.id)&&d.side===e.side),...e.infantry],r=o.reduce((d,u)=>{const h=L(u);return d+(h in H.stacking.weight?H.stacking.weight[h]:0)},0),a=o.filter(d=>L(d)==="leader").length,l=[];return r>H.stacking.squadEquivalents&&l.push(`${s} would hold ${r} squad-equivalents; the limit is ${H.stacking.squadEquivalents}.`),a>H.stacking.leaders&&l.push(`${s} would hold ${a} leaders; the limit is ${H.stacking.leaders}.`),l}function Ht(t,e,s,n,i){var I;const o=Ln(t,e);if("reasons"in o)return{ok:!1,reasons:o.reasons};const r=Ye(t),a=An(t,o,i,r);if(s.length||a.push("Choose a hex to move to."),a.length)return{ok:!1,reasons:a};const l=s[s.length-1];if(r==="setup")return!se[t.scenario.map].cells.has(l)&&!/^(pool|elim)-[ab]$/.test(l)?{ok:!1,reasons:[`${l} is not on the map.`]}:{ok:!0,cost:0,left:0,minimumMove:!1,offRoad:!1,kind:r};if(n){if(r!=="move")return{ok:!1,reasons:["Double Time is only possible in the Movement Phase."]};if(o.infantry.some(P=>P.cx&&!t.move.doubled.has(P.id)))return{ok:!1,reasons:["Units that are already CX cannot Double Time."]}}if(r==="advance"){if(s.length!==1)return{ok:!1,reasons:["Advancing units move exactly one hex."]};const P=F(o.at),C=F(l);if(!P||!C||!Ie(P).some(x=>J(x)===l))return{ok:!1,reasons:["Advance into an adjacent hex."]};const O=We(t,o.at,l,o.side,r);if(O.cost===null)return{ok:!1,reasons:[O.reason]};const B=Ft(t,o,l);return B.length&&!Cn(t,l,o.side)?{ok:!1,reasons:B}:{ok:!0,cost:0,left:0,minimumMove:!1,offRoad:!1,kind:r}}const d=En(t,o,r,n);let u=0,h=o.at.startsWith("pool-")?null:o.at,p=!0;if(h===null&&!Tn(t,o.side,s[0])){const P=t.scenario.sides[o.side].entry;return{ok:!1,reasons:[P?`Enter along the ${P} edge of the map.`:"This side has no entry edge in this scenario; set it up on the map."]}}const k=se[t.scenario.map];h&&!((I=k.cells.get(h))!=null&&I.road)&&(p=!1);for(const P of s){if(h){const O=F(h);if(!O||!Ie(O).some(B=>J(B)===P))return{ok:!1,reasons:[`${P} is not next to ${h}.`]}}const C=We(t,h,P,o.side,r);if(C.cost===null)return{ok:!1,reasons:[C.reason]};(!h||!jt(k,h,P))&&(p=!1),u+=C.cost,h=P}const m=p&&d.roadBonus?H.movement.roadBonusMF:0,b=d.left+m-u,y=s.length===1&&d.minimumMove&&b<0;if(b<-1e-9&&!y)return{ok:!1,reasons:[`That costs ${ge(u)} MF but only ${ge(d.left+m)} MF ${d.left+m===d.allowance?"are available":"remain"}.`]};const w=Ft(t,o,l);return w.length?{ok:!1,reasons:w}:{ok:!0,cost:y?d.left:u,left:y?0:b,minimumMove:y,offRoad:!p,kind:r}}function Kt(t,e,s,n){const i=(m,b=Ye(t))=>({ok:!1,reasons:m,kind:b,left:0,allowance:0,canDoubleTime:!1,hexes:new Map}),o=Ln(t,e);if("reasons"in o)return i(o.reasons);const r=Ye(t),a=An(t,o,s,r);if(a.length)return i(a,r);const l=se[t.scenario.map],d=new Map,u=r==="move"&&o.infantry.every(m=>!m.cx||t.move.doubled.has(m.id))&&o.infantry.some(m=>!t.move.doubled.has(m.id));if(r==="setup"){for(const m of l.cells.keys())d.set(m,{path:[m],cost:0});return{ok:!0,reasons:[],kind:r,left:0,allowance:0,canDoubleTime:!1,hexes:d}}if(r==="advance"){for(const m of Ie(F(o.at))){const b=J(m);Ht(t,e,[b],!1,s).ok&&d.set(b,{path:[b],cost:0})}return{ok:!0,reasons:[],kind:r,left:0,allowance:0,canDoubleTime:!1,hexes:d}}const h=En(t,o,r,n&&u),p=(m,b)=>{var P;const y=new Map,w=[],I=[];if(o.at.startsWith("pool-")){for(const C of l.cells.keys()){if(!Tn(t,o.side,C)||m)continue;const O=We(t,null,C,o.side,r);O.cost!==null&&I.push({hex:C,cost:O.cost})}for(const C of I)C.cost<=b+1e-9&&w.push({hex:C.hex,cost:C.cost,path:[C.hex]})}else{if(m&&!((P=l.cells.get(o.at))!=null&&P.road))return y;w.push({hex:o.at,cost:0,path:[]})}for(;w.length;){w.sort((O,B)=>O.cost-B.cost);const C=w.shift();if(C.path.length){const O=y.get(C.hex);if(O&&O.cost<=C.cost)continue;y.set(C.hex,{path:C.path,cost:C.cost})}for(const O of Ie(F(C.hex))){const B=J(O);if(!l.cells.has(B)||B===o.at||m&&!jt(l,C.hex,B))continue;const x=We(t,C.hex,B,o.side,r);if(x.cost===null)continue;const A=C.cost+x.cost;if(A>b+1e-9)continue;const W=y.get(B);W&&W.cost<=A||w.push({hex:B,cost:A,path:[...C.path,B]})}}return y},k=p(!1,h.left);for(const[m,b]of k)d.set(m,b);if(h.roadBonus)for(const[m,b]of p(!0,h.left+H.movement.roadBonusMF))d.has(m)||d.set(m,b);if(h.minimumMove&&!o.at.startsWith("pool-"))for(const m of Ie(F(o.at))){const b=J(m);!d.has(b)&&l.cells.has(b)&&We(t,o.at,b,o.side,r).cost!==null&&d.set(b,{path:[b],cost:h.left})}for(const m of[...d.keys()])Ft(t,o,m).length&&d.delete(m);return{ok:!0,reasons:[],kind:r,left:h.left,allowance:h.allowance,canDoubleTime:u,hexes:d}}function ge(t){return Number.isInteger(t)?String(t):t.toFixed(1)}function we(t,e){t.counters.set(e.id,e);const s=t.stacks.get(e.at)??[];s.push(e.id),t.stacks.set(e.at,s)}function xe(t,e){const s=t.stacks.get(e.at);if(!s)return;const n=s.filter(i=>i!==e.id);n.length?t.stacks.set(e.at,n):t.stacks.delete(e.at)}function Es(t,e,s){xe(t,e),e.at=s,we(t,e)}class Ps{constructor(e){M(this,"n",0);M(this,"dice");M(this,"used",[]);this.dice=e}get ok(){return this.n<=this.dice.length}die(){const e=this.dice[this.n];return this.n+=1,e===void 0?0:(this.used.push(e),e)}dr(){return[this.die(),this.die()]}pick(e){if(e<=1)return 0;const s=Math.floor(36/e)*e;for(let n=0;n<20;n+=1){const[i,o]=this.dr(),r=(i-1)*6+(o-1);if(r<s)return r%e}return 0}}function Ce(t,e){const s=L(t),n=t.label??e;if(s==="squad"||s==="half"||s==="crew"){const i=/^(\d+)-(\d+)-(\d+)/.exec(n);return i?{role:s,fp:Number(i[1]),range:Number(i[2]),morale:Number(i[3]),leaderDRM:0}:null}if(s==="leader"){const i=/^(\d+)-(\d+)/.exec(n);return i?{role:s,fp:0,range:0,morale:Number(i[1]),leaderDRM:-Number(i[2])}:null}if(s==="weapon"){const i=R.weapons[t.k],o=/(\d+)-(\d+)/.exec(n);return!i&&!o?null:{role:s,fp:o?Number(o[1]):i.fp,range:o?Number(o[2]):i.range,morale:0,leaderDRM:0,b:(i==null?void 0:i.b)??12}}return null}function Fs(t){return["squad","half","crew","leader"].includes(L(t))}function Ee(t,e,s){return(t.stacks.get(e)??[]).map(n=>t.counters.get(n)).filter(n=>n&&n.side===s&&Fs(n))}function Rs(t){t.cx=void 0,t.pin=void 0,t.fired=void 0}function ht(t,e){if(e.side!=="a"&&e.side!=="b"){xe(t,e),t.counters.delete(e.id);return}Rs(e),e.face=0,Es(t,e,`elim-${e.side}`)}function qe(t,e,s){const n=L(e),i=e.label??s;if(n==="squad"&&(e.side==="a"||e.side==="b")){const o=e.k.split("-")[0],r={id:`${e.id}h`,k:`${o}-hs`,side:e.side,face:e.face,rot:0,label:Cs[i]??"HS",at:e.at,pin:e.pin,fired:e.fired,cx:e.cx};return xe(t,e),t.counters.delete(e.id),we(t,r),`${i} reduced to half-squad ${r.label}`}return ht(t,e),`${i} eliminated`}const Be=t=>{var e;return((e=Fe.get(t.k))==null?void 0:e.label)??""},Ns=t=>t.label??Be(t);function Is(t){return t.turn>0&&!t.over&&fe[t.phase]==="Close Combat"}function Pn(t){const e=se[t.scenario.map];return[...t.stacks.keys()].filter(s=>e.cells.has(s)&&Ee(t,s,"a").length&&Ee(t,s,"b").length)}function ln(t,e){return t.reduce((s,n)=>{var i;return e&&n.face===1?s:(!e&&n.face,L(n)==="leader"?s+qt.leaderFP:s+(((i=Ce(n,Be(n)))==null?void 0:i.fp)??0))},0)}function Os(t,e,s){var h;const n=Ee(t,e,s),i=Ee(t,e,s==="a"?"b":"a"),o=ln(n,!0),r=ln(i,!1);if(o<=0||r<=0)return null;const a=o/r,l=[...qt.odds].reverse().find(p=>a+1e-9>=p.ratio);if(!l)return null;const u=n.filter(p=>L(p)==="leader"&&p.face===0).sort((p,k)=>{var m,b;return(((m=Ce(p,Be(p)))==null?void 0:m.leaderDRM)??0)-(((b=Ce(k,Be(k)))==null?void 0:b.leaderDRM)??0)})[0];return{side:s,attackers:n.filter(p=>p.face===0).map(p=>p.id),defenders:i.map(p=>p.id),atk:o,def:r,odds:l.label,kill:l.kill,drm:u?((h=Ce(u,Be(u)))==null?void 0:h.leaderDRM)??0:0,leader:(u==null?void 0:u.id)??null}}function Fn(t,e,s){if(!Is(t))return{ok:!1,reasons:["Close combat happens in the Close Combat Phase."]};if(s==="n")return{ok:!1,reasons:["Take a seat to fight."]};if(!Pn(t).includes(e))return{ok:!1,reasons:[`There is no close combat in ${e}.`]};if(t.used.has(`cc:${e}`))return{ok:!1,reasons:[`Close combat in ${e} is already resolved this phase.`]};const n=["a","b"].map(i=>Os(t,e,i)).filter(i=>!!i);return{ok:!0,hex:e,attacks:n}}function Bs(t,e,s){t.used.add(`cc:${e.hex}`);const n=[],i=[];for(const o of e.attacks){const[r,a]=s.dr(),l=r+a+o.drm,d=t.scenario.sides[o.side].name;let u="no effect";if(l<o.kill){u="all defenders eliminated";for(const h of o.defenders)i.push({id:h,kind:"kill"})}else if(l===o.kill&&qt.equalReduces){const h=o.defenders[s.pick(o.defenders.length)];u="casualty reduction",i.push({id:h,kind:"reduce"})}n.push(`${d} close combat in ${e.hex}: ${o.atk} vs ${o.def} (${o.odds}, kill ${o.kill}), DR ${r}+${a}${o.drm?` ${o.drm}`:""} = ${l} → ${u}`)}for(const o of i){const r=t.counters.get(o.id);!r||r.at!==e.hex||(o.kind==="kill"?(ht(t,r),n.push(`${Ns(r)} eliminated`)):n.push(qe(t,r,Be(r))))}return Ee(t,e.hex,"a").length&&Ee(t,e.hex,"b").length&&n.push(`${e.hex} stays locked in melee`),n}function Ds(t,e){return(t.stacks.get(e)??[]).some(s=>{const n=t.counters.get(s);return(n==null?void 0:n.k)==="mk-smoke"&&n.face===0})}function Rn(t,e,s){const n=se[t.scenario.map],i=F(e),o=F(s),r=n.cells.get(e),a=n.cells.get(s);if(!i||!o||!r||!a)return{clear:!1,reason:"Not on the map.",hindrances:[],hindrance:0,hexes:[]};const l=Mn(i,o).filter(x=>n.cells.has(x.id)),d=N(i),u=N(o),h=(u.x-d.x)**2+(u.y-d.y)**2||1,p=x=>{const A=N(x.hex);return Math.max(0,Math.min(1,((A.x-d.x)*(u.x-d.x)+(A.y-d.y)*(u.y-d.y))/h))},k=r.level,m=a.level,b=x=>{const A=p(x),W=k+(m-k)*A;return x.level+(R.obstacleHeight[x.terrain]??0)>W+1e-9},y=[],w=[],I=[];for(const x of l){const A=n.cells.get(x.id),W=x.id===e||x.id===s;if(Ds(t,x.id)&&y.push({hex:x.id,value:R.smokeHindrance,label:`Smoke ${x.id}`}),W)continue;if(b(A)){if(!x.edge)return{clear:!1,blockedBy:x.id,reason:`Blocked by ${A.level>Math.max(k,m)&&!R.obstacleHeight[A.terrain]?"higher ground":Oe[A.terrain].toLowerCase()} in ${x.id}.`,hindrances:y,hindrance:0,hexes:l.map(V=>V.id)};w.push(A);continue}const he=R.hindrance[A.terrain]??0,$=p(A),E=k+(m-k)*$;he&&E<A.level+1-1e-9&&(x.edge?I.push(A):y.push({hex:x.id,value:he,label:`${Oe[A.terrain]} ${x.id}`}))}for(let x=0;x<w.length;x+=1)for(let A=x+1;A<w.length;A+=1){const W=w[x].hex,he=w[A].hex,$=Math.abs(N(W).x-N(he).x),E=Math.abs(N(W).y-N(he).y);if(Math.hypot($,E)<75)return{clear:!1,blockedBy:w[x].id,reason:`Blocked along the hexside between ${w[x].id} and ${w[A].id}.`,hindrances:y,hindrance:0,hexes:l.map(V=>V.id)}}const P=new Set;for(const x of I)I.find(W=>W!==x&&P.has(W.id)&&Math.hypot(N(W.hex).x-N(x.hex).x,N(W.hex).y-N(x.hex).y)<75)||(P.add(x.id),y.push({hex:x.id,value:R.hindrance[x.terrain]??0,label:`${Oe[x.terrain]} ${x.id} (hexside)`}));const C=y.reduce((x,A)=>x+A.value,0),O=l.map(x=>x.id),B=O.length>=2?O[O.length-2]:void 0;return C>=R.maxHindrance?{clear:!1,reason:`Hindrances total +${C}, which blocks LOS.`,hindrances:y,hindrance:C,hexes:O,approach:B}:{clear:!0,hindrances:y,hindrance:C,hexes:O,approach:B}}function be(t){var e;return((e=Fe.get(t.k))==null?void 0:e.label)??""}const _=t=>t.label??be(t),lt=t=>{var e;return((e=Ce(t,be(t)))==null?void 0:e.morale)??7},He=t=>{var e;return((e=Ce(t,be(t)))==null?void 0:e.leaderDRM)??0};function zt(t){return t>0?`+${t}`:t<0?`${t}`:""}function Rt(t,e){return e.face===1?qe(t,e,be(e)):(e.face=1,e.pin=void 0,`${_(e)} breaks`)}function cn(t,e,s,n,i){const[o,r]=i.dr(),a=o+r+s+n,l=lt(e),d=`${_(e)} ${s?`${s}MC`:"NMC"}: DR ${o}+${r}${zt(s+n)} = ${a} vs ${l}`;if(a<=l&&!(o+r===12&&ie.twelveCasualty))return`${d}: passed`;if(o+r===12&&ie.twelveCasualty){const u=e.face===0?Rt(t,e):"",h=t.counters.has(e.id)?qe(t,e,be(e)):"";return`${d}: original 12. ${[u,h].filter(Boolean).join(", ")}`}return e.face===1&&ie.brokenFailReduces?`${d}: failed, ${qe(t,e,be(e))}`:`${d}: failed, ${Rt(t,e)}`}function qs(t,e,s){if(t.face===1)return`${_(t)} is broken: unaffected by PTC`;const[n,i]=s.dr(),o=n+i+e,r=o<=lt(t);return r||(t.pin=!0),`${_(t)} PTC: DR ${n}+${i}${zt(e)} = ${o} vs ${lt(t)}: ${r?"passed":"pinned"}`}function Mt(t,e,s){const n=[],i=e.filter(l=>L(l)==="leader"),o=e.filter(l=>L(l)!=="leader");for(const l of i)n.push(s(l,0));const r=i.filter(l=>{var d,u;return((d=t.counters.get(l.id))==null?void 0:d.face)===0&&((u=t.counters.get(l.id))==null?void 0:u.at)===l.at}),a=r.length?Math.min(...r.map(He)):0;for(const l of o)t.counters.has(l.id)&&n.push(s(l,a));return n}function js(t,e,s,n){const i=[];let o;if(o=/^(\d)KIA$/.exec(s)){const r=[...e],a=Math.min(Number(o[1]),r.length);for(let l=0;l<a;l+=1){const d=r.splice(n.pick(r.length),1)[0];ht(t,d),i.push(`${_(d)} eliminated`)}for(const l of r)i.push(Rt(t,l));return i}if(o=/^K\/(\d)$/.exec(s)){const r=Number(o[1]),a=e[0].at,l=e[0].side,d=e[n.pick(e.length)];i.push(`Casualty: ${qe(t,d,be(d))}`);const u=(t.stacks.get(a)??[]).map(h=>t.counters.get(h)).filter(h=>h&&h.side===l&&["squad","half","crew","leader"].includes(L(h)));return i.push(...Mt(t,u,(h,p)=>cn(t,h,r,p,n))),i}if((o=/^(\d)MC$/.exec(s))||s==="NMC"){const r=o?Number(o[1]):0;return i.push(...Mt(t,e,(a,l)=>cn(t,a,r,l,n))),i}return s==="PTC"&&i.push(...Mt(t,e,(r,a)=>qs(r,a,n))),i}function Hs(t,e){const s=F(e.at);return s?[e.at,...Ie(s).map(J)].some(i=>(t.stacks.get(i)??[]).some(o=>{const r=t.counters.get(o);return r&&r.side!==e.side&&r.side!=="n"&&["squad","half","crew","leader"].includes(L(r))})):!1}function Wt(t){return t.turn>0&&!t.over&&fe[t.phase]==="Rally"}function Nn(t,e,s){const n=t.counters.get(e);if(!n)return{ok:!1,reasons:["That unit is no longer on the table."]};if(!Wt(t))return{ok:!1,reasons:["Units rally in the Rally Phase."]};if(n.side!==s)return{ok:!1,reasons:["You can only rally your own units."]};if(!["squad","half","crew","leader"].includes(L(n)))return{ok:!1,reasons:["Only infantry rallies."]};if(n.face!==1)return{ok:!1,reasons:[`${_(n)} is not broken.`]};if(!se[t.scenario.map].cells.has(n.at))return{ok:!1,reasons:["Only units on the map can rally."]};if(t.used.has(`rally:${e}`))return{ok:!1,reasons:[`${_(n)} already tried to rally this phase.`]};const i=[],o=L(n)==="leader",a=(t.stacks.get(n.at)??[]).map(d=>t.counters.get(d)).filter(d=>d&&d.id!==n.id&&d.side===n.side&&L(d)==="leader"&&d.face===0).sort((d,u)=>He(d)-He(u))[0];if(!o&&!a&&ie.rallyNeedsLeader)return{ok:!1,reasons:[`${_(n)} needs a good-order leader in its hex to rally.`]};a&&He(a)&&i.push({label:`Leader ${_(a)}`,value:He(a)}),o&&!a&&i.push({label:"Leader rallying himself",value:ie.selfRallyDRM});const l=se[t.scenario.map].cells.get(n.at);return["woods","wood-bldg","stone-bldg"].includes(l.terrain)&&i.push({label:"In woods or a building",value:ie.rallyTerrainDRM}),Hs(t,n)&&i.push({label:"Desperation: enemy adjacent",value:ie.desperationDRM}),{ok:!0,id:e,target:lt(n),drms:i,drm:i.reduce((d,u)=>d+u.value,0)}}function Ks(t,e,s){const n=t.counters.get(e.id);t.used.add(`rally:${e.id}`);const[i,o]=s.dr(),r=i+o+e.drm,a=`${_(n)} rally: DR ${i}+${o}${zt(e.drm)} = ${r} vs ${e.target}`;return i+o===12&&ie.rallyTwelveCasualty?[`${a}: original 12, ${qe(t,n,be(n))}`]:r<=e.target?(n.face=0,[`${a}: rallied`]):[`${a}: failed`]}function In(t,e,s){const n=t.counters.get(e);return n?Wt(t)?n.side!==s?{ok:!1,reasons:["You can only repair your own weapons."]}:L(n)!=="weapon"||n.face!==1?{ok:!1,reasons:[`${_(n)} is not malfunctioned.`]}:t.used.has(`repair:${e}`)?{ok:!1,reasons:[`${_(n)} was already worked on this phase.`]}:(t.stacks.get(n.at)??[]).some(o=>{const r=t.counters.get(o);return r&&r.side===n.side&&r.face===0&&["squad","half","crew"].includes(L(r))})?{ok:!0,id:e}:{ok:!1,reasons:["A good-order squad, half-squad or crew must be with the weapon to repair it."]}:{ok:!1,reasons:["Weapons are repaired in the Rally Phase."]}:{ok:!1,reasons:["That weapon is no longer on the table."]}}function zs(t,e,s){const n=t.counters.get(e);t.used.add(`repair:${e}`);const i=s.die();return i<=ie.repair?(n.face=0,[`${_(n)} repair: dr ${i}: repaired`]):i>=ie.repairLoss?(ht(t,n),[`${_(n)} repair: dr ${i}: weapon lost`]):[`${_(n)} repair: dr ${i}: still malfunctioned`]}function et(t){if(t.turn===0||t.over)return null;const e=fe[t.phase];return e==="Prep Fire"?"prep":e==="Defensive Fire"?"defensive":e==="Advancing Fire"?"advancing":null}function Nt(t){const e=et(t);return e?e==="defensive"?t.active==="a"?"b":"a":t.active:null}const On={prep:"Prep Fire",defensive:"Defensive Fire",advancing:"Advancing Fire"};function me(t){var e;return((e=Fe.get(t.k))==null?void 0:e.label)??""}function te(t){return Ce(t,me(t))}function Ws(t){return t==="a"?"b":"a"}function Bn(t,e,s){const n=et(t),i=e.label??me(e);if(!n)return`There is no fire in the ${t.turn===0?"Setup":fe[t.phase]} Phase.`;if(e.side!==s)return"You can only fire your own units.";if(s!==Nt(t))return`Only the ${t.scenario.sides[Nt(t)].name} side fires in the ${On[n]} Phase.`;const o=L(e);return["squad","half","crew","leader","weapon"].includes(o)?e.face===1?o==="weapon"?`${i} is malfunctioned.`:`${i} is broken.`:e.fired?`${i} has already fired this turn.`:te(e)?o==="weapon"&&!R.weapons[e.k]&&!/(\d+)-(\d+)/.test(e.label??"")?`${i} isn't automated yet.`:null:`${i} has no stat line to fire with. Relabel it like 4-6-7.`:`${i} can't fire (not infantry or a support weapon).`}function _s(t){let e=0;for(const s of R.columns)s<=t+1e-9&&(e=s);return e}function pt(t,e,s,n){const i=et(t),o=se[t.scenario.map],r=e.map($=>t.counters.get($)).filter($=>!!$);if(!r.length||r.length!==e.length)return{ok:!1,reasons:["Some of those counters are no longer on the table."]};const a=r[0].at;if(r.some($=>$.at!==a)||!o.cells.has(a))return{ok:!1,reasons:["Pick firing units from one hex on the map."]};for(const $ of r){const E=Bn(t,$,n);if(E)return{ok:!1,reasons:[E]}}if(!i||n==="n")return{ok:!1,reasons:["There is no fire now."]};if(!o.cells.has(s))return{ok:!1,reasons:[`${s} is not on the map.`]};const l=Ee(t,s,Ws(n));if(!l.length)return{ok:!1,reasons:[`There are no enemy units in ${s}.`]};const d=xn(F(a),F(s)),u=d>0?Rn(t,a,s):null;if(u&&!u.clear)return{ok:!1,reasons:[u.reason??"No line of sight."]};const h=r.filter($=>["squad","half","crew"].includes(L($))),p=r.filter($=>L($)==="weapon").sort(($,E)=>te(E).fp-te($).fp);if(p.length>h.length)return{ok:!1,reasons:["Each support weapon needs its own squad, half-squad or crew to fire it."]};const k=[...h].sort(($,E)=>(L($)==="squad"?0:1)-(L(E)==="squad"?0:1)),m=new Map;p.forEach(($,E)=>m.set($.id,k[E]));const b=new Set([...m.values()].map($=>$.id)),y=[],w=($,E,V,ee)=>{let ae=$;return d>E*R.longRangeMultiple?(ee.push(`out of range (${E}, long ${E*R.longRangeMultiple})`),0):(d>E&&(ae*=R.longRangeFP,ee.push("long range ×½")),d===1?(ae*=R.pbfFP,ee.push("point blank ×2")):d===0&&(ae*=R.tpbfFP,ee.push("same hex ×3")),i==="advancing"&&(ae*=R.advancingFireFP,ee.push("advancing fire ×½")),V&&(ae*=R.pinnedFP,ee.push("pinned ×½")),ae)};for(const $ of h){const E=te($),V=[],ee=L($)==="squad"?R.squadKeepsInherentWithSW:R.halfSquadKeepsInherentWithSW;let ae=0;b.has($.id)&&!ee?V.push("firing its support weapon instead"):ae=w(E.fp,E.range,!!$.pin,V),y.push({id:$.id,name:$.label??me($),fp:ae,notes:V})}for(const $ of p){const E=te($),V=m.get($.id),ee=[`crewed by ${V.label??me(V)}`];y.push({id:$.id,name:$.label??me($),fp:w(E.fp,E.range,!!V.pin,ee),notes:ee})}const I=y.reduce(($,E)=>$+E.fp,0);if(I<1)return{ok:!1,reasons:[`Too little firepower: ${ct(I)} FP. ${y.some($=>$.notes.some(E=>E.startsWith("out of range")))?`${s} is out of range.`:""}`.trim()]};const P=[],C=o.cells.get(s);let O=R.tem[C.terrain],B=Oe[C.terrain];if(u!=null&&u.approach){const $=o.hexsides.find(E=>E.a===s&&E.b===u.approach||E.b===s&&E.a===u.approach);$&&R.hexsideTem[$.kind]>O&&(O=R.hexsideTem[$.kind],B=`${$.kind} ${u.approach}/${s}`)}O&&P.push({label:`TEM: ${B}`,value:O});for(const $ of(u==null?void 0:u.hindrances)??[])P.push({label:`Hindrance: ${$.label}`,value:$.value});const A=r.filter($=>L($)==="leader"&&!$.pin).sort(($,E)=>te($).leaderDRM-te(E).leaderDRM)[0]??null;A&&te(A).leaderDRM&&P.push({label:`Leader ${A.label??me(A)} directing`,value:te(A).leaderDRM}),r.some($=>$.cx)&&P.push({label:"Firer CX",value:R.cxDRM});const W=P.reduce(($,E)=>$+E.value,0),he=h.some($=>R.inexperienced.includes($.label??me($)));return{ok:!0,kind:i,from:a,to:s,range:d,ids:e,lines:y,totalFP:I,column:_s(I),drms:P,drm:W,leader:(A==null?void 0:A.id)??null,inexperienced:he,los:u,targets:l.map($=>$.id),weapons:p.map($=>({id:$.id,b:te($).b??12}))}}function ct(t){return Number.isInteger(t)?String(t):t.toFixed(2).replace(/0+$/,"").replace(/\.$/,"")}function Gs(t,e){const s=R.ift[t];return s?s[Math.max(0,e)]??"-":"-"}function Us(t){return R.ift[t]??[]}function Dn(t,e){const s=R.columns.indexOf(t);return R.columns[Math.max(0,s-e)]}function Ys(t,e,s){const n=[],[i,o]=s.dr(),r=i+o;let a=e.column,l="";if(i===o&&!e.leader){const p=e.inexperienced?R.coweringShift*2:R.coweringShift,k=Dn(a,p);k!==a&&(l=` · cowered ${a}→${k}`),a=k}const d=r+e.drm,u=Gs(a,d);n.push(`${On[e.kind]} ${e.from} → ${e.to}: ${ct(e.totalFP)} FP on the ${a} column, DR ${i}+${o}${e.drm?` ${e.drm>0?"+":""}${e.drm}`:""} = ${d}${l} → ${u==="-"?"no effect":u}`);const h=e.kind==="prep"?"prep":e.kind==="defensive"?"final":"adv";for(const p of e.ids){const k=t.counters.get(p);k&&(k.fired=h)}for(const p of e.weapons){const k=t.counters.get(p.id);k&&r>=p.b&&(k.face=1,n.push(`${k.label??me(k)} malfunctions (original DR ${r} ≥ B${p.b})`))}if(u!=="-"){const p=e.targets.map(k=>t.counters.get(k)).filter(k=>!!k&&k.at===e.to);n.push(...js(t,p,u,s))}return n}const Vs=new Set(["add","move","mv","flip","rot","del","top","label"]),qn=new Set(["fire","rally","repair","cc","roll"]),Zs=48,Xs=["both","attacker","attacker","defender","attacker","both","attacker","both"];function Ve(t){if(t.over)return[];const e=t.active==="a"?"b":"a",s=t.turn===0?"both":Xs[t.phase];return s==="both"?[t.active,e]:s==="attacker"?[t.active]:[e]}function Ze(t){return Fe.get(t.k)??{k:t.k,nat:"mk",type:"marker",label:t.k}}function z(t){const e=Ze(t),s=t.label??e.label;return t.face===1&&e.type!=="marker"&&e.type!=="fort"?`${s} (${e.back??"broken"})`:s}function de(t,e){return e==="n"?"mk":t.sides[e].nat}function Q(t,e){return e==="n"?"Observer":t.sides[e].name}function G(t){return t==="pool-a"||t==="pool-b"?"off-board":t==="elim-a"||t==="elim-b"?"eliminated":t}function Js(t){const e={scenario:t,turn:0,phase:0,active:t.first,counters:new Map,stacks:new Map,lines:[],lastMove:null,lastRoll:null,move:Sn(),ready:new Set,used:new Set,over:!1};let s=0;for(const n of["a","b"])for(const i of t.sides[n].units)for(let o=0;o<(i.count??1);o+=1){const r=i.k.startsWith("mk-")?"n":n,a=i.at==="pool"?`pool-${n}`:i.at;we(e,{id:`s${s}`,k:i.k,side:r,face:0,rot:0,at:a}),s+=1}return e}const Qs=/^(?:[A-Z]{1,2}\d{1,2}|pool-[ab]|elim-[ab])$/;function dn(t,e){return Qs.test(e)?e.includes("-")?!0:mt(t.scenario).cells.has(e):!1}function mt(t){return se[t.map]}function _e(t,e){const s=e.map(n=>t.counters.get(n)).filter(Boolean).map(n=>z(n));return s.length<=3?s.join(", "):`${s.slice(0,3).join(", ")} +${s.length-3}`}function eo(t){const e=t.turn>0&&t.phase===fe.length-1,s=so(t);if(e)for(const i of t.counters.values())i.pin=void 0,i.fired=void 0;if(t.move=Sn(),t.used=new Set,t.ready=new Set,s.turn>t.scenario.turns)return t.over=!0,`Game over after turn ${t.turn}. Check the victory conditions in the Table tab.`;const n=s.side!==t.active||s.turn!==t.turn;if(t.turn=s.turn,t.phase=s.phase,t.active=s.side,n)for(const i of t.counters.values())i.side===s.side&&(i.cx=void 0);return`Turn ${t.turn} · ${le[de(t.scenario,t.active)].short} · ${ce(t)}`}function nt(t,e,s){return t.turn>0&&!e.ovr&&s!=="n"}function to(t,e,s){var r;const n=e.a,i={i:e.i,side:e.side,at:e.at,by:e.by,undoable:!1,undone:s},o=e.side==="n"?"Observer":le[de(t.scenario,e.side)].short;if(s)return{...i,kind:"move",text:`${o} ${no(t,n)}`};switch(n.t){case"add":{if(t.counters.has(n.id)||!Fe.has(n.k)||!dn(t,n.to))return null;if(nt(t,n,n.side))return{...i,kind:"refused",text:`${o} adding a counter refused: after setup this needs an override.`};we(t,{id:n.id,k:n.k,side:n.side,face:0,rot:0,at:n.to});const a=`${o} placed ${_e(t,[n.id])} in ${G(n.to)}`;return n.ovr?{...i,kind:"override",undoable:!0,text:`${a} — OVERRIDE: ${n.ovr}`}:{...i,kind:"move",undoable:!0,text:a}}case"move":{if(!dn(t,n.to))return null;const a=n.ids.map(u=>t.counters.get(u)).filter(u=>!!u);if(!a.length)return null;if(nt(t,n,a.some(u=>u.side!=="n")?"a":"n"))return{...i,kind:"refused",text:`${o} move to ${G(n.to)} refused: moves must follow the rules or be overridden.`};const l=a[0].at;for(const u of a)xe(t,u),u.at=n.to,we(t,u);t.lastMove={ids:a.map(u=>u.id),from:l,to:n.to,path:[n.to],i:e.i};const d=`${o} moved ${_e(t,n.ids)} ${G(l)} → ${G(n.to)}`;return n.ovr?{...i,kind:"override",undoable:!0,text:`${d} — OVERRIDE: ${n.ovr}`}:{...i,kind:"move",undoable:!0,text:d}}case"mv":{const a=Ht(t,n.ids,n.path,!!n.dt,e.side),l=_e(t,n.ids)||"counters";if(!a.ok)return{...i,kind:"refused",text:`${o} move of ${l} refused: ${a.reasons[0]}`};const d=n.ids.map(y=>t.counters.get(y)),u=d[0].at,h=n.path[n.path.length-1];for(const y of d)xe(t,y),y.at=h,we(t,y);const p=t.move;if(a.kind!=="setup"){for(const y of p.mover)n.ids.includes(y)||p.done.add(y);p.mover=[...n.ids];for(const y of d)L(y)!=="marker"&&(p.spent.set(y.id,(p.spent.get(y.id)??0)+a.cost),a.offRoad&&p.offRoad.add(y.id),n.dt&&(p.doubled.add(y.id),y.cx=!0),(a.kind==="advance"||a.minimumMove)&&p.done.add(y.id))}t.lastMove={ids:n.ids,from:u,to:h,path:n.path,i:e.i};const k=n.path.length>1?`${G(u)} → ${n.path.join(" → ")}`:`${G(u)} → ${h}`,m=a.kind==="setup"?"":a.kind==="advance"?" (advance)":` (${ge(a.cost)} MF${a.minimumMove?", minimum move":`, ${ge(a.left)} left`}${n.dt?", Double Time":""})`,b=a.kind==="rout"?"routed":a.kind==="advance"?"advanced":a.kind==="setup"?"set up":"moved";return{...i,kind:"move",undoable:!0,text:`${o} ${b} ${l} ${k}${m}`}}case"flip":{const a=t.counters.get(n.id);if(!a)return null;if(nt(t,n,a.side))return{...i,kind:"refused",text:`${o} flip refused: units break and rally through the rules, or with an override.`};a.face=a.face===1?0:1;const l=`${o} flipped ${z(a)} in ${G(a.at)}`;return n.ovr?{...i,kind:"override",undoable:!0,text:`${l} — OVERRIDE: ${n.ovr}`}:{...i,kind:"move",undoable:!0,text:l}}case"rot":{const a=t.counters.get(n.id);return a?(a.rot=(n.r%6+6)%6,{...i,kind:"move",undoable:!0,text:`${o} turned ${z(a)} to face ${a.rot+1}`}):null}case"del":{const a=t.counters.get(n.id);if(!a)return null;if(nt(t,n,a.side))return{...i,kind:"refused",text:`${o} removing ${z(a)} refused: after setup this needs an override.`};xe(t,a),t.counters.delete(n.id);const l=`${o} removed ${z(a)} from ${G(a.at)}`;return n.ovr?{...i,kind:"override",undoable:!0,text:`${l} — OVERRIDE: ${n.ovr}`}:{...i,kind:"move",undoable:!0,text:l}}case"top":{const a=t.counters.get(n.id);return a?(xe(t,a),we(t,a),{...i,kind:"move",undoable:!0,text:`${o} restacked ${z(a)} on top in ${G(a.at)}`}):null}case"label":{const a=t.counters.get(n.id);if(!a)return null;const l=z(a);return a.label=n.text.trim()||void 0,{...i,kind:"move",undoable:!0,text:`${o} relabelled ${l} as ${z(a)}`}}case"phase":return{...i,kind:"refused",text:`${o} phase jump refused: phases advance with End Phase.`};case"end":{if(e.side==="n")return null;const a=Ve(t),l=t.turn===0?"Setup":`${ce(t)} Phase`;if(!a.includes(e.side))return{...i,kind:"refused",text:`${o} can't end the ${l}: it isn't theirs.`};if(t.ready.has(e.side))return null;t.ready.add(e.side);const d=a.filter(u=>!t.ready.has(u));return d.length?{...i,kind:"info",text:`${o} finished the ${l}; waiting for ${Q(t.scenario,d[0])}`}:{...i,kind:"phase",text:eo(t)}}case"fire":case"rally":case"repair":case"cc":{const a=n.t==="fire"?pt(t,n.ids,n.to,e.side):n.t==="rally"?Nn(t,n.id,e.side):n.t==="repair"?In(t,n.id,e.side):Fn(t,n.to,e.side);if(!a.ok)return{...i,kind:"refused",text:`${o} ${n.t==="cc"?"close combat":n.t} refused: ${a.reasons[0]}`};if(!((r=e.d)!=null&&r.length))return{...i,kind:"fire",text:`${o} ${n.t==="fire"?"fires":n.t==="cc"?"closes in":`${n.t}s`}… rolling`};const l=new Ps(e.d),d=n.t==="fire"?Ys(t,a,l):n.t==="rally"?Ks(t,a,l):n.t==="repair"?zs(t,n.id,l):Bs(t,a,l);return t.lastRoll={dice:l.used.slice(0,2),why:d[0]??n.t,side:e.side,i:e.i},{...i,kind:"fire",dice:l.used.slice(0,n.t==="repair"?1:2),text:`${o} ${d[0]??""}`,detail:d.slice(1)}}case"roll":{const a=e.d??[];if(!a.length)return null;t.lastRoll={dice:a,why:n.why,side:e.side,i:e.i};const l=a.reduce((u,h)=>u+h,0),d=n.why?` for ${n.why}`:"";return{...i,kind:"roll",dice:a,text:`${o} rolled ${a.length===2?`${a[0]}+${a[1]} = ${l}`:l}${d}`}}case"los":return{...i,kind:"los",los:{from:n.from,to:n.to},text:`${o} checked LOS ${n.from} → ${n.to}`};case"chat":return{...i,kind:"chat",text:n.text};case"join":return{...i,kind:"info",text:`${Q(t.scenario,n.side)} player took their seat`};case"undo":return null}}function no(t,e){var s;switch(e.t){case"add":return`placed ${((s=Fe.get(e.k))==null?void 0:s.label)??e.k} (undone)`;case"move":return`moved ${_e(t,e.ids)||"counters"} → ${G(e.to)} (undone)`;case"mv":return`moved ${_e(t,e.ids)||"counters"} → ${e.path[e.path.length-1]} (undone)`;default:return`${e.t} (undone)`}}function un(t,e){const s=Ae.get(t)??Ae.get("open-table"),n=Js(s),i=new Set;for(const o of e){if(o.a.t!=="undo")continue;const r=e[o.a.ref];!r||r.i>=o.i||r.by!==o.by||!Vs.has(r.a.t)||e.slice(r.i+1,o.i).some(l=>l.a.t==="end"||qn.has(l.a.t))||i.add(r.i)}for(const o of e){const r=to(n,o,i.has(o.i));r&&n.lines.push(r)}return n}function jn(t,e){for(let s=t.lines.length-1;s>=0;s-=1){const n=t.lines[s];if(!n.undone&&!["chat","los","info","refused"].includes(n.kind))return n.by===e&&n.undoable?n:null}return null}function so(t){if(t.turn===0)return{turn:1,phase:0,side:t.scenario.first};const e=t.active==="a"?"b":"a";return t.phase<fe.length-1?{turn:t.turn,phase:t.phase+1,side:t.active}:{turn:e===t.scenario.first?t.turn+1:t.turn,phase:0,side:e}}function oo(){const t=new Uint8Array(6);return crypto.getRandomValues(t),`c${Array.from(t,e=>e.toString(36).padStart(2,"0")).join("").slice(0,10)}`}function fn(t){if(!t||typeof t!="object")return!1;const e=t,s=(o,r=64)=>typeof o=="string"&&o.length>0&&o.length<=r,n=(o,r,a)=>Number.isInteger(o)&&o>=r&&o<=a,i=o=>o===void 0||typeof o=="string"&&o.length<=120;switch(e.t){case"add":return s(e.id,24)&&s(e.k,24)&&(e.side==="a"||e.side==="b"||e.side==="n")&&s(e.to,8)&&i(e.ovr);case"move":return Array.isArray(e.ids)&&e.ids.length>0&&e.ids.length<=24&&e.ids.every(o=>s(o,24))&&s(e.to,8)&&(e.ovr===void 0||typeof e.ovr=="string"&&e.ovr.length<=120);case"mv":return Array.isArray(e.ids)&&e.ids.length>0&&e.ids.length<=24&&e.ids.every(o=>s(o,24))&&Array.isArray(e.path)&&e.path.length>0&&e.path.length<=24&&e.path.every(o=>s(o,8))&&(e.dt===void 0||typeof e.dt=="boolean");case"flip":case"del":return s(e.id,24)&&i(e.ovr);case"top":case"rally":case"repair":return s(e.id,24);case"end":return!0;case"fire":return Array.isArray(e.ids)&&e.ids.length>0&&e.ids.length<=24&&e.ids.every(o=>s(o,24))&&s(e.to,8);case"cc":return s(e.to,8);case"rot":return s(e.id,24)&&n(e.r,0,5);case"label":return s(e.id,24)&&typeof e.text=="string"&&e.text.length<=24;case"phase":return n(e.turn,0,99)&&n(e.phase,0,fe.length-1)&&(e.side==="a"||e.side==="b");case"roll":return(e.n===1||e.n===2)&&typeof e.why=="string"&&e.why.length<=60;case"los":return s(e.from,8)&&s(e.to,8);case"chat":return s(e.text,280);case"undo":return n(e.ref,0,1e5);case"join":return e.side==="a"||e.side==="b";default:return!1}}const ne=34,oe={ge:{face:"#97a3a6",edge:"#55605f",ink:"#101517"},us:{face:"#a19a5a",edge:"#5c5829",ink:"#14140a"},ru:{face:"#c4a66f",edge:"#77613a",ink:"#1e150a"},mk:{face:"#e9e1c8",edge:"#8c8268",ink:"#1d1a12"}},io={fire:{face:"#b4462f",edge:"#6e2516",ink:"#fff3df"},move:{face:"#dcb84a",edge:"#86691c",ink:"#211a06"},state:{face:"#e5dcc3",edge:"#8a7f62",ink:"#2a2214"},terrain:{face:"#5c5b52",edge:"#2f2e29",ink:"#f2ead3"},info:{face:"#f3ecd6",edge:"#8c8268",ink:"#1d1a12"}};function g(t){return t.replace(/[&<>"']/g,e=>`&#${e.charCodeAt(0)};`)}function ro(t){return t.nat==="mk"?io[t.tone??"info"]:oe[t.nat]}function ao(t,e){const s=`<rect x="-8" y="-13" width="16" height="9.5" fill="none" stroke="${e}" stroke-width="1.1"/>`;switch(t.type){case"squad":return`${s}<path d="M-8 -13 L8 -3.5 M8 -13 L-8 -3.5" stroke="${e}" stroke-width="1"/>`;case"half":return`${s}<path d="M-8 -3.5 L8 -13" stroke="${e}" stroke-width="1"/>`;case"crew":return`${s}<path d="M-8 -8.25 H8" stroke="${e}" stroke-width="1"/><circle cx="0" cy="-8.25" r="2" fill="${e}"/>`;case"leader":return`<path d="M0 -14.5 L2.1 -10 L7 -9.6 L3.3 -6.5 L4.4 -1.8 L0 -4.3 L-4.4 -1.8 L-3.3 -6.5 L-7 -9.6 L-2.1 -10 Z" fill="${e}"/>`;case"weapon":return`<path d="M-9 -9 H7 M7 -10.5 V-7.5 M-2 -9 L-5 -3.5 M-2 -9 L1 -3.5" stroke="${e}" stroke-width="1.4" fill="none" stroke-linecap="round"/>`;case"gun":return`<circle cx="-4" cy="-6" r="3" fill="none" stroke="${e}" stroke-width="1.2"/><circle cx="4" cy="-6" r="3" fill="none" stroke="${e}" stroke-width="1.2"/><path d="M-6 -11 L10 -14" stroke="${e}" stroke-width="1.6" stroke-linecap="round"/>`;case"vehicle":return`<rect x="-9" y="-9" width="18" height="6" rx="3" fill="none" stroke="${e}" stroke-width="1.2"/><rect x="-4" y="-13" width="8" height="4" fill="${e}"/><path d="M4 -11 H11" stroke="${e}" stroke-width="1.4"/>`;default:return""}}function ot(t,e="",s={}){const n=Ze(t),i=ro(n),o=ne/2,r=g(t.label??n.label),a=n.type==="marker"||n.type==="fort",l=t.face===1,d=[`<rect x="${-o+1.6}" y="${-o+1.6}" width="${ne}" height="${ne}" rx="3" fill="#000" opacity="0.35"/>`,`<rect x="${-o}" y="${-o}" width="${ne}" height="${ne}" rx="3" fill="${i.face}" stroke="${i.edge}" stroke-width="1.4"/>`,`<rect x="${-o+2}" y="${-o+2}" width="${ne-4}" height="${ne-4}" rx="2" fill="none" stroke="#fff" stroke-opacity="0.28" stroke-width="0.8"/>`];if(a){const h=l&&n.back?g(n.back):r,p=h.length>7?6.4:h.length>4?7.6:h.length>2?10:14;d.push(`<text y="${p/3}" text-anchor="middle" font-size="${p}" font-weight="800" fill="${i.ink}" class="ctr-text">${h}</text>`)}else{d.push(ao(n,i.ink)),n.tag&&d.push(`<text y="-0.2" text-anchor="middle" font-size="5" letter-spacing="0.3" fill="${i.ink}" opacity="0.8" class="ctr-text">${g(n.tag.toUpperCase())}</text>`);const h=r.length>6?7:9.5;if(d.push(`<text y="11.5" text-anchor="middle" font-size="${h}" font-weight="800" fill="${i.ink}" class="ctr-text" opacity="${l?.55:1}">${r}</text>`),l&&d.push(`<path d="M${-o} ${o-9} L${o-9} ${-o} H${o} V${-o+6} L${-o+6} ${o} H${-o} Z" fill="#7b1f14" opacity="0.82"/>`,`<text transform="rotate(-45)" y="2" text-anchor="middle" font-size="5.6" font-weight="800" letter-spacing="0.6" fill="#ffe9d6" class="ctr-text">${g(n.back??"BROKEN")}</text>`),(n.type==="vehicle"||n.type==="gun")&&t.rot>=0){const p=-90+t.rot*60;d.push(`<g transform="rotate(${p+90})"><path d="M0 ${-o-5} L4 ${-o-.5} H-4 Z" fill="${i.edge}" stroke="#fff" stroke-width="0.6"/></g>`)}}s.moved&&d.push(`<circle cx="${-o+5}" cy="${-o+5}" r="3.6" fill="#2b2b22" stroke="#f0e6c8" stroke-width="0.7"/><path d="M${-o+3.2} ${-o+5} l1.3 1.4 l2.4 -2.7" stroke="#f0e6c8" stroke-width="0.9" fill="none"/>`),(s.cx??t.cx)&&d.push(`<rect x="${-o+3}" y="${o-8.5}" width="13" height="7" rx="1.5" fill="#dcb84a" stroke="#6b5414" stroke-width="0.6"/><text x="${-o+9.5}" y="${o-3}" text-anchor="middle" font-size="5.4" font-weight="800" fill="#211a06" class="ctr-text">CX</text>`);const u="fired"in s?s.fired:t.fired;if(u){const h=u==="prep"?"PF":u==="final"?"FF":"AF";d.push(`<rect x="${o-14}" y="${-o-3}" width="13" height="7.5" rx="1.5" fill="#b4462f" stroke="#5e1d10" stroke-width="0.6"/><text x="${o-7.5}" y="${-o+2.6}" text-anchor="middle" font-size="5.4" font-weight="800" fill="#fff3df" class="ctr-text">${h}</text>`)}return(s.pin??t.pin)&&d.push(`<rect x="${o-16}" y="${o-8.5}" width="15" height="7" rx="1.5" fill="#e07b2a" stroke="#6e3608" stroke-width="0.6"/><text x="${o-8.5}" y="${o-3}" text-anchor="middle" font-size="5.2" font-weight="800" fill="#1d1004" class="ctr-text">PIN</text>`),`<g class="ctr" data-id="${g(t.id)}" ${e}>${d.join("")}</g>`}function _t(t,e=40){return`<svg class="ctr-icon" viewBox="-21 -21 42 42" width="${e}" height="${e}" aria-hidden="true">${ot(t)}</svg>`}const pe="http://www.w3.org/2000/svg";function lo(t){let e=2166136261;for(let s=0;s<t.length;s+=1)e=Math.imul(e^t.charCodeAt(s),16777619);return()=>(e^=e<<13,e^=e>>>17,e^=e<<5,(e>>>0)%1e4/1e4)}const hn=["#e4dcc0","#d5c296","#c3a874","#b08f5c"];function X(t){return t.map(([e,s])=>`${e.toFixed(1)},${s.toFixed(1)}`).join(" ")}function co(t){const{x:e,y:s}=N(t.hex),n=lo(t.id),i=[];switch(t.terrain){case"woods":{i.push(`<polygon points="${X(Z(t.hex,T*.86))}" fill="#5d7843" opacity="0.55"/>`);for(let o=0;o<9;o+=1){const r=n()*Math.PI*2,a=n()*T*.55,l=7+n()*5;i.push(`<circle cx="${(e+Math.cos(r)*a).toFixed(1)}" cy="${(s+Math.sin(r)*a).toFixed(1)}" r="${l.toFixed(1)}" fill="#46612f" stroke="#2f4320" stroke-width="0.8"/>`)}break}case"brush":for(let o=0;o<26;o+=1){const r=n()*Math.PI*2,a=Math.sqrt(n())*T*.75;i.push(`<circle cx="${(e+Math.cos(r)*a).toFixed(1)}" cy="${(s+Math.sin(r)*a).toFixed(1)}" r="${(1.6+n()*1.8).toFixed(1)}" fill="#7f9152"/>`)}break;case"grain":i.push(`<polygon points="${X(Z(t.hex,T*.8))}" fill="#dcc376" opacity="0.85"/>`);for(let o=-4;o<=4;o+=1)i.push(`<line x1="${e-26}" y1="${s+o*6-6}" x2="${e+26}" y2="${s+o*6+6}" stroke="#b39a4b" stroke-width="0.8" opacity="0.7"/>`);break;case"orchard":for(let o=-1;o<=1;o+=1)for(let r=-1;r<=1;r+=1){const a=e+r*15+(o&1?7:0),l=s+o*15;i.push(`<circle cx="${a}" cy="${l}" r="4.6" fill="#6f8c45" stroke="#45602a" stroke-width="0.8"/>`)}break;case"marsh":for(let o=0;o<9;o+=1){const r=e+(n()-.5)*48,a=s+(n()-.5)*44;i.push(`<path d="M${r-5} ${a} H${r+5} M${r-2.5} ${a} L${r-4} ${a-4} M${r} ${a} V${a-5} M${r+2.5} ${a} L${r+4} ${a-4}" stroke="#4f7a72" stroke-width="0.9" fill="none"/>`)}break;case"water":i.push(`<polygon points="${X(Z(t.hex,T+.5))}" fill="#86a9b6"/>`);for(let o=-1;o<=1;o+=1)i.push(`<path d="M${e-20} ${s+o*12} q5 -4 10 0 t10 0 t10 0 t10 0" stroke="#e8f1ef" stroke-width="1" fill="none" opacity="0.7"/>`);break;case"wood-bldg":case"stone-bldg":{const o=t.terrain==="stone-bldg",r=40+n()*10,a=30+n()*8,l=(n()-.5)*16,d=o?"#8f8b83":"#a5724a",u=o?"#3a3834":"#56351d";i.push(`<g transform="translate(${e} ${s}) rotate(${l.toFixed(1)})"><rect x="${-r/2+2}" y="${-a/2+2}" width="${r}" height="${a}" fill="#000" opacity="0.22"/><rect x="${-r/2}" y="${-a/2}" width="${r}" height="${a}" fill="${d}" stroke="${u}" stroke-width="1.3"/>`+(o?`<path d="M${-r/2} ${-a/2} L${r/2} ${a/2} M${r/2} ${-a/2} L${-r/2} ${a/2}" stroke="${u}" stroke-width="0.7" opacity="0.6"/>`:`<path d="M${-r/2} 0 H${r/2}" stroke="${u}" stroke-width="1"/>`)+"</g>");break}}return i.join("")}function uo(t){const e=[],s=T*1.5*(t.cols-1)+T*2,n=Le*T*(t.rows+.5);e.push(`<rect x="-14" y="-14" width="${s+28}" height="${n+28}" rx="6" fill="#efe7cf" stroke="#2a2a20" stroke-width="2"/>`),e.push(`<rect x="-14" y="-14" width="${s+28}" height="${n+28}" rx="6" fill="none" stroke="#c9b98d" stroke-width="6" opacity="0.6"/>`);for(const i of t.cells.values())e.push(`<polygon points="${X(Z(i.hex,T+.4))}" fill="${hn[i.level]??hn[3]}"/>`);for(const i of t.cells.values()){const o=Z(i.hex);(i.hex.c&1?[[1,1],[0,1],[-1,1],[-1,0],[0,-1],[1,0]]:[[1,0],[0,1],[-1,0],[-1,-1],[0,-1],[1,-1]]).forEach(([l,d],u)=>{const h=t.cells.get(J({c:i.hex.c+l,r:i.hex.r+d}));if(h&&h.level<i.level){const[p,k]=o[u],[m,b]=o[(u+1)%6];e.push(`<line x1="${p.toFixed(1)}" y1="${k.toFixed(1)}" x2="${m.toFixed(1)}" y2="${b.toFixed(1)}" stroke="#7c5f33" stroke-width="2.2" stroke-linecap="round"/>`)}})}for(const i of t.cells.values())e.push(co(i));for(const i of t.roads){const o=i.map(l=>N(F(l)));let r=`M${o[0].x.toFixed(1)} ${o[0].y.toFixed(1)}`;for(let l=1;l<o.length;l+=1){const d=(o[l-1].x+o[l].x)/2,u=(o[l-1].y+o[l].y)/2;r+=` Q${o[l-1].x.toFixed(1)} ${o[l-1].y.toFixed(1)} ${d.toFixed(1)} ${u.toFixed(1)}`}const a=o[o.length-1];r+=` L${a.x.toFixed(1)} ${a.y.toFixed(1)}`,e.push(`<path d="${r}" stroke="#8f8466" stroke-width="12" fill="none" stroke-linejoin="round"/>`),e.push(`<path d="${r}" stroke="#ddd3b4" stroke-width="9" fill="none" stroke-linejoin="round"/>`)}for(const i of t.hexsides){const o=F(i.a),r=F(i.b);if(!o||!r)continue;const[a,l,d,u]=$s(o,r);e.push(i.kind==="hedge"?`<line x1="${a}" y1="${l}" x2="${d}" y2="${u}" stroke="#3a5527" stroke-width="5.5" stroke-linecap="round"/>`:`<line x1="${a}" y1="${l}" x2="${d}" y2="${u}" stroke="#5e5a54" stroke-width="4.5" stroke-linecap="square" stroke-dasharray="3 1.4"/>`)}for(const i of t.cells.values())e.push(`<polygon points="${X(Z(i.hex))}" fill="none" stroke="#5a4628" stroke-opacity="0.42" stroke-width="0.9"/>`);return e.join("")}function fo(t){const e=[];for(const s of t.cells.values()){const{x:n,y:i}=N(s.hex);e.push(`<text x="${n}" y="${i-T*.66}" text-anchor="middle" font-size="7.5" class="coord">${s.id}</text>`),s.level>0&&e.push(`<text x="${n}" y="${i+T*.78}" text-anchor="middle" font-size="6.5" class="coord">L${s.level}</text>`)}return e.join("")}function ho(t,e){const s=e.map(i=>t.counters.get(i)).filter(i=>!!i&&i.side!=="n"),n=[];return s.some(i=>i.cx)&&n.push("mk-cx"),s.some(i=>i.pin)&&n.push("mk-pin"),s.some(i=>i.fired==="final")&&n.push("mk-final"),s.some(i=>i.fired==="prep")&&n.push("mk-prep"),n}class po{constructor(e,s){M(this,"svg");M(this,"view");M(this,"coords");M(this,"counters");M(this,"overlay");M(this,"ghost");M(this,"fx");M(this,"map",null);M(this,"state",null);M(this,"scale",1);M(this,"tx",0);M(this,"ty",0);M(this,"fitted",!1);M(this,"pointers",new Map);M(this,"gesture",{kind:"idle"});M(this,"overlayState",{selected:null,targeting:!1,reach:null,targets:null,fireLine:null,los:null,presence:[],showCoords:!0});M(this,"hoverHex",null);M(this,"dragIds",[]);this.events=s,this.svg=document.createElementNS(pe,"svg"),this.svg.setAttribute("class","board"),this.svg.setAttribute("role","application"),this.svg.setAttribute("aria-label","Hex map. Tap a hex to inspect its stack."),this.view=document.createElementNS(pe,"g"),this.coords=document.createElementNS(pe,"g"),this.counters=document.createElementNS(pe,"g"),this.overlay=document.createElementNS(pe,"g"),this.ghost=document.createElementNS(pe,"g"),this.fx=document.createElementNS(pe,"g"),this.fx.setAttribute("class","fx"),this.ghost.setAttribute("class","ghost"),this.svg.append(this.view),e.append(this.svg),this.bindInput(),new ResizeObserver(()=>{this.fitted?this.applyTransform():this.fit()}).observe(e)}setMap(e){var n;if(((n=this.map)==null?void 0:n.id)===e.id)return;this.map=e;const s=document.createElementNS(pe,"g");s.innerHTML=uo(e),this.coords.innerHTML=fo(e),this.view.replaceChildren(s,this.coords,this.overlay,this.counters,this.fx,this.ghost),this.fitted=!1,this.fit()}hexCenter(e){var n;const s=F(e);return s&&((n=this.map)!=null&&n.cells.has(e))?N(s):null}shake(){this.svg.animate([{transform:"translate(0,0)"},{transform:"translate(-4px,2px)"},{transform:"translate(3px,-3px)"},{transform:"translate(-2px,1px)"},{transform:"translate(0,0)"}],{duration:320,easing:"ease-out"})}fit(){if(!this.map)return;const e=this.svg.getBoundingClientRect();if(e.width<10||e.height<10)return;const s=T*1.5*(this.map.cols-1)+T*2+28,n=Le*T*(this.map.rows+.5)+28;this.scale=Math.min(e.width/s,e.height/n),this.tx=(e.width-s*this.scale)/2+14*this.scale,this.ty=(e.height-n*this.scale)/2+14*this.scale,this.fitted=!0,this.applyTransform()}fitPlayable(e=.62){if(this.fit(),this.scale>=e||!this.map)return;const s=this.svg.getBoundingClientRect(),n=T*1.5*(this.map.cols-1)+T*2,i=Le*T*(this.map.rows+.5);this.scale=e,this.tx=s.width/2-n/2*e,this.ty=s.height/2-i/2*e,this.applyTransform()}zoomBy(e,s,n){const i=this.svg.getBoundingClientRect(),o=s??i.width/2,r=n??i.height/2,a=Math.max(.2,Math.min(4,this.scale*e));this.tx=o-(o-this.tx)*a/this.scale,this.ty=r-(r-this.ty)*a/this.scale,this.scale=a,this.applyTransform()}reveal(e){const s=F(e);if(!s)return;const n=this.svg.getBoundingClientRect(),{x:i,y:o}=N(s),r=i*this.scale+this.tx,a=o*this.scale+this.ty,l=50;(r<l||r>n.width-l||a<l||a>n.height-l)&&(this.tx=n.width/2-i*this.scale,this.ty=n.height/2-o*this.scale,this.applyTransform())}applyTransform(){this.view.setAttribute("transform",`translate(${this.tx.toFixed(2)} ${this.ty.toFixed(2)}) scale(${this.scale.toFixed(4)})`)}render(e,s){var i;this.state=e,this.overlayState=s,this.coords.style.display=s.showCoords?"":"none";const n=[];for(const[o,r]of e.stacks){const a=F(o);if(!a||!((i=this.map)!=null&&i.cells.has(o)))continue;const{x:l,y:d}=N(a);n.push(`<g data-hex="${o}" data-count="${r.length}">`);const u=r.slice(-5),h=ho(e,r),p=3.2,m=(u.length+h.length-1)*p/2;if(u.forEach((b,y)=>{const w=e.counters.get(b);if(!w)return;const I=l-m+y*p,P=d+m-y*p,C={cx:!1,moved:e.move.done.has(b)||(e.move.spent.get(b)??0)>0,fired:void 0,pin:!1};n.push(`<g transform="translate(${I.toFixed(1)} ${P.toFixed(1)})">${ot(w,"",C)}</g>`)}),h.forEach((b,y)=>{const w=u.length+y,I=l-m+w*p,P=d+m-w*p,C={id:`${o}-${b}`,k:b,face:0,rot:0};n.push(`<g class="status-marker" data-marker="${b}" transform="translate(${I.toFixed(1)} ${P.toFixed(1)})">${ot(C)}</g>`)}),r.length>1){const b=l+ne/2+m-1,y=d-ne/2-m+1;n.push(`<g class="badge"><circle cx="${b}" cy="${y}" r="6.5" fill="#1f241d" stroke="#f0e6c8" stroke-width="1"/><text x="${b}" y="${y+2.6}" text-anchor="middle" font-size="7.5" font-weight="800" fill="#f0e6c8">${r.length}</text></g>`)}n.push("</g>")}this.counters.innerHTML=n.join(""),this.renderOverlay()}renderOverlay(){var o,r,a,l,d,u,h;const e=this.state,s=this.overlayState;if(!e)return;const n=[],i=e.lastMove;if(i&&((o=this.map)!=null&&o.cells.has(i.to))){const p=((r=e.counters.get(i.ids[0]))==null?void 0:r.side)??"n",k=oe[de(e.scenario,p)].edge,m=[i.from,...i.path].filter(b=>this.map.cells.has(b));if(m.length>1){const b=m.map(y=>N(F(y)));n.push(`<polyline points="${b.map(y=>`${y.x.toFixed(1)},${y.y.toFixed(1)}`).join(" ")}" fill="none" stroke="${k}" stroke-width="3" stroke-dasharray="6 4" stroke-linejoin="round" opacity="0.85" marker-end="url(#arrow)"/>`,`<circle cx="${b[0].x}" cy="${b[0].y}" r="5" fill="none" stroke="${k}" stroke-width="2" opacity="0.85"/>`)}}if(s.reach)for(const[p,k]of s.reach){const m=F(p);if(!m)continue;const{x:b,y}=N(m);n.push(`<polygon points="${X(Z(m,T-2.5))}" fill="#f7d774" fill-opacity="0.2" stroke="#c99a22" stroke-width="1.6"/>`,`<g class="reach-cost"><rect x="${b-11}" y="${y+T*.52}" width="22" height="11" rx="5.5" fill="#2a2412" opacity="0.85"/><text x="${b}" y="${y+T*.52+8.4}" text-anchor="middle" font-size="8" font-weight="800" fill="#ffe08a">${Number.isInteger(k)?k:k.toFixed(1)}</text></g>`)}if(s.targets)for(const[p,k]of s.targets){const m=F(p);if(!m)continue;const{x:b,y}=N(m),w=((a=s.fireLine)==null?void 0:a.to)===p;n.push(`<polygon points="${X(Z(m,T-2.5))}" fill="#d0452c" fill-opacity="${w?.28:.14}" stroke="#b3361f" stroke-width="${w?3:1.8}"/>`,`<g class="target-label"><rect x="${b-13}" y="${y+T*.5}" width="26" height="12" rx="6" fill="#4a120a" opacity="0.9"/><text x="${b}" y="${y+T*.5+9}" text-anchor="middle" font-size="8" font-weight="800" fill="#ffd9cf">${k}</text></g>`)}if(s.fireLine&&((l=this.map)!=null&&l.cells.has(s.fireLine.from))&&this.map.cells.has(s.fireLine.to)){const p=N(F(s.fireLine.from)),k=N(F(s.fireLine.to));n.push(`<line x1="${p.x}" y1="${p.y}" x2="${k.x}" y2="${k.y}" stroke="#1b1b14" stroke-width="4" opacity="0.5"/>`,`<line x1="${p.x}" y1="${p.y}" x2="${k.x}" y2="${k.y}" stroke="#ff7a5c" stroke-width="2" stroke-dasharray="7 4"/>`)}if(s.los){const p=F(s.los.from),k=s.los.to??this.hoverHex,m=k?F(k):null;if(p){const b=N(p);if(m&&((d=this.map)!=null&&d.cells.has(k))){for(const w of Mn(p,m)){const I=F(w.id);!I||!this.map.cells.has(w.id)||n.push(`<polygon points="${X(Z(I,T-3))}" fill="${w.edge?"none":"#f6d36b"}" fill-opacity="0.18" stroke="#e1b43c" stroke-width="2" ${w.edge?'stroke-dasharray="4 3"':""}/>`)}const y=N(m);n.push(`<line x1="${b.x}" y1="${b.y}" x2="${y.x}" y2="${y.y}" stroke="#1b1b14" stroke-width="3.5" opacity="0.6"/>`),n.push(`<line x1="${b.x}" y1="${b.y}" x2="${y.x}" y2="${y.y}" stroke="#ffe08a" stroke-width="1.6"/>`),n.push(`<circle cx="${y.x}" cy="${y.y}" r="4" fill="#ffe08a" stroke="#1b1b14"/>`)}n.push(`<circle cx="${b.x}" cy="${b.y}" r="5" fill="#ffe08a" stroke="#1b1b14" stroke-width="1.2"/>`)}}for(const p of s.presence){const k=p.hex?F(p.hex):null;if(!k)continue;const m=oe[de(e.scenario,p.side)].face;n.push(`<polygon class="presence" points="${X(Z(k,T-2))}" fill="none" stroke="${m}" stroke-width="3" stroke-dasharray="2 3"/>`)}if(s.selected&&((u=this.map)!=null&&u.cells.has(s.selected))){const p=F(s.selected);n.push(`<polygon points="${X(Z(p,T-1.5))}" fill="#fff6d6" fill-opacity="0.12" stroke="#f7c948" stroke-width="3.2"/>`),n.push(`<polygon points="${X(Z(p,T-1.5))}" fill="none" stroke="#1d1a12" stroke-width="0.9"/>`)}if(s.targeting&&this.hoverHex&&((h=this.map)!=null&&h.cells.has(this.hoverHex))){const p=F(this.hoverHex);n.push(`<polygon points="${X(Z(p,T-2))}" fill="#f7c948" fill-opacity="0.25" stroke="#f7c948" stroke-width="2" stroke-dasharray="5 3"/>`)}this.overlay.innerHTML='<defs><marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="5" markerHeight="5" orient="auto-start-reverse"><path d="M0 0 L10 5 L0 10 z" fill="#3a3a2e"/></marker></defs>'+n.join("")}local(e,s){const n=this.svg.getBoundingClientRect();return{x:e-n.left,y:s-n.top}}hexAt(e,s){var r;const n=this.local(e,s),i=Pt((n.x-this.tx)/this.scale,(n.y-this.ty)/this.scale),o=J(i);return(r=this.map)!=null&&r.cells.has(o)?o:null}setHover(e){e!==this.hoverHex&&(this.hoverHex=e,this.events.hover(e),(this.overlayState.targeting||this.overlayState.los&&!this.overlayState.los.to||this.gesture.kind==="drag")&&this.renderOverlay())}bindInput(){const e=this.svg;e.addEventListener("pointerdown",n=>{e.setPointerCapture(n.pointerId);const i=this.local(n.clientX,n.clientY);if(this.pointers.set(n.pointerId,{...i,type:n.pointerType}),this.pointers.size===2){const[r,a]=[...this.pointers.values()];this.gesture={kind:"pinch",dist:Math.hypot(r.x-a.x,r.y-a.y),scale:this.scale,cx:(r.x+a.x)/2,cy:(r.y+a.y)/2,tx:this.tx,ty:this.ty},this.clearGhost();return}if(this.pointers.size>2)return;const o=this.hexAt(n.clientX,n.clientY);this.gesture={kind:"press",x:i.x,y:i.y,hex:o,mouse:n.pointerType==="mouse",tx:this.tx,ty:this.ty}}),e.addEventListener("pointermove",n=>{var a;const i=this.local(n.clientX,n.clientY);this.pointers.has(n.pointerId)&&this.pointers.set(n.pointerId,{...i,type:n.pointerType});const o=this.gesture;if(o.kind==="pinch"&&this.pointers.size===2){const[l,d]=[...this.pointers.values()],u=Math.hypot(l.x-d.x,l.y-d.y),h=Math.max(.2,Math.min(4,o.scale*u/Math.max(1,o.dist))),p=(l.x+d.x)/2,k=(l.y+d.y)/2;this.tx=p-(o.cx-o.tx)*h/o.scale,this.ty=k-(o.cy-o.ty)*h/o.scale,this.scale=h,this.applyTransform();return}if(o.kind==="press"){if(Math.hypot(i.x-o.x,i.y-o.y)<6)return;const l=o.hex?(a=this.state)==null?void 0:a.stacks.get(o.hex):void 0;o.mouse&&o.hex&&(l!=null&&l.length)?(this.gesture={kind:"drag",from:o.hex},this.showGhost(o.hex)):this.gesture={kind:"pan",x:o.x,y:o.y,tx:o.tx,ty:o.ty}}const r=this.gesture;r.kind==="pan"?(this.tx=r.tx+(i.x-r.x),this.ty=r.ty+(i.y-r.y),this.applyTransform()):r.kind==="drag"?(this.moveGhost(i.x,i.y),this.setHover(this.hexAt(n.clientX,n.clientY))):n.pointerType==="mouse"&&r.kind==="idle"&&this.setHover(this.hexAt(n.clientX,n.clientY))});const s=(n,i)=>{this.pointers.delete(n.pointerId);const o=this.gesture;if(o.kind==="pinch"){if(this.pointers.size===0)this.gesture={kind:"idle"};else{const[r]=[...this.pointers.values()];this.gesture={kind:"pan",x:r.x,y:r.y,tx:this.tx,ty:this.ty}}return}if(!this.pointers.size){if(this.gesture={kind:"idle"},i){this.clearGhost();return}if(o.kind==="press")n.pointerType!=="mouse"&&this.setHover(o.hex),this.events.tap(o.hex);else if(o.kind==="drag"){this.clearGhost();const r=this.hexAt(n.clientX,n.clientY);r&&r!==o.from&&this.events.dragMove(o.from,r),this.renderOverlay()}}};e.addEventListener("pointerup",n=>s(n,!1)),e.addEventListener("pointercancel",n=>s(n,!0)),e.addEventListener("pointerleave",n=>{n.pointerType==="mouse"&&this.gesture.kind==="idle"&&this.setHover(null)}),e.addEventListener("wheel",n=>{n.preventDefault();const i=this.local(n.clientX,n.clientY);this.zoomBy(Math.exp(-n.deltaY*(n.ctrlKey?.01:.0018)),i.x,i.y)},{passive:!1})}setDragSelection(e){this.dragIds=e}showGhost(e){const s=this.state;if(!s)return;const n=this.overlayState.selected===e&&this.dragIds.length?this.dragIds:s.stacks.get(e)??[],i=s.counters.get(n[n.length-1]);i&&(this.ghost.innerHTML=`<g opacity="0.85">${ot(i)}</g>${n.length>1?`<text x="${ne/2+4}" y="${-ne/2}" font-size="9" font-weight="800" fill="#1d1a12">×${n.length}</text>`:""}`,this.ghost.style.display="")}moveGhost(e,s){const n=(e-this.tx)/this.scale,i=(s-this.ty)/this.scale;this.ghost.setAttribute("transform",`translate(${n.toFixed(1)} ${i.toFixed(1)})`)}clearGhost(){this.ghost.innerHTML="",this.ghost.style.display="none"}}const mo="naddr1qvzqqqyf8ypzppy5edjhjzqu2e5nk3ngj69e9a6hytqe5qc49pqlfx5958l4tlsjqqxkuttrxq6rxc3svvcxvvtytnw2e4",Hn={napplet:mo};class Te extends Error{constructor(e,s,n){super(e),this.definitive=s,this.code=n}}function go(t){async function e(s,n){var a,l,d,u;let i;const o=["soy_backend_invoke","soy_backend_changes","soy_backend_purge_plan","soy_backend_purge_confirm"].includes(s);try{i=await t.cvm.registry.call("soy.backends.v1",s,n,{timeoutMs:o?12e4:15e3})}catch(h){throw new Te(h instanceof Error?h.message:"Connection interrupted",!1)}const r=i.structuredContent??JSON.parse(((l=(a=i.content)==null?void 0:a.find(h=>h.type==="text"))==null?void 0:l.text)??"{}");if(i.isError)throw new Te(((d=r.error)==null?void 0:d.message)??"Backend rejected this operation",!0,(u=r.error)==null?void 0:u.code);return r}return{call:e,describe:(s,n)=>e("soy_backend_describe",{module:s,...n?{release:n}:{}}),intent:(s,n,i)=>({target:s,operation:n,input:i,requestId:crypto.randomUUID(),expiresAt:Math.floor(Date.now()/1e3)+240}),invoke:s=>e("soy_backend_invoke",s)}}const bo="asl-table-v1",pn=40,It={napplet:Hn.napplet,name:"tables"},Me=go({cvm:ls});function Kn(){return re("cvm")&&!!Hn.napplet}async function zn(t,e){if(!re("storage"))return e;try{const s=await Qe.getItem(t);return s?JSON.parse(s):e}catch{return e}}async function Gt(t,e){if(!re("storage"))return!1;try{return await Qe.setItem(t,JSON.stringify(e)),!0}catch{return!1}}async function Ut(){const t=await zn("tables",[]);return Array.isArray(t)?t.filter(e=>e&&typeof e.id=="string").sort((e,s)=>s.updated-e.updated):[]}async function Se(t){const e=(await Ut()).filter(s=>!(s.id===t.id&&s.kind===t.kind));e.unshift(t),await Gt("tables",e.slice(0,30))}async function yo(t){const e=(await Ut()).filter(s=>!(s.id===t.id&&s.kind===t.kind));if(await Gt("tables",e),t.kind==="local"&&re("storage"))try{await Qe.removeItem(`local:${t.id}`)}catch{}}function ko(t,e){return`ASL1.${t}.${e}`}function $o(t){const e=/^ASL1\.([A-Za-z0-9_-]{8,128})\.([A-Za-z0-9_-]{8,128})$/.exec(t.trim());return e?{release:e[1],instance:e[2]}:null}function Ke(t){return t instanceof Te?t.code==="ACCOUNT_REQUIRED"?"Sign in with your Nostr account to take part in online tables.":t.message.replace(/^[A-Z_]+:\s*/,""):t instanceof Error?t.message:"Something went wrong."}async function vo(t,e,s){const n=await Me.describe(It);if(n.disabled||!n.active)throw new Error("Online tables are not available from this host yet.");const i=Me.intent({module:It,release:n.active},"createTable",{scenario:t,title:e,side:s});let o;try{o=await Ot(i)}catch(a){throw new Error(Ke(a))}const r={kind:"online",id:o.instance,release:o.release,scenario:t,title:e,side:s,updated:Date.now()};return await Se(r),r}async function Ot(t,e=4){let s=1500;for(let n=0;;n+=1)try{return await Me.invoke(t)}catch(i){if(!(i instanceof Te&&!i.definitive)||n>=e||Date.now()/1e3>t.expiresAt-5)throw i;await new Promise(r=>setTimeout(r,s)),s*=2}}function wo(t,e){return{kind:"local",id:`h${Date.now().toString(36)}${Math.floor(Math.random()*1e6).toString(36)}`,scenario:t,title:e,side:"a",updated:Date.now()}}class xo{constructor(e,s){M(this,"saved");M(this,"meta",null);M(this,"me");M(this,"confirmed",[]);M(this,"pending",[]);M(this,"state");M(this,"status",{sync:"idle",message:"",live:"off",peers:0});M(this,"presence",new Map);M(this,"onChange",()=>{});M(this,"target",null);M(this,"pollTimer",0);M(this,"flushing",!1);M(this,"closed",!1);M(this,"rtcSession",null);M(this,"rtcEvents",null);M(this,"peers",new Set);M(this,"refreshing",null);M(this,"refreshAgain",!1);M(this,"onVisibility",()=>{document.visibilityState==="visible"&&this.refresh(),this.schedulePoll()});M(this,"lastCursor",0);this.saved=e,this.me=s,this.state=un(e.scenario,[]),e.kind==="online"&&(this.target={module:It,release:e.release,instance:e.id})}get online(){return this.saved.kind==="online"}mySide(){return this.online?this.meta?this.meta.seatA&&this.meta.seatA===this.me?"a":this.meta.seatB&&this.meta.seatB===this.me?"b":"n":"n":this.saved.side}async open(){if(!this.online){const e=await zn(`local:${this.saved.id}`,[]);this.confirmed=Array.isArray(e)?e.filter(s=>s&&fn(s.a)):[],this.confirmed.forEach((s,n)=>s.i=n),this.rebuild();return}if(await this.refresh(),!this.meta)throw new Error(this.status.message||"Could not open this table.");this.schedulePoll(),document.addEventListener("visibilitychange",this.onVisibility)}close(){this.closed=!0,window.clearTimeout(this.pollTimer),document.removeEventListener("visibilitychange",this.onVisibility),this.stopLive()}schedulePoll(){if(window.clearTimeout(this.pollTimer),this.closed||!this.online)return;const s=document.visibilityState==="hidden"?3e4:this.peers.size?1e4:4e3;this.pollTimer=window.setTimeout(async()=>{await this.refresh(),this.schedulePoll()},s)}rebuild(){const e=this.pending.flatMap(s=>s.entries);e.forEach((s,n)=>s.i=this.confirmed.length+n),this.state=un(this.saved.scenario,[...this.confirmed,...e])}setStatus(e){this.status={...this.status,...e},this.onChange("status")}refresh(){return!this.online||this.closed?Promise.resolve():this.refreshing?(this.refreshAgain=!0,this.refreshing):(this.refreshing=(async()=>{do this.refreshAgain=!1,await this.fetchNew();while(this.refreshAgain&&!this.closed)})().finally(()=>{this.refreshing=null}),this.refreshing)}async fetchNew(){if(!this.target)return;const e=[];try{for(let s=0;s<50;s+=1){const n=await this.read(this.confirmed.length+e.length);this.meta=n.result.meta;let i=0;for(const o of n.result.entries){if(!o||o.i!==this.confirmed.length+e.length||!fn(o.a))break;e.push(o),i+=1}if(n.result.entries.length<pn||i<n.result.entries.length)break}(this.status.sync==="offline"||this.status.sync==="error")&&this.setStatus({sync:"idle",message:""})}catch(s){this.setStatus({sync:"offline",message:`Can't reach the table: ${Ke(s)}`})}e.length?(this.confirmed.push(...e),this.rebuild(),this.onChange("state",e),Se({...this.saved,side:this.mySide(),updated:Date.now()})):this.meta&&this.onChange("state",[])}async read(e){for(let s=0;;s+=1){const n=Me.intent(this.target,"read",{after:e,limit:pn});try{return await Me.invoke(n)}catch(i){if(!(i instanceof Te&&i.code==="CONFLICT")||s>=4)throw i;await new Promise(o=>setTimeout(o,150+s*250+Math.random()*150))}}}submit(e){if(!e.length)return;const s=this.mySide(),n=Math.floor(Date.now()/1e3),i=e.map(o=>({i:0,by:this.me,side:s,at:n,a:o}));if(!this.online){for(const o of i)o.a.t==="roll"?o.d=mn(o.a.n):qn.has(o.a.t)&&(o.d=mn(Zs)),o.i=this.confirmed.length,this.confirmed.push(o);this.rebuild(),this.onChange("state",i),Gt(`local:${this.saved.id}`,this.confirmed).then(o=>{o||this.setStatus({sync:"error",message:"This game could not be saved on this device."})}),Se({...this.saved,updated:Date.now()});return}if(this.pending.length>=12){this.setStatus({sync:"error",message:"Still saving earlier moves. Wait a moment, then try again."});return}this.pending.push({actions:e,entries:i}),this.rebuild(),this.onChange("state"),this.flush()}async flush(){var e;if(!(this.flushing||!this.target)){this.flushing=!0;try{for(;this.pending.length&&!this.closed;){const s=this.pending[0];this.setStatus({sync:"saving",message:"Saving…"}),s.intent??(s.intent=Me.intent(this.target,"act",{actions:s.actions}));try{const n=await Ot(s.intent);this.pending.shift();const i=n.result.entries;((e=i[0])==null?void 0:e.i)===this.confirmed.length?(this.confirmed.push(...i),this.rebuild(),this.onChange("state",i.filter(o=>{var r;return(r=o.d)==null?void 0:r.length}))):(this.rebuild(),await this.refresh()),this.hint(),this.setStatus({sync:"idle",message:""})}catch(n){if(n instanceof Te&&n.code==="CONFLICT"){s.intent=void 0,await this.refresh();continue}if(n instanceof Te&&n.definitive){this.pending.shift(),this.rebuild(),this.onChange("state"),this.setStatus({sync:"error",message:`Not saved: ${Ke(n)}`});continue}if(this.setStatus({sync:"offline",message:"Connection lost. Your move is waiting to be saved…"}),Date.now()/1e3>s.intent.expiresAt-5){this.pending.shift(),await this.refresh(),this.rebuild(),this.onChange("state"),this.setStatus({sync:"error",message:"A move may not have been saved. Check the log before repeating it."});continue}await new Promise(i=>setTimeout(i,5e3))}}}finally{this.flushing=!1}}}async join(e){if(!this.target)return;const s=Me.intent(this.target,"joinTable",{side:e});try{const n=await Ot(s);this.meta=n.result.meta,await this.refresh(),this.hint(),await Se({...this.saved,side:e,updated:Date.now()})}catch(n){throw new Error(Ke(n))}}setLocalSide(e){this.online||(this.saved.side=e,Se({...this.saved,updated:Date.now()}),this.onChange("status"))}liveAvailable(){return this.online&&re("webrtc")}async startLive(){var e;if(!(!this.liveAvailable()||this.rtcSession)){this.setStatus({live:"connecting"}),this.rtcEvents=tt.onEvent(s=>{s.sessionId===this.rtcSession&&(s.type==="peer"?(s.state==="joined"?(this.peers.add(s.pubkey),this.sendLive({v:1,t:"hello",side:this.mySide()})):(this.peers.delete(s.pubkey),this.presence.delete(s.pubkey),this.onChange("presence")),this.setStatus({live:this.peers.size?"connected":"waiting",peers:this.peers.size}),this.schedulePoll()):s.type==="message"?this.receive(s.from,s.payload):s.type==="closed"?(this.rtcSession=null,this.peers.clear(),this.presence.clear(),this.setStatus({live:"off",peers:0,message:s.reason?`Live link closed: ${s.reason}`:""}),this.onChange("presence")):s.type==="state"&&s.state==="open"&&this.setStatus({live:this.peers.size?"connected":"waiting"}))});try{const{session:s}=await tt.open({scope:{type:"room",room:`asl-${this.saved.id}`},channel:"table",protocol:bo});this.rtcSession=s.id,this.setStatus({live:this.peers.size?"connected":"waiting"})}catch(s){(e=this.rtcEvents)==null||e.close(),this.rtcEvents=null,this.setStatus({live:"off",message:`Live link unavailable: ${Ke(s)}`})}}}async stopLive(){var s;const e=this.rtcSession;if(this.rtcSession=null,this.peers.clear(),this.presence.clear(),(s=this.rtcEvents)==null||s.close(),this.rtcEvents=null,e)try{await tt.close(e,"Left the table")}catch{}this.closed||this.setStatus({live:"off",peers:0})}async sendLive(e){if(!(!this.rtcSession||!this.peers.size))try{await tt.send(this.rtcSession,e)}catch{}}hint(){this.sendLive({v:1,t:"hint",count:this.confirmed.length})}cursor(e){const s=performance.now();s-this.lastCursor<120||(this.lastCursor=s,this.sendLive({v:1,t:"cursor",hex:e,side:this.mySide()}))}receive(e,s){var o;if(!s||typeof s!="object")return;const n=s;if(n.v!==1)return;const i=n.side==="a"||n.side==="b"?n.side:"n";if(n.t==="hint"&&Number.isInteger(n.count)&&n.count>this.confirmed.length)this.refresh();else if(n.t==="cursor"){const r=typeof n.hex=="string"&&/^[A-Z]{1,2}\d{1,2}$/.test(n.hex)?n.hex:null;this.presence.set(e,{side:i,hex:r,at:Date.now()}),this.onChange("presence")}else n.t==="hello"&&(this.presence.set(e,{side:i,hex:((o=this.presence.get(e))==null?void 0:o.hex)??null,at:Date.now()}),this.onChange("presence"))}}function mn(t){const e=[],s=new Uint8Array(1);for(;e.length<t;)crypto.getRandomValues(s),s[0]<252&&e.push(1+s[0]%6);return e}let ve=null,Ge=null,Yt=!0;function Wn(t){Yt=t}function Ue(){return Yt}function Vt(){if(!Yt)return null;try{if(ve??(ve=new AudioContext),ve.state==="suspended"&&ve.resume(),!Ge){Ge=ve.createBuffer(1,ve.sampleRate,ve.sampleRate);const t=Ge.getChannelData(0);for(let e=0;e<t.length;e+=1)t[e]=Math.random()*2-1}return ve}catch{return null}}function Mo(t,e,s,n,i=.12){const o=t.createBufferSource();o.buffer=Ge;const r=t.createBiquadFilter();r.type="bandpass",r.frequency.value=s,r.Q.value=.8;const a=t.createGain();a.gain.setValueAtTime(n,e),a.gain.exponentialRampToValueAtTime(.001,e+i),o.connect(r).connect(a).connect(t.destination),o.start(e,Math.random()*.5,i+.05)}function gn(t,e){const s=Vt();if(!s)return;const n=s.currentTime+.02,i=Math.max(1,Math.min(14,t));for(let o=0;o<i;o+=1){const r=e?n+o*.07:n+o*.11+Math.random()*.06;Mo(s,r,e?1400+Math.random()*300:1900+Math.random()*600,e?.35:.3)}}function So(t=0){const e=Vt();if(!e)return;const s=e.currentTime+t,n=e.createBufferSource();n.buffer=Ge;const i=e.createBiquadFilter();i.type="lowpass",i.frequency.setValueAtTime(900,s),i.frequency.exponentialRampToValueAtTime(80,s+.9);const o=e.createGain();o.gain.setValueAtTime(.9,s),o.gain.exponentialRampToValueAtTime(.001,s+1.1),n.connect(i).connect(o).connect(e.destination),n.start(s,0,1.2)}function Co(){const t=Vt();if(!t)return;const e=t.currentTime;for(const[s,n]of[523,659,784].entries()){const i=t.createOscillator();i.type="triangle",i.frequency.value=n;const o=t.createGain();o.gain.setValueAtTime(1e-4,e+s*.08),o.gain.exponentialRampToValueAtTime(.12,e+s*.08+.02),o.gain.exponentialRampToValueAtTime(1e-4,e+s*.08+.4),i.connect(o).connect(t.destination),i.start(e+s*.08),i.stop(e+s*.08+.45)}}const To="http://www.w3.org/2000/svg";function St(){return window.matchMedia("(prefers-reduced-motion: reduce)").matches}function Ct(t,e){const s=document.createElementNS(To,t);for(const[n,i]of Object.entries(e))s.setAttribute(n,String(i));return s}const Tt={kill:"#ff5a3c",break:"#ff8f5a",hurt:"#ffb066",pin:"#f2c94c",held:"#e9e1c8",good:"#8fe07a",info:"#cfe6ff"};function Lt(t){var i,o;const e=((o=(i=/→ ([^→]+)$/.exec(t.text))==null?void 0:i[1])==null?void 0:o.trim())??"",s=[];let n=!1;for(const r of t.detail??[])/malfunctions/.test(r)?n=!0:/eliminated|weapon lost/.test(r)?s.push({text:"KIA",tone:"kill"}):/reduced to half-squad/.test(r)?s.push({text:"Casualty!",tone:"hurt"}):/breaks|: failed, .*breaks/.test(r)?s.push({text:"Broken!",tone:"break"}):/pinned/.test(r)?s.push({text:"Pinned",tone:"pin"}):/rallied|repaired/.test(r)?s.push({text:r.includes("rallied")?"Rallied!":"Repaired",tone:"good"}):/passed/.test(r)&&s.push({text:"Held",tone:"held"});return/: rallied$/.test(t.text)&&s.push({text:"Rallied!",tone:"good"}),/rally: .*: failed$/.test(t.text)&&s.push({text:"Still broken",tone:"info"}),/: repaired$/.test(t.text)&&s.push({text:"Repaired",tone:"good"}),/weapon lost$/.test(t.text)&&s.push({text:"Weapon lost",tone:"kill"}),{headline:e,outcomes:s,jammed:n}}class Lo{constructor(e){M(this,"board");this.board=e}add(e,s){return this.board.fx.append(e),window.setTimeout(()=>e.remove(),s),e}label(e,s,n,i,o,r=13){const a=Ct("text",{x:e,y:s,"text-anchor":"middle","font-size":r,"font-weight":900,fill:i,class:"fx-label"});a.textContent=n,a.style.opacity="0",this.add(a,o+2400),a.animate([{opacity:0,transform:"translateY(6px) scale(0.8)"},{opacity:1,transform:"translateY(0) scale(1.08)",offset:.15},{opacity:1,transform:"translateY(-14px) scale(1)",offset:.75},{opacity:0,transform:"translateY(-26px) scale(1)"}],{duration:2e3,delay:o,fill:"forwards",easing:"ease-out"})}burst(e,s,n,i,o,r=500){const a=Ct("circle",{cx:e,cy:s,r:o,fill:i});a.style.transformBox="fill-box",a.style.transformOrigin="center",a.style.opacity="0",this.add(a,n+r+50),a.animate([{opacity:.95,transform:"scale(0.2)"},{opacity:.7,transform:"scale(1)",offset:.4},{opacity:0,transform:"scale(1.5)"}],{duration:r,delay:n,easing:"ease-out",fill:"forwards"})}tracer(e,s,n,i){const o=Math.atan2(s.y-e.y,s.x-e.x)*180/Math.PI,r=()=>(Math.random()-.5)*14,a={x:s.x+r(),y:s.y+r()},l=Ct("line",{x1:0,y1:0,x2:i?28:20,y2:0,stroke:i?"#ffcf5a":"#fff1c2","stroke-width":i?4.5:3.5,"stroke-linecap":"round",class:"fx-tracer"});l.style.opacity="0";const d=Math.hypot(a.x-e.x,a.y-e.y),u=Math.max(140,Math.min(420,d*1.4));this.add(l,n+u+50),l.animate([{opacity:1,transform:`translate(${e.x}px, ${e.y}px) rotate(${o}deg)`},{opacity:1,transform:`translate(${a.x}px, ${a.y}px) rotate(${o}deg)`}],{duration:u,delay:n,easing:"linear",fill:"forwards"}),window.setTimeout(()=>{this.burst(a.x,a.y,0,"#ffe7a3",7,160),this.burst(a.x,a.y,60,"rgba(120, 98, 66, 0.6)",10+Math.random()*6,700)},n+u)}fire(e,s,n,i){var y;if(e.a.t!=="fire")return;const o=this.board.hexCenter(n),r=this.board.hexCenter(e.a.to);if(!o||!r)return;const a=Number(((y=/: ([\d.]+) FP/.exec(s.text))==null?void 0:y[1])??4),{headline:l,outcomes:d,jammed:u}=Lt(s),h=Math.round(3+a/2);gn(h,i);const p=d.some(w=>w.tone==="kill");p&&So(.35);const k=St(),m=k?0:(h-1)*(i?70:110);if(!k){for(let w=0;w<h;w+=1){const I=w*(i?70:110);this.burst(o.x+(Math.random()-.5)*12,o.y+(Math.random()-.5)*12,I,"#ffcf5a",12,200),this.tracer(o,r,I,i)}p?(this.burst(r.x,r.y,m+300,"#fff1b8",20,350),this.burst(r.x,r.y,m+330,"#ff8a3c",40,800),this.burst(r.x,r.y,m+450,"rgba(60, 55, 50, 0.65)",52,1600),window.setTimeout(()=>this.board.shake(),m+300)):this.burst(r.x,r.y,m+250,"rgba(110, 96, 70, 0.55)",32,1100)}const b=l==="no effect"?"No effect":l;this.label(r.x,r.y-30,b,l==="no effect"?"#e9e1c8":"#ffe08a",m+300,22),d.forEach((w,I)=>this.label(r.x+(I%2?20:-20),r.y+8+Math.floor(I/2)*20,w.text,Tt[w.tone],m+700+I*260,17)),u&&this.label(o.x,o.y-30,"Jammed!","#ff8f5a",m+400,18)}closeCombat(e,s){if(e.a.t!=="cc")return;const n=this.board.hexCenter(e.a.to);if(!n)return;gn(6,!1);const{outcomes:i}=Lt(s);if(!St()){for(let o=0;o<6;o+=1)this.burst(n.x+(Math.random()-.5)*34,n.y+(Math.random()-.5)*34,o*120,"#ffcf5a",12,280);i.some(o=>o.tone==="kill")&&window.setTimeout(()=>this.board.shake(),500)}this.label(n.x,n.y-30,"Melee!","#ffe08a",0,22),i.forEach((o,r)=>this.label(n.x+(r%2?20:-20),n.y+8+Math.floor(r/2)*20,o.text,Tt[o.tone],700+r*260,17))}rally(e,s){const n=this.board.hexCenter(s);if(!n)return;const{outcomes:i}=Lt(e),o=i.some(r=>r.tone==="good");o&&Co(),St()||this.burst(n.x,n.y,0,o?"rgba(143, 224, 122, 0.5)":"rgba(200, 200, 200, 0.3)",40,1e3),i.forEach((r,a)=>this.label(n.x,n.y-24+a*20,r.text,Tt[r.tone],100+a*200,18))}}const dt=t=>t>0?`+${t}`:`${t}`;function Ao(t,e){var o;const s=t.counters.get(e),n=te(s);if(s.label)return s.label;const i=L(s);return i==="leader"&&n?`${n.morale}-${-n.leaderDRM}`:n&&i!=="weapon"?`${n.fp}-${n.range}-${n.morale}`:((o=s.k.split("-")[1])==null?void 0:o.toUpperCase())??s.k}function Re(t,e){return t.counters.get(e)?Ao(t,e):e}function Eo(t,e){const s=Us(t);return s.length?`<ol class="ift-ladder">${s.map((i,o)=>{const r=o-e;return`<li><span class="dr">${o===0?"≤0":o}</span><span class="res res-${i.replace(/[^A-Z]/g,"").toLowerCase()||"k"}">${g(i)}</span><span class="need">${r<=1?"—":`roll ≤ ${r}`}</span></li>`}).join("")}</ol>`:""}function Po(t,e){const s=e.lines.map(r=>`<tr><td>${g(r.name)}</td><td class="num">${ct(r.fp)}</td><td class="notes">${g(r.notes.join(", "))}</td></tr>`).join(""),n=e.drms.length?e.drms.map(r=>`<li><span>${g(r.label)}</span><b>${dt(r.value)}</b></li>`).join(""):"<li><span>No modifiers</span><b>0</b></li>",i=e.targets.map(r=>g(Re(t,r))).join(", "),o=Dn(e.column,e.inexperienced?R.coweringShift*2:R.coweringShift);return`<section class="fire-card" aria-label="Fire attack">
    <header><b>${g(e.from)} → ${g(e.to)}</b><span>range ${e.range}</span></header>
    <p class="hint">Targets: ${i}</p>
    <table class="fp-table"><thead><tr><th>Firer</th><th class="num">FP</th><th></th></tr></thead><tbody>${s}</tbody>
      <tfoot><tr><td>Total</td><td class="num">${ct(e.totalFP)}</td><td>→ <b>${e.column}</b> column</td></tr></tfoot></table>
    <ul class="drm-list">${n}<li class="drm-total"><span>Total DRM</span><b>${e.drm===0?"0":dt(e.drm)}</b></li></ul>
    ${e.leader?"":`<p class="hint">No leader directing: doubles cower to the ${o} column.</p>`}
    <details class="ladder"><summary>What each roll does</summary>${Eo(e.column,e.drm)}</details>
    <div class="row fire-actions">
      <button type="button" class="btn primary roll-btn" data-act="fire-roll">Roll DR</button>
      <button type="button" class="btn" data-act="fire-cancel">Cancel</button>
    </div>
  </section>`}function Fo(t){var e;return t?`<section class="result-card"><p><b>${g(t.text)}</b></p>${(e=t.detail)!=null&&e.length?`<ul>${t.detail.map(s=>`<li>${g(s)}</li>`).join("")}</ul>`:""}</section>`:""}function Zt(t,e){if(t.over)return{label:"Game over",enabled:!1,waitingFor:null};const s=Ve(t),n=t.turn===0?"Setup":ce(t),i=s.find(o=>!t.ready.has(o))??null;return e==="n"?{label:`${n}`,enabled:!1,waitingFor:i}:s.includes(e)?t.ready.has(e)?{label:`Waiting for ${Q(t.scenario,i)}`,enabled:!1,waitingFor:i}:{label:`End ${n}`,enabled:!0,waitingFor:e}:{label:`${Q(t.scenario,s[0])}'s ${n}`,enabled:!1,waitingFor:s[0]}}function Ro(t){const e=[];for(let s=t.lines.length-1;s>=0;s-=1){const n=t.lines[s];if(n.kind==="phase")break;n.kind==="fire"&&e.push(n)}return e.slice(0,4)}function No(t){const{state:e,me:s}=t,n=[],i=Zt(e,t.endAs),o=e.turn===0?"Setup":`${ce(e)} Phase`,r=Q(e.scenario,e.active);if(e.over)return n.push(`<section class="menu-head"><h3>Game over</h3><p>${g(e.scenario.victory)}</p></section>`),n.join("");n.push(`<section class="menu-head">
    <p class="menu-turn">${e.turn===0?"Before the game":`Turn ${e.turn} of ${e.scenario.turns} · ${g(r)} player turn`}</p>
    <h3>${g(o)}</h3>
  </section>`);const a=s==="a"||s==="b",l=Ye(e),d=et(e),u=Ro(e);if(!a)n.push('<p class="hint">You are watching. Take a seat in the Table tab to play.</p>');else if(e.turn===0)n.push(`<ol class="steps">
      <li>Arrange your units: pick counters, tap <b>Move</b>, then tap a hex. Placement is free during setup.</li>
      <li>Check the scenario's setup instructions in the Table tab.</li>
      <li>Press <b>End Setup</b> when you're ready. The game starts when both sides have.</li></ol>`);else if(d){const h=Nt(e);h!==s?n.push(`<p class="hint">${g(Q(e.scenario,h))} is firing. Results appear below as they happen.</p>`):n.push(Io(t))}else Wt(e)?n.push(Oo(t)):l==="move"||l==="advance"||l==="rout"?n.push(Bo(t,l)):ce(e)==="Close Combat"&&n.push(Do(t));return u.length&&n.push(`<h4 class="menu-sub">Results this phase</h4>${u.map(Fo).join("")}`),n.push(`<div class="menu-end">
    <button type="button" class="btn ${i.enabled?"primary":""} end-btn" data-act="end-phase" ${i.enabled?"":"disabled"}>${g(i.label)}</button>
    ${i.enabled?'<p class="hint">Ending a phase is final: you can’t go back to it.</p>':""}
  </div>`),n.join("")}function Io(t){const{state:e,me:s,selected:n,picked:i,fire:o,firePlan:r,fireError:a}=t,l=et(e),u=[`<p class="hint">${l==="prep"?"Units that Prep Fire cannot move this turn.":l==="defensive"?"Each of your units can fire once.":"Advancing Fire is at half firepower. Units that Prep Fired cannot fire again."}</p>`];if(o!=null&&o.to&&r)return u.push(Po(e,r)),u.join("");if(o&&!o.to)return u.push(`<p class="callout">Tap a highlighted enemy hex on the map to see the attack.${a?` <br><span class="warn">${g(a)}</span>`:""}</p>
      <div class="row"><button type="button" class="btn" data-act="fire-cancel">Cancel</button></div>`),u.join("");const h=new Map;for(const p of e.counters.values()){if(p.side!==s||Bn(e,p,s)||!/^[A-Z]/.test(p.at))continue;const k=h.get(p.at)??[];k.push(p.id),h.set(p.at,k)}if(!h.size)return u.push('<p class="empty">None of your units can fire now.</p>'),u.join("");if(n&&h.has(n)){const p=h.get(n),k=p.filter(m=>i.has(m));u.push(`<p><b>Firing from ${g(n)}</b>: pick the units to fire together (a fire group).</p>
      <ul class="pick-list">${p.map(m=>`<li><button type="button" class="pill" data-pick="${g(m)}" aria-pressed="${i.has(m)}">${g(Re(e,m))}</button></li>`).join("")}</ul>
      <div class="row"><button type="button" class="btn primary" data-act="fire-start" ${k.length?"":"disabled"}>Choose target</button></div>`)}else u.push(`<p>Tap one of your hexes to fire from it:</p><div class="hex-chips">${[...h.keys()].map(p=>`<button type="button" class="chip-btn" data-goto="${p}">${p} · ${h.get(p).length}</button>`).join("")}</div>`);return u.join("")}function Oo(t){const{state:e,me:s}=t,n=[];for(const i of e.counters.values())if(i.side===s){if(i.face===1&&["squad","half","crew","leader"].includes(L(i))&&/^[A-Z]/.test(i.at)){const o=Nn(e,i.id,s);n.push(o.ok?`<li><span><b>${g(Re(e,i.id))}</b> in ${i.at}: needs ≤ ${o.target}${o.drms.map(r=>` · ${g(r.label)} ${dt(r.value)}`).join("")}</span><button type="button" class="btn small primary" data-rally="${g(i.id)}">Rally</button></li>`:`<li class="muted"><span><b>${g(Re(e,i.id))}</b> in ${i.at}: ${g(o.reasons[0])}</span></li>`)}if(L(i)==="weapon"&&i.face===1&&/^[A-Z]/.test(i.at)){const o=In(e,i.id,s);n.push(o.ok?`<li><span><b>${g(Re(e,i.id))}</b> in ${i.at}: malfunctioned. Repair on a 1, lost on a 6.</span><button type="button" class="btn small" data-repair="${g(i.id)}">Repair</button></li>`:`<li class="muted"><span><b>${g(Re(e,i.id))}</b>: ${g(o.reasons[0])}</span></li>`)}}return n.length?`<p class="hint">Both sides rally. Broken units need a good-order leader with them; leaders can rally themselves.</p><ul class="action-list">${n.join("")}</ul>`:'<p class="empty">Nothing of yours to rally or repair. Press End when ready; both sides must.</p>'}function Bo(t,e){const{state:s,me:n}=t,i=e==="move"?"Pick a stack, tap <b>Move</b>, then tap a highlighted hex. Highlighted numbers are MF costs.":e==="advance"?"Each good-order unit may advance one hex, including into an enemy hex to start close combat.":"Broken units of both sides rout. Move them toward cover, away from the enemy.",o=new Map;for(const l of s.counters.values()){if(l.side!==n||!["squad","half","crew","leader"].includes(L(l))||!/^[A-Z]|^pool-/.test(l.at))continue;const d=Kt(s,[l.id],n,!1);d.ok&&d.hexes.size&&o.set(l.at,(o.get(l.at)??0)+1)}const r=[...o].map(([l,d])=>`<button type="button" class="chip-btn" data-goto="${l}">${l.startsWith("pool")?"off-board":l} · ${d}</button>`).join("");return e!=="rout"&&s.active!==n?`<p class="hint">${g(Q(s.scenario,s.active))} is moving. Defensive First Fire isn't automated yet; it is resolved in the Defensive Fire Phase.</p>`:`<p class="hint">${i}</p>${r?`<p>Units that can still ${e==="rout"?"rout":e}:</p><div class="hex-chips">${r}</div>`:'<p class="empty">None of your units can move now.</p>'}${e==="move"?`<p class="hint">Each unit has ${ge(4)} MF (6 with a leader), +1 with Double Time.</p>`:""}`}function Do(t){const{state:e,me:s}=t,n=Pn(e);return n.length?`<ul class="action-list">${n.map(i=>{const o=Fn(e,i,s);if(!o.ok)return`<li class="muted"><span><b>${i}</b>: ${g(o.reasons[0])}</span></li>`;const r=o.attacks.map(a=>`${g(Q(e.scenario,a.side))} ${a.atk}:${a.def} (${a.odds}) kills on < ${a.kill}${a.drm?` ${dt(a.drm)}`:""}`).join(" · ");return`<li><span><b>${i}</b>: ${r}</span><button type="button" class="btn small primary" data-cc="${i}">Resolve</button></li>`}).join("")}</ul><p class="hint">Both sides attack at the same time; either player can resolve.</p>`:'<p class="empty">No close combat this turn. Both sides press End.</p>'}function v(t){const e=document.querySelector(t);if(!e)throw new Error(`Missing required element: ${t}`);return e}const f={lobby:v("#lobby"),table:v("#table"),newScenario:v("#newScenario"),newBrief:v("#newBrief"),newSide:v("#newSide"),createOnline:v("#createOnline"),createLocal:v("#createLocal"),onlineNote:v("#onlineNote"),joinCode:v("#joinCode"),joinButton:v("#joinButton"),joinNote:v("#joinNote"),myTables:v("#myTables"),myTablesEmpty:v("#myTablesEmpty"),identityNote:v("#identityNote"),back:v("#backButton"),endPhase:v("#endPhase"),actionsPanel:v("#actionsPanel"),turnLabel:v("#turnLabel"),activeChip:v("#activeChip"),phaseLabel:v("#phaseLabel"),rollDR:v("#rollDR"),rolldr:v("#rolldr"),los:v("#losButton"),undo:v("#undoButton"),seatChip:v("#seatChip"),syncDot:v("#syncDot"),panelButton:v("#panelButton"),panelClose:v("#panelClose"),panel:v("#panel"),boardHost:v("#boardHost"),zoomIn:v("#zoomIn"),zoomOut:v("#zoomOut"),zoomFit:v("#zoomFit"),modeBar:v("#modeBar"),modeText:v("#modeText"),modeActions:v("#modeActions"),toast:v("#toast"),diceTray:v("#diceTray"),strip:v("#strip"),stripHex:v("#stripHex"),stripCounters:v("#stripCounters"),stripMove:v("#stripMove"),stripMore:v("#stripMore"),stackPanel:v("#stackPanel"),trayPanel:v("#trayPanel"),infoPanel:v("#infoPanel"),logList:v("#logList"),logBadge:v("#logBadge"),chatSend:v("#chatSend"),chatInput:v("#chatInput"),rollWhy:v("#rollWhy"),tooSmallState:v("#tooSmallState")},c={me:"guest",pubkey:"",session:null,selected:null,picked:new Set,mode:{kind:"none"},tab:"actions",overrideTools:!1,endArmed:!1,panelOpen:!0,traySide:"a",newSide:"a",logSeen:0,shownLos:null},U=new po(f.boardHost,{tap:_o,dragMove:(t,e)=>{var n;const s=c.selected===t&&c.picked.size?je(t):[...((n=Y())==null?void 0:n.stacks.get(t))??[]];Wo(s,e)},hover:t=>{var e;Ne.sharePointer&&((e=c.session)==null||e.cursor(t)),c.mode.kind==="los"&&c.mode.from&&!c.mode.to&&ke()}}),At=new Lo(U);function qo(t){var s;const e=Y();if(e)for(const n of t.filter(i=>{var o;return((o=i.d)==null?void 0:o.length)&&i.a.t!=="roll"}).slice(-2)){const i=e.lines.find(r=>r.i===n.i);if(!i||i.kind!=="fire")continue;const o=n.a;if(o.t==="fire"){const r=o.ids.map(l=>e.counters.get(l)).filter(Boolean),a=(s=r[0])==null?void 0:s.at;a&&At.fire(n,i,a,r.some(l=>/-(lmg|mmg|hmg)$/.test(l.k)))}else if(o.t==="cc")At.closeCombat(n,i);else if(o.t==="rally"||o.t==="repair"){const r=e.counters.get(o.id)??e.counters.get(`${o.id}h`);r&&/^[A-Z]/.test(r.at)&&At.rally(i,r.at)}}}function Y(){var t;return((t=c.session)==null?void 0:t.state)??null}function ye(){return window.innerWidth<760}let it=null;async function jo(){if(!re($n)){st("");return}try{st(await on.getPublicKey())}catch{st("")}try{it=on.onChanged(t=>st(t))}catch{}}function st(t){const e=t!==c.pubkey;c.pubkey=t,c.me=t?`nostr:${t}`:"guest",c.session&&e&&c.session.online&&(c.session.me=c.me,c.session.onChange("status")),_n()}function Bt(t){const e=t.replace(/^(nostr|guest):/,"");return e?`${e.slice(0,6)}…${e.slice(-4)}`:""}function Ho(){f.newScenario.innerHTML=Dt.map(t=>`<option value="${t.id}">${g(t.name)}</option>`).join(""),Xt()}function Xt(){const t=Ae.get(f.newScenario.value)??Dt[0];f.newBrief.textContent=t.brief,f.newSide.innerHTML=["a","b"].map(e=>{const s=t.sides[e].nat,n=oe[s];return`<button type="button" role="radio" class="seg-btn" data-side="${e}" aria-checked="${c.newSide===e}">
        <span class="swatch" style="background:${n.face};border-color:${n.edge}"></span>${g(t.sides[e].name)}
      </button>`}).join("")}function _n(){const t=Kn();f.createOnline.disabled=!t,f.joinButton.disabled=!t,t?c.pubkey?f.onlineNote.textContent="Online tables keep the log and roll the dice on the shared server. Share the code with your opponent.":f.onlineNote.textContent="Sign in with your Nostr account to create or sit at an online table. Anyone can watch.":f.onlineNote.textContent="Online tables need a host with shared-backend support. Hotseat games still work here.",f.identityNote.textContent=c.pubkey?`Signed in as ${Bt(c.pubkey)}`:"",re("storage")||(f.myTablesEmpty.textContent="This host has no app storage, so tables are not remembered and hotseat games are not saved.")}async function gt(){const t=await Ut();f.myTablesEmpty.hidden=t.length>0,f.myTables.innerHTML=t.map((e,s)=>{const n=Ae.get(e.scenario),i=e.side==="n"||!n?null:n.sides[e.side].nat,o=new Date(e.updated).toLocaleDateString(void 0,{month:"short",day:"numeric"});return`<li class="table-item">
        <button type="button" class="table-open" data-index="${s}">
          <span class="swatch" style="background:${i?oe[i].face:"#555"}"></span>
          <span class="table-name">${g(e.title||(n==null?void 0:n.name)||"Table")}</span>
          <span class="table-meta">${e.kind==="online"?"Online":"Hotseat"} · ${i?g(le[i].name):"Watching"} · ${o}</span>
        </button>
        <button type="button" class="tool slim table-forget" data-index="${s}" aria-label="Remove from list" title="Remove from list">
          <svg viewBox="0 0 20 20" aria-hidden="true"><path d="m6 6 8 8M14 6l-8 8" /></svg>
        </button>
      </li>`}).join(""),f.myTables.querySelectorAll(".table-open").forEach(e=>e.addEventListener("click",()=>void bt(t[Number(e.dataset.index)]))),f.myTables.querySelectorAll(".table-forget").forEach(e=>e.addEventListener("click",async()=>{const s=t[Number(e.dataset.index)];if(s.kind==="local"&&e.dataset.armed!=="1"){e.dataset.armed="1",e.classList.add("armed"),e.title="Tap again to delete this hotseat game",e.setAttribute("aria-label",e.title),K("Tap × again to delete this hotseat game from this device.");return}await yo(s),await gt()}))}f.newScenario.addEventListener("change",Xt);f.newSide.addEventListener("click",t=>{const e=t.target.closest("[data-side]");e&&(c.newSide=e.dataset.side,Xt())});function Jt(t,e){t.addEventListener("keydown",s=>{s.key==="Enter"&&!s.isComposing&&(s.preventDefault(),e())})}f.createOnline.addEventListener("click",async()=>{if(!c.pubkey){f.onlineNote.textContent="Sign in with your Nostr account first: your seat belongs to that account.";return}f.createOnline.disabled=!0,f.onlineNote.textContent="Setting up the table… your host may ask to confirm your account.";try{const t=Ae.get(f.newScenario.value),e=await vo(t.id,t.name,c.newSide);await bt(e),c.tab="info",ue()}catch(t){f.onlineNote.textContent=t instanceof Error?t.message:"Could not create the table."}finally{f.createOnline.disabled=!Kn()}});f.createLocal.addEventListener("click",async()=>{const t=Ae.get(f.newScenario.value),e=wo(t.id,t.name);e.side=c.newSide,await Se(e),await bt(e)});async function Gn(){const t=$o(f.joinCode.value);if(!t){f.joinNote.textContent="That does not look like a table code. It starts with ASL1.";return}f.joinNote.textContent="Opening…";const e={kind:"online",id:t.instance,release:t.release,scenario:"",title:"",side:"n",updated:Date.now()};try{await bt(e),f.joinNote.textContent="",f.joinCode.value="",c.tab="info",ue()}catch(s){f.joinNote.textContent=s instanceof Error?s.message:"Could not open that table."}}f.joinButton.addEventListener("click",()=>void Gn());Jt(f.joinCode,()=>void Gn());async function bt(t){var s,n,i;Qt();const e=new xo({...t},t.kind==="local"?"local":c.me);if(t.kind==="online"){await e.open();const o=((s=e.meta)==null?void 0:s.scenario)??t.scenario;e.saved.scenario=o,e.saved.title=((n=e.meta)==null?void 0:n.title)||((i=Ae.get(o))==null?void 0:i.name)||"Table",await e.refresh(),await Se({...e.saved,side:e.mySide(),updated:Date.now()})}else await e.open();c.session=e,c.selected=null,c.picked.clear(),c.mode={kind:"none"},c.shownLos=null,c.logSeen=e.state.lines.length,c.traySide=e.mySide()==="b"?"b":"a",e.onChange=Ko,f.lobby.hidden=!0,f.table.hidden=!1,c.panelOpen=!ye(),U.setMap(mt(e.state.scenario)),q(),requestAnimationFrame(()=>ye()?U.fitPlayable():U.fit()),e.liveAvailable()&&Ne.autoLive&&e.startLive()}function Qt(){var t;(t=c.session)==null||t.close(),c.session=null}f.back.addEventListener("click",async()=>{Qt(),f.table.hidden=!0,f.lobby.hidden=!1,await gt()});function Ko(t,e){var n,i;const s=c.session;if(s)if(t==="state"){const o=s.state;for(const r of[...c.picked])((n=o.counters.get(r))==null?void 0:n.at)!==c.selected&&c.picked.delete(r);for(const r of e??[])(i=r.d)!=null&&i.length&&ti(r);requestAnimationFrame(()=>qo(e??[])),c.mode.kind==="fire"&&c.mode.ids.some(r=>{var a,l;return((a=o.counters.get(r))==null?void 0:a.at)!==c.mode.from||((l=o.counters.get(r))==null?void 0:l.fired)})&&(c.mode={kind:"none"}),q()}else t==="presence"?ke():(ut(),c.tab==="info"&&nn())}function j(t=!1){const e=c.session;return e?!e.online||e.mySide()!=="n"?!0:(t||K(c.pubkey?"Take a seat in the Table tab to move counters and roll.":"Sign in and take a seat to play. You are watching."),!1):!1}function D(t){!c.session||!t.length||c.session.submit(t)}function ze(){const t=Y();return t&&t.turn>0?{ovr:"manual edit"}:{}}function Pe(){var t;return((t=c.session)==null?void 0:t.mySide())??"n"}function Xe(){const t=Y(),e=c.mode;return!t||e.kind!=="move"||e.override?null:Kt(t,e.ids,Pe(),e.dt)}function zo(t,e,s,n,i){const o=Ht(t,e,[s],n,Pe());return!o.ok&&!/not next to/.test(o.reasons[0])?o.reasons[0]:i.kind==="advance"?"Advancing units move one hex only.":`${s} is out of reach: ${ge(i.left)} MF left.`}function Un(t,e,s){const n=Y();if(!n||!j()||!t.length)return!1;const i=Kt(n,t,Pe(),s);if(!i.ok)return K(`${i.reasons[0]} Use Move → Override to do it anyway.`,4500),!1;const o=i.hexes.get(e);return o?(D([{t:"mv",ids:t,path:o.path,...s&&i.kind==="move"?{dt:!0}:{}}]),c.selected=e,c.picked=new Set(t),U.reveal(e),!0):(K(zo(n,t,e,s,i),4e3),!1)}function Wo(t,e){Un(t,e,!1),c.mode={kind:"none"},q()}function je(t){var s;return(((s=Y())==null?void 0:s.stacks.get(t))??[]).filter(n=>c.picked.has(n))}function yt(t){var e;c.selected=t,c.picked=new Set(t?((e=Y())==null?void 0:e.stacks.get(t))??[]:[]),q()}function _o(t){const e=c.mode;if(e.kind==="fire"){const s=Y();if(!t||!s)return;const n=pt(s,e.ids,t,Pe());n.ok?(c.mode={...e,to:t},De=null,c.tab="actions",c.panelOpen||$e(!0)):(De=`${t}: ${n.reasons[0]}`,K(De,4e3)),q();return}if(e.kind==="move"){if(!t)c.mode={kind:"none"};else if(e.override)D([{t:"move",ids:e.ids,to:t,ovr:e.override}]),c.selected=t,c.picked=new Set(e.ids),c.mode={kind:"none"};else if(t!==e.from&&Un(e.ids,t,e.dt)){c.mode={...e,from:t,dt:!1};const s=Xe();(!s||!s.ok||!s.hexes.size||s.kind==="setup"||s.kind==="advance")&&(c.mode={kind:"none"})}q();return}if(e.kind==="los"){if(!t)return;!e.from||e.to?c.mode={kind:"los",from:t,to:null}:t!==e.from&&(c.mode={kind:"los",from:e.from,to:t}),q(),c.mode.kind==="los"&&c.mode.to&&(c.tab="stack",ue());return}c.shownLos=null,yt(t===c.selected?null:t),t&&c.panelOpen&&c.tab!=="tray"&&c.tab!=="actions"&&(c.tab="stack",ue())}function en(){if(!c.selected||!j())return;const t=je(c.selected);if(!t.length){K("Pick at least one counter to move.");return}c.mode={kind:"move",ids:t,from:c.selected,dt:!1,override:null},ye()&&$e(!1),q()}function Je(t){if(!j())return;const e=f.rollWhy.value.trim().slice(0,60);f.rollWhy.value="",D([{t:"roll",n:t,why:e}])}function Go(t,e){if(!j())return;const s=Y();if(!s)return;const n=c.selected??(e==="n"?null:`pool-${e}`);if(!n){K("Select a hex first, then add the marker.");return}if(s.turn>0&&e!=="n"&&!c.overrideTools){K("After setup, adding units is an override. Turn on Override tools in the Stack tab first.",4500);return}const i=oo();D([{t:"add",id:i,k:t,side:e,to:n,...e==="n"?{}:ze()}]),c.selected||(c.selected=n),c.picked.add(i),K(`Added to ${G(n)}${n.includes("-")?"":` ${n}`}`)}function q(){ut(),ke(),ue(),kt(),Uo()}function ke(){const t=c.session;if(!t)return;const e=[...t.presence.values()].filter(o=>Date.now()-o.at<6e4),s=c.mode.kind==="los"&&c.mode.from?{from:c.mode.from,to:c.mode.to}:c.shownLos;U.setDragSelection(c.selected?je(c.selected):[]);const n=Xe(),i=c.mode;U.render(t.state,{targets:Xn(),fireLine:i.kind==="fire"&&i.to?{from:i.from,to:i.to}:null,selected:c.selected,targeting:c.mode.kind==="move",reach:n!=null&&n.ok&&n.kind!=="setup"?new Map([...n.hexes].map(([o,r])=>[o,r.cost])):null,los:s,presence:e,showCoords:Ne.showCoords})}function rt(t,e){const s=de(t.scenario,e),n=oe[s];return`<span class="chip-swatch" style="background:${n.face};border-color:${n.edge}"></span>${g(le[s].short)}`}function ut(){const t=c.session;if(!t)return;const e=t.state;f.turnLabel.textContent=e.turn===0?"Setup":`T${e.turn}/${e.scenario.turns}`,f.activeChip.innerHTML=rt(e,e.active),f.activeChip.title=`${Q(e.scenario,e.active)} player turn`,f.phaseLabel.textContent=e.turn===0?"":ce(e);const s=t.mySide(),n=j(!0);for(const d of[f.rollDR,f.rolldr])d.disabled=!n;const i=Zt(e,tn());f.endPhase.disabled=!i.enabled,f.endPhase.textContent=c.endArmed&&i.enabled?"Confirm?":i.enabled?"End":e.over?"Over":"Wait",f.endPhase.title=i.label,f.endPhase.classList.toggle("armed",c.endArmed&&i.enabled),f.undo.disabled=!n||!jn(e,t.me),f.los.setAttribute("aria-pressed",String(c.mode.kind==="los")),t.online?f.seatChip.innerHTML=s==="n"?'<span class="seat-label">watching</span>':`${rt(e,s)}<span class="seat-label">you</span>`:(f.seatChip.innerHTML=`<button type="button" class="seat-toggle" title="Switch which side you are acting for">${rt(e,s)}<span class="seat-label">acting</span></button>`,f.seatChip.querySelector("button").addEventListener("click",()=>{t.setLocalSide(s==="a"?"b":"a"),c.traySide=t.mySide(),q()}));const o=t.status,r=o.live==="connected"?` · live with ${o.peers}`:o.live==="waiting"?" · live link open":"",a=t.online?o.sync==="saving"?`Saving${r}`:o.sync==="offline"?"Offline: retrying":o.sync==="error"?"Not saved":`Synced${r}`:"Hotseat game on this device";f.syncDot.dataset.state=t.online?o.sync==="idle"?o.live==="connected"?"live":"ok":o.sync:"local",f.syncDot.title=o.message||a,f.syncDot.setAttribute("aria-label",o.message||a),o.message&&(o.sync==="error"||o.sync==="offline")&&K(o.message,5e3),f.panelButton.setAttribute("aria-expanded",String(c.panelOpen)),f.table.classList.toggle("panel-open",c.panelOpen);const l=e.lastRoll;f.tooSmallState.textContent=`Turn ${e.turn} · ${le[de(e.scenario,e.active)].short} · ${ce(e)}${l?` · last roll ${l.dice.reduce((d,u)=>d+u,0)} (${le[de(e.scenario,l.side)].short})`:""}`}function Uo(){var n;const t=c.mode;f.modeBar.hidden=t.kind==="none";const e=[];if(t.kind==="move"){const i=Xe(),o=`${t.ids.length} counter${t.ids.length===1?"":"s"}`;t.override?f.modeText.textContent=`Override: tap where to put ${o}`:!i||!i.ok?(f.modeText.textContent=`Can't move: ${(i==null?void 0:i.reasons[0])??"nothing selected"}`,e.push('<button type="button" class="btn small danger" data-mode="override">Override</button>')):i.kind==="setup"?f.modeText.textContent=`Setup: tap a hex for ${o}`:i.kind==="advance"?(f.modeText.textContent=`Advance ${o}: tap an adjacent hex`,e.push('<button type="button" class="btn small" data-mode="override">Override</button>')):(f.modeText.textContent=`${i.kind==="rout"?"Rout":"Move"} ${o} · ${ge(i.left)} MF left`,i.canDoubleTime&&e.push(`<button type="button" class="btn small" data-mode="dt" aria-pressed="${t.dt}" title="+1 MF; the units become CX">Double Time</button>`),e.push('<button type="button" class="btn small" data-mode="override">Override</button>'))}else t.kind==="fire"?f.modeText.textContent=t.to?`Fire ${t.from} → ${t.to}: roll in the Actions panel`:`Fire from ${t.from}: tap a target`:t.kind==="los"&&(f.modeText.textContent=t.from?t.to?`LOS ${t.from} → ${t.to}`:"LOS: tap the target hex":"LOS: tap the firing hex");const s=t.kind==="move"&&!t.override&&!!((n=Xe())!=null&&n.ok);e.push(`<button type="button" class="btn small" data-mode="cancel">${s?"Done":"Cancel"}</button>`),f.modeActions.innerHTML=e.join("")}function $e(t){c.panelOpen=t,f.table.classList.toggle("panel-open",t),f.panelButton.setAttribute("aria-expanded",String(t)),kt(),requestAnimationFrame(()=>ke())}function ue(){var e;f.panel.querySelectorAll(".tab").forEach(s=>s.setAttribute("aria-selected",String(s.dataset.tab===c.tab))),f.panel.querySelectorAll(".tab-body").forEach(s=>s.hidden=s.dataset.body!==c.tab),c.tab==="actions"?Zn():c.tab==="stack"?ft():c.tab==="tray"?Qn():c.tab==="log"?ei():nn();const t=(((e=Y())==null?void 0:e.lines.length)??0)>c.logSeen&&c.tab!=="log";f.logBadge.hidden=!t}function Yn(t,e){const s=mt(t.scenario).cells.get(e);return s?`${Oe[s.terrain]}${s.road?" with road":""}${s.level?` · level ${s.level}`:""}`:""}function Vn(){const t=Y(),e=c.mode;if(!t||e.kind!=="fire"||!e.to)return null;const s=pt(t,e.ids,e.to,Pe());return s.ok?s:null}let De=null;function tn(){const t=c.session;return t?t.online?t.mySide():Ve(t.state).find(e=>!t.state.ready.has(e))??t.mySide():"n"}function Zn(){const t=c.session;if(!t)return;const e=c.mode;f.actionsPanel.innerHTML=No({state:t.state,me:t.mySide(),endAs:tn(),online:t.online,selected:c.selected,picked:c.picked,fire:e.kind==="fire"?{from:e.from,ids:e.ids,to:e.to}:null,firePlan:Vn(),fireError:De})}function Xn(){const t=Y(),e=c.mode;if(!t||e.kind!=="fire")return null;const s=new Map,n=Pe()==="a"?"b":"a";for(const[i,o]of t.stacks){if(!/^[A-Z]/.test(i)||!o.some(a=>{var l;return((l=t.counters.get(a))==null?void 0:l.side)===n}))continue;const r=pt(t,e.ids,i,Pe());r.ok&&s.set(i,`${r.column}${r.drm?r.drm>0?`+${r.drm}`:`${r.drm}`:""}`)}return s}function Jn(){const t=c.session;if(!t||!j())return;const e=Zt(t.state,tn());if(!e.enabled)return;if(!c.endArmed){c.endArmed=!0,ut(),window.setTimeout(()=>{c.endArmed=!1,ut()},3500),K(`Tap again to end the ${e.label.replace(/^End /,"")}. You can't go back.`,3500);return}if(c.endArmed=!1,c.mode={kind:"none"},f.toast.hidden=!0,t.online){D([{t:"end"}]);return}for(const n of Ve(t.state))t.state.ready.has(n)||(t.setLocalSide(n),D([{t:"end"}]));const s=Ve(t.state);t.setLocalSide(s.length===1?s[0]:t.state.active),c.traySide=t.mySide(),q()}f.actionsPanel.addEventListener("click",t=>{var i;const e=t.target.closest("button"),s=c.session;if(!e||!s)return;const n=e.dataset;if(n.act==="end-phase")c.endArmed=!0,Jn();else if(n.goto)yt(n.goto),/^[A-Z]/.test(n.goto)&&U.reveal(n.goto),c.tab="actions",ue();else if(n.pick)c.picked.has(n.pick)?c.picked.delete(n.pick):c.picked.add(n.pick),Zn(),ke();else if(n.act==="fire-start"&&c.selected){const o=je(c.selected).filter(r=>{var a;return!((a=s.state.counters.get(r))!=null&&a.fired)});c.mode={kind:"fire",from:c.selected,ids:o,to:null},De=null,(i=Xn())!=null&&i.size||(De="No enemy unit is in range and line of sight of this group."),ye()&&$e(!1),q()}else if(n.act==="fire-roll"){const o=c.mode,r=Vn();if(o.kind!=="fire"||!o.to||!r||!j())return;D([{t:"fire",ids:o.ids,to:o.to}]),c.mode={kind:"none"},c.picked.clear(),q()}else n.act==="fire-cancel"?(c.mode={kind:"none"},q()):n.rally&&j()?D([{t:"rally",id:n.rally}]):n.repair&&j()?D([{t:"repair",id:n.repair}]):n.cc&&j()&&D([{t:"cc",to:n.cc}])});f.endPhase.addEventListener("click",Jn);function Yo(t,e,s){const n=mt(t.scenario);if(!n.cells.has(e)||!n.cells.has(s))return"";const i=Rn(t,e,s),o=i.hexes.map(a=>{const l=n.cells.get(a),d=i.hindrances.find(h=>h.hex===a),u=i.blockedBy===a;return`<li class="${u?"los-block":d?"los-hinder":""}"><b>${a}</b> ${g(Oe[l.terrain])}${l.level?` · L${l.level}`:""}${u?" · <b>blocks</b>":d?` · hindrance +${d.value}`:""}</li>`}).join(""),r=i.clear?`<p class="los-verdict ok">Clear${i.hindrance?`, hindrance +${i.hindrance}`:""}</p>`:`<p class="los-verdict bad">${g(i.reason??"Blocked")}</p>`;return`<section class="los-report">
    <h3>Line of sight ${e} → ${s}</h3>
    ${r}
    <ol>${o}</ol>
    <p class="hint">Elevation is approximated with a straight sight line; blind hexes aren't modelled exactly.</p>
    <div class="row">
      <button type="button" class="btn small" data-act="los-share">Post to log</button>
      <button type="button" class="btn small" data-act="los-clear">Clear</button>
    </div>
  </section>`}function Vo(t){const e=t.scenario.sides[t.active].name,s=Ye(t),n=s==="setup"?"Setup: place units anywhere, then press End in the top bar (or the Actions tab) to start turn 1.":s==="move"?`Movement Phase: ${e} units move. Pick a stack, tap Move, then tap a highlighted hex.`:s==="rout"?"Rout Phase: broken units of both sides move.":s==="advance"?`Advance Phase: ${e} units may advance one hex.`:`${ce(t)} Phase: no movement.`;return`<p class="phase-banner">${g(n)}</p>`}function Zo(t,e){const s=t.counters.get(e);if(!s)return"";const n=[],i=t.move.spent.get(e)??0;return t.move.done.has(e)?n.push("finished moving"):i>0&&n.push(`${ge(i)} MF used`),s.cx&&n.push("CX"),s.pin&&n.push("pinned"),s.fired&&n.push(s.fired==="prep"?"prep fired":s.fired==="final"?"final fired":"advancing fired"),n.length?` · ${n.join(" · ")}`:""}function ft(){const t=c.session;if(!t)return;const e=t.state,s=c.mode.kind==="los"&&c.mode.from&&c.mode.to?{from:c.mode.from,to:c.mode.to}:c.shownLos,n=[];n.push(Vo(e)),s&&n.push(Yo(e,s.from,s.to));const i=c.selected;if(!i)n.push(`<p class="empty">Tap a hex to see its stack. ${ye()?"":"Drag a stack with the mouse to move it; "}use Move to send picked counters elsewhere.</p>`);else{const o=[...e.stacks.get(i)??[]].reverse(),r=!i.includes("-");n.push(`<div class="stack-head">
      <div><b class="stack-hex">${g(r?i:`${Q(e.scenario,i.endsWith("a")?"a":"b")} ${G(i)}`)}</b>
      <span class="hint">${r?g(Yn(e,i)):""}</span></div>
      <span class="hint">${o.length} counter${o.length===1?"":"s"}</span>
    </div>`);const a=e.turn===0||c.overrideTools;if(o.length){n.push(`<div class="row stack-tools">
        <button type="button" class="btn small primary" data-act="move" ${c.picked.size?"":"disabled"}>Move ${c.picked.size||""}</button>
        <button type="button" class="btn small" data-act="pick-all">${c.picked.size===o.length?"Pick none":"Pick all"}</button>
        ${r&&a?`<button type="button" class="btn small${e.turn?" danger":""}" data-act="to-pool" ${c.picked.size?"":"disabled"}>Off-board</button>
        <button type="button" class="btn small danger" data-act="to-elim" ${c.picked.size?"":"disabled"}>Eliminate</button>`:""}
      </div>`),n.push('<ul class="stack-list">');for(const l of o){const d=e.counters.get(l);if(!d)continue;const u=Ze(d),h=c.picked.has(l),p=u.type==="vehicle"||u.type==="gun";n.push(`<li class="stack-item${h?" picked":""}">
          <button type="button" class="pick" data-pick="${g(l)}" aria-pressed="${h}" title="${h?"Unpick":"Pick"} ${g(z(d))}">
            ${_t(d,44)}
            <span class="pick-text"><span class="pick-name">${g(z(d))}</span>
            <span class="pick-sub">${g(le[u.nat].name)} ${g(u.type==="fort"?"terrain":u.type)}${g(Zo(e,l))}</span></span>
          </button>
          <div class="item-tools">
            ${a?`<button type="button" class="tool slim" data-flip="${g(l)}" title="Flip${e.turn?" (override)":""}" aria-label="Flip ${g(z(d))}">
              <svg viewBox="0 0 20 20" aria-hidden="true"><path d="M4 7h10l-3-3M16 13H6l3 3" /></svg></button>`:""}
            ${p?`<button type="button" class="tool slim" data-rot="${g(l)}" title="Turn facing" aria-label="Turn ${g(z(d))}">
              <svg viewBox="0 0 20 20" aria-hidden="true"><path d="M15 10a5 5 0 1 1-2-4M13 2v4h4" /></svg></button>`:""}
            <button type="button" class="tool slim" data-top="${g(l)}" title="Put on top" aria-label="Put ${g(z(d))} on top">
              <svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 15V5M5 9l5-5 5 5" /></svg></button>
            <button type="button" class="tool slim" data-label="${g(l)}" title="Edit label" aria-label="Edit label of ${g(z(d))}">
              <svg viewBox="0 0 20 20" aria-hidden="true"><path d="M4 16h3l8-8-3-3-8 8z" /></svg></button>
            ${a?`<button type="button" class="tool slim" data-del="${g(l)}" title="Remove from table${e.turn?" (override)":""}" aria-label="Remove ${g(z(d))}">
              <svg viewBox="0 0 20 20" aria-hidden="true"><path d="M5 6h10M8 6V4h4v2M6 6l1 10h6l1-10" /></svg></button>`:""}
          </div>
        </li>`)}n.push("</ul>")}else n.push('<p class="empty">Empty. Add counters from the Counters tab.</p>')}n.push(Xo(e)),e.turn>0&&n.push(`<div class="override-row"><button type="button" class="btn small${c.overrideTools?" danger":""}" data-act="override-tools" aria-pressed="${c.overrideTools}">${c.overrideTools?"Hide override tools":"Override tools…"}</button><span class="hint">Flip, remove, eliminate or add units by hand. Every use is flagged in the log.</span></div>`),f.stackPanel.innerHTML=n.join("")}function Xo(t){const e=(s,n,i)=>{var r;const o=((r=t.stacks.get(s))==null?void 0:r.length)??0;return`<button type="button" class="box${c.selected===s?" active":""}" data-box="${s}">${rt(t,n)} ${i} <b>${o}</b></button>`};return`<div class="boxes"><span class="hint">Off-board</span>
    ${e("pool-a","a","ready")}${e("pool-b","b","ready")}${e("elim-a","a","lost")}${e("elim-b","b","lost")}
  </div>`}f.stackPanel.addEventListener("click",t=>{var i;const e=t.target.closest("button"),s=c.session;if(!e||!s)return;const n=e.dataset;if(n.pick)c.picked.has(n.pick)?c.picked.delete(n.pick):c.picked.add(n.pick),ft(),kt(),ke();else if(n.flip&&j())D([{t:"flip",id:n.flip,...ze()}]);else if(n.rot&&j())D([{t:"rot",id:n.rot,r:((((i=s.state.counters.get(n.rot))==null?void 0:i.rot)??0)+1)%6}]);else if(n.top&&j())D([{t:"top",id:n.top}]);else if(n.del&&j()){const o=s.state.counters.get(n.del);o&&(D([{t:"del",id:n.del,...ze()}]),K(`Removed ${z(o)}. Undo brings it back.`))}else if(n.label&&j())Jo(n.label);else if(n.box)yt(c.selected===n.box?null:n.box);else if(n.act==="move")en();else if(n.act==="pick-all"){const o=s.state.stacks.get(c.selected??"")??[];c.picked=c.picked.size===o.length?new Set:new Set(o),q()}else if((n.act==="to-pool"||n.act==="to-elim")&&c.selected&&j()){const o=je(c.selected),r={a:[],b:[]};for(const d of o){const u=s.state.counters.get(d);u&&r[u.side==="b"?"b":"a"].push(d)}const a=n.act==="to-pool"?"pool":"elim",l=[];r.a.length&&l.push({t:"move",ids:r.a,to:`${a}-a`,...ze()}),r.b.length&&l.push({t:"move",ids:r.b,to:`${a}-b`,...ze()}),D(l),c.picked.clear()}else if(n.act==="override-tools")c.overrideTools=!c.overrideTools,ft();else if(n.act==="los-share"&&j()){const o=c.mode.kind==="los"&&c.mode.from&&c.mode.to?{from:c.mode.from,to:c.mode.to}:c.shownLos;o&&D([{t:"los",from:o.from,to:o.to}]),K("LOS posted to the log")}else n.act==="los-clear"&&(c.mode={kind:"none"},c.shownLos=null,q())});function Jo(t){var r;const e=(r=Y())==null?void 0:r.counters.get(t),s=f.stackPanel.querySelector(`[data-pick="${CSS.escape(t)}"] .pick-name`);if(!e||!s)return;const n=document.createElement("input");n.type="text",n.maxLength=24,n.className="label-input",n.value=e.label??Ze(e).label,n.setAttribute("aria-label","Counter label. Leave empty to restore the printed one."),s.replaceWith(n),n.focus(),n.select();let i=!1;const o=a=>{if(i)return;i=!0;const l=n.value.trim().slice(0,24),d=e.label??Ze(e).label;a&&l!==d?D([{t:"label",id:t,text:l}]):ft()};n.addEventListener("click",a=>a.stopPropagation()),n.addEventListener("keydown",a=>{a.key==="Enter"?o(!0):a.key==="Escape"&&o(!1),a.stopPropagation()}),n.addEventListener("blur",()=>o(!0))}function Qn(){const t=c.session;if(!t)return;const e=t.state,n=["a","b","n"].map(a=>{const l=a==="n"?"mk":e.scenario.sides[a].nat;return`<button type="button" role="radio" class="seg-btn" data-tray="${a}" aria-checked="${c.traySide===a}">
        <span class="swatch" style="background:${oe[l].face};border-color:${oe[l].edge}"></span>${g(le[l].name)}</button>`}).join(""),i=c.traySide==="n"?"mk":e.scenario.sides[c.traySide].nat,o=c.selected?c.selected.includes("-")?G(c.selected):`hex ${c.selected}`:c.traySide==="n"?"a selected hex":"the off-board box",r=bs(i).map(a=>{const l={id:"tray",k:a.k,face:0,rot:0};return`<button type="button" class="tray-item" data-add="${a.k}" title="Add ${g(a.label)}">${_t(l,46)}<span>${g(a.tag?`${a.label} ${a.tag}`:a.label)}</span></button>`}).join("");f.trayPanel.innerHTML=`<div class="seg" role="radiogroup" aria-label="Counter set">${n}</div>
    <p class="hint">Tap a counter to add it to ${g(o)}. Edit its label afterwards to match your unit tables.</p>
    <div class="tray">${r}</div>`}f.trayPanel.addEventListener("click",t=>{const e=t.target.closest("button");e&&(e.dataset.tray?(c.traySide=e.dataset.tray,Qn()):e.dataset.add&&Go(e.dataset.add,c.traySide))});const Qo={1:[[0,0]],2:[[-1,-1],[1,1]],3:[[-1,-1],[0,0],[1,1]],4:[[-1,-1],[1,-1],[-1,1],[1,1]],5:[[-1,-1],[1,-1],[0,0],[-1,1],[1,1]],6:[[-1,-1],[1,-1],[-1,0],[1,0],[-1,1],[1,1]]};function es(t,e,s=34){const n=(Qo[t]??[]).map(([i,o])=>`<circle cx="${12+i*6}" cy="${12+o*6}" r="2.3" fill="${e?"#fff4dc":"#1b1a14"}"/>`).join("");return`<svg class="die" viewBox="0 0 24 24" width="${s}" height="${s}" role="img" aria-label="${t}"><rect x="1" y="1" width="22" height="22" rx="4.5" fill="${e?"#a3321f":"#f3ecd8"}" stroke="#1b1a14" stroke-width="1.2"/>${n}</svg>`}function ei(){const t=c.session;if(!t)return;const e=t.state;c.logSeen=e.lines.length,f.logBadge.hidden=!0;const s=t.confirmed.length;f.logList.innerHTML=e.lines.slice(-300).map(n=>{var u;const i=de(e.scenario,n.side),o=oe[i],r=new Date(n.at*1e3).toLocaleTimeString(void 0,{hour:"2-digit",minute:"2-digit"}),a=n.dice?`<span class="log-dice">${n.dice.map((h,p)=>es(h,p===0,18)).join("")}</span>`:"",l=n.i>=s?" pending":"",d=n.kind==="chat"?`<span class="chat-text">${g(n.text)}</span>`:n.kind==="los"?`<button type="button" class="link" data-los="${n.los.from}:${n.los.to}">${g(n.text)}</button>`:g(n.text);return`<li class="log-line kind-${n.kind}${n.undone?" undone":""}${l}">
        <span class="log-side" style="background:${o.face};border-color:${o.edge}" title="${g(Q(e.scenario,n.side))}"></span>
        <span class="log-body">${a}${d}${(u=n.detail)!=null&&u.length?`<ul class="log-detail">${n.detail.map(h=>`<li>${g(h)}</li>`).join("")}</ul>`:""}</span><time class="log-time">${r}</time></li>`}).join(""),f.logList.scrollTop=f.logList.scrollHeight}f.logList.addEventListener("click",t=>{const e=t.target.closest("[data-los]");if(!e)return;const[s,n]=e.dataset.los.split(":");c.shownLos={from:s,to:n},c.mode={kind:"none"},U.reveal(n),c.tab="stack",q()});function ts(){const t=f.chatInput.value.trim();if(!(!t||!c.session)){if(c.session.online&&!c.pubkey){K("Sign in to chat at an online table.");return}D([{t:"chat",text:t.slice(0,280)}]),f.chatInput.value=""}}f.chatSend.addEventListener("click",ts);Jt(f.chatInput,ts);Jt(f.rollWhy,()=>Je(2));function nn(){const t=c.session;if(!t)return;const s=t.state.scenario,n=t.meta,i=t.mySide(),o=d=>{const u=d==="a"?n==null?void 0:n.seatA:n==null?void 0:n.seatB,h=s.sides[d].nat,p=t.online?u?u===t.me?"You":Bt(u):"Open seat":"This device",k=t.online&&!u&&i==="n"?`<button type="button" class="btn small primary" data-join="${d}" ${c.pubkey?"":"disabled"}>Sit here</button>`:"";return`<li class="seat-row"><span class="swatch" style="background:${oe[h].face};border-color:${oe[h].edge}"></span>
      <span><b>${g(s.sides[d].name)}</b>${s.first===d?" · moves first":""}<br><span class="hint">${g(p)}</span></span>${k}</li>`},r=t.online?ko(t.saved.release,t.saved.id):"",a=t.status.live,l=a==="connected"?`Live: connected to ${t.status.peers} other player${t.status.peers===1?"":"s"}. Moves appear instantly.`:a==="waiting"?"Live link open. Waiting for your opponent to open this table.":a==="connecting"?"Opening live link…":"Live link off. Moves still sync every few seconds.";f.infoPanel.innerHTML=`
    <section class="info-block">
      <h3>${g(s.name)}</h3>
      <p>${g(s.brief)}</p>
      <p><b>Length:</b> ${s.turns} game turns. <b>Victory:</b> ${g(s.victory)}</p>
      <ul class="setup">
        <li><b>${g(s.sides.a.name)}:</b> ${g(s.sides.a.setup)}</li>
        <li><b>${g(s.sides.b.name)}:</b> ${g(s.sides.b.setup)}</li>
      </ul>
    </section>
    <section class="info-block">
      <h3>Seats</h3>
      <ul class="seats">${o("a")}${o("b")}</ul>
      ${t.online&&i==="n"&&!c.pubkey?'<p class="hint">Sign in with your Nostr account to take a seat. You can watch without one.</p>':""}
      ${t.online?"":'<p class="hint">Hotseat: use the side chip in the top bar to switch who is acting. Dice are rolled on this device.</p>'}
    </section>
    ${t.online?`<section class="info-block">
      <h3>Invite</h3>
      <p class="hint">Send this code to your opponent. They paste it under “Join with a code”. Anyone with it can watch.</p>
      <div class="code-row"><input id="shareCode" type="text" readonly value="${g(r)}" aria-label="Table code" data-napplet-select="text" /><button type="button" class="btn small" data-act="copy">Copy</button></div>
    </section>
    <section class="info-block">
      <h3>Live play</h3>
      <p class="hint">${g(l)}</p>
      ${t.liveAvailable()?`<button type="button" class="btn small" data-act="live">${a==="off"?"Go live":"Stop live link"}</button>`:'<p class="hint">This host has no peer connections; the table still syncs through the server.</p>'}
    </section>`:""}
    <section class="info-block">
      <h3>Rules</h3>
      <ul class="setup">${Ss.map(d=>`<li>${g(d)}</li>`).join("")}</ul>
      <p class="hint">Blocked moves show the reason. Override moves anyway; overrides are flagged in red in the log.</p>
    </section>
    <section class="info-block">
      <h3>Map key</h3>
      <ul class="key">
        <li><span class="key-sw" style="background:#46612f"></span>Woods</li>
        <li><span class="key-sw" style="background:#7f9152"></span>Brush</li>
        <li><span class="key-sw" style="background:#dcc376"></span>Grain</li>
        <li><span class="key-sw" style="background:#6f8c45;border-radius:50%"></span>Orchard</li>
        <li><span class="key-sw" style="background:#a5724a"></span>Wooden building</li>
        <li><span class="key-sw" style="background:#8f8b83"></span>Stone building</li>
        <li><span class="key-sw" style="background:#86a9b6"></span>Water</li>
        <li><span class="key-sw" style="background:#4f7a72"></span>Marsh</li>
        <li><span class="key-sw" style="background:#d5c296"></span>Hill level 1</li>
        <li><span class="key-sw" style="background:#c3a874"></span>Hill level 2</li>
        <li><span class="key-line" style="background:#3a5527"></span>Hedge</li>
        <li><span class="key-line" style="background:#5e5a54"></span>Wall</li>
        <li><span class="key-line" style="background:#ddd3b4;outline:1px solid #8f8466"></span>Road</li>
      </ul>
    </section>
    <div class="row"><button type="button" class="btn small" data-act="leave">Back to your tables</button></div>
    <p class="hint">${t.online?`Table ${g(Bt(t.saved.id))} · dice are rolled by the table server and logged for both players.`:""}</p>`}f.infoPanel.addEventListener("click",async t=>{const e=t.target.closest("button"),s=c.session;if(!(!e||!s))if(e.dataset.join){e.disabled=!0;try{await s.join(e.dataset.join),c.traySide=s.mySide()==="b"?"b":"a",K(`You are playing ${Q(s.state.scenario,s.mySide())}`),q()}catch(n){K(n instanceof Error?n.message:"Could not take that seat."),e.disabled=!1}}else if(e.dataset.act==="copy"){const n=f.infoPanel.querySelector("#shareCode");if(!n)return;n.select();try{await navigator.clipboard.writeText(n.value),K("Code copied")}catch{K("Copy the selected code with your keyboard or long-press.")}}else e.dataset.act==="leave"?f.back.click():e.dataset.act==="live"&&(s.status.live==="off"?await s.startLive():await s.stopLive(),nn())});function kt(){const t=c.session,e=!!(t&&c.selected&&!c.panelOpen&&c.mode.kind==="none");if(f.strip.hidden=!e,!e||!t)return;const s=c.selected,n=[...t.state.stacks.get(s)??[]].reverse();f.stripHex.innerHTML=`<b>${g(s.includes("-")?G(s):s)}</b><span>${g(s.includes("-")?"":Yn(t.state,s))}</span>`,f.stripCounters.innerHTML=n.length?n.map(i=>{const o=t.state.counters.get(i);return`<button type="button" class="strip-ctr" data-pick="${g(i)}" aria-pressed="${c.picked.has(i)}" title="${g(z(o))}">${_t(o,38)}</button>`}).join(""):'<span class="hint">Empty hex</span>',f.stripMove.disabled=!c.picked.size}f.stripCounters.addEventListener("click",t=>{const e=t.target.closest("[data-pick]");if(!e)return;const s=e.dataset.pick;c.picked.has(s)?c.picked.delete(s):c.picked.add(s),kt(),ke()});f.stripMove.addEventListener("click",en);f.stripMore.addEventListener("click",()=>{c.tab="stack",$e(!0),ue()});let bn=0,Et="";function K(t,e=2600){t===Et&&!f.toast.hidden||(Et=t,f.toast.textContent=t,f.toast.hidden=!1,window.clearTimeout(bn),bn=window.setTimeout(()=>{f.toast.hidden=!0,Et=""},e))}let yn=0;function ti(t){const e=Y();if(!e||!t.d)return;const s=t.a.t==="roll"?t.d:t.d.slice(0,t.a.t==="repair"?1:2),n=s.reduce((a,l)=>a+l,0),i=de(e.scenario,t.side),o=e.lines.find(a=>a.i===t.i),r=t.a.t==="roll"?t.a.why:(o==null?void 0:o.text.replace(/^[A-Z]{2} /,"").replace(/^.*?→ /,""))??t.a.t;f.diceTray.innerHTML=`<div class="dice-faces">${s.map((a,l)=>es(a,l===0&&s.length===2)).join("")}</div>
    <div class="dice-text"><b>${s.length===2?"DR":"dr"} ${n}</b><span>${g(le[i].short)}${r?` · ${g(r)}`:""}</span></div>`,f.diceTray.hidden=!1,f.diceTray.classList.remove("pop"),f.diceTray.offsetWidth,f.diceTray.classList.add("pop"),window.clearTimeout(yn),yn=window.setTimeout(()=>f.diceTray.hidden=!0,8e3)}f.diceTray.addEventListener("click",()=>f.diceTray.hidden=!0);f.rollDR.addEventListener("click",()=>Je(2));f.rolldr.addEventListener("click",()=>Je(1));f.los.addEventListener("click",()=>{c.mode=c.mode.kind==="los"?{kind:"none"}:{kind:"los",from:c.selected&&!c.selected.includes("-")?c.selected:null,to:null},c.shownLos=null,c.mode.kind==="los"&&ye()&&$e(!1),q()});f.undo.addEventListener("click",()=>{const t=c.session;if(!t||!j())return;const e=jn(t.state,t.me);e&&D([{t:"undo",ref:e.i}])});f.modeActions.addEventListener("click",t=>{const e=t.target.closest("[data-mode]"),s=c.mode;if(e){if(e.dataset.mode==="cancel")c.mode={kind:"none"};else if(e.dataset.mode==="dt"&&s.kind==="move")c.mode={...s,dt:!s.dt};else if(e.dataset.mode==="override"&&s.kind==="move"){const n=Xe(),i=n&&!n.ok?n.reasons[0]:"moved outside the movement rules";c.mode={...s,override:i.slice(0,120)},K("Override: this move skips the rules and is flagged in the log.",4e3)}q()}});f.panelButton.addEventListener("click",()=>{$e(!c.panelOpen),c.panelOpen&&ue()});f.panelClose.addEventListener("click",()=>$e(!1));f.panel.querySelectorAll(".tab").forEach(t=>t.addEventListener("click",()=>{c.tab=t.dataset.tab,ue()}));const at=v("#soundButton");function sn(){at.setAttribute("aria-pressed",String(!Ue())),at.title=Ue()?"Mute battle sounds":"Turn battle sounds on",at.querySelector(".tool-label").textContent=Ue()?"Sound":"Muted"}at.addEventListener("click",()=>{Wn(!Ue()),sn(),ni()});async function ni(){if(re("storage"))try{await Qe.setItem("prefs",JSON.stringify({sound:Ue()}))}catch{}}async function si(){if(re("storage")){try{const t=JSON.parse(await Qe.getItem("prefs")??"{}");typeof t.sound=="boolean"&&Wn(t.sound)}catch{}sn()}}si();sn();f.zoomIn.addEventListener("click",()=>U.zoomBy(1.25));f.zoomOut.addEventListener("click",()=>U.zoomBy(.8));f.zoomFit.addEventListener("click",()=>U.fit());document.addEventListener("keydown",t=>{if(!(!c.session||f.table.hidden||t.target.closest("input, textarea, select"))){if(t.key==="Escape")c.mode.kind!=="none"?c.mode={kind:"none"}:c.shownLos?c.shownLos=null:yt(null),q();else if(t.key==="m"&&c.selected)en();else if(t.key==="l")f.los.click();else if(t.key==="r")Je(2);else if(t.key==="d")Je(1);else if(t.key==="z"&&(t.ctrlKey||t.metaKey)||t.key==="u")f.undo.click();else if(t.key==="f"&&c.selected&&j()){const s=je(c.selected);s.length&&D(s.map(n=>({t:"flip",id:n})))}else if(t.key==="+"||t.key==="=")U.zoomBy(1.25);else if(t.key==="-")U.zoomBy(.8);else return;t.preventDefault()}});let kn=ye();window.addEventListener("resize",()=>{const t=ye();t!==kn&&c.session&&$e(!t),kn=t});gs(()=>{c.session&&ke()});window.addEventListener("pagehide",()=>{Qt(),it==null||it.close()});Ho();_n();jo().then(gt);gt();
</script>
    <style>:root{--felt: #151a15;--felt-2: #1c231c;--panel: #222a21;--panel-2: #2a3328;--panel-3: #333e30;--line: #3d4839;--ink: #efe6cb;--ink-dim: #b2aa8f;--ink-faint: #858069;--brass: #d6a93a;--brass-hi: #f2c94c;--brass-ink: #1d1706;--danger: #c4573c;--ok: #7fb069;--warn: #e3b341;--paper: #efe7cf;--radius: 6px;--hud-h: 48px;--panel-w: 340px;--tap: 40px;--font: "Bahnschrift", "DIN Alternate", "DIN Condensed", "Roboto Condensed", "Arial Narrow", "Liberation Sans Narrow", system-ui, sans-serif;--mono: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;color-scheme:dark;--napplet-text-selection: none}@media(pointer:coarse){:root{--tap: 44px;--hud-h: 52px}}*{box-sizing:border-box}html,body,#app{height:100%;margin:0}body{background:var(--felt);color:var(--ink);font-family:var(--font);font-size:15px;line-height:1.35;overflow:hidden;-webkit-font-smoothing:antialiased;-webkit-user-select:var(--napplet-text-selection);user-select:var(--napplet-text-selection)}input,select,textarea,[data-napplet-select=text]{-webkit-user-select:text;user-select:text}button,input,select{font:inherit;color:inherit}[hidden]{display:none!important}svg{flex:none}.app{position:relative;overflow:hidden;background:radial-gradient(ellipse at 30% 20%,rgba(80,100,70,.18),transparent 60%),radial-gradient(ellipse at 80% 90%,rgba(60,40,20,.25),transparent 60%),var(--felt)}.btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;min-height:var(--tap);padding:0 14px;border:1px solid var(--line);border-radius:var(--radius);background:var(--panel-2);color:var(--ink);font-weight:700;letter-spacing:.02em;cursor:pointer;transition:background .12s,border-color .12s}.btn:hover:not(:disabled){background:var(--panel-3);border-color:#56634f}.btn.primary{background:var(--brass);border-color:#a98222;color:var(--brass-ink)}.btn.primary:hover:not(:disabled){background:var(--brass-hi)}.btn.danger{color:#ffd9cf;border-color:#6f3022;background:#45251c}.btn.small{min-height:34px;padding:0 10px;font-size:13.5px}@media(pointer:coarse){.btn.small{min-height:42px}}.btn:disabled,.tool:disabled{opacity:.42;cursor:not-allowed}.tool{display:inline-flex;align-items:center;justify-content:center;gap:5px;min-width:var(--tap);height:var(--tap);padding:0 8px;border:1px solid transparent;border-radius:var(--radius);background:transparent;color:var(--ink);font-weight:700;cursor:pointer}.tool:hover:not(:disabled){background:var(--panel-3)}.tool[aria-pressed=true]{background:var(--brass);color:var(--brass-ink)}.tool svg{width:20px;height:20px;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}.tool.slim{min-width:32px;padding:0 4px}@media(pointer:coarse){.tool.slim{min-width:40px}}:focus-visible{outline:2px solid var(--brass-hi);outline-offset:2px}.hint{color:var(--ink-dim);font-size:13px;margin:6px 0}.row{display:flex;flex-wrap:wrap;gap:8px;align-items:center}.swatch,.chip-swatch{display:inline-block;width:14px;height:14px;border-radius:3px;border:1.5px solid #000;flex:none}.chip-swatch{width:12px;height:12px;margin-right:4px;vertical-align:-1px}.seg{display:flex;flex-wrap:wrap;gap:6px}.seg-btn{display:inline-flex;align-items:center;gap:7px;min-height:var(--tap);padding:0 12px;border:1px solid var(--line);border-radius:var(--radius);background:var(--panel);cursor:pointer;font-weight:700}.seg-btn[aria-checked=true]{border-color:var(--brass);background:#3a3520;box-shadow:inset 0 0 0 1px var(--brass)}.field{display:grid;gap:5px;margin:0 0 12px;padding:0;border:0}.field>span,.field>legend{color:var(--ink-dim);font-size:12.5px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;padding:0;margin-bottom:5px}input[type=text],select{width:100%;min-height:var(--tap);padding:0 10px;border:1px solid var(--line);border-radius:var(--radius);background:#161b15;color:var(--ink)}input[type=text]:focus,select:focus{border-color:var(--brass);outline:none}.lobby{height:100%;overflow:auto;padding:clamp(12px,3vw,32px)}.lobby-inner{max-width:1080px;margin:0 auto}.lobby-intro{display:grid;gap:4px;margin-bottom:18px;padding-left:14px;border-left:4px solid var(--brass)}.lobby-kicker{margin:0;color:var(--brass);font-size:13px;font-weight:800;letter-spacing:.22em;text-transform:uppercase}.lobby-lede{margin:0;max-width:62ch;color:var(--ink-dim)}.lobby-grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(min(100%,300px),1fr));align-items:start}.card{padding:16px;border:1px solid var(--line);border-radius:8px;background:linear-gradient(180deg,var(--panel-2),var(--panel));box-shadow:0 8px 24px #00000059}.card h2{margin:0 0 12px;font-size:15px;letter-spacing:.14em;text-transform:uppercase}.card-new{grid-row:span 2}.table-list{list-style:none;margin:0;padding:0;display:grid;gap:6px}.table-item{display:flex;gap:4px;align-items:stretch}.table-open{flex:1;display:grid;grid-template-columns:auto 1fr;gap:2px 10px;align-items:center;min-height:48px;padding:6px 10px;border:1px solid var(--line);border-radius:var(--radius);background:var(--panel);text-align:left;cursor:pointer}.table-open:hover{border-color:var(--brass)}.table-open .swatch{grid-row:span 2}.table-name{font-weight:700}.table-meta{color:var(--ink-dim);font-size:12.5px}.identity-note{margin-top:16px;font-family:var(--mono);font-size:12px}.table{position:relative;display:grid;height:100%;grid-template-rows:var(--hud-h) minmax(0,1fr);grid-template-columns:minmax(0,1fr) 0;grid-template-areas:"hud hud" "board panel"}.table.panel-open{grid-template-columns:minmax(0,1fr) var(--panel-w)}@media(min-width:1200px){:root{--panel-w: 380px}}.hud{grid-area:hud;display:flex;align-items:center;gap:4px;padding:0 6px;border-bottom:1px solid #0b0e0b;background:linear-gradient(180deg,#2b3429,#222a21);box-shadow:0 2px 10px #00000073;z-index:3;overflow-x:auto;scrollbar-width:none}.phase{flex:none;display:flex;align-items:center;gap:2px;padding:0 2px;border:1px solid #11160f;border-radius:var(--radius);background:#161b15;min-width:0}.phase-text{display:flex;align-items:baseline;gap:7px;padding:0 4px;white-space:nowrap}.turn{color:var(--brass-hi);font-weight:800;font-variant-numeric:tabular-nums}.chip{font-weight:800;font-size:13px}.phase-name{font-weight:800;letter-spacing:.06em;text-transform:uppercase;font-size:13.5px}.hud-tools{display:flex;gap:2px;margin-left:4px}.tool.dice{font-size:14px;letter-spacing:.04em;border-color:#6b2a1c;background:#5b2318;color:#ffe8d8}.tool.dice:hover:not(:disabled){background:#7a2f20}.tool-label{font-size:13px}.hud-right{display:flex;align-items:center;gap:6px;margin-left:auto}.seat{display:inline-flex;align-items:center;gap:2px;font-weight:800;font-size:13px;white-space:nowrap}.seat-label{color:var(--ink-dim);font-weight:600;margin-left:3px}.seat-toggle{display:inline-flex;align-items:center;min-height:34px;padding:0 8px;border:1px dashed #56634f;border-radius:var(--radius);background:transparent;font-weight:800;cursor:pointer}.sync{width:11px;height:11px;border-radius:50%;background:var(--ink-faint);box-shadow:0 0 0 2px #11160f;flex:none}.sync[data-state=ok]{background:var(--ok)}.sync[data-state=live]{background:#5fd0ff;box-shadow:0 0 0 2px #11160f,0 0 8px #5fd0ff}.sync[data-state=saving]{background:var(--warn);animation:blink 1s infinite}.sync[data-state=offline],.sync[data-state=error]{background:var(--danger)}.sync[data-state=local]{background:#cfc5a5}@keyframes blink{50%{opacity:.35}}.board-wrap{grid-area:board;position:relative;min-height:0;overflow:hidden}.board-host{position:absolute;top:0;right:0;bottom:0;left:0}.board{display:block;width:100%;height:100%;touch-action:none;cursor:grab}.board:active{cursor:grabbing}.ctr-text{font-family:var(--font)}.coord{font-family:var(--font);fill:#4634189e;font-weight:700;pointer-events:none}.ghost{pointer-events:none}.zoom{position:absolute;right:10px;bottom:10px;display:flex;flex-direction:column;gap:4px;padding:4px;border:1px solid var(--line);border-radius:8px;background:#1c231be6;z-index:2}.zoom .tool{font-size:20px;font-weight:600}.board-tools{position:absolute;top:10px;left:10px;display:flex;flex-direction:column;gap:4px;padding:4px;border:1px solid var(--line);border-radius:8px;background:#1c231be6;z-index:2}.board-tools .tool{justify-content:flex-start}.strip:not([hidden])~.dice-tray{bottom:84px}.too-small{display:none}@media(max-width:259px),(max-height:199px){.table .too-small{position:absolute;top:0;right:0;bottom:0;left:0;z-index:20;display:grid;align-content:center;gap:4px;padding:10px;background:var(--felt);text-align:center;font-size:13px}.too-small p{margin:0}.too-small p+p{color:var(--ink-dim)}}.mode-bar{position:absolute;top:10px;left:50%;transform:translate(-50%);display:flex;align-items:center;gap:10px;max-width:calc(100% - 20px);padding:6px 6px 6px 14px;border:1px solid #a98222;border-radius:999px;background:#28220ef0;color:var(--brass-hi);font-weight:800;z-index:2;white-space:nowrap}.mode-bar span{overflow:hidden;text-overflow:ellipsis}.toast{position:absolute;top:58px;left:50%;transform:translate(-50%);max-width:min(440px,calc(100% - 24px));padding:9px 14px;border:1px solid var(--line);border-radius:var(--radius);background:#141813f2;box-shadow:0 6px 20px #00000080;font-size:14px;z-index:4;text-align:center}.dice-tray{position:absolute;left:10px;bottom:10px;display:flex;align-items:center;gap:10px;padding:8px 12px 8px 8px;border:1px solid #6b2a1c;border-radius:10px;background:#1e140ff0;box-shadow:0 8px 22px #0000008c;cursor:pointer;z-index:2}.dice-tray.pop .die{animation:tumble .45s ease-out}.dice-tray.pop .die:nth-child(2){animation-delay:.06s}@keyframes tumble{0%{transform:rotate(-200deg) scale(.4)}70%{transform:rotate(12deg) scale(1.08)}to{transform:none}}.dice-faces{display:flex;gap:6px}.dice-text{display:grid}.dice-text b{font-size:20px;color:#ffe8d8}.dice-text span{color:var(--ink-dim);font-size:12.5px;max-width:16ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.strip{position:absolute;left:8px;right:64px;bottom:8px;display:grid;grid-template-columns:auto 1fr auto;align-items:center;gap:8px;padding:6px 8px;border:1px solid var(--line);border-radius:10px;background:#1c231bf2;box-shadow:0 6px 18px #00000080;z-index:2}.strip-hex{display:grid;line-height:1.15}.strip-hex span{color:var(--ink-dim);font-size:11.5px;max-width:11ch}.strip-counters{display:flex;gap:4px;overflow-x:auto;scrollbar-width:none;min-width:0}.strip-ctr{padding:1px;border:2px solid transparent;border-radius:5px;background:none;cursor:pointer}.strip-ctr[aria-pressed=true]{border-color:var(--brass-hi)}.strip-ctr[aria-pressed=false]{opacity:.55}.strip-actions{display:flex;gap:6px}.panel{grid-area:panel;display:flex;flex-direction:column;min-height:0;min-width:0;overflow:hidden;border-left:1px solid #0b0e0b;background:var(--panel);z-index:3}.table:not(.panel-open) .panel{visibility:hidden}.tabs{display:flex;align-items:stretch;gap:2px;padding:6px 6px 0;border-bottom:1px solid var(--line);background:var(--panel-2)}.tab{position:relative;flex:1;min-height:var(--tap);border:0;border-radius:var(--radius) var(--radius) 0 0;background:transparent;color:var(--ink-dim);font-weight:800;font-size:13px;letter-spacing:.08em;text-transform:uppercase;cursor:pointer}.tab[aria-selected=true]{background:var(--panel);color:var(--brass-hi);box-shadow:inset 0 3px 0 var(--brass)}.badge-dot{position:absolute;top:8px;right:8px;width:7px;height:7px;border-radius:50%;background:var(--brass-hi)}.panel-close{display:none}.tab-body{flex:1;min-height:0;overflow:auto;padding:12px}.tab-log{display:flex;flex-direction:column;padding:0}.empty{color:var(--ink-dim);padding:8px 2px;margin:0}.stack-head{display:flex;justify-content:space-between;align-items:flex-start;gap:8px;margin-bottom:8px}.stack-head>div{display:grid}.stack-hex{font-size:22px;letter-spacing:.04em}.stack-tools{margin-bottom:10px}.stack-list{list-style:none;margin:0;padding:0;display:grid;gap:6px}.stack-item{display:flex;align-items:center;gap:4px;padding:3px;border:1px solid var(--line);border-radius:7px;background:#1c231b}.stack-item.picked{border-color:var(--brass);background:#2f2c1a}.pick{flex:1;display:flex;align-items:center;gap:8px;min-width:0;padding:2px;border:0;background:none;text-align:left;cursor:pointer}.pick-text{display:grid;min-width:0}.pick-name{font-weight:800;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pick-sub{color:var(--ink-dim);font-size:12px}.item-tools{display:flex;flex-wrap:wrap;justify-content:flex-end}.boxes{display:flex;flex-wrap:wrap;gap:6px;align-items:center;margin-top:16px;padding-top:12px;border-top:1px dashed var(--line)}.boxes .hint{width:100%;margin:0;text-transform:uppercase;letter-spacing:.1em;font-size:11.5px;font-weight:800}.box{display:inline-flex;align-items:center;gap:4px;min-height:36px;padding:0 10px;border:1px solid var(--line);border-radius:var(--radius);background:#1c231b;font-size:13px;cursor:pointer}.box.active{border-color:var(--brass);background:#2f2c1a}.los-report{margin-bottom:14px;padding:10px;border:1px solid #8a6d22;border-radius:8px;background:#2b2816}.los-report h3{margin:0 0 4px;font-size:15px;color:var(--brass-hi)}.los-report ol{margin:6px 0;padding-left:20px;font-size:13.5px}.los-report .los-block{color:#ffcf9e}.tray{display:grid;grid-template-columns:repeat(auto-fill,minmax(76px,1fr));gap:6px}.tray-item{display:grid;justify-items:center;gap:2px;padding:6px 2px;border:1px solid var(--line);border-radius:7px;background:#1c231b;font-size:11.5px;font-weight:700;color:var(--ink-dim);cursor:pointer;text-align:center}.tray-item:hover{border-color:var(--brass);color:var(--ink)}.log{flex:1;min-height:0;overflow:auto;list-style:none;margin:0;padding:8px 10px;display:flex;flex-direction:column;gap:2px}.log-line{display:grid;grid-template-columns:8px 1fr auto;gap:8px;align-items:start;padding:4px 2px;font-size:13.5px;border-bottom:1px solid rgba(255,255,255,.04)}.log-side{width:8px;height:100%;min-height:16px;border-radius:2px;border:1px solid}.log-time{color:var(--ink-faint);font-size:11px;font-variant-numeric:tabular-nums;padding-top:2px}.log-dice{display:inline-flex;gap:3px;margin-right:6px;vertical-align:-4px}.kind-phase{color:var(--brass-hi);font-weight:800;letter-spacing:.04em;padding-top:8px}.kind-chat .chat-text{color:#fff;font-style:italic}.kind-info{color:var(--ink-dim)}.log-line.undone .log-body{text-decoration:line-through;opacity:.5}.log-line.pending{opacity:.6}.link{padding:0;border:0;background:none;color:#9fd3ff;text-decoration:underline;cursor:pointer;text-align:left}.chat{display:grid;gap:6px;padding:8px 10px 10px;border-top:1px solid var(--line);background:var(--panel-2)}.chat-row{display:flex;gap:6px}.info-block{margin-bottom:16px}.info-block h3{margin:0 0 6px;font-size:13px;letter-spacing:.14em;text-transform:uppercase;color:var(--brass)}.info-block p{margin:0 0 8px}.setup{margin:0;padding-left:18px;font-size:14px}.seats{list-style:none;margin:0;padding:0;display:grid;gap:6px}.seat-row{display:flex;align-items:center;gap:10px;padding:8px;border:1px solid var(--line);border-radius:var(--radius);background:#1c231b}.seat-row>span:nth-child(2){flex:1}.code-row{display:flex;gap:6px}.code-row input{font-family:var(--mono);font-size:12px}.key{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(auto-fill,minmax(130px,1fr));gap:4px 10px;font-size:13px}.key li{display:flex;align-items:center;gap:7px}.key-sw{width:14px;height:14px;border-radius:3px}.key-line{width:16px;height:4px;border-radius:2px}@media(max-width:759px){.table,.table.panel-open{grid-template-columns:minmax(0,1fr);grid-template-areas:"hud" "board"}.panel{position:absolute;left:0;right:0;bottom:0;height:min(64%,520px);border-left:0;border-top:1px solid #0b0e0b;border-radius:12px 12px 0 0;box-shadow:0 -10px 30px #0000008c;transform:translateY(0);transition:transform .2s ease}.table:not(.panel-open) .panel{transform:translateY(105%)}.panel-close{display:inline-flex;align-self:center}.tool-label{display:none}}@media(max-width:560px){.seat-label,.phase-name{display:none}.mode-bar{left:62px;right:62px;transform:none;justify-content:space-between;border-radius:10px;white-space:normal;font-size:13.5px;line-height:1.2}.hud{gap:2px;padding:0 3px}}@media(max-width:420px){#backButton{display:none}.seat-toggle{padding:0 5px}}@media(max-width:380px){.chip{display:none}.strip{right:8px;bottom:8px;grid-template-columns:1fr auto}.strip-hex{display:none}.zoom{bottom:auto;top:8px}}@media(max-height:420px)and (max-width:759px){.panel{height:82%}}@media(max-height:420px){.tab-body{padding:8px}.zoom{flex-direction:row}}@media(max-width:300px),(max-height:220px){:root{--hud-h: 40px;--tap: 34px}.zoom,.dice-tray .dice-text span{display:none}.tool svg{width:17px;height:17px}.strip{left:4px;right:4px;bottom:4px;padding:3px 4px}.strip-ctr svg{width:28px;height:28px}.toast{top:46px;font-size:12.5px;padding:6px 8px}}@media(prefers-reduced-motion:reduce){.dice-tray.pop .die,.sync[data-state=saving]{animation:none}.panel{transition:none}}.label-input{min-height:32px!important;font-weight:800}.table-forget.armed{background:var(--danger);color:#fff}.phase-banner{margin:0 0 10px;padding:7px 10px;border-left:3px solid var(--brass);border-radius:4px;background:#1a2019;color:var(--ink-dim);font-size:13px}.mode-actions{display:flex;gap:6px;flex:none}.mode-actions .btn[aria-pressed=true]{background:var(--brass);color:var(--brass-ink)}.kind-override .log-body{color:#ff9d87;font-weight:700}.kind-refused .log-body{color:#c98a7c;font-style:italic}.tool.end-phase{min-width:52px;margin-left:2px;border:1px solid #a98222;background:var(--brass);color:var(--brass-ink);font-size:13px;letter-spacing:.04em;text-transform:uppercase}.tool.end-phase:hover:not(:disabled){background:var(--brass-hi)}.tool.end-phase:disabled{opacity:1;border-color:var(--line);background:transparent;color:var(--ink-faint)}.tool.end-phase.armed{background:var(--danger);border-color:#6f3022;color:#fff}.menu-head{margin-bottom:10px}.menu-head h3{margin:0;font-size:22px;letter-spacing:.04em;color:var(--brass-hi)}.menu-turn{margin:0;color:var(--ink-dim);font-size:12.5px;letter-spacing:.08em;text-transform:uppercase}.menu-sub{margin:16px 0 6px;font-size:12.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--ink-dim)}.menu-end{margin-top:18px;padding-top:12px;border-top:1px dashed var(--line)}.end-btn{width:100%}.steps{margin:0;padding-left:20px;display:grid;gap:6px}.callout{padding:10px 12px;border:1px solid #8a6d22;border-radius:8px;background:#2b2816}.warn{color:#ffb39f}.hex-chips,.pick-list{display:flex;flex-wrap:wrap;gap:6px;margin:6px 0 10px;padding:0;list-style:none}.chip-btn,.pill{min-height:36px;padding:0 12px;border:1px solid var(--line);border-radius:999px;background:#1c231b;font-weight:700;cursor:pointer}.chip-btn:hover,.pill:hover{border-color:var(--brass)}.pill[aria-pressed=true]{border-color:var(--brass);background:#3a3520;box-shadow:inset 0 0 0 1px var(--brass)}@media(pointer:coarse){.chip-btn,.pill{min-height:44px}}.action-list{list-style:none;margin:0;padding:0;display:grid;gap:6px}.action-list li{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:8px 10px;border:1px solid var(--line);border-radius:7px;background:#1c231b;font-size:13.5px}.action-list li.muted{color:var(--ink-dim)}.fire-card{padding:12px;border:1px solid #8f3a26;border-radius:10px;background:linear-gradient(180deg,#2f1d16,#241712)}.fire-card header{display:flex;justify-content:space-between;align-items:baseline;font-size:18px}.fire-card header span{color:var(--ink-dim);font-size:13px}.fp-table{width:100%;border-collapse:collapse;font-size:13.5px;margin:8px 0}.fp-table th,.fp-table td{padding:4px;border-bottom:1px solid rgba(255,255,255,.07);text-align:left;vertical-align:top}.fp-table .num{text-align:right;font-variant-numeric:tabular-nums;font-weight:800}.fp-table .notes{color:var(--ink-dim);font-size:12px}.fp-table tfoot td{border-bottom:0;color:var(--brass-hi);font-weight:800}.drm-list{list-style:none;margin:6px 0;padding:0;font-size:13.5px}.drm-list li{display:flex;justify-content:space-between;padding:2px 0}.drm-list .drm-total{margin-top:4px;padding-top:4px;border-top:1px solid rgba(255,255,255,.12);color:var(--brass-hi);font-weight:800}.ladder summary{cursor:pointer;color:var(--ink-dim);font-size:13px;padding:4px 0}.ift-ladder{list-style:none;margin:4px 0 0;padding:0;display:grid;gap:2px;font-size:12.5px}.ift-ladder li{display:grid;grid-template-columns:30px 56px 1fr;gap:6px}.ift-ladder .dr{color:var(--ink-faint);font-variant-numeric:tabular-nums}.ift-ladder .res{font-weight:800}.ift-ladder .res-kia{color:#ff7a5c}.ift-ladder .res-k{color:#ffa36b}.ift-ladder .res-mc{color:#f2c94c}.ift-ladder .res-nmc,.ift-ladder .res-ptc{color:#c9c09f}.ift-ladder .need{color:var(--ink-dim)}.fire-actions{margin-top:10px}.roll-btn{flex:1;background:#a3321f;border-color:#6b2a1c;color:#fff4dc}.roll-btn:hover:not(:disabled){background:#c03c25}.result-card{margin-bottom:8px;padding:8px 10px;border-left:3px solid #b4462f;border-radius:4px;background:#1c1915;font-size:13px}.result-card p{margin:0 0 4px}.result-card ul,.log-detail{margin:2px 0 0;padding-left:16px;color:var(--ink-dim);font-size:12.5px}.kind-fire .log-body{color:#ffd9cf}.los-verdict{margin:4px 0;font-weight:800}.los-verdict.ok{color:var(--ok)}.los-verdict.bad{color:#ff9d87}.los-report .los-hinder{color:#f2c94c}.override-row{display:grid;gap:4px;margin-top:16px}.fx{pointer-events:none}.fx-label{font-family:var(--font);paint-order:stroke;stroke:#1b120c;stroke-width:4px;stroke-linejoin:round;letter-spacing:.03em}#soundButton[aria-pressed=true]{background:transparent;color:var(--ink-faint)}#soundButton[aria-pressed=true] .wave{display:none}.fx-tracer{filter:drop-shadow(0 0 3px #ffb347)}
</style>
    <meta name="napplet-type" content="my-napplet">
    <meta name="napplet-config-schema" content="{&quot;$schema&quot;:&quot;http://json-schema.org/draft-07/schema#&quot;,&quot;$version&quot;:2,&quot;type&quot;:&quot;object&quot;,&quot;additionalProperties&quot;:false,&quot;properties&quot;:{&quot;showCoords&quot;:{&quot;type&quot;:&quot;boolean&quot;,&quot;title&quot;:&quot;Show hex coordinates&quot;,&quot;description&quot;:&quot;Print each hex&#39;s id (such as K6) and hill level on the map.&quot;,&quot;default&quot;:true,&quot;x-napplet-section&quot;:&quot;Map&quot;,&quot;x-napplet-order&quot;:1},&quot;sharePointer&quot;:{&quot;type&quot;:&quot;boolean&quot;,&quot;title&quot;:&quot;Share my pointer&quot;,&quot;description&quot;:&quot;During live play, outline the hex under your pointer for your opponent.&quot;,&quot;default&quot;:true,&quot;x-napplet-section&quot;:&quot;Live play&quot;,&quot;x-napplet-order&quot;:2},&quot;autoLive&quot;:{&quot;type&quot;:&quot;boolean&quot;,&quot;title&quot;:&quot;Open live link automatically&quot;,&quot;description&quot;:&quot;Connect directly to your opponent whenever you open an online table. Your host may ask for permission first.&quot;,&quot;default&quot;:true,&quot;x-napplet-section&quot;:&quot;Live play&quot;,&quot;x-napplet-order&quot;:3}}}">
  </head>
  <body>
    <main id="app" class="app">
      <section id="lobby" class="lobby" aria-label="Tables">
        <div class="lobby-inner">
          <div class="lobby-intro">
            <p class="lobby-kicker">Tactical hex table</p>
            <p class="lobby-lede">
              Set up a squad-level scenario, move counters and roll dice with an opponent: live, or a few moves a day. You bring the
              rulebook; the table keeps the map, the dice and the log.
            </p>
          </div>

          <div class="lobby-grid">
            <div id="newForm" class="card card-new">
              <h2>New table</h2>
              <label class="field">
                <span>Scenario</span>
                <select id="newScenario"></select>
              </label>
              <p id="newBrief" class="hint"></p>
              <fieldset class="field">
                <legend>Your side</legend>
                <div id="newSide" class="seg" role="radiogroup"></div>
              </fieldset>
              <div class="row">
                <button id="createOnline" type="button" class="btn primary">Create online table</button>
                <button id="createLocal" type="button" class="btn">Hotseat on this device</button>
              </div>
              <p id="onlineNote" class="hint" aria-live="polite"></p>
            </div>

            <div id="joinForm" class="card">
              <h2>Join with a code</h2>
              <label class="field">
                <span>Table code from your opponent</span>
                <input id="joinCode" type="text" inputmode="url" spellcheck="false" placeholder="ASL1.…" />
              </label>
              <div class="row">
                <button id="joinButton" type="button" class="btn primary">Open table</button>
              </div>
              <p id="joinNote" class="hint" aria-live="polite"></p>
            </div>

            <section class="card card-list" aria-labelledby="myTablesTitle">
              <h2 id="myTablesTitle">Your tables</h2>
              <ul id="myTables" class="table-list"></ul>
              <p id="myTablesEmpty" class="hint">No tables yet. Create one or join with a code.</p>
            </section>
          </div>
          <p id="identityNote" class="hint identity-note" aria-live="polite"></p>
        </div>
      </section>

      <section id="table" class="table" aria-label="Game table" hidden>
        <header class="hud" role="toolbar" aria-label="Game controls">
          <button id="backButton" class="tool" type="button" title="Back to tables" aria-label="Back to tables">
            <svg viewBox="0 0 20 20" aria-hidden="true"><path d="M12.5 4 6.5 10l6 6" /></svg>
          </button>
          <div class="phase" aria-live="polite">
            <div class="phase-text">
              <span id="turnLabel" class="turn">T1</span>
              <span id="activeChip" class="chip"></span>
              <span id="phaseLabel" class="phase-name"></span>
            </div>
            <button id="endPhase" class="tool end-phase" type="button" title="End this phase. You can't go back.">End</button>
          </div>
          <div class="hud-tools">
            <button id="rollDR" class="tool dice" type="button" title="Roll two dice (DR)">DR</button>
            <button id="rolldr" class="tool dice" type="button" title="Roll one die (dr)">dr</button>
          </div>
          <div class="hud-right">
            <span id="seatChip" class="seat"></span>
            <span id="syncDot" class="sync" role="status"></span>
            <button id="panelButton" class="tool" type="button" aria-expanded="true" aria-controls="panel" title="Show panel">
              <svg viewBox="0 0 20 20" aria-hidden="true"><path d="M3 5h14M3 10h14M3 15h14" /></svg>
            </button>
          </div>
        </header>

        <div class="board-wrap">
          <div id="boardHost" class="board-host"></div>
          <div class="board-tools" role="group" aria-label="Map tools">
            <button id="losButton" class="tool" type="button" aria-pressed="false" title="Check line of sight">
              <svg viewBox="0 0 20 20" aria-hidden="true"><circle cx="4.5" cy="15.5" r="2" /><circle cx="15.5" cy="4.5" r="2" /><path d="M6 14 14 6" stroke-dasharray="2 2" /></svg>
              <span class="tool-label">LOS</span>
            </button>
            <button id="undoButton" class="tool" type="button" title="Undo your last change">
              <svg viewBox="0 0 20 20" aria-hidden="true"><path d="M7 5 3.5 8.5 7 12" /><path d="M4 8.5h7.5a4.5 4.5 0 0 1 0 9H9" /></svg>
              <span class="tool-label">Undo</span>
            </button>
            <button id="soundButton" class="tool" type="button" aria-pressed="false" title="Mute battle sounds">
              <svg viewBox="0 0 20 20" aria-hidden="true"><path d="M3 8h3l4-3v10l-4-3H3z" /><path class="wave" d="M13 7.5a3.5 3.5 0 0 1 0 5M15.5 5.5a6.5 6.5 0 0 1 0 9" /></svg>
              <span class="tool-label">Sound</span>
            </button>
          </div>
          <div class="zoom" role="group" aria-label="Zoom">
            <button id="zoomIn" class="tool" type="button" aria-label="Zoom in">+</button>
            <button id="zoomOut" class="tool" type="button" aria-label="Zoom out">−</button>
            <button id="zoomFit" class="tool" type="button" aria-label="Fit map">
              <svg viewBox="0 0 20 20" aria-hidden="true"><path d="M3 7V3h4M13 3h4v4M17 13v4h-4M7 17H3v-4" /></svg>
            </button>
          </div>
          <div id="modeBar" class="mode-bar" hidden>
            <span id="modeText"></span>
            <div id="modeActions" class="mode-actions"></div>
          </div>
          <div id="toast" class="toast" role="status" aria-live="polite" hidden></div>
          <div id="strip" class="strip" hidden>
            <div id="stripHex" class="strip-hex"></div>
            <div id="stripCounters" class="strip-counters"></div>
            <div class="strip-actions">
              <button id="stripMove" class="btn small primary" type="button">Move</button>
              <button id="stripMore" class="btn small" type="button">Details</button>
            </div>
          </div>
          <div id="diceTray" class="dice-tray" aria-live="polite" hidden></div>
        </div>

        <div id="tooSmall" class="too-small" role="status">
          <p><b>Make the frame larger to play.</b></p>
          <p id="tooSmallState"></p>
        </div>

        <aside id="panel" class="panel" aria-label="Table panel">
          <div class="tabs" role="tablist">
            <button class="tab" role="tab" data-tab="actions" aria-selected="true">Actions</button>
            <button class="tab" role="tab" data-tab="stack" aria-selected="false">Stack</button>
            <button class="tab" role="tab" data-tab="tray" aria-selected="false">Counters</button>
            <button class="tab" role="tab" data-tab="log" aria-selected="false">Log<span id="logBadge" class="badge-dot" hidden></span></button>
            <button class="tab" role="tab" data-tab="info" aria-selected="false">Table</button>
            <button id="panelClose" class="tool slim panel-close" type="button" aria-label="Close panel">
              <svg viewBox="0 0 20 20" aria-hidden="true"><path d="m5 5 10 10M15 5 5 15" /></svg>
            </button>
          </div>
          <div class="tab-body" data-body="actions" role="tabpanel">
            <div id="actionsPanel"></div>
          </div>
          <div class="tab-body" data-body="stack" role="tabpanel" hidden>
            <div id="stackPanel"></div>
          </div>
          <div class="tab-body" data-body="tray" role="tabpanel" hidden>
            <div id="trayPanel"></div>
          </div>
          <div class="tab-body tab-log" data-body="log" role="tabpanel" hidden>
            <ol id="logList" class="log"></ol>
            <div id="chatForm" class="chat">
              <input id="rollWhy" type="text" maxlength="60" placeholder="Roll for… (optional)" aria-label="What the next roll is for" />
              <div class="chat-row">
                <input id="chatInput" type="text" maxlength="280" placeholder="Message" aria-label="Message" />
                <button id="chatSend" class="btn small primary" type="button">Send</button>
              </div>
            </div>
          </div>
          <div class="tab-body" data-body="info" role="tabpanel" hidden>
            <div id="infoPanel"></div>
          </div>
        </aside>
      </section>
    </main>
  </body>
</html>