File size: 18,186 Bytes
3597f5a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
/* Build-menu icons, drawn from the owner's authored Codex packs.

   ============================================================================

   assets/textures/ui/icons-{nova,legion,syndicate,horde}.png are four 1024px

   8x8/128 sheets, 24 labelled cells each, in that faction's own livery: Nova

   blue, Ascendancy red, Coalition green, Brood violet. icon-index.json names

   the cells, and each faction names them in ITS OWN vocabulary β€” the Brood has

   no "main_battle_tank", it has a brute and a behemoth.



   The tables below are the whole of the work: they say, per faction, which

   authored glyph each TYPES row and each BT key is. They were written against

   the ART (tools/preview-codex-glyphs.mjs renders every labelled cell at build

   -menu size on the real card gradient), not against the label strings, because

   the two disagree in places β€” the Ascendancy's "transport" is a rocket leaving

   the ground, so it draws the Missile Bastion, and its "gunship" is a tracked

   gun carriage, so it draws the Vulture rather than an aircraft.



   TWENTY-FOUR GLYPHS AGAINST ~60 ENTRIES, so sharing is structural. The rule

   used here is that no two cards a player can see AT THE SAME TIME may share a

   glyph unless they are the same thing β€” the build and production menus are

   tabbed, so a share across tabs is invisible, and a share inside one tab has

   to be a real pair (two artillery pieces, two armour-killers). Every share is

   listed in the report that accompanied this change.



   Where a faction's vocabulary has no word at all β€” nobody shipped a naval

   glyph, the Ascendancy has no medic and no wall, the Coalition has no tower β€”

   the entry is left out on purpose. A missing entry returns null and the caller

   keeps whatever it drew before (the 3D bake), which is better than a confident

   wrong picture. bm-*.png and MF_BM_STRUCT_ORDER stay on disk and in this file

   so tools/bake-buildmenu-icons.mjs can still re-bake them.

   ========================================================================== */

/* NO LONGER A RUNTIME LOOKUP. This was struct-key -> baked-sheet cell; under

   the Codex scheme a BT key resolves to a LABEL instead (MF_CDX_BLD), so

   nothing here is consulted to draw an icon. It stays because it is also the

   BAKE ORDER: tools/bake-buildmenu-icons.mjs reads this global out of the live

   page to decide which structure lands in which cell of bm-struct-*.png.

   Deleting it would make the bakes unreproducible, which is the one thing this

   change must not do β€” it has to stay reversible. */
const MF_BM_STRUCT_ORDER=[
  'mex','pgen','geo','silo','fab',                       // ECONOMY
  'fac','tgate','airfield',                              // PRODUCTION
  'harbor','seafort',                                    // NAVAL
  'turret','bunker','bastion','aatower','hellstorm','arc',
  'rail','minelaser','missilebastion','plasma','stormcaller',   // DEFENCE
  'wall','gate',                                         // FORTIFICATION
  'techlab',                                             // TECH
  'sgen','uplink',                                       // SUPPORT
  'nova'                                                 // SUPERWEAPON
];
/* Where the retired bakes live. Kept next to the order above so a revert is

   these two lines plus one swap in mfFacUnitIcon/mfFacBldIcon. */
const MF_BM_SHEET={struct:'assets/textures/ui/bm-struct-',unit:'assets/textures/ui/bm-unit-'};

const MF_CDX_DIR='assets/textures/ui/';
const MF_CDX_INDEX='assets/textures/ui/icon-index.json';
/* Runtime kit key -> the pack's own group name. */
const MF_CDX_GROUP={nova:'nova_federation',legion:'red_ascendancy',
                    syndicate:'syndicate_coalition',horde:'horde'};
var MF_BM_URL={};           // 'cdx:nova' -> {url, cells, byLabel}, once decoded
var MF_BM_REJECT={};        // kit -> 'size'|'decode'|'scan'|'empty'  (tests)
const MF_CDX_SHEET=1024, MF_CDX_GRID=8;
let mfBmRefreshTimer=0;

