File size: 35,601 Bytes
b51e03a
3597f5a
fcce7e3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84bc42a
 
fcce7e3
 
 
 
 
 
 
 
 
 
 
 
 
 
42a25fd
 
 
 
fcce7e3
 
 
 
 
 
 
 
 
 
bebd577
b51e03a
bebd577
b51e03a
bebd577
 
 
 
 
 
 
 
fcce7e3
bebd577
fcce7e3
 
bebd577
fcce7e3
bebd577
 
fcce7e3
3597f5a
 
 
 
fcce7e3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bebd577
 
 
 
 
 
fcce7e3
 
bebd577
 
 
369b236
84bc42a
369b236
fcce7e3
 
 
 
 
 
 
 
 
 
 
181ede2
42a25fd
 
 
 
 
b51e03a
181ede2
b51e03a
181ede2
b51e03a
181ede2
 
b51e03a
181ede2
b51e03a
181ede2
b51e03a
181ede2
fcce7e3
 
eeb3b99
 
 
 
 
 
fcce7e3
 
 
 
 
 
 
3597f5a
 
 
42a25fd
3597f5a
 
 
 
fcce7e3
 
84bc42a
3597f5a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84bc42a
3597f5a
 
 
 
 
fcce7e3
84bc42a
 
 
 
 
 
fcce7e3
84bc42a
 
fcce7e3
bebd577
 
 
 
 
fcce7e3
 
 
 
 
 
 
 
b51e03a
bebd577
 
 
fcce7e3
b51e03a
 
fcce7e3
b51e03a
 
 
 
fcce7e3
 
 
 
 
 
 
 
 
84bc42a
fcce7e3
 
84bc42a
fcce7e3
 
84bc42a
fcce7e3
 
84bc42a
fcce7e3
 
84bc42a
fcce7e3
 
84bc42a
fcce7e3
 
84bc42a
fcce7e3
 
84bc42a
fcce7e3
 
84bc42a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
eeb3b99
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fcce7e3
84bc42a
 
 
fcce7e3
 
 
 
 
84bc42a
fcce7e3
84bc42a
 
fcce7e3
84bc42a
fcce7e3
 
84bc42a
 
 
 
fcce7e3
 
84bc42a
 
 
 
 
fcce7e3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84bc42a
 
 
fcce7e3
 
 
 
 
 
 
 
 
 
 
84bc42a
fcce7e3
 
 
 
 
84bc42a
fcce7e3
 
 
 
84bc42a
fcce7e3
 
84bc42a
 
42a25fd
 
 
 
84bc42a
fcce7e3
 
 
 
 
 
84bc42a
 
fcce7e3
 
84bc42a
fcce7e3
 
 
 
 
369b236
84bc42a
 
 
fcce7e3
 
 
 
84bc42a
fcce7e3
84bc42a
 
fcce7e3
 
84bc42a
 
 
fcce7e3
84bc42a
 
fcce7e3
84bc42a
fcce7e3
 
 
84bc42a
fcce7e3
 
 
84bc42a
fcce7e3
 
 
 
 
84bc42a
 
 
 
fcce7e3
84bc42a
 
 
fcce7e3
 
 
b51e03a
 
fcce7e3
b51e03a
fcce7e3
b51e03a
fcce7e3
b51e03a
fcce7e3
b51e03a
fcce7e3
b51e03a
fcce7e3
 
 
 
 
bebd577
fcce7e3
 
 
3597f5a
 
 
 
fcce7e3
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
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
;
;
/* ============================================================================
   DEVELOPMENT β€” research, materials, crafting, and things that wear out
   ----------------------------------------------------------------------------
   The Armory sells permanent percentages. Once bought they are simply true
   forever, which is why the store stops mattering: nothing you own is ever at
   stake. Development is the opposite half of the economy.

     MATERIALS   salvaged from matches β€” spent, not banked forever
       RESEARCH    a tree that unlocks WHAT you may build, never a stat by itself
     MODULES     crafted, equipped, and visible on the machine that carries them
     WEAR        a module is consumed by use and eventually BREAKS

   Wear is the whole point. A module that lasts forever is just another store
   entry with extra steps; one that lasts eleven matches makes you decide when
   to spend it. And because every equipped module puts a mark on the Commander
   and the HQ in the field, its loss is something you SEE β€” the icon goes out,
   the bonus goes with it, and you know exactly what changed.
   ============================================================================ */

/* ---- MATERIALS -------------------------------------------------------------
   Four, deliberately. Enough that recipes can want different things; few
   enough that a player can hold all four in their head. */
const MATS={
  alloy  :{nm:'Alloy',    em:'β–¬', ds:'Structural plate β€” salvaged from wrecks'},
  circuit:{nm:'Circuitry',em:'βŒ—', ds:'Intact logic boards β€” rarer than plate'},
  isotope:{nm:'Isotope',  em:'☒', ds:'Reactor fuel β€” recovered from victories and battlefield kills'},
  relic  :{nm:'Relic Core',em:'β—ˆ',ds:'Pre-collapse tech β€” recovered from hives after Xenobiology'},
};
function matBag(){ META.mats=META.mats||{alloy:0,circuit:0,isotope:0,relic:0}; return META.mats; }
function matHas(cost){ const b=matBag(); for(const k in cost) if((b[k]||0)<cost[k]) return false; return true; }
function matSpend(cost){ const b=matBag(); for(const k in cost) b[k]=(b[k]||0)-cost[k]; metaSave(); }
function matGrant(g){ const b=matBag(); for(const k in g) b[k]=(b[k]||0)+g[k]; metaSave(); }
/* ---- ITEM ART --------------------------------------------------------------
   Every item carries a real painted icon rather than an emoji. Emoji render as
   a different typeface on every platform, carry someone else's design language,
   and cannot be made to share a palette with the game. These are generated by
   tools/make-icons.py from the same primitives and the same palette as the rest
   of the art, so the whole set reads as one family. The emoji stays in the data
   as the alt text and as the fallback if an image ever fails to load. */
