File size: 14,426 Bytes
b51e03a
3597f5a
b51e03a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3597f5a
 
 
 
 
 
 
b51e03a
 
3597f5a
 
 
 
 
b51e03a
 
 
 
3597f5a
 
 
 
 
 
 
b51e03a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3597f5a
b51e03a
3597f5a
 
 
 
b51e03a
 
3597f5a
b51e03a
 
 
 
 
 
 
 
 
3597f5a
 
b51e03a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
;
;
/* ============================================================================
   FACTION DOCTRINES — rules, not flavour text
   ----------------------------------------------------------------------------
   Faction names and models already resolve through playerFaction / AI.fac.
   Keep the gameplay seam here as small query functions so the shared TYPES and
   BT tables can remain save-compatible while each army changes how those
   chassis are paid for and used. Do not cache the selected faction: setup,
   campaign restore and imported sessions may all change it before resetWorld.
   ============================================================================ */

function factionDoctrineKey(team){
  if(team===2) return 'horde';
  if(team===1) return (typeof AI!=='undefined'&&AI.fac)||'legion';
  const f=(typeof playerFaction!=='undefined'&&playerFaction)||'nova';
  return f==='ascendancy'||f==='dominion'?'legion':f==='brood'?'horde':f;
}

/* Frontline Command wins through repeatable engineering rather than a raw
   combat scalar. The card shows the discounted STREAMED total, and production
   pays exactly that same total in sim.js. Brood cost is used by AI production;
   its true biological spawn systems remain in nests / Massflesh. */
function factionDoctrineUnitCostMul(team){
  const f=factionDoctrineKey(team);
  return f==='nova'?.94:f==='horde'?.86:1;
}
function factionDoctrineUnitCost(T,team){
  const m=factionDoctrineUnitCostMul(team);
  return {m:Math.ceil(T.cm*m),e:Math.ceil(T.ce*m)};
}
function factionDoctrineBuildSpeedMul(team){
  const f=factionDoctrineKey(team);
  return f==='nova'?1.12:f==='horde'?1.18:1;
}

/* Shared numeric chassis do not require every faction to expose every slot.
   These arsenal cuts are intentionally small: each side keeps a complete
   counter triangle, but its signature answer cannot be copied by both rivals.
   Existing save queues are not rewritten, so an older session can finish what
   it had already paid for. Only NEW production cards use this roster. */
const FAC_ARSENAL={
  /* Nova keeps every factory, air, naval and titan card. Listing it here makes
     the Four complete without shrinking combined-arms flexibility: a missing
     nova key used to look like an unfinished table. */
  nova:{
    fac:new Set([0,1,2,3,6,7,9,10,11,16,18,19,20,21,22,23,24,26,27,32]),
    tgate:new Set([8,26]),airfield:new Set([5,17,25]),harbor:new Set([14,15])
  },
  legion:{
    fac:new Set([0,1,2,3,7,9,10,16,18,19,20,21,22,26,27,32]),
    /* Titan Gate costs the same 820 blended as every other faction. Flavor
       already names the Legion TITAN (Ascendant) and says the gate builds
       TITANs. No doctrine comment forbids chassis 8 — the Basilisk-only set
       was an incomplete cut, not a written exclude. Match Nova: TITAN + Tyrant. */
    tgate:new Set([8,26]),airfield:new Set([5,17]),harbor:new Set([14,15])
  },
  syndicate:{
    fac:new Set([0,1,2,6,7,10,11,19,20,23,24,26,27,32]),
    tgate:new Set([8]),airfield:new Set([5,17,25]),harbor:new Set([14,15])
  },
  /* Brood keeps a complete counter triangle (runners, walkers, bile, AA,
     support) but does not copy Dominion 400-range siege or Coalition
     beam/shield/sonic plant. Nest organisms 12/13/31 stay hive-spawned. */
  horde:{
    fac:new Set([0,1,2,3,7,9,10,18,19,20,21,22,27,32]),
    tgate:new Set([8]),airfield:new Set([5,17,25]),harbor:new Set([14,15])
  }
};
function factionDoctrineRoster(list,facility,team){
  const rule=FAC_ARSENAL[factionDoctrineKey(team)];
  if(!rule||!rule[facility]) return list.slice();
  return list.filter(t=>rule[facility].has(t));
}