/* The image sheets arrive after the HUD can already be open. Without a redraw,

   the existing cards correctly keep their safe fallback but never upgrade until

   the player closes and reopens the panel. Refresh only visible panels and

   debounce the four faction decodes into one harmless UI pass. */
function mfBmRefreshOpenMenus(){
  if(mfBmRefreshTimer) clearTimeout(mfBmRefreshTimer);
  mfBmRefreshTimer=setTimeout(()=>{
    mfBmRefreshTimer=0;
    const shown=id=>{ const e=document.getElementById(id); return e&&getComputedStyle(e).display!=='none'; };
    if(shown('buildMenu')&&typeof renderBuildMenu==='function') renderBuildMenu();
    if(shown('prodMenu')&&typeof renderProdMenu==='function') renderProdMenu();
    if(shown('bldMenu2')&&typeof renderBldPanel==='function') renderBldPanel();
  },0);
}

/* ---------------------------------------------------------------------------

   UNITS, by TYPES index. Read left to right for one chassis in four armies.

   A dash is deliberate: no glyph in that pack depicts that thing.

   --------------------------------------------------------------------------- */
const MF_CDX_UNIT=[
/*  0 Striker      t1 fast light skirmisher  */ {nova:'infantry',   legion:'assault',          syndicate:'striker',       horde:'swarmer'},
/*  1 Rhino        t1 turreted medium tank   */ {nova:'vehicle',    legion:'main_battle_tank', syndicate:'anti_armor',    horde:'brute'},
/*  2 Goliath      t2 450hp walking heavy    */ {nova:'elite',      legion:'mech',             syndicate:'elite',         horde:'behemoth'},
/*  3 Thumper      t2 265px indirect fire    */ {nova:'artillery',  legion:'artillery',        syndicate:'artillery',     horde:'siege_creature'},
/*  4 Commander    Nova hero                 */ {nova:'hero',       legion:'commander',        syndicate:'boss',          horde:'brood_lord'},
/*  5 Wasp         t1 gun aircraft           */ {nova:'air_unit',   legion:'fighter',          syndicate:'air_unit',      horde:'flyer'},
/*  6 Longbow      t2 205px single-shot beam */ {nova:'anti_tank',  legion:'sniper',           syndicate:'anti_armor',    horde:'psionic'},
/*  7 Hornet       t2 guided explosive veh   */ {nova:'missile',    legion:'artillery',        syndicate:'missile_drone', horde:'acid_spore'},
/*  8 TITAN        t3 experimental colossus  */ {nova:'elite',      legion:'mech',             syndicate:'boss',          horde:'monster'},
/*  9 Pyro         t1 close-range incendiary */ {nova:'infantry',   legion:'infantry',         syndicate:'infantry',      horde:'spitter'},
/* 10 Vulture      t1 air-only flak          */ {nova:'anti_air',   legion:'gunship',          syndicate:'missile_drone', horde:'spitter'},
/* 11 Bulwark      t2 unarmed shield bubble  */ {nova:'support',    legion:'elite',            syndicate:'shield',        horde:'heal_nest'},
/* 12 Ravager      wildlife melee            */ {                                                                        horde:'swarmer'},
/* 13 Alpha Ravager wildlife heavy           */ {                                                                        horde:'elite'},
/* 14 Corvette     naval escort              */ {},   // no pack ships a naval glyph
/* 15 Dreadnought  naval bombardment         */ {},   // ditto β€” both keep their bakes
/* 16 Bombard      t2 400px siege gun        */ {nova:'artillery',  legion:'artillery',        syndicate:'artillery',     horde:'siege_creature'},
/* 17 Raptor       t1 aircraft, bomb splash  */ {nova:'stealth',    legion:'bomber',           syndicate:'missile_drone', horde:'acid_spore'},
/* 18 Scorcher     t2 heavy flame chassis    */ {nova:'vehicle',    legion:'mech',             syndicate:'light_vehicle', horde:'spitter'},
/* 19 Constructor  builder                   */ {nova:'engineer',   legion:'engineer',         syndicate:'engineer',      horde:'evolver'},
/* 20 Reaper       t2 explosive area launcher*/ {nova:'missile',    legion:'transport',        syndicate:'emp',           horde:'spore_cloud'},
/* 21 Cinder       t2 incendiary launcher    */ {nova:'artillery',  legion:'artillery',        syndicate:'artillery',     horde:'acid_spore'},
/* 22 Lancer       t2 gauss armour-piercer   */ {nova:'anti_tank',  legion:'heavy',            syndicate:'anti_armor',    horde:'burrower'},
/* 23 Resonator    t2 sonic emitter vehicle  */ {nova:'radar',      legion:'recon_vehicle',    syndicate:'light_vehicle', horde:'psionic'},
/* 24 Warden       t2 field medic            */ {nova:'medic',                                 syndicate:'repair_drone',  horde:'regeneration'},
/* 25 Kestrel      t1 fastest scout aircraft */ {nova:'scout',      legion:'fighter',          syndicate:'scout',         horde:'flyer'},
/* 26 Basilisk     t3 turreted gauss heavy   */ {nova:'anti_tank',  legion:'main_battle_tank', syndicate:'anti_armor',    horde:'elite'},
/* 27 Harbinger    t3 210px siege walker     */ {nova:'elite',      legion:'main_battle_tank', syndicate:'elite',         horde:'siege_creature'},
/* 28 Lord Darion Vex   Ascendancy hero      */ {nova:'hero',       legion:'commander',        syndicate:'boss',          horde:'brood_lord'},
/* 29 Broker Lys Renn   Coalition hero       */ {nova:'hero',       legion:'commander',        syndicate:'boss',          horde:'brood_lord'},
/* 30 Brood Sovereign   Brood hero           */ {nova:'hero',       legion:'commander',        syndicate:'boss',          horde:'brood_lord'},
/* 31 Brood Tidecaster  grown caster         */ {                                                                        horde:'psionic'},
/* 32 Prospector   mobile mass miner         */ {nova:'drone',      legion:'recon_vehicle',    syndicate:'economy',       horde:'biomass'}
];

