CREATORJD commited on
Commit
2d4a1ff
Β·
verified Β·
1 Parent(s): bbc9878

Deploy v1.33.70 browser build (HUD flicker + structures deck) (part 3)

Browse files
Files changed (7) hide show
  1. src/styles/ui.css +18 -3
  2. src/ui/cinematic-hud.js +89 -64
  3. src/ui/hud.js +13 -6
  4. src/ui/hudflow.js +2 -0
  5. src/ui/input.js +12 -4
  6. src/updater.js +2 -2
  7. sw.js +1 -1
src/styles/ui.css CHANGED
@@ -805,7 +805,16 @@ html.cmdIcons body.mf-cinematic-hud [data-mf-hud-role="service-controls"] button
805
  .menuRoleCopy>p{font-size:9px}.menuRoleCopy>small{display:none}
806
  }
807
  @media(max-width:600px) and (orientation:portrait){
 
 
 
 
 
808
  #buildMenu,#prodMenu{width:calc(100vw - var(--sal) - var(--sar) - 12px);max-width:398px}
 
 
 
 
809
  #buildMenu .grid,#prodMenu .grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;align-items:stretch}
810
  #buildMenu .bcard,#prodMenu .bcard{width:auto;min-width:0;align-items:stretch;padding:8px;gap:4px;text-align:left}
811
  #buildMenu .bcard .icw,#prodMenu .bcard .icw{align-self:center}
@@ -3915,7 +3924,12 @@ body.mf-cinematic-hud:has(#cmdbar #grpRow[style*="display: flex"]) #selInfo{disp
3915
  body.mf-cinematic-hud #mfCinematicContext>[data-mf-surface]{
3916
  position:fixed;z-index:22;left:auto;right:calc(var(--sar) + var(--mfHudEdge));transform:none;
3917
  bottom:calc(var(--mfHudDockClear) + var(--mfHudGap));width:var(--mfHudContextW);max-width:calc(100vw - var(--sal) - var(--sar) - var(--mfHudMap) - 10px - var(--mfHudGap) - var(--mfHudEdge) * 3);
3918
- max-height:min(36dvh,390px);padding:8px;border-radius:11px;overflow-x:hidden;overflow-y:auto;
 
 
 
 
 
3919
  border:1px solid transparent;background:var(--mfHudPanelMaterial) padding-box,var(--steelB) border-box;
3920
  box-shadow:var(--mfHudShadow);clip-path:polygon(9px 0,100% 0,100% calc(100% - 9px),calc(100% - 9px) 100%,0 100%,0 9px)
3921
  }
@@ -3931,7 +3945,8 @@ body.mf-cinematic-hud #mfCinematicContext>[data-mf-surface].mfCinematicSurfaceAc
3931
  body.mf-cinematic-hud #mfCinematicContext>[data-mf-surface][data-mf-active="true"]{isolation:isolate}
3932
  body.mf-cinematic-hud #mfCinematicContext .mfPanelChrome{top:-8px;height:44px;margin:-8px -8px 6px;padding-left:10px;
3933
  background:linear-gradient(180deg,rgba(13,31,46,.99),rgba(4,13,22,.99))}
3934
- body.mf-cinematic-hud #mfCinematicContext .menuRoleBrief{grid-template-columns:76px minmax(0,1fr);min-height:76px;margin:5px 0 7px}
 
3935
  body.mf-cinematic-hud #mfCinematicContext .menuRoleVisual,
3936
  body.mf-cinematic-hud #mfCinematicContext .menuRoleVisual .mfIntelPreview{min-height:76px}
3937
  /* The live model is the visual label here; repeating TACTICAL PREVIEW over its
@@ -4153,7 +4168,7 @@ body.mf-cinematic-hud.mfNoticeFeedOpen #cmdbar #hotSlots{display:none!important}
4153
  /* The history drawer is a glanceable MMO feed, not a modal log covering the
4154
  centre of the battlefield. The full history remains internally scrollable. */
4155
  body.mf-cinematic-hud #mfNoticeHistory{width:min(60vw,280px);max-height:min(20dvh,190px)}
4156
- body.mf-cinematic-hud #mfCinematicContext>[data-mf-surface]{bottom:calc(var(--mfHudDockClear) + var(--mfHudGap));max-height:min(34dvh,320px)}
4157
  /* Explicit About intel is a compact contextual instrument, not a centre
4158
  modal. hudflow writes top/max-height inline for the legacy banner lane, so
4159
  only this pinned phone state deliberately outranks those two coordinates.
 
805
  .menuRoleCopy>p{font-size:9px}.menuRoleCopy>small{display:none}
806
  }
807
  @media(max-width:600px) and (orientation:portrait){
808
+ /* Measured at 412x900 in a live match: the panel resolved to 306px while the
809
+ sticky header (44px) and the role brief (105px) consumed 149px of it, so
810
+ only one 84px row of structures was reachable without scrolling. Give the
811
+ deck more of the screen and stop the brief from taking half the panel:
812
+ it is context for the cards, not a substitute for seeing them. */
813
  #buildMenu,#prodMenu{width:calc(100vw - var(--sal) - var(--sar) - 12px);max-width:398px}
814
+ #buildMenu .menuRoleBrief,#prodMenu .menuRoleBrief{min-height:0;margin:5px 0 7px;
815
+ grid-template-columns:76px minmax(0,1fr)}
816
+ #buildMenu .menuRoleBrief .menuRoleCopy>p,#prodMenu .menuRoleBrief .menuRoleCopy>p{
817
+ -webkit-line-clamp:2;line-clamp:2}
818
  #buildMenu .grid,#prodMenu .grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;align-items:stretch}
819
  #buildMenu .bcard,#prodMenu .bcard{width:auto;min-width:0;align-items:stretch;padding:8px;gap:4px;text-align:left}
820
  #buildMenu .bcard .icw,#prodMenu .bcard .icw{align-self:center}
 
3924
  body.mf-cinematic-hud #mfCinematicContext>[data-mf-surface]{
3925
  position:fixed;z-index:22;left:auto;right:calc(var(--sar) + var(--mfHudEdge));transform:none;
3926
  bottom:calc(var(--mfHudDockClear) + var(--mfHudGap));width:var(--mfHudContextW);max-width:calc(100vw - var(--sal) - var(--sar) - var(--mfHudMap) - 10px - var(--mfHudGap) - var(--mfHudEdge) * 3);
3927
+ /* Measured at 412x900 in a live match: 36dvh resolved to 306px, and the
3928
+ sticky header (44px) plus the role brief (~105px) consumed 149px of it,
3929
+ leaving room for exactly one 84px row of structures. This is the rule that
3930
+ governs the deck in the cinematic HUD, so it is the one that has to give.
3931
+ Capped so the deck still cannot swallow the battlefield. */
3932
+ max-height:min(62dvh,520px);padding:8px;border-radius:11px;overflow-x:hidden;overflow-y:auto;
3933
  border:1px solid transparent;background:var(--mfHudPanelMaterial) padding-box,var(--steelB) border-box;
