Spaces:
Running
Running
| ; | |
| ; | |
| /* ============================================================================ | |
| 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); | |
| } | |
| } | |
| }; | |