/* ---------------------------------------------------------------------------

   STRUCTURES, by BT key, in build-menu tab order so the duplicate check is

   readable: adjacent rows in the same tab must differ unless they are a pair.

   --------------------------------------------------------------------------- */
const MF_CDX_BLD={
/* ECONOMY */
  mex:      {nova:'economy',       legion:'factory',      syndicate:'economy',       horde:'biomass'},
  pgen:     {nova:'tech_lab',      legion:'power_plant',  syndicate:'emp',           horde:'regeneration'},
  geo:      {nova:'tech_lab',      legion:'power_plant',  syndicate:'emp',           horde:'regeneration'},
  silo:     {nova:'supply_depot',  legion:'factory',      syndicate:'supply_cache',  horde:'nest'},
  fab:      {nova:'repair',        legion:'tech_lab',     syndicate:'black_market',  horde:'evolution_chamber'},
/* PRODUCTION */
  fac:      {nova:'command',       legion:'barracks',     syndicate:'market',        horde:'spawn_pit'},
  tgate:    {nova:'elite',         legion:'headquarters', syndicate:'boss',          horde:'monster'},
  airfield: {nova:'air_unit',      legion:'fighter',      syndicate:'air_unit',      horde:'flyer'},
/* NAVAL β€” no faction pack ships a naval glyph, so both keep their bakes. */
  harbor:   {},
  seafort:  {},
/* DEFENCE */
  turret:   {nova:'defense_tower', legion:'watchtower',                              horde:'tentacle'},
  bunker:   {nova:'defense_tower', legion:'watchtower',   syndicate:'shield',        horde:'tentacle'},
  hellstorm:{nova:'defense_tower', legion:'watchtower',                              horde:'tentacle'},
  aatower:  {nova:'anti_air',      legion:'anti_air',                                horde:'spitter'},
  arc:      {nova:'defense_tower', legion:'ion_cannon',   syndicate:'emp',           horde:'psionic'},
  plasma:   {nova:'defense_tower', legion:'ion_cannon',   syndicate:'emp',           horde:'acid_spore'},
  rail:     {nova:'anti_tank',     legion:'watchtower',   syndicate:'anti_armor',    horde:'burrower'},
  minelaser:{nova:'anti_tank',     legion:'watchtower',   syndicate:'anti_armor',    horde:'burrower'},
  bastion:  {nova:'artillery',     legion:'artillery',    syndicate:'artillery',     horde:'siege_creature'},
  stormcaller:{nova:'artillery',   legion:'artillery',    syndicate:'artillery',     horde:'siege_creature'},
  missilebastion:{nova:'missile',  legion:'transport',    syndicate:'missile_drone', horde:'spore_cloud'},
/* FORTIFICATION β€” wall and gate share one model, so one glyph loses nothing. */
  wall:     {nova:'wall',                                 syndicate:'wall',          horde:'spike_wall'},
  gate:     {nova:'wall',                                 syndicate:'wall',          horde:'spike_wall'},
/* TECH / SUPPORT / SUPERWEAPON */
  techlab:  {nova:'tech_lab',      legion:'tech_lab',     syndicate:'tech_lab',      horde:'evolution_chamber'},
  uplink:   {nova:'radar',         legion:'headquarters', syndicate:'data_relay',    horde:'creep_tumor'},
  sgen:     {nova:'support',                              syndicate:'shield',        horde:'heal_nest'},
  nova:     {nova:'orbital',       legion:'orbital_strike',syndicate:'satellite',    horde:'overmind'},
/* Never in the build menu, but reachable through bldIconEl. */
  hq:       {nova:'command',       legion:'headquarters', syndicate:'tech_lab',      horde:'overmind'},
  nest:     {                                                                        horde:'nest'}
};

