/* ============================================================ UNIT-STACK HOTBAR — a compact type selector inside PLATOONS ============================================================ */ const MF_UNIT_STACK_STATUS_MS=500,MF_UNIT_STACK_AUDIT_MS=2000; const mfUnitStackCardMap=new Map(),mfUnitStackPointers=new Set(); const mfUnitStackRowsByType=new Map(),mfUnitStackMembersByType=new Map(); let mfUnitStackRail=null,mfUnitStackNextStatus=0,mfUnitStackNextAudit=0,mfUnitStackDirty=true,mfUnitStackPaintNeeded=true,mfUnitStackPaintFrame=0, mfUnitStackLastType=-1,mfUnitStackLastAt=-1e9; let mfUnitStackFocusCount=0,mfUnitStackLastFocusType=-1,mfUnitStackLastAction=null; let mfUnitStackTopologyScans=0,mfUnitStackStatusScans=0,mfUnitStackSelectionEpoch=0,mfUnitStackSelectionSig=''; function mfUnitStackNow(){return typeof performance!=='undefined'&&performance.now?performance.now():Date.now();} function mfUnitStackName(value){return String(value||'UNIT').replace(/\s+/g,' ').trim();} function mfUnitStackToggle(el,name,on){if(el&&el.classList.contains(name)!==!!on)el.classList.toggle(name,!!on);} function mfUnitStackOwns(i){ return typeof mfLocalOwnsUnit==='function'?mfLocalOwnsUnit(i):uteam[i]===0; } function mfUnitStackKit(){ const raw=typeof playerKitKey==='function'?playerKitKey():'nova'; return typeof mfIntelKit==='function'?mfIntelKit(raw):raw; } function mfUnitStackRows(force){ const now=mfUnitStackNow(); if(typeof running!=='boolean'||!running||typeof matchLive!=='boolean'||!matchLive){ mfUnitStackRowsByType.clear();mfUnitStackMembersByType.clear();mfUnitStackDirty=true;return []; } /* Spawns/deaths mark topology dirty. A deliberately slow audit catches old shells or unusual restore paths that mutate the packed arrays directly, without returning to the former 4.5 full-unit scans per second. */ if(mfUnitStackDirty||now>=mfUnitStackNextAudit){ mfUnitStackTopologyScans++; mfUnitStackRowsByType.clear();mfUnitStackMembersByType.clear(); const byType=new Map(); for(let i=0;i=mfUnitStackNextStatus){ mfUnitStackStatusScans++; for(const row of mfUnitStackRowsByType.values())row.selected=row.ready=row.hp=row.x=row.y=0; for(const [type,members] of mfUnitStackMembersByType){ const row=mfUnitStackRowsByType.get(type);if(!row)continue; for(const i of members){ if(!ualive[i]||utype[i]!==type||!mfUnitStackOwns(i)){mfUnitStackDirty=true;continue;} row.selected+=usel[i]?1:0; row.ready+=(typeof ustun==='undefined'||ustun[i]<=0)&&(ustate[i]===0||uhold[i])?1:0; row.hp+=Math.max(0,Math.min(1,uhp[i]/Math.max(1,uhpm[i])));row.x+=ux[i];row.y+=uy[i]; } row.health=Math.round(row.hp/Math.max(1,row.count)*100); } mfUnitStackNextStatus=now+MF_UNIT_STACK_STATUS_MS; } return [...mfUnitStackRowsByType.values()].sort((a,b)=>{ const A=TYPES[a.type],B=TYPES[b.type],ar=A&&A.cat==='hero'?-2:A&&A.builder?-1:(A&&A.tier||0), br=B&&B.cat==='hero'?-2:B&&B.builder?-1:(B&&B.tier||0); return ar-br||a.type-b.type; }); } function mfUnitStackInvalidate(){ mfUnitStackDirty=true; /* Coalesce a factory completion or mass-casualty burst. A per-spawn rAF can become a full-census scan every frame while several factories finish. */ if(!mfUnitStackPaintFrame)mfUnitStackPaintFrame=setTimeout(()=>{mfUnitStackPaintFrame=0;mfUnitStackSync(false);},240); } function mfUnitStackOverflow(){ const rail=mfUnitStackRail;if(!rail)return; const overflow=rail.scrollWidth>rail.clientWidth+2; const state=!overflow?'none':rail.scrollLeft<=2?'start':rail.scrollLeft+rail.clientWidth>=rail.scrollWidth-2?'end':'middle'; if(rail.dataset.mfOverflow!==state)rail.dataset.mfOverflow=state; const role=overflow?'scrolling unit stack selector':'unit stack selector'; if(rail.getAttribute('aria-roledescription')!==role)rail.setAttribute('aria-roledescription',role); } function mfUnitStackReleasePointer(ev){ if(!mfUnitStackPointers.delete(ev.pointerId)||mfUnitStackPointers.size)return; mfUnitStackSync(true); } function mfUnitStackShell(){ if(mfUnitStackRail&&mfUnitStackRail.isConnected)return mfUnitStackRail; const row=document.getElementById('grpRow');if(!row)return null; mfUnitStackRail=document.getElementById('mfUnitStackRail'); if(!mfUnitStackRail){ mfUnitStackRail=document.createElement('div');mfUnitStackRail.id='mfUnitStackRail'; mfUnitStackRail.setAttribute('role','group');mfUnitStackRail.setAttribute('aria-label','Unit stacks by type'); mfUnitStackRail.setAttribute('data-mf-hud-role','unit-stack-hotbar');mfUnitStackRail.dataset.mfScrollRail='horizontal'; row.appendChild(mfUnitStackRail); } if(mfUnitStackRail.dataset.mfStackBound!=='1'){ mfUnitStackRail.dataset.mfStackBound='1'; mfUnitStackRail.addEventListener('pointerdown',ev=>{if(ev.isPrimary!==false)mfUnitStackPointers.add(ev.pointerId);},{passive:true}); /* A card activation stops propagation so its command cannot leak into the battlefield. Capture owns pointer release before that intentional stop, otherwise the topology guard can remain latched after a normal tap. */ addEventListener('pointerup',mfUnitStackReleasePointer,{passive:true,capture:true}); addEventListener('pointercancel',mfUnitStackReleasePointer,{passive:true,capture:true}); mfUnitStackRail.addEventListener('wheel',ev=>{ if(Math.abs(ev.deltaY)<=Math.abs(ev.deltaX))return; mfUnitStackRail.scrollLeft+=ev.deltaY;ev.preventDefault(); },{passive:false}); mfUnitStackRail.addEventListener('scroll',mfUnitStackOverflow,{passive:true}); if(typeof ResizeObserver==='function')new ResizeObserver(mfUnitStackOverflow).observe(mfUnitStackRail); } return mfUnitStackRail; } function mfUnitStackMountIcon(host,type,kit){ host.replaceChildren();let art=null; try{if(typeof unitIconEl==='function')art=unitIconEl(type,34,kit);}catch(e){art=null;} if(art){art.classList.add('mfUnitStackArt');host.appendChild(art);return;} const fallback=document.createElement('span');fallback.className='mfUnitStackFallback'; fallback.textContent=typeof UNIT_EM==='object'&&UNIT_EM[type]||'◇';host.appendChild(fallback); } function mfUnitStackRefreshSelection(type){ /* updateSelInfo's first-seen lesson opens Unit Intel for a lone chassis. A stack tap must keep PLATOONS present for its documented second activation. The authored icon, name and status card is already that chassis' lesson; its persistent info action remains the route to full Unit Intel. */ if(typeof intelSeenTypes==='object'&&intelSeenTypes)intelSeenTypes[type]=1; if(typeof updateSelInfo==='function')updateSelInfo(); } function mfUnitStackSelect(type,focus){ const members=[];let cx=0,cy=0; for(let i=0;i{ev.preventDefault();ev.stopPropagation();mfUnitStackActivate(type);}); mfUnitStackCardMap.set(type,card);return card; } function mfUnitStackPaint(card,row,kit,order){ const view=card._mfUnitStack;if(!view)return; if(view.kit!==kit&&!mfUnitStackPointers.size){view.kit=kit;mfUnitStackMountIcon(view.icon,row.type,kit);} if(view.name.textContent!==row.name)view.name.textContent=row.name; if(view.name.title!==row.name)view.name.title=row.name; const count='×'+row.count,ready=row.ready===row.count?'':row.ready+' RDY'; if(view.count.textContent!==count)view.count.textContent=count; if(view.ready.textContent!==ready)view.ready.textContent=ready; if(view.ready.hidden===!!ready)view.ready.hidden=!ready; const healthWidth=row.health+'%';if(view.fill.style.width!==healthWidth)view.fill.style.width=healthWidth; if(card.style.order!==String(order))card.style.order=String(order); card.dataset.count=String(row.count);card.dataset.selected=String(row.selected);card.dataset.ready=String(row.ready); card.dataset.health=String(row.health); const allSelected=row.selected===row.count,partial=row.selected>0&&row.selected0?'mixed':'false'; if(card.getAttribute('aria-pressed')!==pressed)card.setAttribute('aria-pressed',pressed); const label='Select all '+row.count+' '+row.name+(row.count===1?'':' units')+'. '+row.ready+' ready. Average health '+row.health+ ' percent. Activate twice to focus camera.'; if(card.getAttribute('aria-label')!==label)card.setAttribute('aria-label',label); const title=row.name+' · '+row.count+' unit'+(row.count===1?'':'s')+' · '+row.health+'% health'; if(card.title!==title)card.title=title; } function mfUnitStackSync(force){ const rail=mfUnitStackShell();if(!rail)return; const now=mfUnitStackNow();if(!force&&!mfUnitStackDirty&&!mfUnitStackPaintNeeded&&now0,kit=mfUnitStackKit(),seen=new Set(); for(let order=0;order