massfront-playtest / src /engine /vfxlayers.js
CREATORJD's picture
Publish MASSFRONT web 1.33.51
84bc42a verified
Raw History Blame Contribute Delete
13.1 kB
/* ============================================================================
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;
})();