/* Coalition extraction is an EFFICIENCY bonus, not a faster emptying of the
   field. economy.js drains the authored amount from the finite node, then
   applies this yield to what reaches storage. */
function factionDoctrineNodeYieldMul(team){
  return factionDoctrineKey(team)==='syndicate'?1.18:1;
}

/* Dominion momentum is per body. A single global timer would let an artillery
   piece inherit a rifleman's combat time; slot generation prevents a newly
   spawned unit inheriting the dead occupant's fury. The engagement grace is
   derived from weapon cooldown so deliberate siege weapons can still ramp. */
const facFightStart=new Float32Array(MAXU),facFightLast=new Float32Array(MAXU);
const facFightGen=new Int32Array(MAXU);
const facFightLit=new Uint8Array(MAXU);
function factionDoctrineReset(){
  facFightStart.fill(0);facFightLast.fill(-999);facFightGen.fill(-1);facFightLit.fill(0);
}
function factionDoctrineAttackMul(team,i){
  if(factionDoctrineKey(team)!=='legion'||i<0) return 1;
  const now=(typeof stats!=='undefined'&&stats.t)||0,T=TYPES[utype[i]];
  if(facFightGen[i]!==ugen[i]){
    facFightGen[i]=ugen[i];facFightStart[i]=now;facFightLast[i]=now;facFightLit[i]=0;
    return 1;
  }
  const grace=Math.max(4,(T&&T.cool||1)*1.8);
  if(now-facFightLast[i]>grace){facFightStart[i]=now;facFightLit[i]=0;}
  facFightLast[i]=now;
  const charge=clamp((now-facFightStart[i])/12,0,1);
  if(charge>=1&&!facFightLit[i]){
    facFightLit[i]=1;
    if(typeof addParticle==='function') addParticle(3,ux[i],uy[i],0,0,.5,(T&&T.size||14)*2.2,255,78,48);
  }
  return 1+.18*charge;
}

/* Called once after loadout and commander perks. The Coalition turns recovered
   battlefield hardware into account economy as well as improving claimed node
   yield; the multiplier composes with modules and match modifiers. */
function applyFactionDoctrineChoice(){
  factionDoctrineReset();
  if(factionDoctrineKey(0)==='syndicate') salvageMult*=1.18;
}

/* ---- ACCOUNT FACTION RESEARCH --------------------------------------------
   These are deliberately narrow consumers of the account tree. Generic
   research and crafted modules still compose normally. A faction unlock only
   runs when the player is actually commanding that faction; owning Dominion
   doctrine must never strengthen a Nova expedition or the opposing AI. */
const MF_FACTION_TECH_CONSUMERS=Object.freeze({
  asc_siege_foundry:{kind:'spawn-rule',consumer:'spawnUnit',faction:'legion'},
  asc_iron_discipline:{kind:'formation-rule',consumer:'formationSpacing',faction:'legion'},
  asc_crown_battery:{kind:'stance-rule',consumer:'setMode/unitTick',faction:'legion'},
  syn_quantum_grid:{kind:'economy-rule',consumer:'drawEnergy',faction:'syndicate'},
  syn_drone_mesh:{kind:'target-rule',consumer:'unitTick/dealDamage',faction:'syndicate'},
  syn_phase_lattice:{kind:'transport-rule',consumer:'mfAirliftPostTick',faction:'syndicate'},
  /* Brood is not selectable in Pre-Alpha. Selling these as live player buffs
     would be false, and binding account ownership to enemy strength would be
     hostile progression. They remain explicit dossier/future gates until the
     horde enters playableFactions(); the existing AI Tidecaster and Massflesh
     doctrines continue to run independently of the player's account. */
  hor_gene_splice:{kind:'future-gate',consumer:'mfFactionTechBroodGate',faction:'horde'},
  hor_synaptic_tide:{kind:'future-gate',consumer:'mfFactionTechBroodGate',faction:'horde'},
  hor_living_siege:{kind:'future-gate',consumer:'mfFactionTechBroodGate',faction:'horde'}
});

function mfFactionTechBroodGate(){
  return false;                         // AI-only until horde is deliberately made playable
}
function mfFactionTechPurchasable(id){
  const R=MF_FACTION_TECH_CONSUMERS[id];
  return !R||R.kind!=='future-gate';
}
function mfFactionTechActive(id){
  const R=MF_FACTION_TECH_CONSUMERS[id];
  if(!R||R.kind==='future-gate'||!devHas(id)) return false;
  return factionDoctrineKey(0)===R.faction;
}
/* Previous UI exposed the future Brood dossier through devBuy(), allowing a
   player to spend scarce account materials on a deliberately inert enemy-only
   node. Keep the nodes visible as intelligence, but fail closed here too. */
