File size: 14,064 Bytes
3597f5a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
502e0c9
 
3597f5a
 
 
 
 
 
502e0c9
3597f5a
502e0c9
 
3597f5a
 
502e0c9
 
3597f5a
 
 
 
 
 
 
 
 
 
502e0c9
 
3597f5a
 
502e0c9
 
3597f5a
 
 
 
 
 
502e0c9
3597f5a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
502e0c9
 
3597f5a
 
 
 
502e0c9
 
 
 
3597f5a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
502e0c9
 
3597f5a
 
 
 
 
 
 
 
 
 
 
 
 
 
502e0c9
3597f5a
 
 
 
 
 
 
 
502e0c9
3597f5a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
502e0c9
 
3597f5a
 
 
 
 
 
 
 
 
 
 
 
 
 
502e0c9
 
 
 
 
 
 
 
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
308
309
310
311
312
313
314
315
316
317
318
/* ============================================================================
   ORGANIC FX — Brood ichor / bile / violet-black fluid
   ----------------------------------------------------------------------------
   Trail/smoke/energy live in gpufx.js and the CPU particle switch. This file
   owns GROWN-tissue liquid only: droplets, short puddles, and wisps.

   Why not gpfxBurst: that path is sparks/embers. A lime GPU fountain reads as
   the same energy Nova/Legion already spray. Ichor has to sit on the ground
   as a wet stain with a sheen, not a point-sprite firework.

   Civic rule: never addCrater / addGroundBurn / deformTerrain for ichor.
   Pavement already has a soot carpet; a crater-sprite stain reads as a
   broken placement preview. Puddles are billboards, not terrain decals.

   Mechanical factions stay sparks/oil/energy. unitIsBrood / horde kit only.
   ============================================================================ */
const ORGFX_CAP=384, ORGFX_DROP=0, ORGFX_SPLAT=1, ORGFX_WISP=2;
const orgX=new Float32Array(ORGFX_CAP), orgY=new Float32Array(ORGFX_CAP);
const orgZ=new Float32Array(ORGFX_CAP), orgVx=new Float32Array(ORGFX_CAP);
const orgVy=new Float32Array(ORGFX_CAP), orgVz=new Float32Array(ORGFX_CAP);
const orgLife=new Float32Array(ORGFX_CAP), orgMax=new Float32Array(ORGFX_CAP);
const orgSize=new Float32Array(ORGFX_CAP), orgRot=new Float32Array(ORGFX_CAP);
const orgAsp=new Float32Array(ORGFX_CAP);
const orgR=new Uint8Array(ORGFX_CAP), orgG=new Uint8Array(ORGFX_CAP), orgB=new Uint8Array(ORGFX_CAP);
const orgKind=new Uint8Array(ORGFX_CAP);
let orgHead=0, orgN=0, orgfxOn=0, orgfxHurtT=null;

/* Fallback if BRD_ICHOR / INF_ICHOR have not loaded. Same triples as the
   brood slime tile and infestation mucus/sac/glow — not human crimson. */
const ORGFX_PAL=[
  [[115,177,77],[62,88,30],[185,255,72]],
  [[214,168,40],[78,48,12],[255,220,110]],
  [[186,82,245],[48,18,64],[220,150,255]]
];