function itemArt(id,em,size){
  const s=size||34;
  const rel=(typeof ITEM_ART!=='undefined'&&ITEM_ART[id])||('assets/icons/items/'+id+'.png');
  /* New research art must resolve from the OTA asset map on older Android
     installers, not only from files bundled into a fresh 1.33.89 package. */
  const src=typeof mf2AssetURL==='function'?mf2AssetURL(rel):('./'+String(rel).replace(/^\.\//,''));
  return '<img class="itemArt" src="'+src+'" alt="'+(em||'')+'" '
    +'style="width:'+s+'px;height:'+s+'px" '
    +'onerror="this.outerHTML=\'<span class=&quot;itemEm&quot;>'+(em||'')+'</span>\'">';
}
function matCostStr(c){
  return Object.keys(c).map(k=>
    '<span class="costOne">'+itemArt('mat_'+k,MATS[k].em,15)+c[k]+'</span>').join('');
}
/* What a match pays out. Scaled by the threat the player chose to fight at, so
   the endgame ladder feeds the crafting economy instead of running beside it. */
function fieldRecoveryFromMatch(res){
  const committed=typeof matchCommitted==='function'?matchCommitted(res.win):(res.win||(stats.t|0)>=180);
  if(!committed) return {alloy:0,circuit:0};
  const lossScale=res.win?1:0.35;
  const reserveAlloy=Math.min(res.win?12:5,Math.floor(Math.max(0,res.fieldMass||0)/300)*lossScale)|0;
  const reclaimedAlloy=Math.min(res.win?8:4,Math.floor(Math.max(0,res.reclaimed||0)/180)*lossScale)|0;
  return {
    alloy:reserveAlloy+reclaimedAlloy,
    circuit:Math.min(res.win?8:3,Math.floor(Math.max(0,res.fieldEnergy||0)/1400)*lossScale)|0,
    reserveAlloy,reclaimedAlloy,
  };
}
function matsFromMatch(res){
  if(typeof matchCommitted==='function'&&!matchCommitted(res.win)) return {alloy:0,circuit:0,isotope:0,relic:0};
  const t=(typeof threatSel==='function')?threatSel():1;
  const s=res.win?1:0.35;
  const recovery=fieldRecoveryFromMatch(res);
  return {
    alloy  : Math.round((8+res.kills*0.004+res.built*0.5)*s*(1+t*0.14))+recovery.alloy,
    circuit: Math.round((3+res.kills*0.0016)*s*(1+t*0.12))+recovery.circuit,
    isotope: Math.round((res.win?2:0)+res.kills*0.0009*s*(1+t*0.10)),
    /* Xenobiology is the RIGHT to bank Relic Cores from hives. Ungated drops
       made the node a displayed cost with no effect β€” you never needed it. */
    relic  : (typeof devHas==='function'&&devHas('xeno')&&res.nests>0)
              ? Math.round(s*(1+t*0.08)) : 0,
  };
}

/* ---- RESEARCH --------------------------------------------------------------
   Three branches, and a rule: a research node never grants a stat. It grants
   the RIGHT to craft something, an extra slot, or better salvage. Stats come
   from modules, which wear out β€” so power always has a running cost. */
const DEVTREE=[
 {id:'metallurgy', br:'FABRICATION', nm:'Field Metallurgy', ds:'Unlocks plating modules',
  cost:{alloy:40}, data:6, req:[]},
 {id:'optics',     br:'FABRICATION', nm:'Gunnery Optics',   ds:'Unlocks targeting modules',
  cost:{alloy:30,circuit:14}, data:10, req:['metallurgy']},
 {id:'servos',     br:'FABRICATION', nm:'Heavy Servos',     ds:'Unlocks mobility and build modules',
  cost:{alloy:60,circuit:20}, data:16, req:['optics']},
 {id:'slot2',      br:'FABRICATION', nm:'Modular Frames',   ds:'A second module slot',
  cost:{alloy:90,circuit:40,isotope:10}, data:24, req:['servos']},

 {id:'salvage',    br:'DOCTRINE',    nm:'Salvage Doctrine', ds:'+35% materials from every match',
  cost:{alloy:25}, data:6, req:[]},
 {id:'logistics',  br:'DOCTRINE',    nm:'Forward Logistics',ds:'Modules wear 25% slower',
  cost:{alloy:50,circuit:18}, data:12, req:['salvage']},
 {id:'breakthrough',br:'DOCTRINE',   nm:'Breakthrough Protocol',
  ds:'Unlocks a timed assault push for infantry, armour and anti-tank platoons',
  cost:{alloy:72,circuit:30,isotope:5}, data:20, req:['logistics'], art:'res_ability'},
 {id:'intercept',  br:'DOCTRINE',    nm:'Interceptor Protocol',
  ds:'Unlocks high-speed interception for aircraft and anti-air screens',
  cost:{alloy:58,circuit:42,isotope:7}, data:22, req:['logistics'], art:'res_optics'},
 {id:'refit',      br:'DOCTRINE',    nm:'Field Refit',      ds:'Unlocks repairing worn modules',
  cost:{alloy:70,circuit:26}, data:18, req:['logistics']},
 {id:'fieldservice',br:'DOCTRINE',   nm:'Field Service Net',
  ds:'Unlocks a selected support platoon repair-and-shield pulse',
  cost:{alloy:82,circuit:48,isotope:6}, data:24, req:['refit'], art:'res_servos'},
 {id:'firemission',br:'DOCTRINE',    nm:'Fire Mission Protocol',
  ds:'Upgrades the basic artillery barrage with a larger coordinated salvo and tighter dispersion',
  cost:{alloy:90,circuit:44,isotope:8}, data:26, req:['logistics'], art:'res_refit'},
 {id:'slot3',      br:'DOCTRINE',    nm:'Command Retrofit', ds:'A third module slot',
  cost:{alloy:140,circuit:70,isotope:26,relic:4}, data:40, req:['refit','slot2']},

 {id:'xeno',       br:'XENOLOGY',    nm:'Xenobiology',      ds:'Hives drop Relic Cores',
  cost:{isotope:8}, data:8, req:[]},
 {id:'reactor',    br:'XENOLOGY',    nm:'Captured Reactors',ds:'Unlocks isotope modules',
  cost:{isotope:22,circuit:20}, data:14, req:['xeno']},
 {id:'relictech',  br:'XENOLOGY',    nm:'Relic Reverse-Eng',ds:'Unlocks relic modules β€” the strongest, and the most fragile',
  cost:{relic:6,circuit:50}, data:26, req:['reactor']},
 {id:'ability',    br:'XENOLOGY',    nm:'Weaponised Relics',ds:'Unlocks craftable Commander abilities',
  cost:{relic:12,isotope:40}, data:42, req:['relictech']},

  /* Faction doctrines are deliberately separate unlock paths. The original
     account tree made the playable armies read as different paint over the same
     technology. These keys are faction-prefixed so ownership can never leak
     between lines. Brood capabilities belong to enemy simulation and Intel,
     never to this player progression tree. */
 {id:'asc_siege_foundry', fac:'ascendancy', br:'FABRICATION', nm:'Siege Foundry', ds:'Dominion artillery leave factories with +12% maximum health',
  cost:{alloy:55,circuit:18}, data:12, req:[]},
 {id:'asc_iron_discipline', fac:'ascendancy', br:'DOCTRINE', nm:'Iron Discipline', ds:'Dominion formation orders use 12% tighter spacing for groups of 4 or more',
  cost:{alloy:42,isotope:8}, data:14, req:['asc_siege_foundry']},
 {id:'asc_crown_battery', fac:'ascendancy', br:'XENOLOGY', nm:'Crown Battery', ds:'Entering Siege overcharges artillery for 8s: cooldowns recover 28% faster for 2.5 energy/s; 18s recovery',
  cost:{circuit:34,isotope:18,relic:1}, data:24, req:['asc_iron_discipline']},

 {id:'syn_quantum_grid', fac:'syndicate', br:'FABRICATION', nm:'Quantum Grid', ds:'Syndicate weapons, shields and active fields draw 15% less grid energy',
  cost:{circuit:36,isotope:14}, data:12, req:[]},
 {id:'syn_drone_mesh', fac:'syndicate', br:'DOCTRINE', nm:'Drone Mesh', ds:'Syndicate scouts and aircraft mark nearby enemies for 3.5s; allied hits deal +10% damage',
  cost:{alloy:30,circuit:42}, data:15, req:['syn_quantum_grid']},
 {id:'syn_phase_lattice', fac:'syndicate', br:'XENOLOGY', nm:'Phase Lattice', ds:'Phase Arks transfer cargo at 96 range instead of requiring a 22-range landing approach',
  cost:{circuit:58,isotope:24,relic:1}, data:26, req:['syn_drone_mesh']},
];
function devDone(){ META.res=META.res||{}; return META.res; }
function devHas(id){
  /* Online's shared neutral setup cannot inherit one browser's paid/unlocked
     account tech. Per-seat progression requires an explicit future protocol. */
  if(window.__MF_NETWORK_SETUP__)return false;
  return !!devDone()[id];
}
function devAvail(n){ return n.req.every(devHas); }
function devMissing(n){
  const b=matBag(), out=[];
  for(const k in n.cost){ const d=n.cost[k]-(b[k]||0); if(d>0) out.push(Math.ceil(d)+' '+MATS[k].nm); }
  const data=n.data-(META.researchData||0); if(data>0) out.push(Math.ceil(data)+' Research Data');
  return out;
}
function devBuy(n,silent){
  if(!n||devHas(n.id)) return false;
  if(typeof mfFactionTechPurchasable==='function'&&!mfFactionTechPurchasable(n.id)){
    if(!silent) toast('BROOD THREAT DATA β€” enemy capabilities cannot be researched or equipped');
    return false;
  }
  if(!devAvail(n)){ if(!silent) toast('πŸ”’ Requires '+n.req.map(r=>DEVTREE.find(x=>x.id===r).nm).join(', ')); return false; }
  if((META.researchData||0)<n.data||!matHas(n.cost)){ if(!silent) toast('Need '+devMissing(n).join(', ')); return false; }
  META.researchData-=n.data; matSpend(n.cost);
  devDone()[n.id]=1; metaSave();
  if(!silent){ toast('πŸ”¬ DEVELOPMENT UNLOCKED β€” '+n.nm); sfx('level'); buzz(30); renderDevelop(); renderMetaHead(); }
  return true;
}
/* The research queue is a spend plan, not a shopping list. After a match pays
   Data and salvage, complete every queued node that is actually affordable so
   the debrief β†’ Development loop does not require a second trip to tap RESEARCH. */
function devFlushQueue(){
  const q=Array.isArray(META.resQueue)?META.resQueue.slice():[];
  if(!q.length) return [];
  const done=[];
  const next=[];
  for(const id of q){
    const n=DEVTREE.find(x=>x.id===id);
    if(!n||devHas(id)) continue;
    if(devBuy(n,true)) done.push(n);
    else next.push(id);
  }
  META.resQueue=next;
  if(done.length){
    metaSave();
    toast('πŸ”¬ DEVELOPMENT COMPLETE β€” '+done.map(n=>n.nm).join(', '));
    if(typeof sfx==='function') sfx('level');
    if(typeof renderDevelop==='function') renderDevelop();
    if(typeof renderMetaHead==='function') renderMetaHead();
  } else if(q.join('|')!==next.join('|')) metaSave();
  return done;
}
/* These values already governed the simulation below; naming them lets the
   Development sheet quote the exact same rules instead of restating numbers in
   UI copy. They are deliberately not save fields. */
const DEV_MODULE_WEAR_MUL=0.75, DEV_MATERIAL_YIELD_MUL=1.35,
      DEV_MODULE_DURABILITY_CAP=2, DEV_REFIT_COST_MUL=0.5,
      DEV_REFIT_RESTORE_MUL=0.6;
function moduleSlots(){ return 1+(devHas('slot2')?1:0)+(devHas('slot3')?1:0); }
function wearRate(){ return devHas('logistics')?DEV_MODULE_WEAR_MUL:1; }
function matYield(){ return devHas('salvage')?DEV_MATERIAL_YIELD_MUL:1; }

/* Every committed operation advances account research, including a defeat.
   The Research Complex remains the fastest source through studies and a live
   network bonus, but making it the ONLY source created a progression deadlock:
   a newcomer who lost the lab earned zero Data and could never improve the lab.
   Short surrender loops still pay nothing. */
function researchDataFromMatch(win){
  const studies=Math.max(0,typeof resDone!=='undefined'?resDone|0:0);
  let labs=0;
  if((stats.t|0)>=90&&typeof blds!=='undefined')
    for(const B of blds) if(B.alive&&B.team===0&&B.type==='techlab'&&B.prog>=1) labs++;
  const studyData=studies*3;
  const network=Math.min(3,labs);
  const hold=win&&labs?2:0;
  const committed=typeof matchCommitted==='function'?matchCommitted(win):(win||(stats.t|0)>=180);
  const mission=committed?(win?4:2):0;
  const challenge=win?clamp(difficulty|0,0,2):0;
  const raw=mission+studyData+network+hold+challenge;
  const threat=(typeof threatSel==='function')?1+Math.max(0,threatSel()-1)*0.06:1;
  const rewardScale=win?1:(committed?.35:0);
  const total=Math.min(24,Math.round(raw*threat*rewardScale));
  META.researchData=(META.researchData||0)+total;
  const parts=win?[['Mission recovery',mission],['Studies',studyData],['Lab network',network],
    ['Objective hold',hold],['Challenge',challenge]].filter(p=>p[1]>0):
    (committed&&total?[['Committed defeat Β· 35% recovery',total]]:[]);
  return {total,parts};
}

/* ---- MODULES ---------------------------------------------------------------
   Each one states plainly what it does, what it costs, how long it lasts, and
   what mark it puts in the field. `dur` is in MATCHES: a player can read
   "eleven matches" and plan around it in a way that an abstract charge count
   never allows. */
const MODULES=[
 {id:'plate',  nm:'Reactive Plating', em:'πŸ›‘', col:[120,200,255], req:'metallurgy',
  ds:'Your units +10% health', compat:'All player combat and support units', dur:12, cost:{alloy:35},
  apply:()=>{ resHpMult*=1.10; }},
 {id:'optic',  nm:'Targeting Uplink', em:'🎯', col:[255,190,110], req:'optics',
  ds:'Your units +8% damage', compat:'All player combat and support units', dur:10, cost:{alloy:24,circuit:10},
  apply:()=>{ armyDmgMult+=0.08; }},
 {id:'range',  nm:'Sensor Mast',      em:'πŸ“‘', col:[150,255,200], req:'optics',
  ds:'Your units +10% range', compat:'All player armed units', dur:10, cost:{alloy:20,circuit:14},
  apply:()=>{ resRngMult*=1.10; }},
 {id:'tempo',  nm:'Drive Governors',  em:'βš™', col:[200,180,255], req:'servos',
  ds:'Structures build 20% faster', compat:'Player structure construction network', dur:14, cost:{alloy:40,circuit:12},
  apply:()=>{ bldSpeedMult*=1.20; }},
 {id:'recl',   nm:'Reclaim Servos',   em:'β™»', col:[140,235,150], req:'servos',
  ds:'+40% salvage reclaimed', compat:'Player reclaim and salvage recovery', dur:14, cost:{alloy:45,circuit:8},
  apply:()=>{ salvageMult*=1.40; }},
 {id:'core',   nm:'Fusion Overdraw',  em:'☒', col:[255,150,90], req:'reactor',
  ds:'+18% energy income', compat:'Player battlefield energy economy', dur:9, cost:{isotope:16,circuit:16},
  apply:()=>{ resEnergyMult*=1.18; }},
 {id:'relic',  nm:'Relic Lattice',    em:'β—ˆ', col:[255,120,220], req:'relictech',
  ds:'+15% health AND +12% damage β€” burns out fast', compat:'All player combat and support units', dur:5, cost:{relic:3,circuit:30},
  apply:()=>{ resHpMult*=1.15; armyDmgMult+=0.12; }},
 {id:'emp',    nm:'EMP Charge',       em:'⚑', col:[120,255,255], req:'ability',
  ds:'Commander ability: stun everything in a wide radius', compat:'Commander active-ability loadout', dur:6, cost:{relic:5,isotope:30},
  apply:()=>{ abUnlock[4]=true; }},
];

/* ---- READ-ONLY DEVELOPMENT PRESENTERS --------------------------------------
   These functions describe the live catalogs; they neither spend resources nor
   add save fields. Keeping the quote beside MODULES/DEVTREE prevents the graph,
   crafting list, and future inspectors from inventing their own numbers. */
function devFactionLabel(id){
  let key=id||'';
  if(typeof facCanonicalId==='function') key=facCanonicalId(key)||key;
  if(typeof facArt==='function'){
    const a=facArt(key)||facArt(id); if(a&&a.nm) return a.nm;
  }
  return key?key.replace(/^./,c=>c.toUpperCase()):'All factions';
}
function devNodeScope(n){
  if(!n) return 'ACCOUNT Β· FUTURE RTS MATCHES';
  if(typeof mfFactionTechPurchasable==='function'&&!mfFactionTechPurchasable(n.id))
    return 'AI DOSSIER Β· NO CURRENT PLAYER EFFECT';
  return n.fac
    ? 'ACCOUNT UNLOCK Β· ACTIVE IN '+devFactionLabel(n.fac).toUpperCase()+' RTS MATCHES'
    : 'ACCOUNT UNLOCK Β· ACTIVE IN FUTURE RTS MATCHES';
}
function devNodeUnlocks(n){
  if(!n) return [];
  const out=[];
  for(const m of MODULES) if(m.req===n.id) out.push({kind:'module',id:m.id,label:'MODULE Β· '+m.nm});
  if(n.id==='slot2'||n.id==='slot3') out.push({kind:'loadout',id:n.id,label:'LOADOUT Β· +1 MODULE SLOT'});
  else if(n.id==='salvage') out.push({kind:'system',id:n.id,label:'MATCH MATERIAL RECOVERY Β· +'+Math.round((DEV_MATERIAL_YIELD_MUL-1)*100)+'%'});
  else if(n.id==='logistics') out.push({kind:'system',id:n.id,label:'EQUIPPED MODULE WEAR Β· -'+Math.round((1-DEV_MODULE_WEAR_MUL)*100)+'%'});
  else if(n.id==='refit') out.push({kind:'system',id:n.id,label:'REFIT Β· '+Math.round(DEV_REFIT_COST_MUL*100)+'% MATERIAL COST Β· RESTORES '+Math.round(DEV_REFIT_RESTORE_MUL*100)+'% DURABILITY'});
  else if(n.id==='xeno') out.push({kind:'system',id:n.id,label:'HIVE RECOVERY Β· RELIC CORE REWARDS ENABLED'});
  if(!out.length) out.push({kind:n.fac?'doctrine':'system',id:n.id,label:(n.fac?'FACTION DOCTRINE Β· ':'SYSTEM Β· ')+n.ds});
  return out;
}
function devNodeImpact(n){
  if(!n) return 0;
  const direct=DEVTREE.filter(x=>x.req&&x.req.indexOf(n.id)>=0).length;
  return devNodeUnlocks(n).length*4+direct;
}
function devRecommendNext(nodes){
  const source=Array.isArray(nodes)?nodes:DEVTREE;
  const pool=source.filter(n=>!devHas(n.id)&&
    (typeof mfFactionTechPurchasable!=='function'||mfFactionTechPurchasable(n.id)));
  if(!pool.length) return null;
  const ranked=pool.map(n=>{
    const open=devAvail(n),ready=open&&(META.researchData||0)>=n.data&&matHas(n.cost);
    const cost=(n.data||0)+Object.values(n.cost||{}).reduce((a,v)=>a+(+v||0),0);
    return {n,state:ready?'ready':open?'funding':'locked',rank:ready?0:open?1:2,impact:devNodeImpact(n),cost,order:DEVTREE.indexOf(n)};
  }).sort((a,b)=>a.rank-b.rank||b.impact-a.impact||a.cost-b.cost||a.order-b.order);
  const r=ranked[0];
  return {id:r.n.id,nm:r.n.nm,state:r.state,
    reason:r.state==='ready'?'READY NOW Β· COSTS AND PREREQUISITES MET':
      r.state==='funding'?'OPEN PATH Β· GATHER '+devMissing(r.n).join(' Β· '):
        'PREREQUISITE PATH Β· '+r.n.req.filter(id=>!devHas(id)).map(id=>(DEVTREE.find(x=>x.id===id)||{nm:id}).nm).join(' Β· ')};
}
function devRecommendAfter(n){
  if(!n||!devHas(n.id)) return null;
  const direct=DEVTREE.filter(x=>x.req&&x.req.indexOf(n.id)>=0&&!devHas(x.id));
  if(direct.length) return devRecommendNext(direct);
  const same=DEVTREE.filter(x=>!devHas(x.id)&&((x.fac||'')===(n.fac||'')));
  return devRecommendNext(same);
}
function devNodeOutcome(n){
  const unlocks=devNodeUnlocks(n);
  return {scope:devNodeScope(n),unlocks,recommendation:devRecommendAfter(n),
    jump:unlocks.some(x=>x.kind==='module')?'craft':unlocks.some(x=>x.kind==='loadout')?'loadout':''};
}
function modRefitCost(m){
  const out={}; for(const k in m.cost) out[k]=Math.max(1,Math.round(m.cost[k]*DEV_REFIT_COST_MUL));
  return out;
}
function modCraftQuote(m,have){
  const cap=m.dur*DEV_MODULE_DURABILITY_CAP;
  have=Math.min(cap,Math.max(0,have==null?(modOwned()[m.id]||0):+have||0));
  const craftResult=Math.min(have+m.dur,cap),craftAdd=craftResult-have;
  const refitRequested=Math.ceil(m.dur*DEV_REFIT_RESTORE_MUL);
  const refitResult=Math.min(have+refitRequested,cap),refitAdd=refitResult-have;
  return {compatibility:m.compat||'Player battlefield loadout',scope:'ACCOUNT INVENTORY Β· ACTIVE IN FUTURE RTS MATCHES WHILE FITTED',
    current:have,cap,atCap:have>=cap,
    craftRequested:m.dur,craftAdd,craftResult,canCraft:craftAdd>0,
    refitCost:modRefitCost(m),refitRequested,refitAdd,refitResult,canRefit:refitAdd>0};
}
function modWearDisplay(n){ return Math.round(Math.max(0,n)*10)/10; }
function modOwned(){
  META.mods=META.mods||{};
  /* Imported/legacy saves can contain durability above the advertised cap.
     Clamp the authoritative inventory itself, not just quotes, or the module
     keeps applying for hidden extra matches while every presenter says FULL. */
  for(const m of MODULES){
    if(!Object.prototype.hasOwnProperty.call(META.mods,m.id)) continue;
    const raw=+META.mods[m.id],cap=m.dur*DEV_MODULE_DURABILITY_CAP;
    META.mods[m.id]=Math.min(cap,Math.max(0,Number.isFinite(raw)?raw:0));
  }
  return META.mods;
}
function modEquipped(){
  /* Imported careers could fit duplicate, broken or unknown module ids and
     bypass the one-to-three slot limit. applyModules then applied duplicates
     twice while the UI still claimed a bounded loadout. Normalize the saved
     fit itself, not only the presenter; preserve unrecognized owned stock for
     forward compatibility, but never let it run as an active module. */
  const raw=Array.isArray(META.equip)?META.equip:[],owned=modOwned(),res=META.res||{};
  const slots=1+(res.slot2?1:0)+(res.slot3?1:0),clean=[];
  for(const id of raw){
    if(clean.length>=slots)break;
    if(typeof id!=='string'||clean.indexOf(id)>=0||!(owned[id]>0)||
       !MODULES.some(m=>m.id===id))continue;
    clean.push(id);
  }
  if(raw.length!==clean.length||raw.some((id,i)=>id!==clean[i]))META.equip=clean;
  else META.equip=raw;
  return META.equip;
}
function modCraft(m){
  if(!devHas(m.req)){ toast('πŸ”’ Unlock '+(DEVTREE.find(r=>r.id===m.req)||{}).nm+' in Development first'); return; }
  const quote=modCraftQuote(m);
  if(!quote.canCraft){ toast(m.nm+' is already at durability cap ('+quote.current+' / '+quote.cap+')'); return false; }
  if(!matHas(m.cost)){ toast('Not enough materials'); return; }
  matSpend(m.cost);
  const o=modOwned();
  /* Crafting a module you already own TOPS IT UP rather than making a second
     copy β€” inventory management is not the interesting part of this system. */
  o[m.id]=quote.craftResult;
  metaSave();
  toast('πŸ”§ CRAFTED β€” '+m.nm+' Β· +'+modWearDisplay(quote.craftAdd)+' durability Β· '
    +modWearDisplay(quote.current)+' β†’ '+modWearDisplay(quote.craftResult)+' / '+quote.cap);
  sfx('level'); buzz(25); renderDevelop();
  return true;
}
function modRepair(m){
  if(!devHas('refit')){ toast('πŸ”’ Unlock Field Refit in Development to repair modules'); return; }
  const quote=modCraftQuote(m);
  if(!quote.canRefit){ toast(m.nm+' is already at durability cap ('+quote.current+' / '+quote.cap+')'); return false; }
  const half=quote.refitCost;
  if(!matHas(half)){ toast('Repair needs '+matCostStr(half)); return; }
  matSpend(half);
  const o=modOwned(); o[m.id]=quote.refitResult;
  metaSave(); toast('πŸ”§ REFITTED β€” '+m.nm+' Β· +'+modWearDisplay(quote.refitAdd)+' durability Β· '
    +modWearDisplay(quote.current)+' β†’ '+modWearDisplay(quote.refitResult)+' / '+quote.cap);
  sfx('ui'); renderDevelop();
  return true;
}
function modToggle(m){
  const eq=modEquipped(), i=eq.indexOf(m.id);
  if(i>=0){ eq.splice(i,1); }
  else{
    if(!(modOwned()[m.id]>0)){ toast('Craft '+m.nm+' first'); return; }
    if(eq.length>=moduleSlots()){ toast('All '+moduleSlots()+' slots are full β€” unequip one first'); return; }
    eq.push(m.id);
  }
  metaSave(); renderDevelop(); sfx('ui');
}
/* Applied once when a match starts, after the Armory perks. */
function applyModules(){
  bldSpeedMult=1;
  for(const id of modEquipped()){
    const m=MODULES.find(x=>x.id===id);
    if(m&&modOwned()[id]>0) try{ m.apply(); }catch(e){}
  }
}
/* Consumed once when a match ends. Anything that hits zero BREAKS: it comes
   off the machine, its mark goes out, and the player is told which one. */
function wearModules(){
  const o=modOwned(), eq=modEquipped();
  const broke=[];
  const w=wearRate();
  for(let i=eq.length-1;i>=0;i--){
    const id=eq[i], m=MODULES.find(x=>x.id===id);
    if(!m) { eq.splice(i,1); continue; }
    o[id]=Math.max(0,(o[id]||0)-w);
    if(o[id]<=0){ o[id]=0; eq.splice(i,1); broke.push(m); }
  }
  metaSave();
  return broke;
}
/* What the field renderer asks for: the marks currently riding on the player's
   machines. An empty list is exactly what a broken module looks like. */
function activeModuleMarks(){
  const out=[];
  for(const id of modEquipped()){
    const m=MODULES.find(x=>x.id===id);
    if(m&&modOwned()[id]>0) out.push(m);
  }
  return out;
}

/* ---- UI --------------------------------------------------------------------- */
let devTab='research', devBranch='FABRICATION';
function devBranchNav(){
  return '<div class="devBranchNav">'+['FABRICATION','DOCTRINE','XENOLOGY'].map(br=>{
    const ns=DEVTREE.filter(n=>n.br===br), done=ns.filter(n=>devHas(n.id)).length;
    return '<button class="devBranchBtn'+(devBranch===br?' on':'')+'" data-br="'+br+'">'
      +br+' <span>'+done+'/'+ns.length+'</span></button>';
  }).join('')+'</div>';
}
function devResearchExtra(n,done,open){
  let h='';
  const outcome=devNodeOutcome(n);
  h+='<div class="devReqInline">AFFECTS Β· '+outcome.scope+'</div>'
    +'<div class="devReqInline">OUTCOME Β· '+outcome.unlocks.map(x=>x.label).join(' Β· ')+'</div>';
  if(n.req.length){
    h+='<div class="devReqs"><b>REQUIRES</b>'+n.req.map(id=>{
      const r=DEVTREE.find(x=>x.id===id);
      return '<span class="'+(devHas(id)?'ok':'no')+'">'+(devHas(id)?'βœ“ ':'')+(r?r.nm:id)+'</span>';
    }).join('')+'</div>';
  }
  if(open&&!done){ const miss=devMissing(n); if(miss.length) h+='<div class="devNeed">NEED '+miss.join(' Β· ')+'</div>'; }
  if(done){
    const mods=MODULES.filter(m=>m.req===n.id);
    if(mods.length) h+='<button class="devSub devJump" data-jump="craft">OPEN CRAFTING Β· '+mods.length+' UNLOCKED</button>';
    else if(n.id==='slot2'||n.id==='slot3') h+='<button class="devSub devJump" data-jump="loadout">OPEN LOADOUT</button>';
    if(outcome.recommendation) h+='<div class="devReqInline">RECOMMENDED NEXT Β· '+outcome.recommendation.nm+' Β· '+outcome.recommendation.reason+'</div>';
  }
  return h;
}
function renderDevelop(){
  const scr=document.getElementById('devScr'); if(!scr) return;
  const title=scr.querySelector('h2'); if(title) title.textContent='πŸ”¬ DEVELOPMENT';
  const b=matBag();
  const mr=document.getElementById('matRow');
  if(mr) mr.innerHTML=Object.keys(MATS).map(k=>
    '<div class="matChip">'+itemArt('mat_'+k,MATS[k].em,20)+Math.floor(b[k]||0)+'</div>').join('')
    +'<div class="matChip data"><span>β—†</span>'+Math.floor(META.researchData||0)+' DATA Β· DEV ONLY</div>';
  const tr=document.getElementById('devTabs');
  if(tr){
    tr.setAttribute('role','tablist');
    tr.setAttribute('aria-label','Development categories');
    /* Arsenal owns the broad account LOADOUT. This tab is specifically the
       crafted module fit, so naming it prevents two destinations promising the
       same job while preserving the separate progression system. */
    tr.innerHTML=[['research','πŸ”¬','UNLOCKS'],['craft','πŸ”§','CRAFTING'],['loadout','β˜…','MODULE FIT']]
      .map(([k,e,n])=>'<button class="tabBtn'+(devTab===k?' on':'')+'" data-k="'+k+'" role="tab" aria-selected="'+(devTab===k?'true':'false')+'" aria-controls="devBody">'
        +'<span class="tEm">'+e+'</span>'+n+'</button>').join('');
    tr.querySelectorAll('.tabBtn').forEach(x=>mfBindTap(x,ev=>{
      ev.stopPropagation(); devTab=x.dataset.k; sfx('ui'); renderDevelop();
    }));
  }
  const g=document.getElementById('devBody'); if(!g) return;
  g.setAttribute('role','tabpanel');
  let h=mfProgressionGuideHTML('development');
  if(devTab==='research'){
    const total=DEVTREE.filter(n=>devHas(n.id)).length;
    h+='<div class="devProgress"><b>'+total+'/'+DEVTREE.length+'</b>'+mfOwnershipBadgeHTML('permanent')+'<span>ACCOUNT UNLOCKS CARRY INTO FUTURE MATCHES Β· FIELD STUDIES BANK β—† DATA</span></div>'+devBranchNav();
    h+='<div class="devBr">'+devBranch+' PATH</div>';
      for(const n of DEVTREE.filter(x=>x.br===devBranch)){
        const done=devHas(n.id), open=devAvail(n);
        const can=open&&!done&&(META.researchData||0)>=n.data&&matHas(n.cost);
        h+='<div class="devItem'+(done?' done':open?'':' lock')+'" data-r="'+n.id+'">'
          +'<div class="modEm">'+itemArt(n.art||'res_'+n.id,'πŸ”¬',34)+'</div>'
          +'<div class="devTx">'+mfOwnershipBadgeHTML('permanent')+'<b>'+n.nm+'</b><span>'+n.ds+'</span>'
          +'<div class="devCost">'+(done?'UNLOCKED':matCostStr(n.cost)+'   β—† '+n.data+' DATA')+'</div></div>'
          +'<div class="devAct'+(can?' go':'')+'">'+(done?'βœ“':open?'UNLOCK':'πŸ”’')+'</div></div>'
          +devResearchExtra(n,done,open);
      }
  } else if(devTab==='craft'){
    const o=modOwned();
    h+='<div class="devProgress"><b>ACCOUNT</b>'+mfOwnershipBadgeHTML('crafted')+'<span>CRAFTED MODULES CARRY BETWEEN MATCHES AND WEAR WITH USE</span></div>';
    for(const m of MODULES){
      const locked=!devHas(m.req), quote=modCraftQuote(m,o[m.id]),have=quote.current;
      const canCraft=!locked&&quote.canCraft;
      h+='<div class="devItem'+(locked?' lock':'')+'" data-c="'+m.id+'">'
        +'<div class="modEm">'+itemArt('mod_'+m.id,m.em,34)+'</div>'
        +'<div class="devTx">'+mfOwnershipBadgeHTML('crafted')+'<b>'+m.nm+'</b><span>'+m.ds+'</span>'
        +'<div class="devReqInline">COMPATIBLE Β· '+quote.compatibility+'</div>'
        +'<div class="devReqInline">OUTPUT Β· +'+modWearDisplay(quote.craftAdd)+' DURABILITY Β· RESULT '+modWearDisplay(quote.current)+' β†’ '+modWearDisplay(quote.craftResult)+' / '+quote.cap+' CAP</div>'
        +(locked?'<div class="devReqInline">REQUIRES '+((DEVTREE.find(x=>x.id===m.req)||{}).nm||m.req)+'</div>':'')
        +'<div class="devCost">'+matCostStr(m.cost)+'<span class="costSep">RECIPE +'+m.dur+' Β· APPLIED +'+modWearDisplay(quote.craftAdd)+(quote.atCap?' Β· CAP REACHED':'')+'</span></div>'
        +(have>0?'<div class="wearO"><div class="wearF" style="width:'+Math.min(100,have/quote.cap*100)+'%"></div></div>':'')
        +'</div>'
        +'<div class="devAct'+(canCraft?' go':'')+'">'+(locked?'πŸ”’':!quote.canCraft?'CAP REACHED':(have>0?'CRAFT +'+modWearDisplay(quote.craftAdd):'CRAFT'))+'</div></div>';
    }
  } else {
    const eq=modEquipped(), o=modOwned();
    h+='<div class="devProgress"><b>ACCOUNT</b>'+mfOwnershipBadgeHTML('crafted')+'<span>EQUIPPED MODULES APPLY IN FUTURE MATCHES UNTIL DURABILITY IS SPENT</span></div>';
    h+='<div class="devBr">EQUIPPED β€” '+eq.length+' / '+moduleSlots()+' SLOTS</div>';
    if(!eq.length) h+='<div class="devNone">Nothing fitted. Equipped modules appear as marks on your Commander and HQ in the field.</div>';
    for(const m of MODULES){
      const quote=modCraftQuote(m,o[m.id]),have=quote.current;
      if(have<=0&&eq.indexOf(m.id)<0) continue;
      const on=eq.indexOf(m.id)>=0;
      const worn=have<=m.dur*0.25;
      h+='<div class="devItem'+(on?' done':'')+'" data-e="'+m.id+'">'
        +'<div class="modEm">'+itemArt('mod_'+m.id,m.em,34)+'</div>'
        +'<div class="devTx">'+mfOwnershipBadgeHTML('crafted')+'<b>'+m.nm+'</b><span>'+m.ds+'</span>'
        +'<div class="devReqInline">COMPATIBLE Β· '+quote.compatibility+'</div>'
        +'<div class="wearO"><div class="wearF'+(worn?' low':'')+'" style="width:'+Math.min(100,have/quote.cap*100)+'%"></div></div>'
        +'<div class="devCost">'+Math.ceil(have)+' matches left Β· '+modWearDisplay(have)+' / '+quote.cap+' DURABILITY CAP'+(worn?'  Β·  WEARING OUT':'')+'</div></div>'
        +'<div class="devAct'+(on?'':' go')+'">'+(on?'FITTED':'FIT')+'</div></div>';
      if(quote.canRefit&&devHas('refit'))
        h+='<div class="devSub" data-x="'+m.id+'">πŸ”§ REFIT β€” '+matCostStr(quote.refitCost)
          +' Β· REFIT RESTORES +'+modWearDisplay(quote.refitAdd)+' Β· RESULT '+modWearDisplay(quote.current)+' β†’ '+modWearDisplay(quote.refitResult)+' / '+quote.cap+'</div>';
    }
  }
  g.innerHTML=h;
  const bind=function(el,fn){ if(typeof mfBindTap==='function') mfBindTap(el,fn); else el.addEventListener('click',fn); };
  g.querySelectorAll('[data-br]').forEach(el=>bind(el,ev=>{
    ev.stopPropagation(); devBranch=el.dataset.br; sfx('ui'); renderDevelop(); }));
  g.querySelectorAll('[data-jump]').forEach(el=>bind(el,ev=>{
    ev.stopPropagation(); devTab=el.dataset.jump; sfx('ui'); renderDevelop(); }));
  g.querySelectorAll('[data-r]').forEach(el=>bind(el,()=>{
    const n=DEVTREE.find(x=>x.id===el.dataset.r); if(n) devBuy(n); }));
  g.querySelectorAll('[data-c]').forEach(el=>bind(el,()=>{
    const m=MODULES.find(x=>x.id===el.dataset.c); if(m) modCraft(m); }));
  g.querySelectorAll('[data-e]').forEach(el=>bind(el,()=>{
    const m=MODULES.find(x=>x.id===el.dataset.e); if(m) modToggle(m); }));
  g.querySelectorAll('[data-x]').forEach(el=>bind(el,ev=>{
    ev.stopPropagation();
    const m=MODULES.find(x=>x.id===el.dataset.x); if(m) modRepair(m); }));
}
/* Called at match end, after rewards. */
function developRecord(res){
  const recovery=fieldRecoveryFromMatch(res),g=matsFromMatch(res), y=matYield();
  for(const k in g) g[k]=Math.round(g[k]*y);
  matGrant(g);
  const broke=wearModules();
  /* metaGrant already banked Research Data. Flush now so queued nodes spend
     the payout in this same debrief rather than sitting READY until tapped. */
  const researched=typeof devFlushQueue==='function'?devFlushQueue():[];
  return {mats:g,recovery,broke,researched};
}
function initDevelop(){
  matBag(); devDone(); modOwned(); modEquipped();
  /* Cloud merge used to pick the career with more unused Data over the one
     that had already spent it on nodes. Count ownership so research persists. */
  if(typeof careerWeight==='function'&&!careerWeight._devProgress){
    const _cw=careerWeight;
    careerWeight=function(m){
      let nodes=0, mats=0;
      if(m&&m.res) for(const k in m.res) if(m.res[k]) nodes++;
      if(m&&m.mats) for(const k in m.mats) mats+=(m.mats[k]|0);
      return _cw(m)+nodes*80+mats*2;
    };
    careerWeight._devProgress=true;
  }
  renderDevelop();
  /* Field Research Archives bank Data mid-match. Spend it into the queued
     node immediately so the pickup is the unlock, not a number that waits. */
  if(typeof applyCrate==='function'&&!applyCrate._devFlush){
    const _ac=applyCrate;
    applyCrate=function(k,x,y){
      const before=META.researchData||0;
      const r=_ac(k,x,y);
      if((META.researchData||0)>before) devFlushQueue();
      return r;
    };
    applyCrate._devFlush=true;
  }
}