const mfFactionTechDevBuyBase=devBuy;
devBuy=function(n,silent){
  if(n&&!mfFactionTechPurchasable(n.id)){
    if(!silent){
      toast('AI DOSSIER — Brood evolution becomes researchable only when the faction is playable');
      if(typeof sfx==='function')sfx('ui');
    }
    return false;
  }
  return mfFactionTechDevBuyBase(n,silent);
};
function mfFactionTechArtillery(type){
  const T=TYPES[type];return !!(T&&T.cat==='art');
}

/* Hardened Dominion gun carriages. Apply after the complete spawn takeover
   chain, so transports, session restore and factory production all receive
   the same maximum-health rule without cloning any production code. */
const mfFactionTechSpawnBase=spawnUnit;
spawnUnit=function(type,team,x,y,cmdSlot){
  const i=mfFactionTechSpawnBase(type,team,x,y,cmdSlot);
  if(i>=0){
    /* Slots are recycled immediately. Tech timers and target marks are not
       part of sim.js's core arrays, so this takeover owns clearing them. */
    mfFactionTechOverStart[i]=0;mfFactionTechOverEnd[i]=0;
    mfFactionTechOverReady[i]=0;mfFactionTechOverFx[i]=0;
    mfFactionTechMarkUntil[i]=0;mfFactionTechMarkGen[i]=0;
  }
  if(i>=0&&team===0&&mfFactionTechActive('asc_siege_foundry')&&mfFactionTechArtillery(type)){
    uhpm[i]*=1.12;uhp[i]*=1.12;
  }
  return i;
};

/* Iron Discipline changes footprint rather than collision radius. Physical
   separation in sim.js remains authoritative, so a dense formation cannot
   collapse into a single-unit stack. Small squads retain normal clearance. */
const mfFactionTechFormationSpacingBase=formationSpacing;
formationSpacing=function(sel){
  const spacing=mfFactionTechFormationSpacingBase(sel);
  return sel&&sel.length>=4&&mfFactionTechActive('asc_iron_discipline')
    ?Math.max(24,spacing*.88):spacing;
};

/* Crown overcharge begins after the ordinary Siege deployment lock. It speeds
   cooldown recovery rather than multiplying impact damage, which preserves
   the authored projectile/splash model and cannot apply twice to one shell. */
const mfFactionTechOverStart=new Float32Array(MAXU);
const mfFactionTechOverEnd=new Float32Array(MAXU);
const mfFactionTechOverReady=new Float32Array(MAXU);
const mfFactionTechOverFx=new Float32Array(MAXU);
const mfFactionTechSetModeBase=setMode;
setMode=function(i,m){
  const changed=mfFactionTechSetModeBase(i,m);
  if(!changed)return false;
  if(m!==1){mfFactionTechOverEnd[i]=0;return true;}
  const now=(typeof stats!=='undefined'&&stats.t)||0;
  if(uteam[i]===0&&mfFactionTechArtillery(utype[i])&&
     mfFactionTechActive('asc_crown_battery')&&now>=mfFactionTechOverReady[i]){
    mfFactionTechOverStart[i]=now+MODE_SWITCH;
    mfFactionTechOverEnd[i]=mfFactionTechOverStart[i]+8;
    mfFactionTechOverReady[i]=mfFactionTechOverEnd[i]+18;
    mfFactionTechOverFx[i]=0;
  }
  return true;
};

/* Quantum routing discounts only requests made through drawEnergy(). Income,
   storage, construction escrow and account resources are intentionally not
   touched, so the stated 15% field-system saving is the complete effect. */
const mfFactionTechDrawEnergyBase=drawEnergy;
drawEnergy=function(team,e){
  if(team===0&&mfFactionTechActive('syn_quantum_grid'))e*=.85;
  return mfFactionTechDrawEnergyBase(team,e);
};

/* A lightweight per-target generation mark avoids allocating objects during
   combat and cannot survive a recycled unit slot. The spatial grid bounds the
   scout search; this runs three times every two seconds, not every frame. */