function orgfxQ(){
  return (typeof mfVfxQ==='function')?mfVfxQ()
    :((typeof GFX!=='undefined'&&GFX.particles!=null)?GFX.particles:1);
}
function orgfxH(x,y){
  return (typeof terrainH==='function')?terrainH(x,y):0.4;
}
function orgfxCaste(sz,name){
  const s=String(name||'');
  if(s.indexOf('Nest')>=0||s.indexOf('Sovereign')>=0||s.indexOf('Hive')>=0
     ||s.indexOf('Tidecaster')>=0) return 2;
  if(s.indexOf('Bile')>=0||s.indexOf('Acid')>=0||s.indexOf('Spore')>=0
     ||s.indexOf('Geyser')>=0||s.indexOf('Toxic')>=0) return 1;
  return sz>=28?2:sz>=18?1:0;
}
function orgfxPal(caste){
  const c=caste|0;
  if(typeof BRD_ICHOR!=='undefined'&&BRD_ICHOR[c]&&BRD_ICHOR[c][0])
    return BRD_ICHOR[c];
  if(c===0&&typeof INF_ICHOR!=='undefined'&&INF_ICHOR.wet)
    return [INF_ICHOR.wet,INF_ICHOR.dark,INF_ICHOR.hi];
  return ORGFX_PAL[c]||ORGFX_PAL[0];
}
function orgfxUnitOrganic(i){
  if(typeof unitIsBrood==='function') return unitIsBrood(i);
  return !!(typeof TYPES!=='undefined'&&TYPES[utype[i]]&&TYPES[utype[i]].brood) || uteam[i]===2;
}
function orgfxBldOrganic(B){
  if(!B) return false;
  if(B.team===2||B.type==='nest') return true;
  if(typeof bldFactionKit==='function') return bldFactionKit(B)==='horde';
  if(B.team===1&&typeof AI!=='undefined'&&AI&&AI.fac==='horde') return true;
  const pf=(typeof playerFaction!=='undefined'&&playerFaction)||'';
  return B.team===0&&(pf==='horde'||pf==='brood');
}
function orgfxStrategic(){
  return (typeof orthoSpan==='number'&&orthoSpan>900);
}
function orgfxOverview(){
  return (typeof orthoSpan==='number'&&orthoSpan>2400);
}

function orgfxAdd(kind,x,y,z,vx,vy,vz,life,size,r,g,b,rot,asp){
  if(orgN>=ORGFX_CAP) return;
  const i=orgHead; orgHead=(orgHead+1)%ORGFX_CAP;
  if(!orgLife[i]) orgN++;
  orgKind[i]=kind; orgX[i]=x; orgY[i]=y; orgZ[i]=z;
  orgVx[i]=vx; orgVy[i]=vy; orgVz[i]=vz;
  orgLife[i]=life; orgMax[i]=life; orgSize[i]=size;
  orgRot[i]=rot||0; orgAsp[i]=asp>0.05?asp:1;
  orgR[i]=r; orgG[i]=g; orgB[i]=b;
}