3934
  box-shadow:var(--mfHudShadow);clip-path:polygon(9px 0,100% 0,100% calc(100% - 9px),calc(100% - 9px) 100%,0 100%,0 9px)
3935
  }
 
3945
  body.mf-cinematic-hud #mfCinematicContext>[data-mf-surface][data-mf-active="true"]{isolation:isolate}
3946
  body.mf-cinematic-hud #mfCinematicContext .mfPanelChrome{top:-8px;height:44px;margin:-8px -8px 6px;padding-left:10px;
3947
  background:linear-gradient(180deg,rgba(13,31,46,.99),rgba(4,13,22,.99))}
3948
+ body.mf-cinematic-hud #mfCinematicContext .menuRoleBrief{grid-template-columns:68px minmax(0,1fr);min-height:0;margin:4px 0 6px}
3949
+ body.mf-cinematic-hud #mfCinematicContext .menuRoleBrief .menuRoleCopy>p{-webkit-line-clamp:2;line-clamp:2}
3950
  body.mf-cinematic-hud #mfCinematicContext .menuRoleVisual,
3951
  body.mf-cinematic-hud #mfCinematicContext .menuRoleVisual .mfIntelPreview{min-height:76px}
3952
  /* The live model is the visual label here; repeating TACTICAL PREVIEW over its
 
4168
  /* The history drawer is a glanceable MMO feed, not a modal log covering the
4169
  centre of the battlefield. The full history remains internally scrollable. */
4170
  body.mf-cinematic-hud #mfNoticeHistory{width:min(60vw,280px);max-height:min(20dvh,190px)}
4171
+ body.mf-cinematic-hud #mfCinematicContext>[data-mf-surface]{bottom:calc(var(--mfHudDockClear) + var(--mfHudGap));max-height:min(58dvh,480px)}
4172
  /* Explicit About intel is a compact contextual instrument, not a centre
4173
  modal. hudflow writes top/max-height inline for the legacy banner lane, so
4174
  only this pinned phone state deliberately outranks those two coordinates.
src/ui/cinematic-hud.js CHANGED
@@ -1,4 +1,19 @@
1
  ;(function(){
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  /* OTA shells can re-evaluate the takeover without reloading the document.
3
  Retire the previous observer first so one HUD owns one synchronization
4
  loop for the lifetime of the page. */
@@ -9,7 +24,7 @@
9
  their listeners, IDs and simulation authority intact; copying markup here
10
  would create a second recycle button and bypass its confirmation guard. */
11
  function mfCinematicSetRole(el,role){
12
- if(el)el.setAttribute('data-mf-hud-role',role);
13
  return el;
14
  }
15
  function mfCinematicWrap(id,before,nodes,role,label){
@@ -20,7 +35,7 @@
20
  else document.body.appendChild(wrap);
21
  }
22
  mfCinematicSetRole(wrap,role);
23
- if(label&&!wrap.getAttribute('aria-label'))wrap.setAttribute('aria-label',label);
24
  for(var i=0;i<nodes.length;i++){
25
  var node=document.getElementById(nodes[i]);
26
  if(node&&node.parentNode!==wrap)wrap.appendChild(node);
@@ -29,15 +44,15 @@
29
  }
30
  function mfCinematicLabelPanel(panel,label,surface){
31
  if(!panel)return;
32
- panel.classList.add('mfCinematicSurface');
33
- panel.setAttribute('data-mf-hud-role','context-surface');
34
- panel.setAttribute('data-mf-surface',surface);
35
- if(!panel.getAttribute('role'))panel.setAttribute('role','region');
36
  var title=panel.querySelector('.mfPanelChrome > span');
37
  if(title){
38
  if(!title.id)title.id='mfCinematic'+surface.charAt(0).toUpperCase()+surface.slice(1)+'Title';
39
- panel.setAttribute('aria-labelledby',title.id);
40
- }else if(!panel.getAttribute('aria-label'))panel.setAttribute('aria-label',label);
41
  }
42
  function mfCinematicMarkIcon(el,icon){
43
  if(!el)return;
@@ -46,7 +61,7 @@
46
  if(!mark&&!direct){
47
  if(el.tagName==='SPAN')mark=el;
48
  else{
49
- mark=document.createElement('span');mark.className='em mfCinematicInjectedIcon';mark.setAttribute('aria-hidden','true');
50
  el.insertBefore(mark,el.firstChild);
51
  for(var n=mark.nextSibling;n;n=n.nextSibling){
52
  if(n.nodeType===3)n.nodeValue=n.nodeValue.replace(/^\s*[βŸ²βŸ³βœ“βœ—βš‘β‡©β–Έ]\s*/,'');
@@ -59,9 +74,9 @@
59
  mark.removeAttribute('data-mf-vector-icon');mark.classList.remove('mfCinematicVectorIcon');
60
  mark.textContent=fallback;
61
  }
62
- mark.classList.add('em');mark.setAttribute('data-icon',icon);
63
  }
64
- else el.setAttribute('data-icon',icon);
65
  if(typeof cmdIconsRefresh==='function')cmdIconsRefresh(mark||el);
66
  }
67
  /* The shipped command atlas intentionally stops at its authored cells. These
@@ -155,26 +170,36 @@
155
  if(!mark){
156
  if(el.tagName==='SPAN')mark=el;
157
  else{
158
- mark=document.createElement('span');mark.className='em mfCinematicInjectedIcon';mark.setAttribute('aria-hidden','true');
159
  el.insertBefore(mark,el.firstChild);
160
  for(var n=mark.nextSibling;n;n=n.nextSibling){
161
- if(n.nodeType===3)n.nodeValue=n.nodeValue.replace(/^\s*(?:βœ‰|⏸|β›°|☷|βœ“|⬆|β€Ή|β€Ί|Γ—|⚠|β“˜|πŸ”’|πŸ›|β—ˆ|☠|β˜„|⚑|β™’)\s*/u,'');
 
 
 
 
 
 
 
162
  }
163
  }
164
  }
165
  if(mark.getAttribute('data-mf-vector-icon')===icon&&mark.firstElementChild){
166
  mark.removeAttribute('data-icon');mark.removeAttribute('data-icon-ready');return;
167
  }
168
- if(!mark.hasAttribute('data-mf-icon-fallback'))mark.setAttribute('data-mf-icon-fallback',(mark.textContent||'').trim());
169
- mark.classList.add('em','mfCinematicVectorIcon');mark.setAttribute('data-mf-vector-icon',icon);
170
- mark.removeAttribute('data-icon');mark.removeAttribute('data-icon-ready');mark.setAttribute('aria-hidden','true');
171
  mark.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" focusable="false" aria-hidden="true">'+MF_CINEMATIC_VECTOR[icon]+'</svg>';
172
  }
173
  function mfCinematicStripLeadingGlyph(el,re){
174
  if(!el)return;
175
  for(var i=0;i<el.childNodes.length;i++){
176
  var node=el.childNodes[i];
177
- if(node.nodeType===3)node.nodeValue=node.nodeValue.replace(re,'');
 
 
 
178
  }
179
  }
