File size: 13,100 Bytes
84bc42a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
/* ============================================================================
   VFX LAYER RECIPES — faction shape, timing and material language
   ----------------------------------------------------------------------------
   This module is the rich styling layer behind mfEmitMacroFx(). It does not
   spawn a second explosion. One recipe describes a textured/raymarched core,
   one depth-tested shock front and one bounded ballistic debris group; the
   core itself evolves from ignition through fire/soot into its smoke phase.

   Compatibility entry points remain public for older integrations, but route
   into the same owner. GPU point sprays are deliberately absent: readable
   fragments are real rigid pieces created by the authoritative recipe.
   ============================================================================ */
(function(){
'use strict';

function vfxOpts(arg){
  if(arg==null) return {};
  if(typeof arg==='string') return {faction:arg};
  return arg;
}
function vfxFaction(o){
  return o.faction||(typeof playerFaction!=='undefined'?playerFaction:'nova');
}
function vfxTeam(o){
  if(o.team!==undefined) return o.team|0;
  const f=vfxFaction(o);
  if(f==='brood') return 2;
  if(f==='horde') return 3;
  if(typeof playerFaction!=='undefined'&&f===playerFaction) return 0;
  return 1;
}
function vfxGround(x,y){
  return typeof terrainH==='function'?terrainH(x,y):0;
}

function vfxPut(o,key,value){
  if(o[key]===undefined||o[key]===null) o[key]=value;
}

function vfxMix(a,b,t){
  return [
    Math.round(a[0]+(b[0]-a[0])*t),
    Math.round(a[1]+(b[1]-a[1])*t),
    Math.round(a[2]+(b[2]-a[2])*t)
  ];
}
function vfxWeaponKind(kind,o){
  if(kind===3) return 'strategic';
  if(kind===4) return 'collapse';
  if(kind===5) return 'shield';
  if(kind===6) return 'beam';
  const raw=String(o.weaponClass||'').toLowerCase();
  if(raw) return raw;
  if(o.coreType==='air') return 'airburst';
  if(o.coreType==='flame') return 'flame';
  return kind===1?'kinetic':'explosive';
}

/* Weapon family owns silhouette, timing and material. Faction colour is mixed
   into these profiles below; it must not turn an artillery shell, rail strike
   and bile impact into the same differently-coloured blob. */
const VFX_WEAPON_STYLE={
  kinetic:{hot:[255,232,188],rim:[222,214,194],dust:[116,112,104],tint:[88,84,78],
    aspect:[1.46,.62,.76],dens:.58,emis:.72,rise:1.2,life:.30,shock:0,debris:1,trail:1,
    stages:'contact>excavation>fragment'},
  explosive:{hot:[255,220,144],rim:[255,169,70],dust:[116,108,98],tint:[94,78,64],
    aspect:[1.30,1.52,1.24],dens:.84,emis:1.20,rise:11,life:1.24,shock:1,debris:2,trail:1,
    stages:'flash>expansion>combustion>soot'},
  missile:{hot:[255,224,154],rim:[255,174,72],dust:[104,102,98],tint:[88,76,66],
    aspect:[1.25,1.62,1.20],dens:.86,emis:1.25,rise:14,life:1.34,shock:1,debris:2,trail:1,
    stages:'puncture>fireball>soot'},
  artillery:{hot:[255,232,176],rim:[255,184,86],dust:[126,116,104],tint:[96,82,70],
    aspect:[1.42,1.92,1.34],dens:.90,emis:1.30,rise:16,life:1.58,shock:1,debris:3,trail:1,
    stages:'white-flash>pressure>fireball>soot-plume'},
  bombardment:{hot:[255,238,190],rim:[255,190,92],dust:[132,120,106],tint:[102,84,68],
    aspect:[1.48,1.78,1.40],dens:.58,emis:.92,rise:14,life:1.88,shock:1,debris:3,trail:1,
    stages:'white-flash>overpressure>rolling-fire>heavy-soot'},
  airburst:{hot:[255,230,174],rim:[255,190,108],dust:[116,116,112],tint:[90,86,82],
    aspect:[1.48,1.84,1.42],dens:.84,emis:1.24,rise:22,life:1.22,shock:1,debris:2,trail:1,
    stages:'air-flash>radial-bloom>falling-soot'},
  /* A falling hull is not an air-to-air detonation. Keep its fuel flash low,
     let the crown cool into dark soot, and reserve its clearly visible mass
     for the same bounded rigid-debris group the macro recipe already owns. */
  aircrash:{hot:[255,196,118],rim:[214,108,54],dust:[52,50,46],tint:[64,56,49],
    /* These are volume-only inputs on High/Cinematic.  A crashed hull rises
       into a narrow soot crown rather than spreading the generic blast's
       satellite fire lobes across the ground; its low/Medium flipbook is
       deliberately unchanged. */
    aspect:[1.04,2.08,1.00],dens:.74,emis:.56,rise:8,life:.98,shock:1,debris:2,trail:1,
    stages:'impact>fuel-flash>falling-soot'},
  flame:{hot:[255,170,62],rim:[255,92,24],dust:[76,70,66],tint:[116,54,26],
    aspect:[.92,2.18,.92],dens:.88,emis:1.10,rise:24,life:.82,shock:0,debris:0,trail:0,
    stages:'ignition>rolling-flame>smoke'},
  gauss:{hot:[236,250,255],rim:[70,220,255],dust:[82,92,98],tint:[54,126,154],
    aspect:[1.72,.84,.82],dens:.82,emis:1.24,rise:2,life:.38,shock:1,debris:0,trail:0,energy:1,
    stages:'contact>ionization>fade'},
  ion:{hot:[218,250,255],rim:[82,204,255],dust:[72,82,94],tint:[54,112,160],
    aspect:[1.26,1.12,1.26],dens:.80,emis:1.28,rise:6,life:.52,shock:1,debris:0,trail:0,energy:1,
    stages:'contact>charge-bloom>fade'},
  sonic:{hot:[220,246,255],rim:[178,112,255],dust:[86,80,98],tint:[106,72,152],
    aspect:[1.44,.82,1.44],dens:.70,emis:1.18,rise:1,life:.46,shock:1,debris:0,trail:0,energy:1,
    stages:'contact>compression>rarefaction'},
  beam:{hot:[245,252,255],rim:[106,214,255],dust:[78,84,92],tint:[62,132,164],
    aspect:[1.22,1.06,1.22],dens:.76,emis:1.26,rise:3,life:.42,shock:1,debris:0,trail:0,energy:1,
    stages:'contact>energy-bloom>fade'},
  /* Machine-faction phase ordnance is not a recoloured conventional blast.
     One compact volume carries its directional spear, asymmetric crescent and
     collapsing violet bloom; no fire, soot, debris spray or decorative ring is
     added around it. */
  void:{hot:[250,244,255],rim:[144,78,255],dust:[54,52,76],tint:[68,44,184],
    aspect:[1.58,1.20,1.12],dens:.72,emis:1.42,rise:1.2,life:.44,shock:0,debris:0,trail:0,energy:1,
    stages:'phase-spear>crescent-bloom>collapse'},
  organic:{hot:[174,244,92],rim:[152,88,208],dust:[68,88,54],tint:[72,116,48],
    aspect:[1.34,1.18,1.28],dens:1.10,emis:.52,rise:5,life:.68,shock:0,debris:0,trail:0,
    stages:'wet-impact>spread>vapour'},
  strategic:{hot:[255,238,196],rim:[255,190,98],dust:[120,108,96],tint:[92,76,66],
    aspect:[1.22,3.55,1.22],dens:.98,emis:1.28,rise:26,life:4.8,shock:1,debris:3,trail:1,
    stages:'flash>overpressure>fire-column>soot-plume'},
  collapse:{hot:[176,166,150],rim:[150,146,140],dust:[142,134,122],tint:[112,104,94],
    aspect:[1.36,.90,1.36],dens:.96,emis:0,rise:7,life:1.72,shock:0,debris:3,trail:0,
    stages:'fracture>dust-roll>settle'}
};

/* Rigid fragments are part of the weapon language too. Heavy shells need
   enough horizontal/vertical energy to clear the fireball they created;
   otherwise telemetry reports debris while the volume hides every piece.
   Values are launch envelopes, not extra emitters: the recipe still owns one
   bounded group of at most three bodies. */
const VFX_DEBRIS_MOTION={
  kinetic:    {base:18,mul:1.25,up0:22,upMul:1.15,spread:.22,launch:.14,tint:[86,82,76]},
  explosive:  {base:24,mul:1.22,up0:38,upMul:1.20,spread:.62,launch:.15,tint:[82,70,58]},
  missile:    {base:30,mul:1.58,up0:45,upMul:1.48,spread:.48,launch:.16,tint:[76,68,60]},
  artillery:  {base:34,mul:1.86,up0:56,upMul:1.68,spread:.58,launch:.18,tint:[72,62,54]},
  bombardment:{base:42,mul:2.15,up0:65,upMul:1.90,spread:.68,launch:.20,tint:[66,55,46]},
  airburst:   {base:32,mul:1.72,up0:38,upMul:1.12,spread:.82,launch:.18,tint:[82,78,72]},
  aircrash:   {base:34,mul:1.68,up0:46,upMul:1.30,spread:.58,launch:.20,tint:[54,48,42]},
  strategic:  {base:48,mul:2.00,up0:78,upMul:2.05,spread:.76,launch:.22,tint:[62,50,42]}
};

/* Enrich, do not replace, caller intent. Faction identity changes the density
   silhouette, buoyancy, combustion colour and shock timing inside the owned
   layers rather than adding a decorative particle flourish. */
function vfxRecipe(kind,size,arg){
  const o=Object.assign({},vfxOpts(arg)),s=Math.max(3,Number(size)||8);
  const f=vfxFaction(o),p=mfEnergyProfile(f),imp=p.impact;
  const weapon=vfxWeaponKind(kind,o),style=VFX_WEAPON_STYLE[weapon]||VFX_WEAPON_STYLE.explosive;
  /* Void keeps the requested white-violet language. Faction identity remains a
     restrained tint inside that material rather than repainting it green. */
  const factionWeight=weapon==='void'?.08:style.energy?.48:(weapon==='organic'?.16:.18);
  o.faction=f;o.size=s;o.weaponClass=weapon;
  vfxPut(o,'hot',vfxMix(style.hot,imp.flashCore,factionWeight));
  vfxPut(o,'rim',vfxMix(style.rim,imp.shockwaveColor,weapon==='void'?.06:style.energy?.58:.24));
  vfxPut(o,'dust',vfxMix(style.dust,imp.smokeColor,.18));
  vfxPut(o,'volumeTint',vfxMix(style.tint,p.primary,factionWeight));
  vfxPut(o,'volumeAspect',style.aspect.slice());
  vfxPut(o,'volumeDensity',style.dens);
  vfxPut(o,'volumeEmission',style.emis);
  vfxPut(o,'volumeRise',style.rise);
  /* Style is consumed only by the High/Cinematic raymarch material. It is an
     internal volume profile—not another recipe layer—and lets an airframe
     crash cool its outer crown without altering the single Low/Medium card. */
  vfxPut(o,'volumeStyle',weapon==='aircrash'?1:weapon==='void'?2:0);
  const heavyTail=weapon==='artillery'||weapon==='bombardment';
  vfxPut(o,'coreLife',style.life+(kind===2&&!heavyTail?Math.min(.28,s*.006):0));
  vfxPut(o,'shockLife',kind===3?.76:(weapon==='bombardment'?.48:weapon==='artillery'?.42:.34)/Math.max(.72,imp.shockwaveSpeed));
  vfxPut(o,'shockOpacity',kind===3?1:style.energy?.82:.90);
  const areaShock=(kind===2||kind===3)&&weapon!=='organic'&&weapon!=='flame';
  vfxPut(o,'shock',areaShock||!!style.shock);
  vfxPut(o,'debrisTrails',!!style.trail);
  vfxPut(o,'stageProfile',style.stages);
  vfxPut(o,'physicsProfile',weapon);
  const dm=VFX_DEBRIS_MOTION[weapon];
  if(dm){
    const physSize=Math.max(s,Number(o.physicsSize)||s);
    vfxPut(o,'debrisSpeed',dm.base+physSize*dm.mul);
    vfxPut(o,'debrisUp',dm.up0+physSize*dm.upMul);
    vfxPut(o,'debrisSpread',dm.spread);
    vfxPut(o,'debrisLaunchRadius',physSize*dm.launch);
    vfxPut(o,'debrisTint',dm.tint.slice());
    vfxPut(o,'debrisChunks',1);
  }

  if(o.debrisCount===undefined) o.debrisCount=style.debris;
  o.debrisCount=Math.max(0,Math.min(3,o.debrisCount|0));
  /* A direct event is core + ring OR core + one directional fragment. */
  if(kind===1){
    if(o.shock) o.debrisCount=0;
    else o.debrisCount=Math.min(1,o.debrisCount);
  }
  return o;
}

/* Backward-compatible public presentation helper. It delegates directly to
   the authoritative macro VISUAL recipe: callers using this styling API must
   never trigger damage, burns, camera shake or the strategic gameplay path. */
function vfxExplosion(x,y,size,arg){
  if(typeof mfEmitMacroFx!=='function') return 0;
  const kind=typeof MF_MACRO_FX_EXPLOSIVE!=='undefined'?MF_MACRO_FX_EXPLOSIVE:2;
  /* mfEmitMacroFx owns the one vfxRecipe() application. Clone caller options
     here so normalizing this compatibility signature cannot mutate them. */
  const o=Object.assign({},vfxOpts(arg));
  o.size=Math.max(6,Number(size)||8);
  return mfEmitMacroFx(kind,x,y,o);
}

function vfxImpact(x,y,size,dirX,dirY,arg){
  if(typeof mfEmitMacroFx!=='function') return 0;
  /* Normalize only. mfEmitMacroFx is the single authoritative recipe owner and
     applies vfxRecipe exactly once. Pre-applying it here allowed compatibility
     callers to enter the owner with a half-resolved recipe. */
  const s=Math.max(4,size||8),o=Object.assign({},vfxOpts(arg));
  const dl=Math.hypot(dirX||0,dirY||0)||1;
  o.size=s;
  o.coreRadius=o.coreRadius||s*.72;
  o.direction=[(dirX||0)/dl,(dirY||0)/dl];
  return mfEmitMacroFx(MF_MACRO_FX_DIRECT,x,y,o);
}

/* Muzzle flash is one authored card. It is not an explosion recipe and never
   emits a GPU point spray. Beam and projectile geometry remain separate. */
function vfxMuzzleFlash(x,y,h,dirX,dirY,arg){
  const o=vfxOpts(arg),f=vfxFaction(o),p=mfEnergyProfile(f);
  if(typeof macroFxQueue!=='function') return 0;
  const dl=Math.hypot(dirX||0,dirY||0)||1,dx=(dirX||0)/dl,dy=(dirY||0)/dl;
  const s=Math.max(4,o.size||14),c=p.impact.sparkColor;
  return macroFxQueue(typeof MF_MACROFX_ENERGY_HIT!=='undefined'?MF_MACROFX_ENERGY_HIT:16,
    x+dx*s*.42,y+dy*s*.42,h===undefined?vfxGround(x,y)+1:h,s*.55,.06,200,
    (c[0]<<16)|(c[1]<<8)|c[2],Math.atan2(dy,dx));
}

/* Normal shield damage uses mfQueueShieldHit with a stable source id. This
   legacy helper has no entity identity, so its safe fallback is one ripple. */
function vfxForcefieldHit(x,y,faction){
  if(typeof mfEmitMacroFx!=='function') return 0;
  const c=mfEnergyProfile(faction||'nova').forcefield.color;
  return mfEmitMacroFx(MF_MACRO_FX_SHIELD,x,y,{size:12,radius:14,life:.28,hot:c});
}

/* Kept as a no-op compatibility symbol. Shockwave lifetime is simulation
   time and is advanced beside volFxTick(), never by render frequency. */
function vfxTick(){ }

window.vfxExplosion=vfxExplosion;
window.vfxImpact=vfxImpact;
window.vfxMuzzleFlash=vfxMuzzleFlash;
window.vfxForcefieldHit=vfxForcefieldHit;
window.vfxTick=vfxTick;
window.vfxRecipe=vfxRecipe;
})();