function orgfxBurst(x,y,size,dirX,dirY,death){
  const q=orgfxQ();
  /* LOW particles is 0.5. Below ~0.28 is "FX off" leftovers. */
  if(q<0.28) return;
  const strat=orgfxStrategic(), over=orgfxOverview();
  const caste=arguments.length>6?arguments[6]:orgfxCaste(size);
  const pal=orgfxPal(caste);
  const wet=pal[0], dark=pal[1], hi=pal[2];
  const floor=orgfxH(x,y);
  const hasDir=dirX*dirX+dirY*dirY>0.01;
  const dl=hasDir?Math.hypot(dirX,dirY):1;
  const dx=hasDir?dirX/dl:0, dy=hasDir?dirY/dl:0;
  /* Preset split:
       LOW    (q<0.65)  tinted puffs only — no pools, no beads
       MEDIUM (q<0.95)  few large splats, still readable
       HIGH+            volume + wet sheen + lingering pools
     Strategic zoom keeps wisps; a puddle field vanishes into noise. */
  let nDrop, nSplat, nWisp;
  if(over){ nDrop=0; nSplat=0; nWisp=death?2:1; }
  else if(strat){ nDrop=death?2:0; nSplat=0; nWisp=death?3:1; }
  else if(q<0.65){ nDrop=0; nSplat=0; nWisp=death?3:2; }
  else if(q<0.95){ nDrop=death?5:3; nSplat=death?3:2; nWisp=death?2:1; }
  else { nDrop=death?(q>=1.25?12:8):(q>=1.25?6:5); nSplat=death?(q>=1.25?6:4):3; nWisp=death?(q>=1.25?3:2):1; }
  const scale=Math.max(10,size);
  for(let k=0;k<nDrop;k++){
    const sway=rr(-0.55,0.55), c=Math.cos(sway), s=Math.sin(sway);
    const spd=(death?16:9)+Math.random()*(death?20:11);
    const vx=hasDir?(dx*c-dy*s)*spd:Math.cos(Math.random()*TAU)*spd;
    const vy=hasDir?(dx*s+dy*c)*spd:Math.sin(Math.random()*TAU)*spd;
    const hot=Math.random()<0.58;
    const col=hot?hi:wet;
    /* Do not aim the quad along velocity — that turned a 1.12 aspect
       bead into a short tracer. Round disc, random spin. */
    orgfxAdd(ORGFX_DROP,x,y,floor+scale*0.42+Math.random()*2.4,
      vx,vy,12+Math.random()*(death?24:14),
      0.48+Math.random()*0.32, (q<0.95?7.2:6.4)+Math.random()*2.6,
      col[0],col[1],col[2], Math.random()*TAU, 1.04+Math.random()*0.10);
  }
  if(!strat&&nSplat){
    const civic=typeof cityGroundAt==='function'&&cityGroundAt(x,y)>=1;
    const splatN=civic?Math.max(1,nSplat-1):nSplat;
    /* HIGH lingering pools. MEDIUM shorter but still a wet mark. Civic
       stays a stain, never a crater-sprite carpet. */
    const splatLife=civic?(q>=0.95?1.6:0.85)
      :(q>=1.25?4.0:q>=0.95?2.8:1.25);
    for(let k=0;k<splatN;k++){
      const a=Math.random()*TAU, d=Math.random()*scale*(death?0.62:0.32);
      /* Cap the puddle. Even 16 wu at tactical close reads as an AoE ring. */
      const sz=Math.min(7.5,(q<0.95?scale*0.42:scale*0.36)*(civic?0.72:1)*(0.80+Math.random()*0.35));
      orgfxAdd(ORGFX_SPLAT,x+Math.cos(a)*d,y+Math.sin(a)*d,floor+0.42,
        0,0,0, splatLife*(0.75+Math.random()*0.35),
        sz, (dark[0]*0.45+wet[0]*0.55)|0,(dark[1]*0.45+wet[1]*0.55)|0,
        (dark[2]*0.45+wet[2]*0.55)|0, a, 1.08+Math.random()*0.16);
    }
  }
  for(let k=0;k<nWisp;k++){
    const a=Math.random()*TAU, d=Math.random()*scale*0.28;
    orgfxAdd(ORGFX_WISP,x+Math.cos(a)*d,y+Math.sin(a)*d,floor+scale*0.22,
      rr(-5,5),rr(-5,5),7+Math.random()*11,
      (strat?0.22:0.50)+Math.random()*0.18, scale*(strat?0.34:0.55),
      wet[0],wet[1],wet[2], a, 1);
  }
}
function orgfxHit(x,y,size,dirX,dirY,caste){
  orgfxBurst(x,y,size,dirX||0,dirY||0,false,caste==null?orgfxCaste(size):caste);
}
function orgfxDeath(x,y,size,caste){
  orgfxBurst(x,y,size,0,0,true,caste==null?orgfxCaste(size):caste);
}
function orgfxCount(){ return orgN; }

/* Tiny sim.js hooks. Sampling lives here so dealDamage stays a one-liner. */
function orgfxOnHit(j,dmg,attacker){
  if(j<0||!ualive[j]||!orgfxUnitOrganic(j)) return;
  const q=orgfxQ();
  if(q<0.28) return;
  const now=(typeof stats!=='undefined')?stats.t:0;
  if(!orgfxHurtT&&typeof MAXU==='number') orgfxHurtT=new Float32Array(MAXU);
  if(orgfxHurtT&&orgfxHurtT[j]>now) return;
  if(orgfxHurtT) orgfxHurtT[j]=now+(q>=0.95?0.08:0.14);
  /* HIGH: almost every real hit sprays. MEDIUM must still read as liquid. */
  const keep=q>=0.95?0.92:q>=0.65?0.78:0.34;
  if(Math.random()>keep) return;
  const T=TYPES[utype[j]], sz=T&&T.size||12;
  let dx=0,dy=0;
  if(attacker>=0&&ualive[attacker]){ dx=ux[j]-ux[attacker]; dy=uy[j]-uy[attacker]; }
  /* Rim of the carapace facing the shot — hull-center spray looked like
     a green energy fountain sitting on the navel. */
  const il=Math.hypot(dx,dy)||1;
  orgfxHit(ux[j]-(dx/il)*sz*0.42, uy[j]-(dy/il)*sz*0.42, sz, dx, dy, orgfxCaste(sz,T&&T.name));
}
function orgfxOnDeath(x,y,size,name){
  orgfxDeath(x,y,size,orgfxCaste(size,name));
}
function orgfxOnBld(B,dmg,died){
  if(!orgfxBldOrganic(B)||!B||B.x==null) return;
  const sz=(typeof BT!=='undefined'&&BT[B.type]&&BT[B.type].size)||B.r*2||24;
  const caste=orgfxCaste(sz,B.type);
  if(died){ orgfxDeath(B.x,B.y,sz,caste); return; }
  if(!(dmg>=10)||Math.random()>0.45) return;
  orgfxHit(B.x,B.y,sz,0,0,caste);
}
function orgfxSeep(x,y,size){
  const q=orgfxQ();
  if(q<0.95||orgfxStrategic()) return;
  const pal=orgfxPal(orgfxCaste(size));
  const wet=pal[0], floor=orgfxH(x,y);
  const a=Math.random()*TAU, spd=6+Math.random()*8;
  orgfxAdd(ORGFX_DROP,x,y,floor+Math.max(8,size)*0.38,
    Math.cos(a)*spd,Math.sin(a)*spd,8+Math.random()*10,
    0.36+Math.random()*0.18, 5.4+Math.random()*1.8,
    wet[0],wet[1],wet[2], a, 1.06);
}