function mfFacKit(kit){
  const k=(typeof factionKitKey==='function')?factionKitKey(kit):(kit||'nova');
  return k||'nova';
}

/* One cell of a faction sheet. Returns null unless the sheet loaded AND the

   index actually named that cell: drawing an unnamed cell would put a slice of

   whatever is next to it on the card, which is worse than the fallback.

   The 8x8 maths and the DOM shape are the ones the baked sheets used β€” these

   packs are the same 1024/8x8/128 layout, so nothing about the element changes

   and .facIcon in ui.css still owns the shadow that lifts a glyph off the

   panel. */
function mfBmIconEl(family,cell,kit,size){
  if(!(cell>=0)) return null;
  const E=MF_BM_URL[family+':'+kit];
  if(!E||!E.cells[cell]) return null;
  const d=document.createElement('div');
  d.className='facIcon bmIcon';
  d.style.width=size+'px'; d.style.height=size+'px';
  d.style.backgroundImage='url("'+E.url+'")';
  d.style.backgroundSize=(size*8)+'px '+(size*8)+'px';
  d.style.backgroundPosition=(-(cell%8)*size)+'px '+(-Math.floor(cell/8)*size)+'px';
  return d;
}
/* Named cells that actually have ink. A labelled but blank cell would draw an

   empty well instead of the 3D fallback, which is worse than a missing label.

   Cheap: the 1024 sheet is drawn into 32px, 4 samples per 128px cell. */