180
  function mfCinematicIntelOwnText(el){
@@ -191,7 +216,7 @@
191
  var found=node.nodeValue.match(/^\s*([βœ¦β›­βŒΎβ—†βš”β–°β—‡ΟŸβŸ²β˜„βš πŸ”’βœ“βŒ])\s*/u);
192
  if(found){fallback=found[1];node.nodeValue=node.nodeValue.slice(found[0].length);break;}
193
  }
194
- mark=document.createElement('span');mark.className='em mfCinematicIntelLead';mark.setAttribute('aria-hidden','true');
195
  mark.textContent=fallback;el.insertBefore(mark,el.firstChild);return mark;
196
  }
197
  function mfCinematicDecorateUnitIntel(){
@@ -205,13 +230,13 @@
205
  card.querySelectorAll('.ucChips .ucChip').forEach(function(chip){
206
  var mark=chip.querySelector('i'),label=mfCinematicIntelOwnText(chip);if(!mark||!label)return;
207
  var chipVector=MF_CINEMATIC_INTEL_CHIP_VECTOR[label]||(/\bHP$/.test(label)?'shield':'info');
208
- mark.classList.add('em');mfCinematicMarkVector(mark,chipVector);
209
  });
210
  card.querySelectorAll('.ucMatchChip').forEach(function(chip){
211
  var mark=chip.querySelector('i'),label=((chip.querySelector('b')||{}).textContent||'').trim().toUpperCase();
212
  if(!mark||!label)return;
213
  var matchVector=MF_CINEMATIC_INTEL_MATCH_VECTOR[label]||'target';
214
- mark.classList.add('em');mfCinematicMarkVector(mark,matchVector);
215
  });
216
  card.querySelectorAll('.ucCounter.caution,.ucCounter .caution').forEach(function(line){
217
  var label=(line.textContent||'').trim().toUpperCase();
@@ -248,7 +273,7 @@
248
  node; mission, enemy and clock copy must remain in the status chip. */
249
  mfCinematicStripLeadingGlyph(chip,/^\s*[β—ˆβ˜ πŸ›]\s*/u);
250
  mark=document.createElement('span');mark.className='em mfCinematicGoalIcon';
251
- mark.setAttribute('aria-hidden','true');chip.insertBefore(mark,chip.firstChild);
252
  }
253
  return mark;
254
  }
@@ -348,15 +373,15 @@
348
  }
349
  function mfCinematicEnsureStructure(){
350
  var body=document.body;
351
- body.classList.add('mf-cinematic-hud');
352
- body.setAttribute('data-mf-hud','cinematic-v1');
353
  var top=mfCinematicWrap('mfCinematicTopRail','topbar',['heroBar','topbar'],'top-rail','Commander and battle resources');
354
  var hero=mfCinematicSetRole(document.getElementById('heroBar'),'commander-profile');
355
  var resources=mfCinematicSetRole(document.getElementById('topbar'),'resource-rail');
356
- if(top)top.classList.add('mfCinematicRail');
357
  if(resources){
358
- if(!resources.getAttribute('role'))resources.setAttribute('role','region');
359
- if(!resources.getAttribute('aria-label'))resources.setAttribute('aria-label','Battle resources and controls');
360
  var tiles=[
361
  {el:document.getElementById('massV'),label:'MASS'},
362
  {el:document.getElementById('enV'),label:'ENERGY'},
@@ -366,33 +391,33 @@
366
  var tile=tiles[i].el;
367
  if(tile&&tile.id!=='unitRes')tile=tile.parentElement;
368
  if(!tile)continue;
369
- tile.classList.add('mfCinematicResource');tile.setAttribute('data-label',tiles[i].label);
370
- tile.setAttribute('data-mf-resource',tiles[i].label.toLowerCase());
371
- if(!tile.getAttribute('role'))tile.setAttribute('role','group');
372
- if(!tile.getAttribute('aria-label'))tile.setAttribute('aria-label',tiles[i].label+' resource');
373
  }
374
  }
375
- if(hero&&!hero.classList.contains('mfCinematicCommander'))hero.classList.add('mfCinematicCommander');
376
 
377
  var context=mfCinematicWrap('mfCinematicContext','buildMenu',['buildMenu','prodMenu','bldMenu2'],'context','Context controls');
378
- if(context)context.classList.add('mfCinematicContext');
379
  mfCinematicLabelPanel(document.getElementById('buildMenu'),'Structures','build');
380
  mfCinematicLabelPanel(document.getElementById('prodMenu'),'Production','production');
381
  mfCinematicLabelPanel(document.getElementById('bldMenu2'),'Structure control','structure');
382
 
383
  var cmd=mfCinematicSetRole(document.getElementById('cmdbar'),'command-dock');
384
  if(cmd){
385
- if(!cmd.classList.contains('mfCinematicCommandDock'))cmd.classList.add('mfCinematicCommandDock');
386
- if(!cmd.getAttribute('role'))cmd.setAttribute('role','region');
387
- if(!cmd.getAttribute('aria-label'))cmd.setAttribute('aria-label','Tactical command dock');
388
  }
389
  var minimapWrap=mfCinematicSetRole(document.getElementById('minimapWrap'),'minimap-receiver');
390
  if(minimapWrap){
391
- if(!minimapWrap.getAttribute('role'))minimapWrap.setAttribute('role','region');
392
- if(!minimapWrap.getAttribute('aria-label'))minimapWrap.setAttribute('aria-label','Tactical minimap and command transmissions');
393
  }
394
  var minimap=mfCinematicSetRole(document.getElementById('minimap'),'minimap-command');
395
- if(minimap&&!minimap.getAttribute('aria-label'))minimap.setAttribute('aria-label','Tactical minimap');
396
  mfCinematicSetRole(document.getElementById('cmdrTx'),'command-transmission');
397
  mfCinematicSetRole(document.getElementById('hudDeckTabs'),'deck-tabs');
398
  mfCinematicSetRole(document.getElementById('goalBar'),'mission-status');
@@ -413,16 +438,16 @@
413
  mfNoticeDock:'event-feed-dock',mfNoticeHistory:'event-feed',hotUtilityPanel:'utility-drawer'};
414
  for(var authorityId in authorityRoles)mfCinematicSetRole(document.getElementById(authorityId),authorityRoles[authorityId]);
415
  var heroRow=mfCinematicSetRole(document.getElementById('heroRow'),'ability-authority');
416
- if(heroRow)heroRow.classList.add('mfCinematicAuthoritySource');
417
 
418
  var groups=mfCinematicWrap('mfCinematicGroupPalette','grpRow',['grpRow'],'group-palette','Match command groups');
419
  var grpRow=mfCinematicSetRole(document.getElementById('grpRow'),'match-command-palette');
