File size: 7,400 Bytes
aed627e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
/* ============================================================================
   BROOD CROWD — RENDER-ONLY SWARM DENSITY
   ----------------------------------------------------------------------------
   These bodies are deliberately NOT units. They never enter the simulation,
   pathfinder, selection, targeting, fog, save, replay or network state. A
   visible real Brood ground unit is the only anchor allowed to submit one
   deterministic cluster, so crowd art cannot reveal a hidden army or create a
   second source of combat truth.

   Three LOD meshes keep the illusion honest. Close view adds one subordinate
   crawler; tactical view adds a three-body knot; distant view adds four very
   cheap silhouettes beneath the strategic icon tier. Every tier remains one
   InstMesh draw, and one global frame budget is shared across all three.
   ============================================================================ */

let MF_BROOD_CROWD_STREAMS=null;
let MF_BROOD_CROWD_EPOCH=-1;
let MF_BROOD_CROWD_FRAME={time:0,span:0,perf:1,diff:1,seed:0,budget:0,density:0,queued:0};
let MF_BROOD_CROWD_STAT={
  enabled:false,quality:'high',budget:0,visibleAnchors:0,eligibleAnchors:0,
  clusterInstances:0,visualBodies:0,drawCalls:0,triangles:0,
  skippedDensity:0,skippedBudget:0,near:0,mid:0,far:0
};

function mfBroodCrowdHash(v){
  v=(v|0)^0x6d2b79f5;
  v=Math.imul(v^(v>>>15),v|1);
  v^=v+Math.imul(v^(v>>>7),v|61);
  return (v^(v>>>14))>>>0;
}
function mfBroodCrowdStringHash(s){
  s=String(s||'');
  let h=2166136261;
  for(let i=0;i<s.length;i++) h=Math.imul(h^s.charCodeAt(i),16777619);
  return h>>>0;
}

/* One crawler is intentionally less detailed than a real Skitterling. Its
   small subordinate silhouette prevents a cosmetic body being mistaken for a
   target with its own health, while CHITIN/LEAF keep the live organic shader
   response and animation instead of introducing a flat sprite species. */
function mfBroodCrowdCrawler(m,x,z,yaw,scale,legs){
  m.translate(x,0,z);m.rotateY(yaw);m.scale3d(scale,scale,scale);
  m.mat(MAT.CHITIN).team(0);
  m.sphere(-.25,.72,0,1.34,5,CHIT_D,.55,false);
  m.sphere(1.02,.64,0,.72,4,CHITIN,.66,false);
  m.sphere(-1.22,.65,0,.62,4,CHIT_D,.58,false);
  /* A small team-bearing dorsal plate reads allegiance, but most of the body
     keeps authored chitin colour so a dense group does not become a neon blob. */
  m.mat(MAT.CHITIN).team(1);
  m.sphere(-.15,1.19,0,.48,4,BIO_TEAM,.34,true);
  m.team(0).mat(MAT.LEAF);
  const n=Math.max(1,legs|0);
  for(let k=0;k<n;k++) for(const sd of [-1,1]){
    const lx=n===1?.05:-.62+k*(1.24/Math.max(1,n-1));
    const lz=sd*(1.12+(k&1)*.10);
    m.box(lx,.18,lz,1.72,.22,.25,BIO_LEG,sd*(.30+k*.09));
  }
  m.popTransform();m.popTransform();m.popTransform();
}

function mfBroodCrowdGeo(tier){
  const m=MB();
  if(tier==='near'){
    mfBroodCrowdCrawler(m,2.7,1.2,.28,.82,3);
  }else if(tier==='mid'){
    mfBroodCrowdCrawler(m,-2.8,-1.7,-.42,.76,2);
    mfBroodCrowdCrawler(m, 2.4,-1.2, .34,.70,2);
    mfBroodCrowdCrawler(m, -.2, 2.7,1.92,.66,2);
  }else{
    mfBroodCrowdCrawler(m,-2.7,-1.8,-.34,.55,1);
    mfBroodCrowdCrawler(m, 2.5,-1.5, .46,.52,1);
    mfBroodCrowdCrawler(m,-1.7, 2.3,2.36,.48,1);
    mfBroodCrowdCrawler(m, 2.0, 2.1,1.18,.46,1);
  }
  return m.build();
}

function mfBroodCrowdInit(){
  if(typeof gl==='undefined'||!gl||typeof InstMesh!=='function'||typeof MB!=='function') return false;
  const epoch=typeof glEpoch==='number'?glEpoch:0;
  if(MF_BROOD_CROWD_STREAMS&&MF_BROOD_CROWD_EPOCH===epoch) return true;
  const near=mfBroodCrowdGeo('near'),mid=mfBroodCrowdGeo('mid'),far=mfBroodCrowdGeo('far');
  MF_BROOD_CROWD_STREAMS={
    near:new InstMesh(gl,near,64),
    mid:new InstMesh(gl,mid,256),
    far:new InstMesh(gl,far,256),
    bodies:{near:1,mid:3,far:4},
    tris:{near:near.count/3,mid:mid.count/3,far:far.count/3}
  };
  MF_BROOD_CROWD_EPOCH=epoch;
  return true;
}

