File size: 8,172 Bytes
3597f5a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
;
;
/* ============================================================================
   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;
    };
  }
})();