function orgfxTick(dt){
  if(!orgN) return;
  dt=Math.min(dt,0.05);
  for(let i=0;i<ORGFX_CAP;i++){
    if(!orgLife[i]) continue;
    orgLife[i]-=dt;
    if(orgLife[i]<=0){ orgLife[i]=0; orgN--; continue; }
    const k=orgKind[i];
    if(k===ORGFX_SPLAT) continue;
    orgX[i]+=orgVx[i]*dt; orgY[i]+=orgVy[i]*dt; orgZ[i]+=orgVz[i]*dt;
    if(k===ORGFX_DROP){
      orgVz[i]-=78*dt; orgVx[i]*=0.91; orgVy[i]*=0.91;
      /* Keep the authored spin. Re-aiming at velocity made every bead a streak. */
      const floor=orgfxH(orgX[i],orgY[i])+0.32;
      if(orgZ[i]<=floor&&orgVz[i]<0){
        orgZ[i]=floor; orgVz[i]=0;
        /* Landing bead becomes a brief puddle at tactical MEDIUM+. */
        const q=orgfxQ();
        if(!orgfxStrategic()&&q>=0.65&&Math.random()<(q>=0.95?0.72:0.38)){
          orgKind[i]=ORGFX_SPLAT;
          orgLife[i]=q>=0.95?Math.max(orgLife[i],1.6):Math.max(orgLife[i],0.7);
          orgMax[i]=orgLife[i]; orgSize[i]=Math.min(8,orgSize[i]*1.55); orgAsp[i]=1.10+Math.random()*0.16;
          orgR[i]=orgR[i]*0.48|0; orgG[i]=orgG[i]*0.52|0; orgB[i]=orgB[i]*0.42|0;
        } else orgLife[i]=Math.min(orgLife[i],0.10);
      }
    } else {
      orgVz[i]*=0.94; orgVx[i]*=0.96; orgVy[i]*=0.96;
    }
  }
}