function mfBroodCrowdBeginFrame(time,ortho,perf,difficulty){
  const quality=typeof mfGfxKey==='function'?mfGfxKey():'high';
  const base=quality==='low'?96:quality==='medium'?160:256;
  const p=Number.isFinite(perf)?perf:1;
  /* Population pressure already drives perfScale. Honour that signal here
     rather than making the presentation layer compete with authoritative AI. */
  const budget=p<.36?Math.min(base,64):p<.56?Math.min(base,112):base;
  const d=Math.max(0,Math.min(2,difficulty|0));
  const density=[.28,.58,1.0][d];
  const mapSeed=(typeof MAPDEFS!=='undefined'&&typeof curMap!=='undefined'&&MAPDEFS[curMap])?
    (MAPDEFS[curMap].seed|0):mfBroodCrowdStringHash(typeof curMap!=='undefined'?curMap:'');
  MF_BROOD_CROWD_FRAME={
    time:Number.isFinite(time)?time:0,span:Number.isFinite(ortho)?ortho:0,
    perf:p,diff:d,seed:mapSeed,budget,density,queued:0
  };
  MF_BROOD_CROWD_STAT={
    enabled:budget>0,quality,budget,visibleAnchors:0,eligibleAnchors:0,
    clusterInstances:0,visualBodies:0,drawCalls:0,triangles:0,
    skippedDensity:0,skippedBudget:0,near:0,mid:0,far:0
  };
  if(!mfBroodCrowdInit()){ MF_BROOD_CROWD_STAT.enabled=false; return false; }
  MF_BROOD_CROWD_STREAMS.near.clear();
  MF_BROOD_CROWD_STREAMS.mid.clear();
  MF_BROOD_CROWD_STREAMS.far.clear();
  return true;
}

/* Called only after the real anchor passed camera and fog visibility gates.
   isBrood must come from the resolved faction kit, never from an assumed team
   number: an AI commander can field the Brood kit in an ordinary seat. */
function mfBroodCrowdQueue(unitIndex,type,x,y,h,yaw,T,teamColour,uLod,iconQ,moving,isBrood){
  MF_BROOD_CROWD_STAT.visibleAnchors++;
  if(!MF_BROOD_CROWD_STAT.enabled||!isBrood||!T||T.air||T.naval||T.cat==='hero'||T.hero||T.size>24) return false;
  MF_BROOD_CROWD_STAT.eligibleAnchors++;
  const F=MF_BROOD_CROWD_FRAME;
  const hv=mfBroodCrowdHash((unitIndex|0)^Math.imul((type|0)+17,0x45d9f3b)^F.seed);
  if((hv&65535)/65535>F.density){ MF_BROOD_CROWD_STAT.skippedDensity++; return false; }
  if(F.queued>=F.budget){ MF_BROOD_CROWD_STAT.skippedBudget++; return false; }
  const iq=Number.isFinite(iconQ)?iconQ:0;
  let tier;
  if(iq>.12||F.span>1800||uLod===1) tier='far';
  else if(F.span>760) tier='mid';
  else tier='near';
  /* Close view is where a tethered cosmetic body is easiest to notice. Keep
     only one in five eligible anchors there; mid/far carry the mass fantasy. */
  if(tier==='near'&&((hv>>>16)%5)!==0){ MF_BROOD_CROWD_STAT.skippedDensity++; return false; }
  const stream=MF_BROOD_CROWD_STREAMS[tier],tc=teamColour||[190,92,230];
  const jitter=.88+((hv>>>24)&255)/255*.22;
  const sc=Math.max(.68,Math.min(1.28,(T.size||12)/16))*jitter;
  const turn=((hv>>>8)&1023)/1023*6.2831853;
  const anim=tier==='far'||F.perf<.36?0:F.time*(moving?6.2:2.05)+(hv&2047)*.0137;
  stream.add(x,y,h,sc,yaw+turn,tc[0],tc[1],tc[2],255,sc,anim,0);
  F.queued++;
  MF_BROOD_CROWD_STAT.clusterInstances++;
  MF_BROOD_CROWD_STAT.visualBodies+=MF_BROOD_CROWD_STREAMS.bodies[tier];
  MF_BROOD_CROWD_STAT.triangles+=MF_BROOD_CROWD_STREAMS.tris[tier];
  MF_BROOD_CROWD_STAT[tier]++;
  return true;
}

function mfBroodCrowdFlush(){
  if(!MF_BROOD_CROWD_STREAMS||typeof gl==='undefined'||!gl) return;
  for(const tier of ['near','mid','far']){
    const stream=MF_BROOD_CROWD_STREAMS[tier];
    if(stream.n){ MF_BROOD_CROWD_STAT.drawCalls++; stream.flush(gl); }
  }
}

function mfBroodCrowdStats(){
  return Object.assign({},MF_BROOD_CROWD_STAT);
}