Spaces:
Running
Running
| ; | |
| ; | |
| /* ============================================================================ | |
| RUMBLE — camera shake + device vibration for heavy chassis and big blasts | |
| ---------------------------------------------------------------------------- | |
| Isolated from render3d.js so terrain/HQ/tower work does not collide with | |
| the camera nudge. The renderer already applies the global `shake` as an | |
| eye-space jitter; this file only REQUESTS a value and fires haptics. | |
| Why a takeover instead of editing every sim.js `shake=` site: Nova/Legion | |
| FX authors own those numbers (particle size, gpfxBurst counts, super pow). | |
| We wrap the events, then replace an unscaled assignment with a distance- | |
| and quality-scaled request so a far-off TITAN does not rattle the phone. | |
| ============================================================================ */ | |
| const RUMBLE_STEP_R=720; | |
| const RUMBLE_BLAST_R=1400; | |
| let rumbleReduced=false; | |
| let rumbleLastHap=0; | |
| let rumbleStepHap=0; | |
| const rumbleAcc=typeof MAXU==='number'?new Float32Array(MAXU):new Float32Array(1); | |
| function rumbleReadReduced(){ | |
| try{ | |
| rumbleReduced=!!(window.matchMedia&&matchMedia('(prefers-reduced-motion: reduce)').matches); | |
| }catch(e){ rumbleReduced=false; } | |
| } | |
| rumbleReadReduced(); | |
| if(typeof matchMedia==='function'){ | |
| try{ | |
| const mq=matchMedia('(prefers-reduced-motion: reduce)'); | |
| const on=()=>{ rumbleReadReduced(); if(typeof applySettings==='function') applySettings(); }; | |
| if(mq.addEventListener) mq.addEventListener('change',on); | |
| else if(mq.addListener) mq.addListener(on); | |
| }catch(e){} | |
| } | |
| function rumbleGfxMul(){ | |
| /* MEDIUM/LOW must actually cheapen the kick — a 16-unit blast on a mid | |
| preset is the same fill-rate tax as HIGH if we only skip particles. */ | |
| const q=typeof qualityKey==='function'?qualityKey():((typeof META!=='undefined'&&META.settings&&META.settings.quality)||'high'); | |
| return q==='low'?0.42:q==='medium'?0.68:q==='cinematic'?1.08:1; | |
| } | |
| function rumbleDistMul(x,y,reach){ | |
| if(typeof cam==='undefined'||typeof x!=='number') return 1; | |
| const d=Math.hypot(x-cam.x,y-cam.y); | |
| if(d>=reach) return 0; | |
| const t=1-d/reach; | |
| return t*t; | |
| } | |
| function rumbleHaptic(ms,pattern){ | |
| if(rumbleReduced) return; | |
| if(typeof META!=='undefined'&&META.settings&&META.settings.haptics===false) return; | |
| const now=performance.now(); | |
| if(now-rumbleLastHap<72) return; | |
| rumbleLastHap=now; | |
| const payload=pattern||ms; | |
| try{ | |
| const H=window.Capacitor&&Capacitor.Plugins&&Capacitor.Plugins.Haptics; | |
| if(H){ | |
| if(H.impact){ | |
| const style=ms>=40?'HEAVY':ms>=22?'MEDIUM':'LIGHT'; | |
| H.impact({style:style}); | |
| return; | |
| } | |
| if(H.vibrate){ H.vibrate({duration:typeof ms==='number'?ms:40}); return; } | |
| } | |
| }catch(e){} | |
| try{ if(navigator.vibrate) navigator.vibrate(payload); }catch(e){} | |
| } | |
| function requestShake(x,y,power,kind){ | |
| if(!(power>0)) return; | |
| const step=kind==='step'; | |
| const settings=typeof META!=='undefined'&&META.settings; | |
| if(settings&&settings.shake===false) return; | |
| if(typeof shakeMult==='number'&&shakeMult<=0) return; | |
| if(rumbleReduced&&step) return; | |
| if(step&&typeof camDist==='number'&&camDist>2200) return; | |
| const reach=step?RUMBLE_STEP_R:RUMBLE_BLAST_R; | |
| const mul=rumbleDistMul(x,y,reach)*rumbleGfxMul(); | |
| const amt=power*mul; | |
| if(amt<0.35) return; | |
| if(typeof shake==='number') shake=Math.max(shake,amt); | |
| if(step){ | |
| const t=performance.now(); | |
| if(t-rumbleStepHap<90) return; | |
| rumbleStepHap=t; | |
| rumbleHaptic(amt>=2.6?18:12); | |
| }else{ | |
| rumbleHaptic(amt>=12?55:amt>=7?36:22, amt>=12?[40,28,48]:null); | |
| } | |
| } | |
| function rumbleHeavyWeight(type,T){ | |
| if(!T) return 0; | |
| if(type===8) return 1; | |
| if(T.cat==='hero'||T.hero) return 0.82; | |
| if(typeof MF_UT_MASSFLESH==='number'&&(type===MF_UT_MASSFLESH||type===MF_UT_MASSFLESH_AIR)) return 0.9; | |
| if(typeof MF_UT_AIRLIFT==='number'&&type===MF_UT_AIRLIFT) return 0.55; | |
| if(T.name==='Basilisk'||T.name==='Harbinger') return 0.7; | |
| if(T.name==='Goliath'||T.name==='Alpha Ravager') return 0.5; | |
| if(T.cat==='exp'&&T.size>=24) return 0.7; | |
| if(T.legs&&T.size>=20&&(T.tier|0)>=2) return 0.45; | |
| return 0; | |
| } | |
| function rumbleUnitMove(i,T,travel,prevWalk){ | |
| const w=rumbleHeavyWeight(utype[i],T); | |
| if(!w||!(travel>0.02)) return; | |
| if(T.legs){ | |
| /* Plant when the gait sine falls through zero — one foot down, matching | |
| the vertex bob (abs(sin)) that hits twice per TAU. */ | |
| const s0=Math.sin(prevWalk), s1=Math.sin(uwalk[i]); | |
| if(!(s0>0&&s1<=0)) return; | |
| const power=(T.size>=40?3.4:T.cat==='hero'?2.6:2.0)*w; | |
| requestShake(ux[i],uy[i],power,'step'); | |
| }else{ | |
| rumbleAcc[i]+=travel; | |
| const period=T.air?28:18; | |
| if(rumbleAcc[i]<period) return; | |
| rumbleAcc[i]=0; | |
| requestShake(ux[i],uy[i],1.8*w,'step'); | |
| } | |
| } | |
| function rumbleUndoRaw(prevShake,raw){ | |
| /* Callers that wrote `shake=16` clobber a larger pending kick. Put that | |
| pending value back, then requestShake Math.max's the scaled amount. */ | |
| if(typeof shake!=='number') return; | |
| if(shake===raw||(raw>0&&shake>=raw&&shake<=raw+0.01)) shake=prevShake; | |
| } | |
| (function rumbleTakeover(){ | |
| if(typeof applySettings==='function'){ | |
| const prev=applySettings; | |
| applySettings=function(){ | |
| prev.apply(this,arguments); | |
| /* Settings already has Impact Camera Shake. prefers-reduced-motion | |
| minimizes the leftover unwrapped `shake=` sites (rifle, buildings) | |
| without adding a second toggle. */ | |
| if(rumbleReduced&&typeof shakeMult==='number') shakeMult*=0.25; | |
| }; | |
| } | |
| if(typeof killUnit==='function'){ | |
| const prev=killUnit; | |
| killUnit=function(i,silent){ | |
| const t=utype[i], x=ux[i], y=uy[i]; | |
| const T=TYPES[t]; | |
| const titanOrCdr=t===8||t===4; | |
| const big=titanOrCdr||(T&&(T.cat==='hero'||T.hero||T.size>=40)); | |
| const prevShake=shake; | |
| prev.apply(this,arguments); | |
| if(!big) return; | |
| if(titanOrCdr) rumbleUndoRaw(prevShake,16); | |
| requestShake(x,y,titanOrCdr?16:14,'blast'); | |
| }; | |
| } | |
| if(typeof novaFire==='function'){ | |
| const prev=novaFire; | |
| novaFire=function(b,wx,wy){ | |
| const prevShake=shake; | |
| const r=prev.apply(this,arguments); | |
| if(r){ rumbleUndoRaw(prevShake,22); requestShake(wx,wy,22,'blast'); } | |
| return r; | |
| }; | |
| } | |
| if(typeof superDetonation==='function'){ | |
| const prev=superDetonation; | |
| superDetonation=function(x,y,pow,byTeam){ | |
| const prevShake=shake; | |
| const r=prev.apply(this,arguments); | |
| shake=prevShake; | |
| requestShake(x,y,14*(pow||1),'blast'); | |
| return r; | |
| }; | |
| } | |
| if(typeof updateSingularities==='function'){ | |
| const prev=updateSingularities; | |
| updateSingularities=function(dt){ | |
| const collapsing=[]; | |
| if(typeof singularities!=='undefined'){ | |
| for(let i=0;i<singularities.length;i++){ | |
| const S=singularities[i]; | |
| if(S&&S.phase===1) collapsing.push(S); | |
| } | |
| } | |
| const prevShake=shake; | |
| prev.apply(this,arguments); | |
| shake=prevShake; | |
| for(let k=0;k<collapsing.length;k++){ | |
| const S=collapsing[k]; | |
| if(S.phase>=2) requestShake(S.x,S.y,16*(S.pow||1),'blast'); | |
| } | |
| }; | |
| } | |
| if(typeof projImpact==='function'){ | |
| const prev=projImpact; | |
| projImpact=function(i){ | |
| const cluster=ptype[i]===9&&!pSplit[i]; | |
| const x=px[i], y=py[i]; | |
| prev.apply(this,arguments); | |
| if(cluster) requestShake(x,y,8,'blast'); | |
| }; | |
| } | |
| if(typeof spawnExplosion==='function'){ | |
| const prev=spawnExplosion; | |
| spawnExplosion=function(x,y,size,victimTeam){ | |
| const r=prev.apply(this,arguments); | |
| /* Superweapons (size>=40) already went through superDetonation. This | |
| catches Atlas/Massflesh-scale hulls that stay under that handoff. */ | |
| if(size>=30&&size<40) requestShake(x,y,6+size*0.18,'blast'); | |
| return r; | |
| }; | |
| } | |
| if(typeof damageBld==='function'){ | |
| const prev=damageBld; | |
| damageBld=function(b,dmg,attTeam){ | |
| const B=blds[b]; | |
| const typ=B&&B.type, x=B&&B.x, y=B&&B.y, was=B&&B.alive; | |
| const r=prev.apply(this,arguments); | |
| if(was&&B&&!B.alive&&(typ==='nova'||typ==='silo')) | |
| requestShake(x,y,typ==='nova'?12:8,'blast'); | |
| return r; | |
| }; | |
| } | |
| })(); | |