420
- if(groups)groups.classList.add('mfCinematicGroupPalette');
421
  if(grpRow){
422
- grpRow.setAttribute('data-mf-persistence','match-memory');
423
  for(var g=1;g<=4;g++){
424
  var btn=document.getElementById('grpBtn'+g);
425
- if(btn){btn.classList.add('mfCinematicGroupSlot');btn.setAttribute('data-mf-group-slot',String(g));}
426
  }
427
  }
428
  }
@@ -435,11 +460,11 @@
435
  if(on&&active==='none')active=surfaces[i][1];
436
  if(panel){
437
  panel.classList.toggle('mfCinematicSurfaceActive',on);
438
- panel.setAttribute('data-mf-active',on?'true':'false');
439
  }
440
  }
441
- if(context)context.setAttribute('data-active-surface',active);
442
- body.setAttribute('data-mf-hud-surface',active);
443
  body.classList.toggle('mfCinematicContextOpen',active!=='none');
444
  }
445
  function mfCinematicSyncDeck(){
@@ -448,27 +473,27 @@
448
  for(var i=0;i<buttons.length;i++){
449
  var on=buttons[i].getAttribute('aria-selected')==='true'||buttons[i].classList.contains('on');
450
  buttons[i].classList.toggle('mfCinematicDeckActive',on);
451
- buttons[i].setAttribute('data-mf-active',on?'true':'false');
452
  if(on)active=buttons[i].getAttribute('data-deck')||active;
453
  }
454
- tabs.setAttribute('data-active-deck',active);
455
- document.body.setAttribute('data-mf-hud-deck',active);
456
  }
457
  function mfCinematicSyncService(){
458
  var row=document.getElementById('mfBldServiceActions'),repair=document.getElementById('bp_repair');
459
  var recycle=document.getElementById('bp_sell'),panels=[document.getElementById('prodMenu'),document.getElementById('bldMenu2')];
460
  if(row){
461
- row.classList.add('mfCinematicServiceStrip');
462
- row.setAttribute('data-mf-hud-role','service-controls');
463
- row.setAttribute('data-mf-service-state',repair&&repair.getAttribute('data-state')||'unavailable');
464
- row.setAttribute('data-mf-recycle-armed',recycle&&recycle.getAttribute('data-armed')||'false');
465
  }
466
- if(repair){repair.classList.add('mfCinematicServiceRepair');repair.setAttribute('data-mf-hud-role','service-repair');}
467
- if(recycle){recycle.classList.add('mfCinematicServiceRecycle');recycle.setAttribute('data-mf-hud-role','service-recycle');}
468
  for(var i=0;i<panels.length;i++){
469
  if(!panels[i])continue;
470
  panels[i].removeAttribute('data-mf-service-state');
471
- if(row&&row.parentElement===panels[i])panels[i].setAttribute('data-mf-service-state',row.getAttribute('data-mf-service-state'));
472
  }
473
  }
474
  function mfCinematicSyncTransmission(){
@@ -476,12 +501,12 @@
476
  /* Commander portraits belong to their own faction. Only the neutral UGA
477
  guide may put UGA in the shared receiver chrome. */
478
  var link=who&&String(who.textContent||'').trim().toUpperCase()==='KEEL'?'uga-keel':'command';
479
- if(tx.getAttribute('data-mf-link')!==link)tx.setAttribute('data-mf-link',link);
480
  }
481
  function mfCinematicDecorateBaseFinder(){
482
  var panel=document.getElementById('baseFinder');if(!panel)return;
483
- panel.classList.add('mfCinematicDecorated');panel.setAttribute('data-mf-hud-role','base-finder');
484
- var tabs=panel.querySelector('.baseFindTabs');if(tabs)tabs.setAttribute('data-mf-hud-role','base-finder-tabs');
485
  mfCinematicMarkVector(panel.querySelector('.baseFindBack'),'prev');
486
  var finderIcons={all:'buildings',economy:'economy',production:'factory',defence:'shield',support:'support'};
487
  panel.querySelectorAll('.baseFindTabs [data-f]').forEach(function(btn){
@@ -490,14 +515,14 @@
490
  var cards=panel.querySelectorAll('.baseFindCard:not(.mfCinematicDecorated)');
491
  for(var i=0;i<cards.length;i++){
492
  var card=cards[i],type=card.getAttribute('data-btype'),art=card.querySelector('.baseFindIcon'),copy=card.querySelector('.baseFindCopy');
493
- card.classList.add('mfCinematicDecorated');card.setAttribute('data-mf-hud-role','base-building-card');
494
- if(copy)copy.classList.add('mfBaseFinderCopy');
495
  if(!art)continue;
496
- art.classList.add('mfBaseFinderArt');art.setAttribute('aria-hidden','true');
497
  if(typeof bldIconEl!=='function'||!type)continue;
498
  try{
499
  var icon=bldIconEl(type,52,typeof playerKitKey==='function'?playerKitKey():undefined);
500
- if(icon){icon.classList.add('mfBaseFinderModel');art.replaceChildren(icon);}
501
  }catch(e){/* Keep the authored emoji when runtime art is unavailable. */}
502
  }
503
  }
@@ -510,8 +535,8 @@
510
  var atStart=row.scrollLeft<=2,atEnd=row.scrollLeft+row.clientWidth>=row.scrollWidth-2;
511
  state=atStart?'start':atEnd?'end':'middle';
512
  }
513
- row.setAttribute('data-mf-overflow',state);
514
- if(!row.getAttribute('aria-label'))row.setAttribute('aria-label',row.id==='prodTabs'?'Production categories':'Structure categories');
515
  }