function mfCdxInkLabels(img,byLabel){
  const G=MF_CDX_GRID*4;
  const c=document.createElement('canvas'); c.width=c.height=G;
  const x=c.getContext('2d',{willReadFrequently:true});
  x.drawImage(img,0,0,G,G);
  const d=x.getImageData(0,0,G,G).data, keep={};
  for(const lb in byLabel){
    const cell=byLabel[lb], cx=(cell%MF_CDX_GRID)*4, cy=Math.floor(cell/MF_CDX_GRID)*4;
    let a=0;
    for(let y=0;y<4;y++)for(let k=0;k<4;k++) a+=d[((cy+y)*G+(cx+k))*4+3];
    if(a>120) keep[lb]=cell;
  }
  return keep;
}
/* label -> element. Unknown label, unknown kit or an index that never named

   that label all return null by the same path as a sheet that failed to load. */
function mfCdxIconEl(label,kit,size){
  if(!label) return null;
  const E=MF_BM_URL['cdx:'+kit];
  if(!E) return null;
  const c=E.byLabel[label];
  return (c===undefined)?null:mfBmIconEl('cdx',c,kit,size);
}
function mfFacUnitIcon(tIdx,size,kit){
  const k=mfFacKit(kit), row=MF_CDX_UNIT[tIdx];
  return row?mfCdxIconEl(row[k],k,size):null;
}
function mfFacBldIcon(key,size,kit){
  const k=mfFacKit(kit), row=MF_CDX_BLD[key];
  return row?mfCdxIconEl(row[k],k,size):null;
}

/* The index is the only thing that says which cell a label is, so a sheet

   cannot be trusted before it loads. Both halves stay optional: no index, a

   malformed index, a group missing from it, or a PNG that fails to decode, and

   every caller silently keeps the art it drew before β€” the icon is registered

   inside img.onload, so a half-loaded pack never draws a blank square. */
function bmIconsBind(){
  try{
    const rel=(typeof mf2AssetURL==='function')?mf2AssetURL(MF_CDX_INDEX)
                                               :('./'+MF_CDX_INDEX);
    fetch(new URL(rel,document.baseURI).href).then(r=>r.ok?r.json():null).then(ix=>{
      if(!ix) return;
      for(const kit in MF_CDX_GROUP){
        const G=ix[MF_CDX_GROUP[kit]];
        if(!G||!G.sheet||!G.cells) continue;
        /* Absolute, because these URLs end up in style strings whose resolution

           base is not the document. */
        const u=new URL((typeof mf2AssetURL==='function')
          ? mf2AssetURL(MF_CDX_DIR+G.sheet)
          : ('./'+MF_CDX_DIR+G.sheet), document.baseURI).href;
        const byLabel={};
        for(const lb in G.cells){
          const c=G.cells[lb];
          if(typeof c==='number'&&c>=0&&c<64) byLabel[lb]=c;
        }
        const img=new Image();
        img.onload=()=>{
          /* Same trap as tacticons.js: a valid PNG of the wrong size decodes

             and never hits onerror. CSS then slices it as 8x8 of `size` px,

             so a 512 sheet puts the wrong emblem on every card β€” worse than

             keeping the 3D thumb the caller already drew. */
          if(img.naturalWidth!==MF_CDX_SHEET||img.naturalHeight!==MF_CDX_SHEET){
            MF_BM_REJECT[kit]='size'; return;
          }
          let ink=byLabel;
          try{ ink=mfCdxInkLabels(img,byLabel); }
          catch(e){ MF_BM_REJECT[kit]='scan'; return; }
          const live={};
          for(const lb in ink) live[ink[lb]]=1;
          if(!Object.keys(ink).length){ MF_BM_REJECT[kit]='empty'; return; }
          MF_BM_URL['cdx:'+kit]={url:u,cells:live,byLabel:ink};
          mfBmRefreshOpenMenus();
        };
        img.onerror=()=>{ MF_BM_REJECT[kit]='decode'; };
        img.src=u;
      }
    }).catch(()=>{});
  }catch(e){}
}
bmIconsBind();