massfront-playtest / src /develop.js
CREATORJD's picture
Publish MASSFRONT web 1.33.33 (part 3)
3597f5a verified
Raw
History Blame Contribute Delete
26.5 kB
;
;
/* ============================================================================
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 β€” dropped by heavy kills'},
relic :{nm:'Relic Core',em:'β—ˆ',ds:'Pre-collapse tech β€” only from ruins and hives'},
};
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 src=(typeof ITEM_ART!=='undefined'&&ITEM_ART[id])||('./assets/icons/items/'+id+'.png');
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:'Unlocks the player-commanded Charged Barrage for selected artillery',
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 four armies read as different paint over the same
technology. These keys are faction-prefixed so ownership can never leak
between lines when player-faction selection becomes available. */
{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']},
{id:'hor_gene_splice', fac:'horde', br:'FABRICATION', nm:'Gene Splice Pit', ds:'AI DOSSIER - future Brood player tech for biomass kill-recovery; no player combat effect while Brood is AI-only',
cost:{alloy:24,isotope:16}, data:12, req:[]},
{id:'hor_synaptic_tide', fac:'horde', br:'DOCTRINE', nm:'Synaptic Tide', ds:'AI DOSSIER - Tidecasters form automatically at critical Brood mass; player unlock is reserved until Brood is playable',
cost:{circuit:26,isotope:28}, data:16, req:['hor_gene_splice']},
{id:'hor_living_siege', fac:'horde', br:'XENOLOGY', nm:'Living Siege', ds:'AI DOSSIER - Massflesh is currently an AI breakthrough carrier; player unlock is reserved until Brood is playable',
cost:{alloy:60,isotope:32,relic:2}, data:28, req:['hor_synaptic_tide']},
];
function devDone(){ META.res=META.res||{}; return META.res; }
function devHas(id){ 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('AI DOSSIER β€” Brood evolution becomes researchable only when the faction is playable');
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('πŸ”¬ RESEARCHED β€” '+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('πŸ”¬ RESEARCH 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;
}
function moduleSlots(){ return 1+(devHas('slot2')?1:0)+(devHas('slot3')?1:0); }
function wearRate(){ return devHas('logistics')?0.75:1; }
function matYield(){ return devHas('salvage')?1.35: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', 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', 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', 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', 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', 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', 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', 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', dur:6, cost:{relic:5,isotope:30},
apply:()=>{ abUnlock[4]=true; }},
];
function modOwned(){ META.mods=META.mods||{}; return META.mods; }
function modEquipped(){ META.equip=META.equip||[]; return META.equip; }
function modCraft(m){
if(!devHas(m.req)){ toast('πŸ”’ Research '+(DEVTREE.find(r=>r.id===m.req)||{}).nm+' first'); return; }
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]=Math.min((o[m.id]||0)+m.dur, m.dur*2);
metaSave();
toast('πŸ”§ CRAFTED β€” '+m.nm+' Β· '+o[m.id]+' matches of wear');
sfx('level'); buzz(25); renderDevelop();
}
function modRepair(m){
if(!devHas('refit')){ toast('πŸ”’ Research Field Refit to repair modules'); return; }
const half={}; for(const k in m.cost) half[k]=Math.max(1,Math.round(m.cost[k]*0.5));
if(!matHas(half)){ toast('Repair needs '+matCostStr(half)); return; }
matSpend(half);
const o=modOwned(); o[m.id]=Math.min((o[m.id]||0)+Math.ceil(m.dur*0.6), m.dur*2);
metaSave(); toast('πŸ”§ Refitted β€” '+m.nm); sfx('ui'); renderDevelop();
}
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='';
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>';
}
return h;
}
function renderDevelop(){
const scr=document.getElementById('devScr'); if(!scr) return;
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</div>'
+'<div class="matChip core"><span>⬑</span>'+(META.cores|0)+'</div>';
const tr=document.getElementById('devTabs');
if(tr){
tr.innerHTML=[['research','πŸ”¬','RESEARCH'],['craft','πŸ”§','CRAFTING'],['loadout','β˜…','LOADOUT']]
.map(([k,e,n])=>'<button class="tabBtn'+(devTab===k?' on':'')+'" data-k="'+k+'">'
+'<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;
let h='';
if(devTab==='research'){
const total=DEVTREE.filter(n=>devHas(n.id)).length;
h+='<div class="devProgress"><b>'+total+'/'+DEVTREE.length+'</b><span>ACCOUNT RESEARCH Β· FIELD LABS 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"><b>'+n.nm+'</b><span>'+n.ds+'</span>'
+'<div class="devCost">'+(done?'RESEARCHED':matCostStr(n.cost)+' β—† '+n.data+' DATA')+'</div></div>'
+'<div class="devAct'+(can?' go':'')+'">'+(done?'βœ“':open?'RESEARCH':'πŸ”’')+'</div></div>'
+devResearchExtra(n,done,open);
}
} else if(devTab==='craft'){
const o=modOwned();
for(const m of MODULES){
const locked=!devHas(m.req), have=o[m.id]||0;
h+='<div class="devItem'+(locked?' lock':'')+'" data-c="'+m.id+'">'
+'<div class="modEm">'+itemArt('mod_'+m.id,m.em,34)+'</div>'
+'<div class="devTx"><b>'+m.nm+'</b><span>'+m.ds+'</span>'
+(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">lasts '+m.dur+' matches</span></div>'
+(have>0?'<div class="wearO"><div class="wearF" style="width:'+Math.min(100,have/m.dur*100)+'%"></div></div>':'')
+'</div>'
+'<div class="devAct'+(locked?'':' go')+'">'+(locked?'πŸ”’':(have>0?'+'+m.dur:'CRAFT'))+'</div></div>';
}
} else {
const eq=modEquipped(), o=modOwned();
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 have=o[m.id]||0;
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"><b>'+m.nm+'</b><span>'+m.ds+'</span>'
+'<div class="wearO"><div class="wearF'+(worn?' low':'')+'" style="width:'+Math.min(100,have/m.dur*100)+'%"></div></div>'
+'<div class="devCost">'+Math.ceil(have)+' matches left'+(worn?' Β· WEARING OUT':'')+'</div></div>'
+'<div class="devAct'+(on?'':' go')+'">'+(on?'FITTED':'FIT')+'</div></div>';
if(have<m.dur&&devHas('refit'))
h+='<div class="devSub" data-x="'+m.id+'">πŸ”§ REFIT β€” '+matCostStr(
Object.fromEntries(Object.entries(m.cost).map(([k,v])=>[k,Math.max(1,Math.round(v*0.5))])))+'</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;
}
}