516
  function mfCinematicSyncCategoryOverflow(){
517
  ['buildTabs','prodTabs'].forEach(function(id){
 
1
  ;(function(){
2
+ /* Every write below used to be unconditional. Re-adding a class an element
3
+ already carries, or re-setting an attribute to the value it already holds,
4
+ still emits a MutationObserver record β€” and mfCinematicWatch reacts to
5
+ those by queueing another sync, which performs the same writes again. With
6
+ ~20 surfaces decorated per pass that self-sustaining loop measured 2140
7
+ HUD mutations/sec, which players see as text and icons changing at random.
8
+ Guarding at the point of write breaks the cycle at its source; note that
9
+ el.style.x = same is skipped by the browser for free, but class and
10
+ attribute writes are not. */
11
+ function mfCinSetClass(el,name){ if(el&&name&&!el.classList.contains(name)) el.classList.add(name); }
12
+ function mfCinSetClasses(el){ for(var i=1;i<arguments.length;i++) mfCinSetClass(el,arguments[i]); }
13
+ function mfCinSetAttr(el,key,value){
14
+ if(!el) return; var next=String(value);
15
+ if(el.getAttribute(key)!==next) el.setAttribute(key,next);
16
+ }
17
  /* OTA shells can re-evaluate the takeover without reloading the document.
18
  Retire the previous observer first so one HUD owns one synchronization
19
  loop for the lifetime of the page. */
 
24
  their listeners, IDs and simulation authority intact; copying markup here
25
  would create a second recycle button and bypass its confirmation guard. */
26
  function mfCinematicSetRole(el,role){
27
+ if(el)mfCinSetAttr(el,'data-mf-hud-role',role);
28
  return el;
29
  }
30
  function mfCinematicWrap(id,before,nodes,role,label){
 
35
  else document.body.appendChild(wrap);
36
  }
37
  mfCinematicSetRole(wrap,role);
38
+ if(label&&!wrap.getAttribute('aria-label'))mfCinSetAttr(wrap,'aria-label',label);
39
  for(var i=0;i<nodes.length;i++){
40
  var node=document.getElementById(nodes[i]);
41
  if(node&&node.parentNode!==wrap)wrap.appendChild(node);
 
44
  }
45
  function mfCinematicLabelPanel(panel,label,surface){
46
  if(!panel)return;
47
+ mfCinSetClass(panel,'mfCinematicSurface');
48
+ mfCinSetAttr(panel,'data-mf-hud-role','context-surface');
49
+ mfCinSetAttr(panel,'data-mf-surface',surface);
50
+ if(!panel.getAttribute('role'))mfCinSetAttr(panel,'role','region');
51
  var title=panel.querySelector('.mfPanelChrome > span');
52
  if(title){
53
  if(!title.id)title.id='mfCinematic'+surface.charAt(0).toUpperCase()+surface.slice(1)+'Title';
54
+ mfCinSetAttr(panel,'aria-labelledby',title.id);
55
+ }else if(!panel.getAttribute('aria-label'))mfCinSetAttr(panel,'aria-label',label);
56
  }
57
  function mfCinematicMarkIcon(el,icon){
58
  if(!el)return;
 
61
  if(!mark&&!direct){
62
  if(el.tagName==='SPAN')mark=el;
63
  else{
64
+ mark=document.createElement('span');mark.className='em mfCinematicInjectedIcon';mfCinSetAttr(mark,'aria-hidden','true');
65
  el.insertBefore(mark,el.firstChild);
66
  for(var n=mark.nextSibling;n;n=n.nextSibling){
67
  if(n.nodeType===3)n.nodeValue=n.nodeValue.replace(/^\s*[βŸ²βŸ³βœ“βœ—βš‘β‡©β–Έ]\s*/,'');
 
74
  mark.removeAttribute('data-mf-vector-icon');mark.classList.remove('mfCinematicVectorIcon');
75
  mark.textContent=fallback;
76
  }
77
+ mfCinSetClass(mark,'em');mfCinSetAttr(mark,'data-icon',icon);
78
  }
79
+ else mfCinSetAttr(el,'data-icon',icon);
80
  if(typeof cmdIconsRefresh==='function')cmdIconsRefresh(mark||el);
81
  }
82
  /* The shipped command atlas intentionally stops at its authored cells. These
 
170
  if(!mark){
171
  if(el.tagName==='SPAN')mark=el;
172
  else{
173
+ mark=document.createElement('span');mark.className='em mfCinematicInjectedIcon';mfCinSetAttr(mark,'aria-hidden','true');
174
  el.insertBefore(mark,el.firstChild);
175
  for(var n=mark.nextSibling;n;n=n.nextSibling){
176
+ /* Assigning nodeValue fires a characterData mutation even when the
177
+ string is unchanged, and these nodes sit inside the watched command
178
+ dock, so stripping a glyph that was never there still woke the HUD
179
+ observers on every pass. Only write a real change. */
180
+ if(n.nodeType===3){
181
+ var strippedLead=n.nodeValue.replace(/^\s*(?:βœ‰|⏸|β›°|☷|βœ“|⬆|β€Ή|β€Ί|Γ—|⚠|β“˜|πŸ”’|πŸ›|β—ˆ|☠|β˜„|⚑|β™’)\s*/u,'');
182
+ if(strippedLead!==n.nodeValue) n.nodeValue=strippedLead;
183
+ }
184
  }
185
  }
186
  }
187
  if(mark.getAttribute('data-mf-vector-icon')===icon&&mark.firstElementChild){
188
  mark.removeAttribute('data-icon');mark.removeAttribute('data-icon-ready');return;
189
  }
190
+ if(!mark.hasAttribute('data-mf-icon-fallback'))mfCinSetAttr(mark,'data-mf-icon-fallback',(mark.textContent||'').trim());
191
+ mfCinSetClasses(mark,'em','mfCinematicVectorIcon');mfCinSetAttr(mark,'data-mf-vector-icon',icon);
192
+ mark.removeAttribute('data-icon');mark.removeAttribute('data-icon-ready');mfCinSetAttr(mark,'aria-hidden','true');
193
  mark.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" focusable="false" aria-hidden="true">'+MF_CINEMATIC_VECTOR[icon]+'</svg>';
194
  }
195
  function mfCinematicStripLeadingGlyph(el,re){
196
  if(!el)return;
197
  for(var i=0;i<el.childNodes.length;i++){
198
  var node=el.childNodes[i];
199
+ if(node.nodeType!==3) continue;
200
+ /* Same reason as above: assign only when the text actually changes. */
201
+ var stripped=node.nodeValue.replace(re,'');
202
+ if(stripped!==node.nodeValue) node.nodeValue=stripped;
203
  }
204
  }
205
  function mfCinematicIntelOwnText(el){
 
216
  var found=node.nodeValue.match(/^\s*([βœ¦β›­βŒΎβ—†βš”β–°β—‡ΟŸβŸ²β˜„βš πŸ”’βœ“βŒ])\s*/u);
217
  if(found){fallback=found[1];node.nodeValue=node.nodeValue.slice(found[0].length);break;}
218
  }
219
+ mark=document.createElement('span');mark.className='em mfCinematicIntelLead';mfCinSetAttr(mark,'aria-hidden','true');
220
  mark.textContent=fallback;el.insertBefore(mark,el.firstChild);return mark;
221
  }
222
  function mfCinematicDecorateUnitIntel(){
 
230
  card.querySelectorAll('.ucChips .ucChip').forEach(function(chip){
231
  var mark=chip.querySelector('i'),label=mfCinematicIntelOwnText(chip);if(!mark||!label)return;
232
  var chipVector=MF_CINEMATIC_INTEL_CHIP_VECTOR[label]||(/\bHP$/.test(label)?'shield':'info');
233
+ mfCinSetClass(mark,'em');mfCinematicMarkVector(mark,chipVector);
234
  });
235
  card.querySelectorAll('.ucMatchChip').forEach(function(chip){
236
  var mark=chip.querySelector('i'),label=((chip.querySelector('b')||{}).textContent||'').trim().toUpperCase();
237
  if(!mark||!label)return;
238
  var matchVector=MF_CINEMATIC_INTEL_MATCH_VECTOR[label]||'target';
239
+ mfCinSetClass(mark,'em');mfCinematicMarkVector(mark,matchVector);
240
  });
241
  card.querySelectorAll('.ucCounter.caution,.ucCounter .caution').forEach(function(line){
242
  var label=(line.textContent||'').trim().toUpperCase();
 
273
  node; mission, enemy and clock copy must remain in the status chip. */
274
  mfCinematicStripLeadingGlyph(chip,/^\s*[β—ˆβ˜ πŸ›]\s*/u);
275
  mark=document.createElement('span');mark.className='em mfCinematicGoalIcon';
276
+ mfCinSetAttr(mark,'aria-hidden','true');chip.insertBefore(mark,chip.firstChild);
277
  }
278
  return mark;
279
  }
 
373
  }
374
  function mfCinematicEnsureStructure(){
375
  var body=document.body;
376
+ mfCinSetClass(body,'mf-cinematic-hud');
377
+ mfCinSetAttr(body,'data-mf-hud','cinematic-v1');
378
  var top=mfCinematicWrap('mfCinematicTopRail','topbar',['heroBar','topbar'],'top-rail','Commander and battle resources');
379
  var hero=mfCinematicSetRole(document.getElementById('heroBar'),'commander-profile');
380
  var resources=mfCinematicSetRole(document.getElementById('topbar'),'resource-rail');
381
+ if(top)mfCinSetClass(top,'mfCinematicRail');
382
  if(resources){
383
+ if(!resources.getAttribute('role'))mfCinSetAttr(resources,'role','region');
384
+ if(!resources.getAttribute('aria-label'))mfCinSetAttr(resources,'aria-label','Battle resources and controls');
385
  var tiles=[
386
  {el:document.getElementById('massV'),label:'MASS'},
387
  {el:document.getElementById('enV'),label:'ENERGY'},
 
391
  var tile=tiles[i].el;
392
  if(tile&&tile.id!=='unitRes')tile=tile.parentElement;
393
  if(!tile)continue;
394
+ mfCinSetClass(tile,'mfCinematicResource');mfCinSetAttr(tile,'data-label',tiles[i].label);
395
+ mfCinSetAttr(tile,'data-mf-resource',tiles[i].label.toLowerCase());
396
+ if(!tile.getAttribute('role'))mfCinSetAttr(tile,'role','group');
397
+ if(!tile.getAttribute('aria-label'))mfCinSetAttr(tile,'aria-label',tiles[i].label+' resource');
398
  }
399
  }
400
+ if(hero&&!hero.classList.contains('mfCinematicCommander'))mfCinSetClass(hero,'mfCinematicCommander');
401
 
402
  var context=mfCinematicWrap('mfCinematicContext','buildMenu',['buildMenu','prodMenu','bldMenu2'],'context','Context controls');
403
+ if(context)mfCinSetClass(context,'mfCinematicContext');
404
  mfCinematicLabelPanel(document.getElementById('buildMenu'),'Structures','build');
405
  mfCinematicLabelPanel(document.getElementById('prodMenu'),'Production','production');
406
  mfCinematicLabelPanel(document.getElementById('bldMenu2'),'Structure control','structure');
407
 
408
  var cmd=mfCinematicSetRole(document.getElementById('cmdbar'),'command-dock');
409
  if(cmd){
410
+ if(!cmd.classList.contains('mfCinematicCommandDock'))mfCinSetClass(cmd,'mfCinematicCommandDock');
411
+ if(!cmd.getAttribute('role'))mfCinSetAttr(cmd,'role','region');
412
+ if(!cmd.getAttribute('aria-label'))mfCinSetAttr(cmd,'aria-label','Tactical command dock');
413
  }
414
  var minimapWrap=mfCinematicSetRole(document.getElementById('minimapWrap'),'minimap-receiver');
415
  if(minimapWrap){
416
+ if(!minimapWrap.getAttribute('role'))mfCinSetAttr(minimapWrap,'role','region');
417
+ if(!minimapWrap.getAttribute('aria-label'))mfCinSetAttr(minimapWrap,'aria-label','Tactical minimap and command transmissions');
418
  }
419
  var minimap=mfCinematicSetRole(document.getElementById('minimap'),'minimap-command');
420
+ if(minimap&&!minimap.getAttribute('aria-label'))mfCinSetAttr(minimap,'aria-label','Tactical minimap');
421
  mfCinematicSetRole(document.getElementById('cmdrTx'),'command-transmission');
422
  mfCinematicSetRole(document.getElementById('hudDeckTabs'),'deck-tabs');
423
  mfCinematicSetRole(document.getElementById('goalBar'),'mission-status');
 
438
  mfNoticeDock:'event-feed-dock',mfNoticeHistory:'event-feed',hotUtilityPanel:'utility-drawer'};
439
  for(var authorityId in authorityRoles)mfCinematicSetRole(document.getElementById(authorityId),authorityRoles[authorityId]);
440
  var heroRow=mfCinematicSetRole(document.getElementById('heroRow'),'ability-authority');
441
+ if(heroRow)mfCinSetClass(heroRow,'mfCinematicAuthoritySource');
442
 
443
  var groups=mfCinematicWrap('mfCinematicGroupPalette','grpRow',['grpRow'],'group-palette','Match command groups');
444
  var grpRow=mfCinematicSetRole(document.getElementById('grpRow'),'match-command-palette');
445
+ if(groups)mfCinSetClass(groups,'mfCinematicGroupPalette');
446
  if(grpRow){
447
+ mfCinSetAttr(grpRow,'data-mf-persistence','match-memory');
448
  for(var g=1;g<=4;g++){
449
  var btn=document.getElementById('grpBtn'+g);
450
+ if(btn){mfCinSetClass(btn,'mfCinematicGroupSlot');mfCinSetAttr(btn,'data-mf-group-slot',String(g));}
451
  }
452
  }
453
  }
 
