;/* ============================================================================ SHIELD / FORCE-FIELD FX ---------------------------------------------------------------------------- One field source owns one translucent surface. Protected units do not grow their own bubbles: Bulwark/Archon/Aegis/Techlab state is sampled directly, while blocked hits enter the bounded mfShieldHit queue below. INTEGRATION CONTRACT shieldFxBoot() after initGL3D() shieldFxTick(dt) once per simulation/render frame shieldFxDraw(S_nA, animationSeconds) after opaque meshes, before bloom mfShieldHit(x,y,team,r,dx,dy,key,h,piercing) shieldFxGLReset() before GL resource rebuild shieldFxReset() on resetWorld() `dx,dy` is the projectile travel direction. `h` is an optional world-space vertical centre; omitting it anchors the ripple from terrain height. A true `piercing` flag deliberately queues nothing. ============================================================================ */ (function(){ 'use strict'; const MF_SF_DOME_CAP=72; const MF_SF_PLATE_CAP=192; const MF_SF_INST_CAP=Math.max(MF_SF_DOME_CAP,MF_SF_PLATE_CAP); const MF_SF_INST_FLOATS=12; const MF_SF_HIT_CAP=48; const MF_SF_RATE_CAP=64; const MF_SF_RATE_SECONDS=0.12; const MF_SF_INST=new Float32Array(MF_SF_INST_CAP*MF_SF_INST_FLOATS); /* Domes and plates used to share one staging array because plates were packed only after dome submission. Collecting both before touching GL needs their CPU data to coexist; this tiny fixed sidecar preserves both original caps and order without allocating during a battle. */ const MF_SF_DOME_INST=new Float32Array(MF_SF_DOME_CAP*MF_SF_INST_FLOATS); const MF_SF_HX=new Float32Array(MF_SF_HIT_CAP); const MF_SF_HY=new Float32Array(MF_SF_HIT_CAP); const MF_SF_HH=new Float32Array(MF_SF_HIT_CAP); const MF_SF_HR=new Float32Array(MF_SF_HIT_CAP); const MF_SF_HDX=new Float32Array(MF_SF_HIT_CAP); const MF_SF_HDY=new Float32Array(MF_SF_HIT_CAP); const MF_SF_HAGE=new Float32Array(MF_SF_HIT_CAP); const MF_SF_HTEAM=new Uint8Array(MF_SF_HIT_CAP); const MF_SF_RATE_KEY=new Array(MF_SF_RATE_CAP); const MF_SF_RATE_TIME=new Float64Array(MF_SF_RATE_CAP); let mfSfHitN=0, mfSfRateHead=0, mfSfPackedHits=0; let mfSfProg=null, mfSfVAO=null, mfSfMeshVBO=null, mfSfInstVBO=null; let mfSfDomeFirst=0, mfSfDomeVerts=0, mfSfPlateFirst=0, mfSfPlateVerts=0; let mfSfU={}; const MF_SF_TELEM={ ready:false, drawCalls:0, domes:0, plates:0, hits:0, liveHits:0, acceptedHits:0, rateLimitedHits:0, droppedHits:0, culled:0, lastError:'' }; const MF_SF_VS=`#version 300 es layout(location=0) in vec3 aPos; layout(location=1) in vec3 aNrm; layout(location=2) in vec4 aI0; // world centre xyz, radius / half-width layout(location=3) in vec4 aI1; // tint rgb, opacity layout(location=4) in vec4 aI2; // yaw, kind, normalised age, height scale uniform mat4 uVP; out vec3 vWorld; out vec3 vNrm; out vec3 vLocal; flat out vec4 vTint; flat out vec2 vKindAge; void main(){ float c=cos(aI2.x),s=sin(aI2.x),kind=aI2.y; vec3 lp=aPos; vec3 ln=aNrm; if(kind<0.5){ lp=vec3(lp.x*aI0.w,lp.y*aI0.w*aI2.w,lp.z*aI0.w); ln=normalize(vec3(ln.x,ln.y/max(0.08,aI2.w),ln.z)); }else{ lp=vec3(lp.x*aI0.w*0.04,lp.y*aI0.w*aI2.w,lp.z*aI0.w); } vec3 rp=vec3(lp.x*c-lp.z*s,lp.y,lp.x*s+lp.z*c); vec3 rn=normalize(vec3(ln.x*c-ln.z*s,ln.y,ln.x*s+ln.z*c)); vWorld=aI0.xyz+rp; vNrm=rn; vLocal=aPos; vTint=aI1; vKindAge=aI2.yz; gl_Position=uVP*vec4(vWorld,1.0); }`; const MF_SF_FS=`#version 300 es precision highp float; in vec3 vWorld; in vec3 vNrm; in vec3 vLocal; flat in vec4 vTint; flat in vec2 vKindAge; uniform vec3 uEye; uniform float uTime; uniform float uHex; out vec4 o; float hexEdge(vec2 p){ const vec2 hs=vec2(1.0,1.7320508); vec2 h=hs*0.5; vec2 a=mod(p,hs)-h; vec2 b=mod(p-h,hs)-h; vec2 g=dot(a,a)mfSfBX1||y+rmfSfBY1); if(!ok) MF_SF_TELEM.culled++; return ok; } function mfSfMesh(){ const out=[]; const put=(p,n)=>{out.push(p[0],p[1],p[2],n[0],n[1],n[2]);}; const lat=8,seg=20; mfSfDomeFirst=0; for(let iy=0;iyMF_SF_HAGE[slot]) slot=i; MF_SF_TELEM.droppedHits++; } let dl=Math.hypot(Number(dx)||0,Number(dy)||0); if(dl<0.001){dx=1;dy=0;dl=1;} dx=(Number(dx)||0)/dl;dy=(Number(dy)||0)/dl; MF_SF_HX[slot]=x;MF_SF_HY[slot]=y; MF_SF_HH[slot]=Number.isFinite(Number(height))?Number(height):mfSfGround(x,y,null,-1)+radius*0.58; MF_SF_HR[slot]=radius;MF_SF_HDX[slot]=dx;MF_SF_HDY[slot]=dy; MF_SF_HAGE[slot]=0;MF_SF_HTEAM[slot]=team; MF_SF_TELEM.acceptedHits++;MF_SF_TELEM.liveHits=mfSfHitN; return true; } function shieldFxTick(dt){ dt=Number(dt); if(!(dt>0)) return; dt=Math.min(0.25,dt); for(let i=mfSfHitN-1;i>=0;i--){ MF_SF_HAGE[i]+=dt/0.46; if(MF_SF_HAGE[i]<1) continue; const n=--mfSfHitN; if(i!==n){ MF_SF_HX[i]=MF_SF_HX[n];MF_SF_HY[i]=MF_SF_HY[n];MF_SF_HH[i]=MF_SF_HH[n]; MF_SF_HR[i]=MF_SF_HR[n];MF_SF_HDX[i]=MF_SF_HDX[n];MF_SF_HDY[i]=MF_SF_HDY[n]; MF_SF_HAGE[i]=MF_SF_HAGE[n];MF_SF_HTEAM[i]=MF_SF_HTEAM[n]; } } MF_SF_TELEM.liveHits=mfSfHitN; } function mfSfPut(n,x,h,y,r,team,alpha,yaw,kind,age,heightScale,out){ if(n>=MF_SF_INST_CAP) return n; let cr=120,cg=205,cb=255; try{ if(typeof TEAMC!=='undefined'&&TEAMC[team]){cr=TEAMC[team][0];cg=TEAMC[team][1];cb=TEAMC[team][2];} }catch(_){ } const o=n*MF_SF_INST_FLOATS,d=out||MF_SF_INST; d[o]=x;d[o+1]=h;d[o+2]=y;d[o+3]=r; /* Pull every livery slightly toward ion-blue so red/orange teams still read as a shield surface rather than a second fire effect. */ d[o+4]=Math.min(1,cr/255*0.72+0.16); d[o+5]=Math.min(1,cg/255*0.72+0.22); d[o+6]=Math.min(1,cb/255*0.72+0.27); d[o+7]=alpha; d[o+8]=yaw||0;d[o+9]=kind;d[o+10]=age||0;d[o+11]=heightScale; return n+1; } function mfSfCollectDomes(){ let n=0; try{ if(typeof unitHigh==='number'&&typeof ualive!=='undefined'&&typeof utype!=='undefined'){ for(let i=0;i0&&(B.shield>0||B.guardT>0)){ const sz=typeof BT!=='undefined'&&BT.techlab?BT.techlab.size:B.r*2; R=Math.max(B.r*1.15,sz*0.82);a=B.guardT>0?0.66:0.58; }else continue; if(!mfSfFog(B.team,B.x,B.y,true)||!mfSfInView(B.x,B.y,R)) continue; n=mfSfPut(n,B.x,mfSfGround(B.x,B.y,null,-1)+0.35,B.y,R,B.team,a,0,0,0,B.type==='sgen'?0.55:0.68,MF_SF_DOME_INST); } }catch(e){ MF_SF_TELEM.lastError='collect domes: '+String(e&&e.message||e).slice(0,120); } return n; } function mfSfCollectPlates(){ let n=0;mfSfPackedHits=0; /* Transient shield contacts are packed first. They must remain visible in a huge Guard formation even when the persistent plate budget is saturated. */ for(let i=0;ibefore)mfSfPackedHits++; } try{ if(typeof unitHigh==='number'&&typeof umode!=='undefined') for(let i=0;i0)) continue; const x=ux[i],y=uy[i],team=uteam[i],T=TYPES[utype[i]]; if(!T) continue; const yaw=uang[i]-Math.PI*0.5,r=T.size*0.73; if(!mfSfFog(team,x,y,true)||!mfSfInView(x,y,r*1.5)) continue; const ahead=Math.max(T.r||4,T.size*0.42)*1.18; n=mfSfPut(n,x+Math.cos(yaw)*ahead,mfSfGround(x,y,T,i)+T.size*0.50, y+Math.sin(yaw)*ahead,r,team,0.52,yaw,1,0,0.57); } }catch(e){ MF_SF_TELEM.lastError='collect plates: '+String(e&&e.message||e).slice(0,120); } return n; } function mfSfSubmit(first,count,n,data){ if(n<=0) return; gl.bindBuffer(gl.ARRAY_BUFFER,mfSfInstVBO); gl.bufferSubData(gl.ARRAY_BUFFER,0,data||MF_SF_INST,0,n*MF_SF_INST_FLOATS); gl.drawArraysInstanced(gl.TRIANGLES,first,count,n); MF_SF_TELEM.drawCalls++; try{ if(typeof drawCalls==='number') drawCalls++; if(typeof triCount==='number') triCount+=count/3*n; }catch(_){ } } function shieldFxDraw(nightAmount,animationSeconds){ mfSfSyncBounds(); MF_SF_TELEM.drawCalls=0;MF_SF_TELEM.domes=0;MF_SF_TELEM.plates=0;MF_SF_TELEM.hits=0;MF_SF_TELEM.culled=0; const domes=mfSfCollectDomes(); const plates=mfSfCollectPlates(); /* Empty frames are the normal case. CPU visibility/fog collection is enough to prove there is no field to show, so do not boot the pass, query/alter GL state, or bind anything. This also leaves the renderer's expected 3D program intact without a restoration round trip. */ if(!domes&&!plates) return true; if(!shieldFxBoot()) return false; const wasProg=gl.getParameter(gl.CURRENT_PROGRAM); const wasVAO=gl.getParameter(gl.VERTEX_ARRAY_BINDING); const wasArr=gl.getParameter(gl.ARRAY_BUFFER_BINDING); const wasBlend=gl.isEnabled(gl.BLEND),wasCull=gl.isEnabled(gl.CULL_FACE),wasDepth=gl.isEnabled(gl.DEPTH_TEST); const wasMask=gl.getParameter(gl.DEPTH_WRITEMASK),wasDepthFunc=gl.getParameter(gl.DEPTH_FUNC); const srcRGB=gl.getParameter(gl.BLEND_SRC_RGB),dstRGB=gl.getParameter(gl.BLEND_DST_RGB); const srcA=gl.getParameter(gl.BLEND_SRC_ALPHA),dstA=gl.getParameter(gl.BLEND_DST_ALPHA); try{ gl.useProgram(mfSfProg); gl.uniformMatrix4fv(mfSfU.uVP,false,matVP); gl.uniform3f(mfSfU.uEye,typeof eyeX==='number'?eyeX:0,typeof eyeY==='number'?eyeY:3000,typeof eyeZ==='number'?eyeZ:0); gl.uniform1f(mfSfU.uTime,Number.isFinite(Number(animationSeconds))?Number(animationSeconds):mfSfNow()); gl.uniform1f(mfSfU.uHex,mfSfQuality()==='low'?0:1); gl.enable(gl.DEPTH_TEST);gl.depthFunc(gl.LEQUAL);gl.depthMask(false); gl.disable(gl.CULL_FACE); gl.enable(gl.BLEND);gl.blendFunc(gl.ONE,gl.ONE_MINUS_SRC_ALPHA); gl.bindVertexArray(mfSfVAO); if(domes){mfSfSubmit(mfSfDomeFirst,mfSfDomeVerts,domes,MF_SF_DOME_INST);MF_SF_TELEM.domes=domes;} if(plates){ mfSfSubmit(mfSfPlateFirst,mfSfPlateVerts,plates,MF_SF_INST); MF_SF_TELEM.plates=plates;MF_SF_TELEM.hits=mfSfPackedHits; } }catch(e){ MF_SF_TELEM.lastError='draw: '+String(e&&e.message||e).slice(0,150); return false; }finally{ gl.bindVertexArray(wasVAO);gl.bindBuffer(gl.ARRAY_BUFFER,wasArr); gl.blendFuncSeparate(srcRGB,dstRGB,srcA,dstA); gl.depthFunc(wasDepthFunc);gl.depthMask(wasMask); if(wasDepth)gl.enable(gl.DEPTH_TEST);else gl.disable(gl.DEPTH_TEST); if(wasCull)gl.enable(gl.CULL_FACE);else gl.disable(gl.CULL_FACE); if(wasBlend)gl.enable(gl.BLEND);else gl.disable(gl.BLEND); /* Every custom GL pass must hand control back through begin3D. Besides the program, it repairs model samplers 0-8 after optional post/FX passes. */ try{ if(typeof begin3D==='function'){ let nA=Number(nightAmount); if(!Number.isFinite(nA)) nA=typeof nightAmt==='function'?nightAmt():0; begin3D(nA); }else gl.useProgram(wasProg); }catch(_){gl.useProgram(wasProg);} } return true; } function shieldFxTelemetry(){ return { ready:MF_SF_TELEM.ready,drawCalls:MF_SF_TELEM.drawCalls,domes:MF_SF_TELEM.domes, plates:MF_SF_TELEM.plates,hits:MF_SF_TELEM.hits,liveHits:mfSfHitN, acceptedHits:MF_SF_TELEM.acceptedHits,rateLimitedHits:MF_SF_TELEM.rateLimitedHits, droppedHits:MF_SF_TELEM.droppedHits,culled:MF_SF_TELEM.culled,lastError:MF_SF_TELEM.lastError }; } if(typeof window!=='undefined'){ window.shieldFxBoot=shieldFxBoot; window.shieldFxTick=shieldFxTick; window.shieldFxDraw=shieldFxDraw; window.shieldFxGLReset=shieldFxGLReset; window.shieldFxReset=shieldFxReset; window.shieldFxTelemetry=shieldFxTelemetry; window.mfShieldHit=mfShieldHit; window.MF_SHIELD_FX_TELEMETRY=MF_SF_TELEM; } })();