Spaces:
Running
Running
Download src/engine/vfxlayers.js from CREATORJD/massfront-playtest: direct link, hf CLI and curl.
- Browser
- Download file 13.1 kB
-
https://huggingface.co/spaces/CREATORJD/massfront-playtest/resolve/main/src/engine/vfxlayers.js
- Command line
-
hf download hf://spaces/CREATORJD/massfront-playtest/src/engine/vfxlayers.js
-
curl -L -o vfxlayers.js https://huggingface.co/spaces/CREATORJD/massfront-playtest/resolve/main/src/engine/vfxlayers.js
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(){ | |
| ; | |
| 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; | |
| })(); | |