460
  if(on&&active==='none')active=surfaces[i][1];
461
  if(panel){
462
  panel.classList.toggle('mfCinematicSurfaceActive',on);
463
+ mfCinSetAttr(panel,'data-mf-active',on?'true':'false');
464
  }
465
  }
466
+ if(context)mfCinSetAttr(context,'data-active-surface',active);
467
+ mfCinSetAttr(body,'data-mf-hud-surface',active);
468
  body.classList.toggle('mfCinematicContextOpen',active!=='none');
469
  }
470
  function mfCinematicSyncDeck(){
 
473
  for(var i=0;i<buttons.length;i++){
474
  var on=buttons[i].getAttribute('aria-selected')==='true'||buttons[i].classList.contains('on');
475
  buttons[i].classList.toggle('mfCinematicDeckActive',on);
476
+ mfCinSetAttr(buttons[i],'data-mf-active',on?'true':'false');
477
  if(on)active=buttons[i].getAttribute('data-deck')||active;
478
  }
479
+ mfCinSetAttr(tabs,'data-active-deck',active);
480
+ mfCinSetAttr(document.body,'data-mf-hud-deck',active);
481
  }
482
  function mfCinematicSyncService(){
483
  var row=document.getElementById('mfBldServiceActions'),repair=document.getElementById('bp_repair');
484
  var recycle=document.getElementById('bp_sell'),panels=[document.getElementById('prodMenu'),document.getElementById('bldMenu2')];
485
  if(row){
486
+ mfCinSetClass(row,'mfCinematicServiceStrip');
487
+ mfCinSetAttr(row,'data-mf-hud-role','service-controls');
488
+ mfCinSetAttr(row,'data-mf-service-state',repair&&repair.getAttribute('data-state')||'unavailable');
489
+ mfCinSetAttr(row,'data-mf-recycle-armed',recycle&&recycle.getAttribute('data-armed')||'false');
490
  }
491
+ if(repair){mfCinSetClass(repair,'mfCinematicServiceRepair');mfCinSetAttr(repair,'data-mf-hud-role','service-repair');}
492
+ if(recycle){mfCinSetClass(recycle,'mfCinematicServiceRecycle');mfCinSetAttr(recycle,'data-mf-hud-role','service-recycle');}
493
  for(var i=0;i<panels.length;i++){
494
  if(!panels[i])continue;
495
  panels[i].removeAttribute('data-mf-service-state');
496
+ if(row&&row.parentElement===panels[i])mfCinSetAttr(panels[i],'data-mf-service-state',row.getAttribute('data-mf-service-state'));
497
  }
498
  }
