massfront-playtest / src /rumble.js
CREATORJD's picture
Publish MASSFRONT web 1.33.33 (part 3)
3597f5a verified
Raw
History Blame Contribute Delete
8.17 kB
;
;
/* ============================================================================
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;
};
}
})();