File size: 9,738 Bytes
3597f5a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
502e0c9
3597f5a
 
 
 
 
502e0c9
 
 
 
 
 
 
3597f5a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
502e0c9
 
 
 
 
 
 
3597f5a
 
 
 
 
 
 
502e0c9
 
 
 
 
 
 
 
 
3597f5a
 
502e0c9
 
 
 
3597f5a
502e0c9
3597f5a
 
 
 
502e0c9
 
3597f5a
502e0c9
 
 
3597f5a
502e0c9
 
 
3597f5a
502e0c9
 
 
 
 
 
 
 
3597f5a
502e0c9
3597f5a
502e0c9
 
 
 
 
 
3597f5a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
502e0c9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3597f5a
502e0c9
 
 
 
 
 
 
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
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
;
;
/* ============================================================================
   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=1100;
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);
const rumbleHumT=typeof MAXU==='number'?new Float32Array(MAXU):new Float32Array(1);

function rumbleZoomClose(){
  /* Tactical zoom: a commander fills the frame. Command zoom is a map
     read — footsteps should not rattle the phone there. */
  return typeof camDist==='number'&&camDist<980;
}

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 rumbleInMatch(){
  /* The menu / primer / attract loop share the GL camera. A leftover
     `shake` value would nudge the war-room backdrop. `running` lives in
     main.js (after this file); typeof is safe before that binding exists. */
  return typeof running==='undefined'||!!running;
}

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;
  /* Chrome and Android WebView both implement navigator.vibrate. The
     WebView path is a silent no-op unless the APK declares VIBRATE.
     Call it first — @capacitor/haptics is not a dependency, and a stub
     Haptics plugin must not swallow the Chrome path. */
  let sent=false;
  try{
    if(navigator.vibrate){ navigator.vibrate(payload); sent=true; }
  }catch(e){}
  if(sent) return;
  try{
    const H=window.Capacitor&&Capacitor.Plugins&&Capacitor.Plugins.Haptics;
    if(H&&H.impact){
      const style=ms>=40?'HEAVY':ms>=22?'MEDIUM':'LIGHT';
      H.impact({style:style});
      return;
    }
    if(H&&H.vibrate) H.vibrate({duration:typeof ms==='number'?ms:40});
  }catch(e){}
}

function requestShake(x,y,power,kind){
  if(!(power>0)||!rumbleInMatch()) return;
  const step=kind==='step'||kind==='hum';
  const settings=typeof META!=='undefined'&&META.settings;
  const wantShake=!(settings&&settings.shake===false)&&!(typeof shakeMult==='number'&&shakeMult<=0);
  const wantHap=!(settings&&settings.haptics===false);
  if(!wantShake&&!wantHap) return;
  if(rumbleReduced&&step) return;
  if(step&&typeof camDist==='number'&&camDist>1800) return;
  const zoomed=rumbleZoomClose();
  const reach=step?(zoomed?1600:RUMBLE_STEP_R):RUMBLE_BLAST_R;
  const mul=rumbleDistMul(x,y,reach)*rumbleGfxMul();
  /* World-space cam nudge of ±2.6 is invisible at command zoom and still
     shy at SPAN_MIN 420. Boost only when the chassis fills the view. */
  const zoomBoost=zoomed?clamp(980/Math.max(420,camDist),1,2.35):1;
  const amt=power*mul*(kind==='hum'?1:zoomBoost);
  if(amt<0.28) return;
  if(wantShake&&typeof shake==='number') shake=Math.max(shake,amt);
  if(!wantHap) return;
  if(kind==='hum'){
    const t=performance.now();
    if(t-rumbleStepHap<420) return;
    rumbleStepHap=t;
    rumbleHaptic(10,[8,40,8]);
  }else if(step){
    const t=performance.now();
    if(t-rumbleStepHap<(zoomed?55:90)) return;
    rumbleStepHap=t;
    rumbleHaptic(zoomed?(amt>=3.2?28:18):(amt>=2.6?18:12), zoomed?[14,36,12]:null);
  }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) return;
  const zoomed=rumbleZoomClose();
  if(travel>0.02){
    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?4.2:T.cat==='hero'?3.6:2.4)*w*(zoomed?1.35:1);
      requestShake(ux[i],uy[i],power,'step');
    }else{
      rumbleAcc[i]+=travel;
      const period=T.air?28:(zoomed?8:14);
      if(rumbleAcc[i]<period) return;
      rumbleAcc[i]=0;
      requestShake(ux[i],uy[i],(zoomed?2.6:1.8)*w,'step');
    }
    return;
  }
  /* Idle reactor / gyro hum — only when a commander or titan fills the view. */
  if(!zoomed) return;
  if(!(T.cat==='hero'||T.hero||utype[i]===8||T.size>=40)) return;
  rumbleHumT[i]+=(typeof dt==='number'?dt:0.016);
  if(rumbleHumT[i]<0.55) return;
  rumbleHumT[i]=0;
  requestShake(ux[i],uy[i],0.95*w,'hum');
}

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 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;
    };
  }
})();