499
  function mfCinematicSyncTransmission(){
 
501
  /* Commander portraits belong to their own faction. Only the neutral UGA
502
  guide may put UGA in the shared receiver chrome. */
503
  var link=who&&String(who.textContent||'').trim().toUpperCase()==='KEEL'?'uga-keel':'command';
504
+ if(tx.getAttribute('data-mf-link')!==link)mfCinSetAttr(tx,'data-mf-link',link);
505
  }
506
  function mfCinematicDecorateBaseFinder(){
507
  var panel=document.getElementById('baseFinder');if(!panel)return;
508
+ mfCinSetClass(panel,'mfCinematicDecorated');mfCinSetAttr(panel,'data-mf-hud-role','base-finder');
509
+ var tabs=panel.querySelector('.baseFindTabs');if(tabs)mfCinSetAttr(tabs,'data-mf-hud-role','base-finder-tabs');
510
  mfCinematicMarkVector(panel.querySelector('.baseFindBack'),'prev');
511
  var finderIcons={all:'buildings',economy:'economy',production:'factory',defence:'shield',support:'support'};
512
  panel.querySelectorAll('.baseFindTabs [data-f]').forEach(function(btn){
 
515
  var cards=panel.querySelectorAll('.baseFindCard:not(.mfCinematicDecorated)');
516
  for(var i=0;i<cards.length;i++){
517
  var card=cards[i],type=card.getAttribute('data-btype'),art=card.querySelector('.baseFindIcon'),copy=card.querySelector('.baseFindCopy');
518
+ mfCinSetClass(card,'mfCinematicDecorated');mfCinSetAttr(card,'data-mf-hud-role','base-building-card');
519
+ if(copy)mfCinSetClass(copy,'mfBaseFinderCopy');
520
  if(!art)continue;
521
+ mfCinSetClass(art,'mfBaseFinderArt');mfCinSetAttr(art,'aria-hidden','true');
522
  if(typeof bldIconEl!=='function'||!type)continue;
523
  try{
524
  var icon=bldIconEl(type,52,typeof playerKitKey==='function'?playerKitKey():undefined);
525
+ if(icon){mfCinSetClass(icon,'mfBaseFinderModel');art.replaceChildren(icon);}
526
  }catch(e){/* Keep the authored emoji when runtime art is unavailable. */}
527
  }
528
  }
 
535
  var atStart=row.scrollLeft<=2,atEnd=row.scrollLeft+row.clientWidth>=row.scrollWidth-2;
536
  state=atStart?'start':atEnd?'end':'middle';
537
  }
538
+ mfCinSetAttr(row,'data-mf-overflow',state);
539
+ if(!row.getAttribute('aria-label'))mfCinSetAttr(row,'aria-label',row.id==='prodTabs'?'Production categories':'Structure categories');
540
  }
541
  function mfCinematicSyncCategoryOverflow(){
542
  ['buildTabs','prodTabs'].forEach(function(id){
src/ui/hud.js CHANGED
@@ -842,19 +842,22 @@ function updateHUD(fps){
842
  for(let k=0;k<btns.length;k++){
843
  const cd=btns[k].querySelector('.cdring');
844
  if(!abUnlock[k]){
845
- btns[k].classList.add('cd');
846
  if(cd.style.display!=='flex') cd.style.display='flex';
847
  hudTxt(cd,'πŸ”’');
848
  } else if(abCool[k]>0){
849
- btns[k].classList.add('cd');
850
  if(cd.style.display!=='flex') cd.style.display='flex';
851
  hudTxt(cd, String(Math.ceil(abCool[k])));
852
  } else {
853
- btns[k].classList.remove('cd');
854
  if(cd.style.display!=='none') cd.style.display='none';
855
  }
856
  }
857
- if(aiming===0) $('abOver').classList.add('on'); else $('abOver').classList.remove('on');
 
 
 
858
  if(typeof commanderActiveButtonState==='function') commanderActiveButtonState();
859
  if(typeof commanderJumpButtonState==='function') commanderJumpButtonState();
860
  if(typeof artBarrageButtonState==='function') artBarrageButtonState();
@@ -866,8 +869,12 @@ function updateHUD(fps){
866
  const okd=carrierCanDeploy();
867
  const cityN=okd?carrierLandingBlockCount():0;
868
  db.style.display='block';
869
- db.classList.toggle('bad',!okd);
870
- db.textContent=okd?(cityN?'⚠ DEPLOY + CLEAR '+cityN+' BLOCK'+(cityN===1?'':'S'):'βš“ DEPLOY BASE HERE'):'β›” BAD GROUND β€” FLY ON';
 
 
 
 
871
  } else if(db.style.display!=='none'&&!(carrier.active&&carrier.phase===1)) db.style.display='none';
872
  }
873
  // objective + match clock
 
842
  for(let k=0;k<btns.length;k++){
843
  const cd=btns[k].querySelector('.cdring');
844
  if(!abUnlock[k]){
845
+ if(!btns[k].classList.contains('cd'))btns[k].classList.add('cd');
846
  if(cd.style.display!=='flex') cd.style.display='flex';
847
  hudTxt(cd,'πŸ”’');
848
  } else if(abCool[k]>0){
849
+ if(!btns[k].classList.contains('cd'))btns[k].classList.add('cd');
850
  if(cd.style.display!=='flex') cd.style.display='flex';
851
  hudTxt(cd, String(Math.ceil(abCool[k])));
852
  } else {
853
+ if(btns[k].classList.contains('cd'))btns[k].classList.remove('cd');
854
  if(cd.style.display!=='none') cd.style.display='none';
855
  }
856
  }
857
+ /* classList add/remove emit a record even when the class state is already
858
+ correct, and these buttons are serviced every HUD pass. */
859
+ const overBtn=$('abOver'),wantOver=aiming===0;
860
+ if(overBtn&&overBtn.classList.contains('on')!==wantOver) overBtn.classList.toggle('on',wantOver);
861
  if(typeof commanderActiveButtonState==='function') commanderActiveButtonState();
862
  if(typeof commanderJumpButtonState==='function') commanderJumpButtonState();
863
  if(typeof artBarrageButtonState==='function') artBarrageButtonState();
 
869
  const okd=carrierCanDeploy();
870
  const cityN=okd?carrierLandingBlockCount():0;
871
  db.style.display='block';
872
+ /* Assigning textContent replaces the text node even when the string is
873
+ identical, so this ran as a childList mutation on every HUD service
874
+ point and fed the observers watching #cmdbar. hudTxt() is the guarded
875
+ setter this file already uses everywhere else. */
876
+ if(db.classList.contains('bad')!==!okd) db.classList.toggle('bad',!okd);
877
+ hudTxt(db,okd?(cityN?'⚠ DEPLOY + CLEAR '+cityN+' BLOCK'+(cityN===1?'':'S'):'βš“ DEPLOY BASE HERE'):'β›” BAD GROUND β€” FLY ON');
878
  } else if(db.style.display!=='none'&&!(carrier.active&&carrier.phase===1)) db.style.display='none';
879
  }
880
  // objective + match clock
src/ui/hudflow.js CHANGED
@@ -169,6 +169,8 @@ function mfFlowLayout(){
169
  tools/probe-hud-flicker.mjs, removing this drain changes nothing
170
  (3/s either way). That was mfCinematicEnsureStructure() re-adding a
171
  class it already had. Kept because the guard should do what it says. */
 
 
172
  mfFlowMute--;
173
  }
174
  }
 