function orgfxEnqueue(){
  if(!orgN||typeof bbAdd==='undefined'||!bbAdd) return;
  if(typeof sprites==='undefined'||!sprites.glow) return;
  const glow=sprites.glow, smoke=sprites.smoke||glow;
  const q=orgfxQ(), strat=orgfxStrategic(), over=orgfxOverview();
  const t=(typeof performance!=='undefined')?performance.now()*0.001:0;
  const addRect=bbAlpha.addOrientedRect?bbAlpha.addOrientedRect.bind(bbAlpha):null;
  const addGlow=bbAdd.addOrientedRect?bbAdd.addOrientedRect.bind(bbAdd):null;
  for(let i=0;i<ORGFX_CAP;i++){
    if(!orgLife[i]) continue;
    if(over&&(i&1)) continue;
    const X=orgX[i], Y=orgY[i];
    if(typeof cam!=='undefined'){
      const lim=(typeof orthoSpan==='number'?orthoSpan:900)*0.9;
      const dx=X-cam.x, dy=Y-cam.y;
      if(dx*dx+dy*dy>lim*lim) continue;
    }
    if(typeof fogPointVisible==='function'&&!fogPointVisible(X,Y)) continue;
    const lf=orgLife[i]/orgMax[i], k=orgKind[i], H=orgZ[i];
    const rot=orgRot[i], asp=orgAsp[i]||1, sz=orgSize[i];
    if(k===ORGFX_SPLAT){
      if(strat) continue;
      /* Dark elliptical stain + saturated wet body + moving sheen.
         Sheen is HIGH-only so MEDIUM stays a cheaper readable slick. */
      const fade=Math.min(1,lf*1.15);
      const aStain=Math.min(200,175*fade);
      const w=sz*asp, h=sz;
      if(addRect) addRect.call(bbAlpha,smoke,X,Y,H,w,h,rot,orgR[i],orgG[i],orgB[i],aStain);
      else bbAlpha.add(smoke,X,Y,H,sz*(1.05+(1-lf)*0.3),rot,orgR[i],orgG[i],orgB[i],aStain);
      const wr=Math.min(255,orgR[i]+48), wg=Math.min(255,orgG[i]+62), wb=Math.min(255,orgB[i]+28);
      if(addRect) addRect.call(bbAlpha,glow,X,Y,H+0.12,w*0.70,h*0.64,rot,wr,wg,wb,88*fade);
      else bbAlpha.add(glow,X,Y,H+0.12,sz*0.62,rot,wr,wg,wb,95*fade);
      if(q>=0.95){
        const shimmer=0.55+0.45*Math.sin(t*5.2+i);
        const ox=Math.cos(rot)*sz*0.12, oy=Math.sin(rot)*sz*0.12;
        if(addGlow) addGlow.call(bbAdd,glow,X+ox,Y+oy,H+0.22,w*0.38,h*0.22,rot,
          Math.min(255,wr+40),Math.min(255,wg+30),Math.min(255,wb+20),52*fade*shimmer);
        else bbAdd.add(glow,X+ox,Y+oy,H+0.22,sz*0.34,rot,
          Math.min(255,wr+40),Math.min(255,wg+30),Math.min(255,wb+20),52*fade*shimmer);
      }
    } else if(k===ORGFX_WISP){
      const lift=H+(1-lf)*12;
      bbAlpha.add(smoke,X,Y,lift,sz*(0.95+(1-lf)*1.15),t*0.28+i,
        orgR[i],orgG[i],orgB[i], (strat?80:130)*lf);
    } else {
      /* In-flight bead: circular body + wet core. Aspect stays ~1. */
      const w=sz*asp, h=sz;
      if(addRect) addRect.call(bbAlpha,smoke,X,Y,H-0.25,w*1.12,h*1.12,rot,
        orgR[i]*0.45|0,orgG[i]*0.5|0,orgB[i]*0.4|0,150*lf);
      else bbAlpha.add(smoke,X,Y,H-0.25,sz*1.35,rot,
        orgR[i]*0.45|0,orgG[i]*0.5|0,orgB[i]*0.4|0,150*lf);
      if(addGlow) addGlow.call(bbAdd,glow,X,Y,H,w*0.92,h*0.92,rot,orgR[i],orgG[i],orgB[i],230*lf);
      else bbAdd.add(glow,X,Y,H,sz,rot,orgR[i],orgG[i],orgB[i],230*lf);
    }
  }
}

function orgfxInstall(){
  if(orgfxOn) return;
  orgfxOn=1;
  if(typeof MAXU==='number'&&!orgfxHurtT) orgfxHurtT=new Float32Array(MAXU);
}

(function orgfxHook(){
  if(typeof beginBB==='function'){
    const prev=beginBB;
    beginBB=function(tex){
      orgfxInstall();
      const r=prev.apply(this,arguments);
      /* Enqueue after beginBB so frustum cull matches this camera. */
      if(!tex) orgfxEnqueue();
      return r;
    };
  }
  if(typeof gpfxFrame==='function'){
    const prev=gpfxFrame;
    gpfxFrame=function(dt,matVP,viewH){
      orgfxInstall();
      const r=prev.apply(this,arguments);
      orgfxTick((typeof dt==='number'&&dt>0)?dt:0.016);
      return r;
    };
  }
})();