Spaces:
Running
Running
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;
})();
|