169
  tools/probe-hud-flicker.mjs, removing this drain changes nothing
170
  (3/s either way). That was mfCinematicEnsureStructure() re-adding a
171
  class it already had. Kept because the guard should do what it says. */
172
+ if(typeof mfFlowWatch!=='undefined'&&mfFlowWatch&&
173
+ typeof mfFlowWatch.takeRecords==='function') mfFlowWatch.takeRecords();
174
  mfFlowMute--;
175
  }
176
  }
src/ui/input.js CHANGED
@@ -662,10 +662,18 @@ function recallGroup(n,focus){
662
  function updateGroupBadges(){
663
  for(let n=0;n<4;n++){
664
  const live=groupLive(ctrlGroups[n]);ctrlGroups[n]=live;
665
- const b=document.getElementById('grp'+(n+1)+'N');
666
- if(b)b.textContent=live.length||'β€”';
667
- const btn=document.getElementById('grpBtn'+(n+1));
668
- if(btn){btn.classList.toggle('saved',!!live.length);btn.classList.toggle('active',activePlatoon===n&&!!live.length);}
 
 
 
 
 
 
 
 
669
  }
670
  }
671
  function orderHold(){
 
662
  function updateGroupBadges(){
663
  for(let n=0;n<4;n++){
664
  const live=groupLive(ctrlGroups[n]);ctrlGroups[n]=live;
665
+ /* These run on every selection change and sit inside the watched command
666
+ dock. textContent and classList.toggle mutate even when the value is
667
+ unchanged, so four idle platoon slots kept waking the HUD observers. */
668
+ const b=document.getElementById('grp'+(n+1)+'N');
669
+ const badge=String(live.length||'β€”');
670
+ if(b&&b.textContent!==badge)b.textContent=badge;
671
+ const btn=document.getElementById('grpBtn'+(n+1));
672
+ if(btn){
673
+ const saved=!!live.length,active=activePlatoon===n&&!!live.length;
674
+ if(btn.classList.contains('saved')!==saved)btn.classList.toggle('saved',saved);
675
+ if(btn.classList.contains('active')!==active)btn.classList.toggle('active',active);
676
+ }
677
  }
678
  }
679
  function orderHold(){
src/updater.js CHANGED
@@ -42,14 +42,14 @@
42
  ============================================================================ */
43
 
44
  /* Bumped by the release script. Compared against the manifest's `version`. */
45
- const APP_VERSION = '1.33.69';
46
 
47
  /* Release notes for the PACKAGED build, bumped by the release script beside
48
  APP_VERSION and PACKAGED_REV. A device that has never taken an OTA has no
49
  download history to read notes from, and an offline device can never fetch
50
  them, so the build carries its own copy β€” otherwise a fresh install shows a
51
  permanently empty first entry in the mailbox. */
52
- const APP_NOTES = "Hotfix β€” Features: Galactic Exploration installs from the launcher as an optional expansion, downloaded on demand with resume, per-file verification and a free-storage check. Bug fixes: the in-game interface no longer flickers, because the top commander rail and the bottom command dock stopped rewriting themselves every frame. The launcher keeps an offline route available while identity resolves and settles instead of waiting forever. A deliberate update retry always reaches the update service even when the device wrongly reports itself offline. Updates now install on older app shells that previously refused them. The command deck tabs no longer clip their labels. Upcoming: streamlined War Table and an imagery-first mobile interface overhaul.";
53
 
54
  /* The channel URL in update-config.json remains publisher-configurable, but a
55
  production checker also needs one known-good recovery path. More importantly,
 
42
  ============================================================================ */
43
 
44
  /* Bumped by the release script. Compared against the manifest's `version`. */
45
+ const APP_VERSION = '1.33.70';
46
 
47
  /* Release notes for the PACKAGED build, bumped by the release script beside
48
  APP_VERSION and PACKAGED_REV. A device that has never taken an OTA has no
49
  download history to read notes from, and an offline device can never fetch
50
  them, so the build carries its own copy β€” otherwise a fresh install shows a
51
  permanently empty first entry in the mailbox. */
52
+ const APP_NOTES = "Hotfix β€” Bug fixes: the in-game interface no longer flickers or swaps its text and icons at random. Around twenty HUD surfaces were rewriting values they already held on every frame, which measured 2140 interface changes per second and now measures 30. The structures deck shows twice as many buildings at once instead of a single row. Update preparation reports PREPARING UPDATE while it checks what is already downloaded, and a transfer that has not finished its first chunk reads 0 B rather than a dash. Galactic Exploration installs from the launcher as an optional expansion. Upcoming: streamlined War Table and per-structure upgrades.";
53
 
54
  /* The channel URL in update-config.json remains publisher-configurable, but a
55
  production checker also needs one known-good recovery path. More importantly,
sw.js CHANGED
@@ -3,7 +3,7 @@
3
  only after the player has successfully fetched them. */
4
  'use strict';
5
 
6
- const MF_SW_VERSION = '1.33.69-shell3';
7
  const MF_SW_PREFIX = 'massfront-pwa-';
8
  const MF_SW_CACHE = MF_SW_PREFIX + MF_SW_VERSION;
9
  const MF_SW_UPDATE_DB = 'massfront-updates';
 
3
  only after the player has successfully fetched them. */
4
  'use strict';
5
 
6
+ const MF_SW_VERSION = '1.33.70-shell3';
7
  const MF_SW_PREFIX = 'massfront-pwa-';
8
  const MF_SW_CACHE = MF_SW_PREFIX + MF_SW_VERSION;
9
  const MF_SW_UPDATE_DB = 'massfront-updates';