const mfFactionTechMarkUntil=new Float32Array(MAXU);
const mfFactionTechMarkGen=new Int32Array(MAXU);
let mfFactionTechScanAt=0,mfFactionTechLastT=0;
function mfFactionTechIsDrone(i){
  const T=TYPES[utype[i]];
  return !!(T&&(T.scout||(T.air&&!T.airTransport))&&T.cat!=='hero');
}
function mfFactionTechScanDrones(now){
  if(!mfFactionTechActive('syn_drone_mesh'))return;
  for(let i=0;i<unitHigh;i++){
    if(!ualive[i]||uteam[i]!==0||!mfFactionTechIsDrone(i))continue;
    let target=-1,best=250*250;
    forUnitsIn(ux[i],uy[i],250,j=>{
      if(uteam[j]===0)return;
      const d=dist2(ux[i],uy[i],ux[j],uy[j]);
      if(d<best){best=d;target=j;}
    });
    if(target>=0){
      const fresh=mfFactionTechMarkGen[target]!==ugen[target]||mfFactionTechMarkUntil[target]<=now;
      mfFactionTechMarkGen[target]=ugen[target];mfFactionTechMarkUntil[target]=now+3.5;
      if(fresh&&perfScale>.45)addParticle(3,ux[target],uy[target],0,0,.32,TYPES[utype[target]].size*1.25,104,255,146);
    }
  }
}
const mfFactionTechDealDamageBase=dealDamage;
dealDamage=function(j,dmg,attTeam,attacker,mu,wk){
  const now=(typeof stats!=='undefined'&&stats.t)||0;
  if(attTeam===0&&mfFactionTechActive('syn_drone_mesh')&&
     mfFactionTechMarkGen[j]===ugen[j]&&mfFactionTechMarkUntil[j]>now)dmg*=1.10;
  return mfFactionTechDealDamageBase(j,dmg,attTeam,attacker,mu,wk);
};

const mfFactionTechUnitTickBase=unitTick;
unitTick=function(dt){
  mfFactionTechUnitTickBase(dt);
  const now=(typeof stats!=='undefined'&&stats.t)||0;
  if(now<mfFactionTechLastT){
    mfFactionTechOverStart.fill(0);mfFactionTechOverEnd.fill(0);mfFactionTechOverReady.fill(0);
    mfFactionTechMarkUntil.fill(0);mfFactionTechMarkGen.fill(0);mfFactionTechScanAt=0;
  }
  mfFactionTechLastT=now;
  if(now>=mfFactionTechScanAt){mfFactionTechScanAt=now+.66;mfFactionTechScanDrones(now);}
  if(!mfFactionTechActive('asc_crown_battery'))return;
  for(let i=0;i<unitHigh;i++){
    if(!ualive[i]||uteam[i]!==0||umode[i]!==1||now<mfFactionTechOverStart[i]||now>=mfFactionTechOverEnd[i])continue;
    if(drawEnergy(0,2.5*dt)<.999){mfFactionTechOverEnd[i]=0;continue;}
    ucool[i]=Math.max(0,ucool[i]-dt*.28);
    mfFactionTechOverFx[i]-=dt;
    if(mfFactionTechOverFx[i]<=0){
      mfFactionTechOverFx[i]=.72;
      if(perfScale>.45)addParticle(3,ux[i],uy[i],0,0,.24,TYPES[utype[i]].size*1.35,255,112,72);
    }
  }
};

/* The Phase Ark already serializes passengers and renders the transfer beam.
   This hook changes only the final approach distance; it does not teleport the
   carrier itself, bypass map bounds, or duplicate cargo. */
const mfFactionTechAirliftPostBase=mfAirliftPostTick;
mfAirliftPostTick=function(dt){
  mfFactionTechAirliftPostBase(dt);
  if(!mfFactionTechActive('syn_phase_lattice'))return;
  for(let i=0;i<mfAirliftHolds.length;i++){
    const H=mfAirliftHolds[i];
    if(!H||!H.mission||!mfAirliftIsLive(i,H.gen)||uteam[i]!==0)continue;
    if(dist2(ux[i],uy[i],H.mission.x,H.mission.y)<=96*96){
      addParticle(3,H.mission.x,H.mission.y,0,0,.48,56,104,255,146);
      mfAirliftUnloadNow(i,H);
    }
  }
};