massfront-playtest / src /engine /models-infestation.js
CREATORJD's picture
Publish MASSFRONT web 1.33.33 (part 3)
3597f5a verified
Raw
History Blame Contribute Delete
44.8 kB
/* Concatenator leftover `;;` lived here; do not restore it. */
/* ============================================================================
INFESTATION SWARM - PROCEDURAL STRUCTURE LIBRARY
----------------------------------------------------------------------------
Load after engine/models.js. These are complete one-piece organic buildings,
not skins over Terran bases. Their silhouette vocabulary is rooted creep,
asymmetrical chitin, wet tissue, egg sacs, bone tools and genuinely hollow
mouths. Low radial segment counts keep the full faction practical on phones.
This module intentionally has no runtime takeover. Integration can choose
BLD_MDL_INFESTATION without making this optional art package own faction or
renderer policy. Keeping every helper inside the closure also protects the
game's single classic-script global scope from another name collision.
============================================================================ */
var BLD_MDL_INFESTATION, BLD_TUR_MDL_INFESTATION,
BLD_TIER_MDL_INFESTATION, BLD_TUR_H_INFESTATION, BLD_TUR_S_INFESTATION;
/* Ichor RGB — same wet green as INF_MUCUS / INF_SAC / INF_GLOW inside the
IIFE. organicfx.js prefers this for caste-0 infestation so a nest and a
Ravager bleed the same substance the sac tiles are painted with. */
var INF_ICHOR=Object.freeze({
wet:[115,177,77], dark:[62,88,30], hi:[185,255,72]
});
(function(){
/* Existing atlas cells are deliberately zoned by biological function. Using
one CHITIN tile for everything recreated the noisy single-material look
this faction pass is meant to replace. */
const INF_CHITIN=C(118,78,58), INF_CHITIN_H=C(166,116,76);
const INF_FLESH=C(119,56,68), INF_FLESH_H=C(174,84,91);
const INF_SAC=C(126,151,65), INF_MUCUS=C(115,177,77);
const INF_GLOW=C(185,255,72), INF_GLOW_H=C(226,255,151);
const INF_ROOT=C(58,43,34), INF_BONE=C(219,198,151);
const INF_SILK=C(191,184,169), INF_BORE=C(12,8,10);
COL_MAT.set(INF_CHITIN,MAT.CHITIN); COL_MAT.set(INF_CHITIN_H,MAT.CHITIN);
COL_MAT.set(INF_FLESH,MAT.RUST); COL_MAT.set(INF_FLESH_H,MAT.LEAF);
COL_MAT.set(INF_SAC,MAT.GLASS); COL_MAT.set(INF_MUCUS,MAT.CRYST);
/* TWR_GLOW's atlas tile is Nova cyan (#78b6c4). Brood lumen painted through
that id read as Frontline energy on every hive, nest and glow node. Slime
keeps the wet-green emissive the ichor VFX already uses. */
COL_MAT.set(INF_GLOW,MAT.BROOD_SLIME);COL_MAT.set(INF_GLOW_H,MAT.BROOD_SLIME);
COL_MAT.set(INF_ROOT,MAT.EARTH); COL_MAT.set(INF_BONE,MAT.STONE);
COL_MAT.set(INF_SILK,MAT.TRIM); COL_MAT.set(INF_BORE,MAT.TWR_BORE);
function infRand(seed){
let s=(seed|0)||1;
return function(){ s=(s*1664525+1013904223)>>>0; return s/4294967296; };
}
function infMix(a,b,t){ return a+(b-a)*t; }
function infPoint(a,b,t){ return [infMix(a[0],b[0],t),infMix(a[1],b[1],t),infMix(a[2],b[2],t)]; }
/* Rotate a temporary +Y primitive onto the line a->b. Positions and normals
use the same orthonormal frame; UV and material data remain untouched. */
function infMergeAlong(m,mm,a,b){
let dx=b[0]-a[0],dy=b[1]-a[1],dz=b[2]-a[2];
const len=Math.hypot(dx,dy,dz)||1; dx/=len;dy/=len;dz/=len;
const rx=Math.abs(dy)>.92?0:0, ry=Math.abs(dy)>.92?0:1, rz=Math.abs(dy)>.92?1:0;
let ux=ry*dz-rz*dy,uy=rz*dx-rx*dz,uz=rx*dy-ry*dx;
const ul=Math.hypot(ux,uy,uz)||1;ux/=ul;uy/=ul;uz/=ul;
const wx=uy*dz-uz*dy,wy=uz*dx-ux*dz,wz=ux*dy-uy*dx;
for(let i=0;i<mm.v.length;i+=VFLOATS){
const px=mm.v[i],py=mm.v[i+1],pz=mm.v[i+2];
mm.v[i]=a[0]+ux*px+dx*py+wx*pz;
mm.v[i+1]=a[1]+uy*px+dy*py+wy*pz;
mm.v[i+2]=a[2]+uz*px+dz*py+wz*pz;
const nx=mm.v[i+3],ny=mm.v[i+4],nz=mm.v[i+5];
mm.v[i+3]=ux*nx+dx*ny+wx*nz;
mm.v[i+4]=uy*nx+dy*ny+wy*nz;
mm.v[i+5]=uz*nx+dz*ny+wz*nz;
}
const off=m.n;
for(const value of mm.v)m.v.push(value);
for(const index of mm.i)m.i.push(index+off);
m.n+=mm.n; m.m=mm.m; m.tm=mm.tm;
return m;
}
function infLimb(m,a,b,r1,r2,col,seg,cap){
const len=Math.hypot(b[0]-a[0],b[1]-a[1],b[2]-a[2]);
const mm=MB(); mm.m=m.m;mm.tm=m.tm;
mm.cyl(0,0,0,r1,r2,len,seg||7,col,cap);
return infMergeAlong(m,mm,a,b);
}
function infTube(m,a,b,rOut,rIn,col,seg){
const len=Math.hypot(b[0]-a[0],b[1]-a[1],b[2]-a[2]);
const mm=MB(); mm.m=m.m;mm.tm=m.tm;
mm.tube(0,0,0,rOut,rIn,len,seg||8,col);
return infMergeAlong(m,mm,a,b);
}
function infOpenLimb(m,a,b,r1,r2,seg){
const q=infPoint(a,b,.76);
infLimb(m,a,q,r1,r2*1.08,INF_CHITIN_H,seg||8,false);
infTube(m,q,b,r2*1.14,r2*.53,INF_BORE,seg||8);
return m;
}
function infSpike(m,a,b,r,heavy){
infLimb(m,a,b,r,heavy?r*.10:0,heavy?INF_CHITIN_H:INF_BONE,6,true);
return m;
}
function infBulb(m,x,y,z,r,col,squash){
const sy=squash===undefined?.78:squash;
m.sphere(x,y+r*sy,z,r,6,col||INF_SAC,sy,false);
return m;
}
function infGlowNode(m,x,y,z,r){
m.sphere(x,y+r,z,r,6,INF_GLOW,.82,false);
m.ring(x,y+r*.95,z,r*1.08,r*1.34,10,INF_GLOW_H);
return m;
}
function infRoots(m,r,count,seed,tier){
const rr=infRand(seed), t=tier||1;
m.cyl(0,0,0,r*.66,r*.78,2.2,10,INF_ROOT);
m.cyl(0,2.2,0,r*.58,r*.49,2.4,10,INF_FLESH);
for(let i=0;i<count;i++){
const a=i/count*TAU+(rr()-.5)*.22;
const reach=r*(.78+rr()*.35), bend=a+(rr()-.5)*.34;
const p0=[Math.cos(a)*r*.24,2.8,Math.sin(a)*r*.24];
const p1=[Math.cos(bend)*reach*.58,1.25+rr()*.55,Math.sin(bend)*reach*.58];
const p2=[Math.cos(a)*reach,.28,Math.sin(a)*reach];
infLimb(m,p0,p1,r*(.12+rr()*.025),r*.085,INF_ROOT,7,true);
infLimb(m,p1,p2,r*.088,r*.026,INF_ROOT,6,true);
if(t>1&&i%2===0){
const s=a+(i%4<2?.34:-.34),p3=[Math.cos(s)*reach*.88,.22,Math.sin(s)*reach*.88];
infLimb(m,p1,p3,r*.052,r*.012,INF_FLESH,6,true);
}
}
const lobes=3+Math.min(t,2);
for(let i=0;i<lobes;i++){
const a=(i/lobes)*TAU+.35,rad=r*(.44+(i%2)*.08),sz=r*(.17+(i%3)*.012);
m.sphere(Math.cos(a)*rad,3.4+sz*.44,Math.sin(a)*rad,sz,5,i%2?INF_CHITIN:INF_FLESH_H,.46,false);
}
m.ring(0,4.72,0,r*.25,r*.43,18,INF_GLOW);
return m;
}
function infMound(m,x,y,z,r,seed,tier){
const rr=infRand(seed),t=tier||1;
m.sphere(x,y+r*.52,z,r,7,INF_FLESH,.52,false);
const lobes=3+t;
for(let i=0;i<lobes;i++){
const a=i/lobes*TAU+rr()*.4,rad=r*(.54+rr()*.15),sz=r*(.25+rr()*.08);
m.sphere(x+Math.cos(a)*rad,y+sz*.46,z+Math.sin(a)*rad,sz,5,i%3?INF_CHITIN:INF_FLESH_H,.46,false);
}
const sacs=1+t;
for(let i=0;i<sacs;i++){
const a=(i+.5)/sacs*TAU+seed*.13,rad=r*.55,sz=r*(.15+i*.012);
infBulb(m,x+Math.cos(a)*rad,y+r*.48,z+Math.sin(a)*rad,sz,i%2?INF_SAC:INF_MUCUS,.86);
}
return m;
}
function infTrunk(m,x,y,z,r,h,seed,tier){
const t=tier||1,rr=infRand(seed);
m.cyl(x,y,z,r,r*.78,h*.42,9,INF_FLESH);
m.cyl(x,y+h*.39,z,r*.82,r*.55,h*.36,9,INF_CHITIN);
m.cyl(x,y+h*.72,z,r*.57,r*.36,h*.28,8,INF_FLESH_H);
const plates=2+t;
for(let i=0;i<plates;i++){
const a=i/plates*TAU+rr()*.2,py=y+h*(.22+i/plates*.58),rad=r*(.68-i/plates*.22);
m.sphere(x+Math.cos(a)*rad,py,z+Math.sin(a)*rad,r*(.28+rr()*.05),5,INF_CHITIN_H,.34,false);
}
for(let i=0;i<t;i++){
const a=(i+.5)/t*TAU+seed*.1;
infGlowNode(m,x+Math.cos(a)*r*.50,y+h*(.40+i*.13),z+Math.sin(a)*r*.50,r*.16);
}
return m;
}
function infCrownSpikes(m,x,y,z,r,n,h,seed){
const rr=infRand(seed);
for(let i=0;i<n;i++){
const a=i/n*TAU+(rr()-.5)*.18,inner=[x+Math.cos(a)*r*.35,y,z+Math.sin(a)*r*.35];
const outer=[x+Math.cos(a)*r*(.82+rr()*.25),y+h*(.78+rr()*.22),z+Math.sin(a)*r*(.82+rr()*.25)];
infSpike(m,inner,outer,r*(.10+rr()*.025),i%3===0);
}
return m;
}
/* ---- explicit contact-sheet defenses -------------------------------- */
function mdlInfSpineSpiker(tier){
const t=tier||1,m=MB(),r=17+t*2;
infRoots(m,r,7+t,101+t,t); infMound(m,0,4.6,0,r*.58,111+t,t);
infTrunk(m,-2,5,0,6.8+t,12+t*2,117+t,t);
const mouthA=[0,13+t*1.6,0],mouthB=[11+t*2,20+t*2,0];
infOpenLimb(m,mouthA,mouthB,4.1+t*.35,2.2+t*.18,9);
for(let i=0;i<4+t*2;i++){
const a=i/(4+t*2)*TAU,base=[Math.cos(a)*8,7,Math.sin(a)*8];
infSpike(m,base,[Math.cos(a)*(16+t*2),16+(i%2)*4+t,Math.sin(a)*(16+t*2)],1.8+t*.12,i%2===0);
}
infGlowNode(m,-4,13+t,0,2.0+t*.15);
return m.build();
}
function mdlInfToxicGusher(tier){
const t=tier||1,m=MB(),r=18+t*2;
infRoots(m,r,8,201+t,t); infMound(m,0,4.6,0,r*.63,211+t,t);
infTrunk(m,0,5,0,7.4+t,15+t*2,217+t,t);
infOpenLimb(m,[0,15,0],[2,29+t*3,0],4.6+t*.4,3.0+t*.2,10);
for(let i=0;i<3+t;i++){
const a=i/(3+t)*TAU+.5,rad=8+t,sz=2.5+t*.25;
infBulb(m,Math.cos(a)*rad,11+i*.8,Math.sin(a)*rad,sz,i%2?INF_SAC:INF_MUCUS,.92);
infGlowNode(m,Math.cos(a)*rad,8+i*.8,Math.sin(a)*rad,sz*.35);
}
infCrownSpikes(m,0,21+t,0,8,4+t,8+t,221+t);
return m.build();
}
function mdlInfSporeLauncher(tier){
const t=tier||1,m=MB(),r=19+t*2;
infRoots(m,r,8+t,301+t,t); infMound(m,-2,4.6,0,r*.65,311+t,t);
const tubes=2+t;
for(let i=0;i<tubes;i++){
const z=(i-(tubes-1)/2)*6.4,a=[-4,10,z],b=[11+t*2,23+(i%2)*4+t*2,z*1.12];
infOpenLimb(m,a,b,4.3+t*.25,2.25+t*.16,9);
infBulb(m,-6,9,z,3.2+t*.2,i%2?INF_SAC:INF_FLESH_H,.88);
}
infCrownSpikes(m,-7,10,0,10,4+t,10+t,321+t);
infGlowNode(m,-6,13,0,2.2+t*.2);
return m.build();
}
function mdlInfSilkTrap(tier){
const t=tier||1,m=MB(),r=20+t*2;
infRoots(m,r,9+t,401+t,t); infMound(m,0,4.5,0,r*.56,411+t,t);
m.tube(0,6.5,0,7.2+t,3.6+t*.4,2.6+t*.4,12,INF_BORE);
m.ring(0,9.25+t*.4,0,4.0+t*.4,7.6+t,20,INF_SILK);
const n=6+t;
for(let i=0;i<n;i++){
const a=i/n*TAU,p0=[Math.cos(a)*r*.72,4,Math.sin(a)*r*.72];
const p1=[Math.cos(a)*r*.48,13+t*1.8,Math.sin(a)*r*.48];
const p2=[Math.cos(a)*r*.18,9+t,Math.sin(a)*r*.18];
infLimb(m,p0,p1,1.15+t*.08,.52,INF_CHITIN,6,true);
infLimb(m,p1,p2,.55,.16,INF_SILK,6,true);
}
infGlowNode(m,0,5.1,0,2.2+t*.25);
return m.build();
}
function mdlInfAcidGeyser(tier){
const t=tier||1,m=MB(),r=18+t*2;
infRoots(m,r,8,501+t,t); infMound(m,0,4.6,0,r*.62,511+t,t);
m.cyl(0,7.0,0,8+t,6+t*.7,5+t,10,INF_CHITIN,false);
m.tube(0,11+t,0,6.3+t*.7,3.1+t*.38,3.2+t*.4,12,INF_BORE);
for(let i=0;i<6+t;i++){
const a=i/(6+t)*TAU,base=[Math.cos(a)*(8+t),7,Math.sin(a)*(8+t)];
infSpike(m,base,[Math.cos(a)*(13+t*1.4),16+(i%3)*2+t,Math.sin(a)*(13+t*1.4)],1.35+t*.08,false);
}
for(let i=0;i<3+t;i++){
const a=(i+.5)/(3+t)*TAU;infBulb(m,Math.cos(a)*9,7,Math.sin(a)*9,2.4+t*.2,INF_MUCUS,.8);
}
infGlowNode(m,0,9.4+t,0,2.4+t*.2);
return m.build();
}
function mdlInfThornNest(tier){
const t=tier||1,m=MB(),r=21+t*2;
infRoots(m,r,10,601+t,t); infMound(m,-1,4.6,0,r*.69,611+t,t);
const n=7+t*2;
for(let i=0;i<n;i++){
const a=i/n*TAU,p0=[Math.cos(a)*r*.52,7,Math.sin(a)*r*.52];
const p1=[Math.cos(a)*r*(.86+(i%2)*.12),15+(i%3)*3+t,Math.sin(a)*r*(.86+(i%2)*.12)];
infSpike(m,p0,p1,1.45+t*.10,i%2===0);
}
for(let i=0;i<2+t;i++){
const a=i/(2+t)*TAU+.4;infBulb(m,Math.cos(a)*5,7,Math.sin(a)*5,3.0+t*.2,INF_SAC,.96);
}
infOpenLimb(m,[-5,8,0],[9+t,12+t,0],3.5+t*.25,2.1+t*.15,9);
infGlowNode(m,-2,8.5,0,2.0+t*.18);
return m.build();
}
function mdlInfSonicShrieker(tier){
const t=tier||1,m=MB(),r=17+t*2;
infRoots(m,r,7+t,701+t,t); infMound(m,0,4.6,0,r*.55,711+t,t);
infTrunk(m,0,5,0,6.3+t*.5,23+t*3,717+t,t);
infOpenLimb(m,[0,24+t*2,0],[10+t*2,33+t*3,0],5.1+t*.35,3.7+t*.25,11);
for(let i=0;i<4+t;i++){
const a=i/(4+t)*TAU,p0=[Math.cos(a)*5,19+t,Math.sin(a)*5];
infSpike(m,p0,[Math.cos(a)*(10+t),29+t*2,Math.sin(a)*(10+t)],1.1+t*.08,false);
}
for(let y=12;y<23+t*2;y+=5)m.ring(0,y,0,4.2+(y-12)*.08,5.2+(y-12)*.08,16,INF_SILK);
infGlowNode(m,-3,22+t,0,2.0+t*.2);
return m.build();
}
function mdlInfBroodSpire(tier){
const t=tier||1,m=MB(),r=28+t*4;
infRoots(m,r,10+t*2,801+t,t); infMound(m,0,4.6,0,r*.62,811+t,t);
infTrunk(m,-2,6,0,10+t,29+t*5,817+t,t);
infTrunk(m,5,9,-3,6+t*.5,20+t*3,823+t,t);
const sacs=4+t*2;
for(let i=0;i<sacs;i++){
const a=i/sacs*TAU+.25,rad=10+(i%2)*4,py=12+(i%3)*7;
infBulb(m,Math.cos(a)*rad,py,Math.sin(a)*rad,3.4+t*.32,i%2?INF_SAC:INF_FLESH_H,.94);
}
infOpenLimb(m,[-1,31+t*3,0],[12+t*2,42+t*5,0],5.0+t*.4,3.0+t*.25,10);
infCrownSpikes(m,-2,34+t*4,0,12+t,6+t*2,17+t*2,831+t);
for(let i=0;i<t+1;i++)infGlowNode(m,-4+i*5,22+i*7,-2+i%2*4,2.2+t*.18);
return m.build();
}
/* ---- economy, production and support -------------------------------- */
function mdlInfMex(){
const m=MB(); infRoots(m,19,8,901,1);infMound(m,-2,4.6,0,11,907,1);
for(let i=0;i<3;i++){
const a=(i-1)*.72;infOpenLimb(m,[-4,8,Math.sin(a)*5],[10+Math.cos(a)*5,12+i*3,Math.sin(a)*13],3.1,1.45,8);
}
infGlowNode(m,-5,9,0,2.1);infBulb(m,-8,6,-6,3.0,INF_SAC,.82);
return m.build();
}
function mdlInfPgen(){
const m=MB();infRoots(m,20,8,921,1);infMound(m,0,4.6,0,11.5,927,1);
for(let i=0;i<3;i++){
const a=i*TAU/3,x=Math.cos(a)*8,z=Math.sin(a)*8;
infBulb(m,x,8,z,4.4,i===0?INF_MUCUS:INF_SAC,1.22);
infSpike(m,[x,10,z],[x*1.35,21+(i%2)*3,z*1.35],1.25,false);
}
m.tube(0,8,0,4.0,2.1,3,10,INF_BORE);infGlowNode(m,0,12,0,2.8);
return m.build();
}
function mdlInfFactory(){
/* Compact BIRTH-MAW. Factory / harbor / airfield used to be the same
recipe — roots, two mounds, one open limb on +X — so at tactical zoom
they were three cousins. Production has to read as a fat hive with a
dark mouth, not a dock and not a runway. The slit is a real tube; a
painted recess disappears under baked lighting. */
const m=MB();
m.cyl(0,0,0,18,16,3.2,10,INF_ROOT);
m.sphere(-4,11,0,16,8,INF_FLESH,.72,false);
m.sphere(-6,16,0,11,7,INF_CHITIN,.78,false);
m.cyl(2,7,0,10,8.2,12,10,INF_CHITIN_H,false);
m.tube(10,12,0,6.4,3.1,8.5,11,INF_BORE);
m.ring(18.2,12,0,3.2,5.4,14,INF_GLOW);
for(const s of [-1,1]){
infBulb(m,-12,8,s*14,5.2,s>0?INF_SAC:INF_FLESH_H,.95);
infBulb(m,-2,10,s*16,4.0,INF_MUCUS,.82);
infLimb(m,[-8,14,s*10],[-2,22,s*15],2.4,.7,INF_CHITIN,7,true);
}
infGlowNode(m,-6,20,0,2.4);
return m.build();
}
function mdlInfShield(){
const m=MB();infRoots(m,22,9,961,2);infMound(m,0,4.6,0,12,967,1);
infBulb(m,0,12,0,7.5,INF_MUCUS,1.12);m.ring(0,19.8,0,7.5,10.5,24,INF_GLOW);
for(let i=0;i<6;i++){
const a=i*TAU/6,p0=[Math.cos(a)*15,4,Math.sin(a)*15],p1=[Math.cos(a)*10,24,Math.sin(a)*10],p2=[0,31,0];
infLimb(m,p0,p1,2.0,.8,INF_CHITIN,7,true);infLimb(m,p1,p2,.82,.18,INF_SILK,6,true);
}
infGlowNode(m,0,12,0,3.0);return m.build();
}
function mdlInfTransitGate(){
const m=MB();infRoots(m,31,10,981,2);infMound(m,-18,4.6,0,11,987,1);infMound(m,18,4.6,0,11,991,1);
const left=[[-19,7,0],[-21,25,0],[-10,41,0],[0,46,0]],right=[[19,7,0],[21,25,0],[10,41,0],[0,46,0]];
for(const path of [left,right])for(let i=0;i<path.length-1;i++)infLimb(m,path[i],path[i+1],4.0-i*.55,3.4-i*.55,i%2?INF_FLESH_H:INF_CHITIN,8,true);
ringX(m,.2,27,0,13,16,26,INF_GLOW);ringX(m,0,27,0,16.5,18.5,26,INF_SILK);
for(const s of [-1,1])infGlowNode(m,s*16,12,0,2.5);return m.build();
}
function mdlInfHarbor(){
/* Living U-SLIP. Nova's drydock taught the same lesson: the empty dark
berth is the silhouette, not another mound with a limb glued on. Two
chitin quays flank a recessed bore opening +X so a shipyard never
reads as a factory maw or an airfield strip. */
const m=MB();
m.cyl(-8,0,0,16,14,2.6,10,INF_ROOT);
m.box(6,0.6,0,40,1.4,16,INF_BORE);
m.box(8,1.4,0,36,.55,10,INF_ROOT);
infMound(m,-16,3.2,0,12,1007,1);
infBulb(m,-18,8,0,5.0,INF_FLESH_H,.88);
for(const s of [-1,1]){
m.box(8,4.2,s*14,38,7.2,7.5,INF_CHITIN);
m.box(8,8.0,s*14,34,1.1,5.2,INF_CHITIN_H);
for(const x of [-6,6,18]){
infLimb(m,[x,7.5,s*17],[x+2,13,s*20],1.6,.45,INF_BONE,6,true);
infGlowNode(m,x,8.6,s*14,1.15);
}
infBulb(m,-10,6,s*16,3.4,INF_SAC,.74);
}
infGlowNode(m,-16,12,0,2.2);
return m.build();
}
function mdlInfTech(){
const m=MB();infRoots(m,23,9,1031,2);infMound(m,0,4.6,0,13,1037,2);infTrunk(m,0,6,0,7.5,19,1043,2);
for(let i=0;i<5;i++){
const a=i*TAU/5+.3;infBulb(m,Math.cos(a)*8,20+(i%2)*4,Math.sin(a)*8,3.4,i%2?INF_SAC:INF_FLESH_H,.86);
infLimb(m,[0,17,0],[Math.cos(a)*8,22+(i%2)*4,Math.sin(a)*8],1.0,.35,INF_SILK,6,true);
}
infGlowNode(m,0,28,0,3.0);return m.build();
}
function mdlInfAirfield(){
/* LONG RUNWAY. The cousin pass hid a +X open limb under wing-limbs, so
this still read as a factory at forty pixels. The strip itself is the
tell: a flat bone deck on +X, hangar mound only at the stern, two
spore masts instead of another mouth. */
const m=MB();
m.cyl(-16,0,0,14,12,2.4,10,INF_ROOT);
infMound(m,-18,3.8,0,13,1067,2);
infTrunk(m,-18,6,0,6.2,11,1073,1);
m.box(8,2.4,0,52,3.6,12,INF_BONE);
m.box(8,4.4,0,50,.55,3.4,INF_SILK);
for(const s of [-1,1]){
m.box(8,4.5,s*8.4,48,.7,1.6,INF_GLOW);
infLimb(m,[-16,8,s*6],[-14,18,s*8],1.8,.55,INF_CHITIN,7,true);
infGlowNode(m,-14,19,s*8,1.6);
for(let i=0;i<3;i++) infBulb(m,-6+i*12,3.4,s*12,2.6-i*.2,INF_SAC,.62);
}
infGlowNode(m,-18,16,0,2.1);
return m.build();
}
function mdlInfArc(){
const m=MB();infRoots(m,20,8,1081,2);infMound(m,0,4.6,0,11,1087,1);
for(let i=0;i<5;i++){
const a=i*TAU/5,p0=[Math.cos(a)*8,8,Math.sin(a)*8],p1=[Math.cos(a)*12,24+(i%2)*4,Math.sin(a)*12];
infLimb(m,p0,p1,1.8,.35,i%2?INF_CHITIN_H:INF_BONE,7,true);infGlowNode(m,p1[0],p1[1]-1.2,p1[2],1.3);
}
infBulb(m,0,13,0,5.0,INF_MUCUS,1.0);infGlowNode(m,0,16,0,2.5);return m.build();
}
function mdlInfBoneLance(){
const m=MB();infRoots(m,24,9,1101,2);infMound(m,-5,4.6,0,14,1107,2);infTrunk(m,-6,6,0,7,15,1113,2);
infOpenLimb(m,[-3,18,0],[29,25,0],5.0,2.2,10);
for(const s of [-1,1])infSpike(m,[-5,14,s*5],[18,23,s*9],1.5,true);
for(let x=2;x<25;x+=6)ringX(m,x,22.5+(x+3)*.10,0,2.7,3.5,14,x>20?INF_GLOW:INF_BONE);
infGlowNode(m,-7,18,0,2.3);return m.build();
}
function mdlInfWall(){
const m=MB();
for(let i=-3;i<=3;i++){
const x=i*5.2,h=11+(i%2===0?5:0);
infLimb(m,[x,0,-6],[x+(i%2)*2,h,0],2.5,1.25,i%2?INF_ROOT:INF_CHITIN,7,true);
infSpike(m,[x,h*.55,0],[x,h+8,-(i%3-1)*4],1.1,false);
if(i<3){
infLimb(m,[x,5,0],[x+5.2,5.8,0],1.05,.82,INF_FLESH,6,true);
infLimb(m,[x,h*.55,0],[x+5.2,9,0],.75,.48,INF_SILK,6,true);
}
}
m.box(0,0,0,35,2.2,14,INF_ROOT);m.ring(0,2.3,0,8,15,18,INF_GLOW);return m.build();
}
function mdlInfProboscis(){
const m=MB();infRoots(m,20,8,1141,2);infMound(m,-4,4.6,0,12,1147,1);
infOpenLimb(m,[-5,12,0],[27,17,0],4.2,1.55,9);
for(let i=0;i<4;i++){
const x=2+i*6;ringX(m,x,13.1+x*.15,0,1.9-i*.08,2.7-i*.08,14,i===3?INF_GLOW:INF_CHITIN_H);
}
for(const s of [-1,1])infSpike(m,[-3,9,s*5],[10,17,s*8],1.1,false);
infGlowNode(m,-6,13,0,2.2);return m.build();
}
function mdlInfPlasma(){
const m=MB();infRoots(m,21,9,1161,2);infMound(m,0,4.6,0,12,1167,2);
for(let i=0;i<5;i++){
const a=i*TAU/5,rad=8+(i%2)*2;infBulb(m,Math.cos(a)*rad,9+(i%2)*3,Math.sin(a)*rad,3.4,INF_MUCUS,.95);
infLimb(m,[0,9,0],[Math.cos(a)*rad,11+(i%2)*3,Math.sin(a)*rad],1.0,.42,INF_FLESH_H,6,true);
}
m.tube(0,11,0,5.0,2.5,3.5,12,INF_BORE);infGlowNode(m,0,14,0,3.3);
infCrownSpikes(m,0,10,0,9,5,12,1171);return m.build();
}
function mdlInfGate(){
const m=MB();m.box(0,0,0,43,2.2,17,INF_ROOT);
for(const s of [-1,1]){
infMound(m,s*16,2.2,0,9,1191+s,1);
infLimb(m,[s*16,5,0],[s*18,23,0],4.3,2.8,INF_CHITIN,8,true);
infLimb(m,[s*18,23,0],[0,33,0],2.9,.65,INF_BONE,7,true);
infGlowNode(m,s*16,12,0,1.8);
}
ringX(m,.1,20,0,8.5,10.3,22,INF_SILK);return m.build();
}
function mdlInfGeo(){
const m=MB();infRoots(m,23,9,1211,2);infMound(m,0,4.6,0,13,1217,2);
const cells=[[-7,-5],[7,-5],[-7,6],[7,6]];
for(let i=0;i<cells.length;i++){
const p=cells[i],h=12+i*2;
m.cyl(p[0],7,p[1],3.8,2.6,h,8,i%2?INF_FLESH_H:INF_CHITIN,false);
m.tube(p[0],7+h,p[1],2.75,1.25,2.1,9,INF_BORE);
infGlowNode(m,p[0],8+i,p[1],1.2);
}
infBulb(m,0,13,0,4.2,INF_MUCUS,1.15);return m.build();
}
function mdlInfSilo(){
const m=MB();infRoots(m,24,9,1231,2);infMound(m,0,4.6,0,13,1237,2);
for(let i=0;i<5;i++){
const a=i*TAU/5,rad=9+(i%2)*2,r=4.0+(i%2)*.7;
infBulb(m,Math.cos(a)*rad,8,Math.sin(a)*rad,r,i%2?INF_SAC:INF_FLESH_H,1.65);
m.ring(Math.cos(a)*rad,8+r*1.55,Math.sin(a)*rad,r*.35,r*.72,12,INF_GLOW);
}
m.tube(0,10,0,4.2,2.1,3,10,INF_BORE);infGlowNode(m,0,13,0,2.5);return m.build();
}
function mdlInfFab(){
const m=MB();infRoots(m,27,10,1251,2);infMound(m,-4,4.6,0,16,1257,2);infTrunk(m,-5,6,0,8,17,1263,2);
for(let i=0;i<6;i++){
const a=i*TAU/6,x=Math.cos(a)*12,z=Math.sin(a)*12;
infBulb(m,x,8+(i%2)*4,z,3.2,i%2?INF_SAC:INF_MUCUS,.88);
infOpenLimb(m,[x*.55,11,z*.55],[x*1.18,13+(i%2)*3,z*1.18],2.1,1.0,7);
}
infGlowNode(m,-5,21,0,2.7);infCrownSpikes(m,-5,18,0,8,5,9,1267);return m.build();
}
/* ---- tracking weapon assemblies ---------------------------------------
Organic weapons used to be grown as one rigid mesh. Their projectiles
tracked correctly, but the visible proboscis kept staring east. These
sockets leave roots and circulatory tissue in the base while V.tur owns
the mouth, so yawing never spins the whole creep mound. All bores remain
real tubes; a dark cap would turn flat again under mobile baked lighting. */
function infAimSocket(m,y,r,tier){
const t=tier||1;
m.cyl(0,y-3,0,r*1.18,r,3.0,9,INF_FLESH);
m.cyl(0,y,0,r,r*.88,1.2,9,INF_CHITIN);
m.ring(0,y+1.25,0,r*.56,r*.92,14,INF_GLOW);
for(let i=0;i<Math.min(3,t);i++){
const a=i*TAU/Math.min(3,t)+.4,x=Math.cos(a)*r*.88,z=Math.sin(a)*r*.88;
infBulb(m,x,y-2.4,z,r*.26,i%2?INF_SAC:INF_FLESH_H,.72);
}
return m;
}
function infTurJoint(m,r,tier){
const t=tier||1;
m.cyl(0,0,0,r*.92,r*.82,1.4,9,INF_CHITIN);
m.sphere(-r*.18,2.0,0,r,7,INF_FLESH_H,.62,false);
m.ring(0,1.45,0,r*.48,r*.84,14,INF_GLOW);
if(t>1) for(const s of [-1,1]) infSpike(m,[-r*.3,2,s*r*.55],[-r*.9,6+t,s*r*.95],r*.18,false);
return m;
}
function mdlInfSpineBase(tier){
const t=tier||1,m=MB(),r=17+t*2;
infRoots(m,r,7+t,1401+t,t);infMound(m,-2,4.6,0,r*.58,1407+t,t);
infTrunk(m,-2,5,0,6.2+t*.5,9,1413+t,t);infAimSocket(m,16,4.5+t*.25,t);
for(let i=0;i<4+t;i++){
const a=i/(4+t)*TAU,base=[Math.cos(a)*8,7,Math.sin(a)*8];
infSpike(m,base,[Math.cos(a)*(14+t),13+(i%2)*3,Math.sin(a)*(14+t)],1.25+t*.08,i%2===0);
}
infGlowNode(m,-4,11,0,1.8+t*.12);return m.build();
}
function mdlInfSpineTur(tier){
const t=tier||1,m=MB();infTurJoint(m,4.4+t*.25,t);
infOpenLimb(m,[-1,2,0],[13+t*3,9+t*1.5,0],4.1+t*.35,2.2+t*.18,9);
for(const s of [-1,1]) infSpike(m,[1,3,s*3.0],[11+t*2,10+t,s*(5+t*.4)],1.2+t*.10,true);
infGlowNode(m,-3,3.8,0,1.8+t*.16);return m.build();
}
/* Concussion Mortar used to be the Sentinel spine family at a different
label. Its new siege organ stores pressure in rear sacs, braces against
recoil with bone outriggers, and raises one wide lobbed-fire throat. */
function mdlInfMortarBase(tier){
const t=tier||1,m=MB(),r=18+t*1.5;
infRoots(m,r,8+t,1421+t,t);infMound(m,-3,4.6,0,r*.62,1427+t,t);
infTrunk(m,-6,5,0,6.5+t*.45,10+t,1433+t,t);infAimSocket(m,16.0,4.8+t*.22,t);
for(const s of [-1,1]){
infBulb(m,-9,8.0,s*(6.2+t*.45),3.5+t*.28,s>0?INF_SAC:INF_MUCUS,.90);
infLimb(m,[-7,7,s*4],[-13,2,s*(10+t)],1.45+t*.08,.45,INF_ROOT,7,true);
infSpike(m,[-3,8,s*5],[6,13,s*(10+t*.45)],1.15+t*.07,true);
}
infGlowNode(m,-7,12,0,2.0+t*.12);return m.build();
}
function mdlInfMortarTur(tier){
const t=tier||1,m=MB();infTurJoint(m,4.8+t*.22,t);
infBulb(m,-5,3.0,0,4.0+t*.34,INF_SAC,.94);
infOpenLimb(m,[-2,2,0],[11+t*2,13+t*1.8,0],5.1+t*.34,2.65+t*.19,10);
for(const s of [-1,1]){
infLimb(m,[-2,3,s*2.8],[7+t,8+t*.7,s*(5.8+t*.45)],1.25,.62,INF_FLESH_H,7,true);
infSpike(m,[-4,4,s*3.5],[4+t,10+t,s*(8+t*.5)],1.0+t*.07,true);
}
infCrownSpikes(m,-4,4,0,4.6,3+t,6+t,1437+t);
infGlowNode(m,-5,5,0,2.2+t*.14);return m.build();
}
function mdlInfThornBase(tier){
const t=tier||1,m=MB(),r=20+t*2;
infRoots(m,r,9+t,1441+t,t);infMound(m,-1,4.6,0,r*.67,1447+t,t);
infTrunk(m,-3,5,0,5.8+t*.4,6.5,1453+t,t);infAimSocket(m,13,4.4+t*.22,t);
const n=5+t;
for(let i=0;i<n;i++){
const a=i/n*TAU,p0=[Math.cos(a)*r*.52,7,Math.sin(a)*r*.52];
infSpike(m,p0,[Math.cos(a)*r*.88,13+(i%3)*2,Math.sin(a)*r*.88],1.25+t*.08,i%2===0);
}
return m.build();
}
function mdlInfThornTur(tier){
const t=tier||1,m=MB();infTurJoint(m,4.4+t*.22,t);
infOpenLimb(m,[-1,2,0],[12+t*2,6+t,0],3.8+t*.28,2.0+t*.16,9);
for(const s of [-1,1]) for(let i=0;i<t+1;i++)
infSpike(m,[1+i*3,3,s*2.5],[9+i*3+t,8+i,s*(5+i)],.9+t*.08,false);
infGlowNode(m,-2,3,0,1.7+t*.14);return m.build();
}
function mdlInfSporeBase(tier){
const t=tier||1,m=MB(),r=19+t*2;
infRoots(m,r,8+t,1481+t,t);infMound(m,-2,4.6,0,r*.65,1487+t,t);
infTrunk(m,-3,5,0,6.4+t*.45,9.5,1493+t,t);infAimSocket(m,16.5,4.8+t*.22,t);
for(let i=0;i<2+t;i++){
const a=i/(2+t)*TAU+.3;infBulb(m,Math.cos(a)*8,8+(i%2)*2,Math.sin(a)*8,2.6+t*.18,i%2?INF_SAC:INF_FLESH_H,.88);
}
return m.build();
}
function mdlInfSporeTur(tier){
const t=tier||1,m=MB();infTurJoint(m,4.8+t*.22,t);
const tubes=2+t;
for(let i=0;i<tubes;i++){
const z=(i-(tubes-1)/2)*5.6,a=[-1,2,z*.38],b=[12+t*2,8+(i%2)*3+t,z];
infOpenLimb(m,a,b,3.35+t*.22,1.75+t*.14,8);
infBulb(m,-3,2,z*.55,2.4+t*.16,i%2?INF_SAC:INF_FLESH_H,.86);
}
infGlowNode(m,-3,3,0,1.9+t*.15);return m.build();
}
/* Missile Bastion is now a deliberate brood-launch organ rather than the
Skyguard spore mouth. Clustered pressure eggs feed divergent open throats;
bone fins make the battery readable even when its mucus colour is muted. */
function mdlInfMissileBase(tier){
const t=tier||1,m=MB(),r=17+t*1.4;
infRoots(m,r,8+t,1501+t,t);infMound(m,-2,4.6,0,r*.64,1507+t,t);
infTrunk(m,-5,5,0,6.0+t*.42,8+t,1513+t,t);infAimSocket(m,15.2,4.7+t*.22,t);
const n=3+t;
for(let i=0;i<n;i++){
const a=i/n*TAU+.25,rad=8+t*.45;
infBulb(m,Math.cos(a)*rad-2,7+(i&1)*1.2,Math.sin(a)*rad,2.7+t*.17,
i&1?INF_SAC:INF_MUCUS,.91);
if(!(i&1)) infSpike(m,[Math.cos(a)*rad*.65,8,Math.sin(a)*rad*.65],
[Math.cos(a)*(rad+5),13+t,Math.sin(a)*(rad+5)],.9+t*.06,false);
}
return m.build();
}
function mdlInfMissileTur(tier){
const t=tier||1,m=MB();infTurJoint(m,4.7+t*.22,t);
const cells=2+t;
for(let i=0;i<cells;i++){
const z=(i-(cells-1)/2)*4.25,spread=(i-(cells-1)/2)*.62;
infBulb(m,-4,2.2,z*.42,2.35+t*.13,i&1?INF_SAC:INF_MUCUS,.92);
infOpenLimb(m,[-2,2,z*.38],[12+t*1.55,8+(i&1)*2+t, z+spread],3.05+t*.18,1.50+t*.10,8);
infSpike(m,[-1,3,z*.48],[7+t,11+t*.65,z+(z<0?-3:3)],.74+t*.05,i===0||i===cells-1);
}
for(const s of [-1,1]) infSpike(m,[-5,3,s*3],[-1,10+t,s*(8+t*.4)],1.0+t*.06,true);
infGlowNode(m,-4,4,0,2.0+t*.15);return m.build();
}
function mdlInfToxicBase(tier){
const t=tier||1,m=MB(),r=18+t*2;
infRoots(m,r,8,1521+t,t);infMound(m,0,4.6,0,r*.63,1527+t,t);
infTrunk(m,-2,5,0,6.8+t*.45,10.5,1533+t,t);infAimSocket(m,17.5,4.8+t*.25,t);
for(let i=0;i<3+t;i++){
const a=i/(3+t)*TAU+.5;infBulb(m,Math.cos(a)*(8+t),8+i*.6,Math.sin(a)*(8+t),2.4+t*.2,i%2?INF_SAC:INF_MUCUS,.92);
}
return m.build();
}
function mdlInfToxicTur(tier){
const t=tier||1,m=MB();infTurJoint(m,4.8+t*.25,t);
infOpenLimb(m,[-1,2,0],[12+t*2,11+t*2,0],4.5+t*.35,2.8+t*.2,10);
infCrownSpikes(m,-1,3,0,5.5,3+t,7+t,1541+t);
infGlowNode(m,-3,4,0,1.9+t*.18);return m.build();
}
function mdlInfBoneBase(tier){
const t=tier||1,m=MB(),r=22+t*2;
infRoots(m,r,9,1561+t,t);infMound(m,-5,4.6,0,14+t,1567+t,t);
infTrunk(m,-6,6,0,7+t*.3,9,1573+t,t);infAimSocket(m,17,5.0+t*.2,t);
for(const s of [-1,1]) infSpike(m,[-6,9,s*6],[5,16,s*11],1.35,true);
return m.build();
}
function mdlInfBoneTur(tier){
const t=tier||1,m=MB();infTurJoint(m,5.0+t*.2,t);
infOpenLimb(m,[-1,2,0],[27+t*4,8+t,0],5.0+t*.28,2.2+t*.16,10);
for(const s of [-1,1]) infSpike(m,[-2,3,s*4.5],[20+t*3,7+t,s*(8+t)],1.35+t*.08,true);
for(let x=4;x<24+t*3;x+=6) ringX(m,x,6.4+x*.06,0,2.7,3.5,14,x>19?INF_GLOW:INF_BONE);
infGlowNode(m,-4,4,0,2.1+t*.12);return m.build();
}
function mdlInfAcidBase(tier){
const t=tier||1,m=MB(),r=18+t*2;
infRoots(m,r,8,1601+t,t);infMound(m,0,4.6,0,r*.64,1607+t,t);
m.cyl(0,7,0,8+t,6+t*.7,3.2,10,INF_CHITIN,false);infAimSocket(m,11.2,5.3+t*.3,t);
for(let i=0;i<3+t;i++){
const a=(i+.5)/(3+t)*TAU;infBulb(m,Math.cos(a)*9,7,Math.sin(a)*9,2.4+t*.2,INF_MUCUS,.8);
}
return m.build();
}
function mdlInfAcidTur(tier){
const t=tier||1,m=MB();infTurJoint(m,5.3+t*.3,t);
infOpenLimb(m,[-1,2,0],[11+t*2,6+t,0],4.8+t*.35,2.6+t*.22,10);
infBulb(m,-4,2,0,3.2+t*.25,INF_MUCUS,.94);
for(const s of [-1,1]) infSpike(m,[0,3,s*3.4],[8+t,8+t,s*(6+t*.4)],1.0+t*.07,false);
infGlowNode(m,-3,3,0,2.1+t*.16);return m.build();
}
function mdlInfProboscisBase(tier){
const t=tier||1,m=MB(),r=19+t*1.5;
infRoots(m,r,8,1641+t,t);infMound(m,-4,4.6,0,12+t*.6,1647+t,t);
infTrunk(m,-5,5,0,5.8+t*.35,6.5,1653+t,t);infAimSocket(m,13,4.2+t*.22,t);
return m.build();
}
function mdlInfProboscisTur(tier){
const t=tier||1,m=MB();infTurJoint(m,4.2+t*.22,t);
infOpenLimb(m,[-1,2,0],[25+t*3,6+t,0],4.2+t*.2,1.55+t*.12,9);
for(let x=3;x<23+t*2;x+=6) ringX(m,x,4.6+x*.04,0,1.9,2.7,14,x>18?INF_GLOW:INF_CHITIN_H);
for(const s of [-1,1]) infSpike(m,[-2,2,s*3.5],[10+t*2,7,s*(7+t*.4)],1.0,false);
infGlowNode(m,-3,3,0,1.8+t*.14);return m.build();
}
function mdlInfPlasmaBase(tier){
const t=tier||1,m=MB(),r=20+t*1.5;
infRoots(m,r,9,1681+t,t);infMound(m,0,4.6,0,12+t*.6,1687+t,t);
for(let i=0;i<4+t;i++){
const a=i/(4+t)*TAU,rad=8+(i%2)*2;infBulb(m,Math.cos(a)*rad,8,Math.sin(a)*rad,2.8+t*.18,INF_MUCUS,.95);
infLimb(m,[0,8,0],[Math.cos(a)*rad,10,Math.sin(a)*rad],.85,.34,INF_FLESH_H,6,true);
}
infAimSocket(m,12.5,5.0+t*.25,t);return m.build();
}
function mdlInfPlasmaTur(tier){
const t=tier||1,m=MB();infTurJoint(m,5.0+t*.25,t);
infBulb(m,-3,3,0,3.8+t*.3,INF_MUCUS,1.0);
infOpenLimb(m,[0,2,0],[13+t*2,7+t,0],4.4+t*.25,2.5+t*.18,10);
for(const s of [-1,1]) infSpike(m,[-2,3,s*3.6],[9+t,8+t,s*(6+t*.5)],1.0+t*.08,false);
infGlowNode(m,-3,4,0,2.7+t*.2);return m.build();
}
function mdlInfSeafortBase(tier){
const t=tier||1,m=MB(),r=18+t;
infMound(m,0,2.2,0,r*.72,1601+t,t);
infTrunk(m,-4,4,0,6.2+t*.3,8+t,1607+t,t);infAimSocket(m,14.8,4.6+t*.2,t);
for(const s of [-1,1]){
infOpenLimb(m,[-8,6,s*6],[16,8,s*(14+t)],4.2+t*.2,2.2,8);
infBulb(m,-10,7,s*(8+t*.3),3.2+t*.2,s>0?INF_SAC:INF_MUCUS,.88);
}
infGlowNode(m,-6,11,0,2.1+t*.12);return m.build();
}
function mdlInfSeafortTur(tier){
const t=tier||1,m=MB();infTurJoint(m,4.6+t*.2,t);
infOpenLimb(m,[-1,2,0],[12+t*2,8+t,0],4.6+t*.28,2.4+t*.16,9);
for(const s of [-1,1]) infOpenLimb(m,[0,4,s*2],[10+t,10+t,s*(4+t*.3)],2.6+t*.14,1.3,7);
infGlowNode(m,-3,4,0,1.9+t*.14);return m.build();
}
function mdlInfStormBase(tier){
const t=tier||1,m=MB(),r=17+t*1.2;
infRoots(m,r,8+t,1621+t,t);infMound(m,-2,4.6,0,r*.62,1627+t,t);
infTrunk(m,-5,5,0,6.0+t*.4,8+t,1633+t,t);infAimSocket(m,15.4,4.7+t*.2,t);
for(let i=0;i<6;i++){
const a=i/6*TAU;infBulb(m,Math.cos(a)*9,9+(i%2)*2,Math.sin(a)*9,2.8+t*.16,i%2?INF_SAC:INF_MUCUS,.9);
}
infGlowNode(m,0,16,0,2.6+t*.16);return m.build();
}
function mdlInfStormTur(tier){
const t=tier||1,m=MB();infTurJoint(m,4.7+t*.2,t);
for(let r=0;r<4;r++) for(let c=0;c<4;c++){
const y=3.6+(r-1.5)*1.7,z=(c-1.5)*2.6;
infOpenLimb(m,[-1,2,z*.4],[10+t,y,z],2.2+t*.1,1.15,6);
}
infGlowNode(m,-3,3.6,0,2.0+t*.12);return m.build();
}
/* Every simulation key receives its own wrapper even when it shares an art
family. That keeps old saves stable and gives balance/art tuning an escape
hatch without renaming a building later. */
BLD_MDL_INFESTATION={
mex:()=>mdlInfMex(), pgen:()=>mdlInfPgen(), fac:()=>mdlInfFactory(),
turret:()=>mdlInfSpineBase(1), bunker:()=>mdlInfThornBase(1),
sgen:()=>mdlInfShield(), tgate:()=>mdlInfTransitGate(), nest:()=>mdlInfThornNest(2),
harbor:()=>mdlInfHarbor(), bastion:()=>mdlInfMortarBase(1), techlab:()=>mdlInfTech(),
aatower:()=>mdlInfSporeBase(1), airfield:()=>mdlInfAirfield(),
uplink:()=>mdlInfSonicShrieker(2), hq:()=>mdlInfBroodSpire(3),
hellstorm:()=>mdlInfToxicBase(2), arc:()=>mdlInfArc(), rail:()=>mdlInfBoneBase(1),
nova:()=>mdlInfAcidBase(3), wall:()=>mdlInfWall(), minelaser:()=>mdlInfProboscisBase(1),
missilebastion:()=>mdlInfMissileBase(1), plasma:()=>mdlInfPlasmaBase(1), gate:()=>mdlInfGate(),
geo:()=>mdlInfGeo(), silo:()=>mdlInfSilo(), fab:()=>mdlInfFab(),
seafort:()=>mdlInfSeafortBase(1), stormcaller:()=>mdlInfStormBase(1),
spinespiker:mdlInfSpineBase, gorespiker:mdlInfSpineBase, spineburrow:mdlInfSpineBase,
toxicgusher:mdlInfToxicBase, acidgusher:mdlInfToxicBase, toxicspewer:mdlInfToxicBase,
sporelauncher:mdlInfSporeBase, sporetower:mdlInfSporeBase,
silktrap:mdlInfSilkTrap, tendriltrap:mdlInfSilkTrap, tendrilmaw:mdlInfSilkTrap,
acidgeyser:mdlInfAcidBase, creeppustule:mdlInfAcidBase,
thornnest:mdlInfThornBase, raptornest:mdlInfThornBase,
sonicshrieker:mdlInfSonicShrieker,
broodspire:mdlInfBroodSpire, broodchamber:mdlInfBroodSpire,
};
BLD_TUR_MDL_INFESTATION={
turret:mdlInfSpineTur,bunker:mdlInfThornTur,bastion:mdlInfMortarTur,
aatower:mdlInfSporeTur,hellstorm:mdlInfToxicTur,rail:mdlInfBoneTur,
nova:mdlInfAcidTur,minelaser:mdlInfProboscisTur,
missilebastion:mdlInfMissileTur,plasma:mdlInfPlasmaTur,
seafort:mdlInfSeafortTur,stormcaller:mdlInfStormTur,
spinespiker:mdlInfSpineTur,gorespiker:mdlInfSpineTur,spineburrow:mdlInfSpineTur,
toxicgusher:mdlInfToxicTur,acidgusher:mdlInfToxicTur,toxicspewer:mdlInfToxicTur,
sporelauncher:mdlInfSporeTur,sporetower:mdlInfSporeTur,
acidgeyser:mdlInfAcidTur,creeppustule:mdlInfAcidTur,
thornnest:mdlInfThornTur,raptornest:mdlInfThornTur,
};
BLD_TUR_H_INFESTATION={
turret:17.25,bunker:14.25,bastion:17.25,aatower:17.75,hellstorm:18.75,
rail:18.25,nova:12.45,minelaser:14.25,missilebastion:16.45,plasma:13.75,
seafort:16.05,stormcaller:16.65,
spinespiker:17.25,gorespiker:17.25,spineburrow:17.25,
toxicgusher:18.75,acidgusher:18.75,toxicspewer:18.75,
sporelauncher:17.75,sporetower:17.75,acidgeyser:12.45,creeppustule:12.45,
thornnest:14.25,raptornest:14.25,
};
BLD_TUR_S_INFESTATION={
turret:1.08,bunker:1.05,bastion:1.04,aatower:1.05,hellstorm:1.06,
rail:1.08,nova:1.08,minelaser:1.07,missilebastion:1.04,plasma:1.07,
seafort:1.06,stormcaller:1.05,
spinespiker:1.08,gorespiker:1.08,spineburrow:1.08,
toxicgusher:1.06,acidgusher:1.06,toxicspewer:1.06,
sporelauncher:1.05,sporetower:1.05,acidgeyser:1.08,creeppustule:1.08,
thornnest:1.05,raptornest:1.05,
};
const tiered={
spinespiker:[mdlInfSpineBase,mdlInfSpineTur],gorespiker:[mdlInfSpineBase,mdlInfSpineTur],
spineburrow:[mdlInfSpineBase,mdlInfSpineTur],
toxicgusher:[mdlInfToxicBase,mdlInfToxicTur],acidgusher:[mdlInfToxicBase,mdlInfToxicTur],
toxicspewer:[mdlInfToxicBase,mdlInfToxicTur],
sporelauncher:[mdlInfSporeBase,mdlInfSporeTur],sporetower:[mdlInfSporeBase,mdlInfSporeTur],
silktrap:[mdlInfSilkTrap,null],acidgeyser:[mdlInfAcidBase,mdlInfAcidTur],
creeppustule:[mdlInfAcidBase,mdlInfAcidTur],thornnest:[mdlInfThornBase,mdlInfThornTur],
raptornest:[mdlInfThornBase,mdlInfThornTur],
sonicshrieker:[mdlInfSonicShrieker,null],broodspire:[mdlInfBroodSpire,null],
turret:[mdlInfSpineBase,mdlInfSpineTur],bunker:[mdlInfThornBase,mdlInfThornTur],
bastion:[mdlInfMortarBase,mdlInfMortarTur],aatower:[mdlInfSporeBase,mdlInfSporeTur],
uplink:[mdlInfSonicShrieker,null],hellstorm:[mdlInfToxicBase,mdlInfToxicTur],
rail:[mdlInfBoneBase,mdlInfBoneTur],nova:[mdlInfAcidBase,mdlInfAcidTur],
minelaser:[mdlInfProboscisBase,mdlInfProboscisTur],
missilebastion:[mdlInfMissileBase,mdlInfMissileTur],plasma:[mdlInfPlasmaBase,mdlInfPlasmaTur],
seafort:[mdlInfSeafortBase,mdlInfSeafortTur],stormcaller:[mdlInfStormBase,mdlInfStormTur],
};
BLD_TIER_MDL_INFESTATION={};
for(const k in tiered){
const pair=tiered[k],base=pair[0],tur=pair[1];
BLD_TIER_MDL_INFESTATION[k]=[1,2,3].map(t=>({base:()=>base(t),tur:tur?()=>tur(t):null}));
}
const INF_BLD_BESPOKE_PACKS=Object.freeze({
nest: Object.freeze({id:'brood-nest-v2', source:'semantic-bake', maps:null, surfaces:Object.freeze({})}),
hq: Object.freeze({id:'brood-hive-v2', source:'semantic-bake', maps:null, surfaces:Object.freeze({})}),
fac: Object.freeze({id:'brood-hive-v2', source:'semantic-bake', maps:null, surfaces:Object.freeze({})}),
harbor: Object.freeze({id:'brood-hive-v2', source:'semantic-bake', maps:null, surfaces:Object.freeze({})}),
seafort: Object.freeze({id:'brood-hive-v2', source:'semantic-bake', maps:null, surfaces:Object.freeze({})}),
stormcaller: Object.freeze({id:'brood-spore-v2', source:'semantic-bake', maps:null, surfaces:Object.freeze({})}),
airfield: Object.freeze({id:'brood-hive-v2', source:'semantic-bake', maps:null, surfaces:Object.freeze({})}),
tgate: Object.freeze({id:'brood-hive-v2', source:'semantic-bake', maps:null, surfaces:Object.freeze({})}),
spire: Object.freeze({id:'brood-spire-v2', source:'semantic-bake', maps:null, surfaces:Object.freeze({})}),
broodspire: Object.freeze({id:'brood-spire-v2', source:'semantic-bake', maps:null, surfaces:Object.freeze({})}),
broodchamber: Object.freeze({id:'brood-spire-v2', source:'semantic-bake', maps:null, surfaces:Object.freeze({})}),
techlab: Object.freeze({id:'brood-spire-v2', source:'semantic-bake', maps:null, surfaces:Object.freeze({})}),
aatower: Object.freeze({id:'brood-spore-v2', source:'semantic-bake', maps:null, surfaces:Object.freeze({})}),
sporelauncher: Object.freeze({id:'brood-spore-v2', source:'semantic-bake', maps:null, surfaces:Object.freeze({})}),
sporetower: Object.freeze({id:'brood-spore-v2', source:'semantic-bake', maps:null, surfaces:Object.freeze({})}),
missilebastion: Object.freeze({id:'brood-spore-v2', source:'semantic-bake', maps:null, surfaces:Object.freeze({})}),
wall: Object.freeze({id:'brood-carapace-v2', source:'semantic-bake', maps:null, surfaces:Object.freeze({})}),
gate: Object.freeze({id:'brood-carapace-v2', source:'semantic-bake', maps:null, surfaces:Object.freeze({})}),
bunker: Object.freeze({id:'brood-carapace-v2', source:'semantic-bake', maps:null, surfaces:Object.freeze({})}),
bastion: Object.freeze({id:'brood-carapace-v2', source:'semantic-bake', maps:null, surfaces:Object.freeze({})}),
rail: Object.freeze({id:'brood-carapace-v2', source:'semantic-bake', maps:null, surfaces:Object.freeze({})}),
arc: Object.freeze({id:'brood-carapace-v2', source:'semantic-bake', maps:null, surfaces:Object.freeze({})}),
nova: Object.freeze({id:'brood-carapace-v2', source:'semantic-bake', maps:null, surfaces:Object.freeze({})}),
plasma: Object.freeze({id:'brood-carapace-v2', source:'semantic-bake', maps:null, surfaces:Object.freeze({})}),
minelaser: Object.freeze({id:'brood-carapace-v2', source:'semantic-bake', maps:null, surfaces:Object.freeze({})}),
mex: Object.freeze({id:'brood-sac-v2', source:'semantic-bake', maps:null, surfaces:Object.freeze({})}),
pgen: Object.freeze({id:'brood-sac-v2', source:'semantic-bake', maps:null, surfaces:Object.freeze({})}),
geo: Object.freeze({id:'brood-sac-v2', source:'semantic-bake', maps:null, surfaces:Object.freeze({})}),
silo: Object.freeze({id:'brood-sac-v2', source:'semantic-bake', maps:null, surfaces:Object.freeze({})}),
fab: Object.freeze({id:'brood-sac-v2', source:'semantic-bake', maps:null, surfaces:Object.freeze({})}),
acidgeyser: Object.freeze({id:'brood-sac-v2', source:'semantic-bake', maps:null, surfaces:Object.freeze({})}),
creeppustule: Object.freeze({id:'brood-sac-v2', source:'semantic-bake', maps:null, surfaces:Object.freeze({})}),
toxicgusher: Object.freeze({id:'brood-sac-v2', source:'semantic-bake', maps:null, surfaces:Object.freeze({})}),
acidgusher: Object.freeze({id:'brood-sac-v2', source:'semantic-bake', maps:null, surfaces:Object.freeze({})}),
toxicspewer: Object.freeze({id:'brood-sac-v2', source:'semantic-bake', maps:null, surfaces:Object.freeze({})}),
hellstorm: Object.freeze({id:'brood-sac-v2', source:'semantic-bake', maps:null, surfaces:Object.freeze({})}),
turret: Object.freeze({id:'brood-mound-v2', source:'semantic-bake', maps:null, surfaces:Object.freeze({})}),
spinespiker: Object.freeze({id:'brood-mound-v2', source:'semantic-bake', maps:null, surfaces:Object.freeze({})}),
gorespiker: Object.freeze({id:'brood-mound-v2', source:'semantic-bake', maps:null, surfaces:Object.freeze({})}),
spineburrow: Object.freeze({id:'brood-mound-v2', source:'semantic-bake', maps:null, surfaces:Object.freeze({})}),
silktrap: Object.freeze({id:'brood-tendril-v2', source:'semantic-bake', maps:null, surfaces:Object.freeze({})}),
tendriltrap: Object.freeze({id:'brood-tendril-v2', source:'semantic-bake', maps:null, surfaces:Object.freeze({})}),
tendrilmaw: Object.freeze({id:'brood-tendril-v2', source:'semantic-bake', maps:null, surfaces:Object.freeze({})}),
uplink: Object.freeze({id:'brood-tendril-v2', source:'semantic-bake', maps:null, surfaces:Object.freeze({})}),
sonicshrieker: Object.freeze({id:'brood-tendril-v2', source:'semantic-bake', maps:null, surfaces:Object.freeze({})}),
sgen: Object.freeze({id:'brood-tendril-v2', source:'semantic-bake', maps:null, surfaces:Object.freeze({})}),
thornnest: Object.freeze({id:'brood-nest-v2', source:'semantic-bake', maps:null, surfaces:Object.freeze({})}),
raptornest: Object.freeze({id:'brood-nest-v2', source:'semantic-bake', maps:null, surfaces:Object.freeze({})})
});
function infOrganicSurfacePass(geo,pack){
if(!geo)return geo;
if(geo.v){
const v=geo.v;
for(let o=11;o<v.length;o+=VFLOATS){
const raw=v[o],sgn=raw<0?-1:1,packed=Math.abs(raw);
const whole=Math.floor(packed),src=whole-1;
const dst=pack&&pack.surfaces&&pack.surfaces[src]!==undefined?pack.surfaces[src]
:(src===MAT.TWR_GLOW?MAT.BROOD_SLIME:src);
if(dst!==undefined)v[o]=sgn*((dst+1)+(packed-whole));
}
}
return geo;
}
function infBldFactory(fn, k) {
if (!fn) return fn;
const wrapped = function(...args) {
const g = fn(...args);
const pack = INF_BLD_BESPOKE_PACKS[k] || null;
if (g && typeof g === 'object') {
if (g.hull) infOrganicSurfacePass(g.hull, pack);
if (g.tur) infOrganicSurfacePass(g.tur, pack);
if (g.v) infOrganicSurfacePass(g, pack);
}
return g;
};
Object.defineProperty(wrapped, 'name', {value: 'infBld_' + k});
return wrapped;
}
for (const k in INF_BLD_BESPOKE_PACKS) {
if (typeof BLD_MDL_INFESTATION !== 'undefined' && BLD_MDL_INFESTATION[k]) {
BLD_MDL_INFESTATION[k] = infBldFactory(BLD_MDL_INFESTATION[k], k);
}
if (typeof BLD_TUR_MDL_INFESTATION !== 'undefined' && BLD_TUR_MDL_INFESTATION[k]) {
BLD_TUR_MDL_INFESTATION[k] = infBldFactory(BLD_TUR_MDL_INFESTATION[k], k);
}
if (typeof BLD_TIER_MDL_INFESTATION !== 'undefined' && BLD_TIER_MDL_INFESTATION[k]) {
const orig = BLD_TIER_MDL_INFESTATION[k];
BLD_TIER_MDL_INFESTATION[k] = orig.map(V => ({
base: infBldFactory(V.base, k),
tur: V.tur ? infBldFactory(V.tur, k) : null
}));
}
}
})();