perfectionist-plugins 2.4.5 → 2.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/auto-accept.js +19 -109
- package/dodge-helper.js +835 -560
- package/package.json +40 -40
- package/perfectionist-utils.js +7 -7
package/dodge-helper.js
CHANGED
|
@@ -1,560 +1,835 @@
|
|
|
1
|
-
const PLUGIN_ID = "dodge-helper";
|
|
2
|
-
export const version = "1.4.
|
|
3
|
-
|
|
4
|
-
const PHASE_URI = "/lol-gameflow/v1/gameflow-phase";
|
|
5
|
-
const SESSION_URI = "/lol-gameflow/v1/session";
|
|
6
|
-
const CHAMP_SELECT_URI = "/lol-champ-select/v1/session";
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
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
|
-
if (
|
|
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
|
-
return
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
if (
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const
|
|
199
|
-
if (
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
if (!
|
|
209
|
-
const
|
|
210
|
-
if (
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
if (
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const
|
|
237
|
-
if (!
|
|
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
|
-
if (
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
return
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
function
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
const
|
|
319
|
-
if (
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
if (
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
if (
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
removeDodgeWatcher();
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
return;
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
1
|
+
const PLUGIN_ID = "dodge-helper";
|
|
2
|
+
export const version = "1.4.3";
|
|
3
|
+
|
|
4
|
+
const PHASE_URI = "/lol-gameflow/v1/gameflow-phase";
|
|
5
|
+
const SESSION_URI = "/lol-gameflow/v1/session";
|
|
6
|
+
const CHAMP_SELECT_URI = "/lol-champ-select/v1/session";
|
|
7
|
+
const TIMER_URI = "/lol-champ-select/v1/session/timer";
|
|
8
|
+
const SUMMONER_URI = "/lol-summoner/v1/current-summoner";
|
|
9
|
+
const RANKED_STATS_URI = "/lol-ranked/v1/current-ranked-stats";
|
|
10
|
+
const DODGE_URI = "/lol-gameflow/v1/session/dodge";
|
|
11
|
+
const QUIT_URI = "/lol-lobby-team-builder/champ-select/v1/session/quit";
|
|
12
|
+
const STORE_KEY = "perfectionist:dodge-helper:accounts:v1";
|
|
13
|
+
const PENDING_KEY = "perfectionist:dodge-helper:pending:v1";
|
|
14
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
15
|
+
const PENDING_MAX_AGE_MS = 2 * 60 * 60 * 1000;
|
|
16
|
+
const STYLE_ID = "perf-dodge-helper-style";
|
|
17
|
+
const LAYER_ID = "perfDodgeHelperLayer";
|
|
18
|
+
const ROOT_ID = "perfDodgeHelper";
|
|
19
|
+
const BUTTON_ID = "perfDodgeHelperButton";
|
|
20
|
+
const COST_ID = "perfDodgeHelperCost";
|
|
21
|
+
const FOOTER_SELECTOR = ".bottom-right-buttons";
|
|
22
|
+
const SCREEN_SELECTOR = ".champion-select";
|
|
23
|
+
|
|
24
|
+
const RANKED_QUEUES = new Map([
|
|
25
|
+
[420, "RANKED_SOLO_5x5"],
|
|
26
|
+
[440, "RANKED_FLEX_SR"],
|
|
27
|
+
]);
|
|
28
|
+
const ARAM_QUEUES = new Set([450, 2400, 2401, 2402]);
|
|
29
|
+
const APEX_TIERS = new Set(["MASTER", "GRANDMASTER", "CHALLENGER"]);
|
|
30
|
+
const PENALTY_TIERS = {
|
|
31
|
+
1: { lp: 5, lockout: 6 },
|
|
32
|
+
2: { lp: 15, lockout: 30 },
|
|
33
|
+
3: { lp: 15, lockout: 720 },
|
|
34
|
+
};
|
|
35
|
+
const GAME_PHASES = new Set([
|
|
36
|
+
"GameStart",
|
|
37
|
+
"InProgress",
|
|
38
|
+
"Reconnect",
|
|
39
|
+
"WaitingForStats",
|
|
40
|
+
"PreEndOfGame",
|
|
41
|
+
"EndOfGame",
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
const CSS = `
|
|
45
|
+
#${LAYER_ID} {
|
|
46
|
+
position: fixed !important; inset: 0 !important; z-index: 99999 !important;
|
|
47
|
+
pointer-events: none !important;
|
|
48
|
+
}
|
|
49
|
+
#${ROOT_ID} {
|
|
50
|
+
position: fixed !important; display: flex !important; align-items: flex-end !important;
|
|
51
|
+
justify-content: center !important; width: 125px !important; height: 32px !important;
|
|
52
|
+
pointer-events: auto !important; z-index: 1 !important;
|
|
53
|
+
}
|
|
54
|
+
#${ROOT_ID} lol-uikit-flat-button { width: 125px !important; height: 32px !important; }
|
|
55
|
+
#${ROOT_ID} .perf-dodge-cost {
|
|
56
|
+
position: absolute !important; left: 50% !important; bottom: calc(100% + 4px) !important;
|
|
57
|
+
transform: translateX(-50%) !important; box-sizing: border-box !important;
|
|
58
|
+
max-width: 245px !important; padding: 3px 7px !important;
|
|
59
|
+
background: rgba(1, 10, 19, 0.88) !important; border: 1px solid #785a28 !important;
|
|
60
|
+
color: #cdbe91 !important; font-family: var(--font-body, "LoL Body", Arial, sans-serif) !important;
|
|
61
|
+
font-size: 10px !important; font-weight: 700 !important; letter-spacing: 0.3px !important;
|
|
62
|
+
line-height: 1.25 !important; text-align: center !important; white-space: nowrap !important;
|
|
63
|
+
pointer-events: none !important;
|
|
64
|
+
}
|
|
65
|
+
`;
|
|
66
|
+
|
|
67
|
+
let api = null;
|
|
68
|
+
let active = false;
|
|
69
|
+
let dodging = false;
|
|
70
|
+
let unsubscribes = [];
|
|
71
|
+
let unwatchers = [];
|
|
72
|
+
let removeStyle = null;
|
|
73
|
+
let resizeObserver = null;
|
|
74
|
+
let mountFrame = 0;
|
|
75
|
+
let positionFrame = 0;
|
|
76
|
+
let networkWatch = null;
|
|
77
|
+
let penaltyToken = 0;
|
|
78
|
+
let pendingWriteAt = 0;
|
|
79
|
+
let accountPromise = null;
|
|
80
|
+
const state = {
|
|
81
|
+
phase: null,
|
|
82
|
+
queueId: 0,
|
|
83
|
+
isCustom: false,
|
|
84
|
+
showQuitButton: false,
|
|
85
|
+
gameMode: "",
|
|
86
|
+
account: null,
|
|
87
|
+
penalty: null,
|
|
88
|
+
lastSelfDodgeAt: 0,
|
|
89
|
+
selfDodgeRecorded: false,
|
|
90
|
+
lockedLocalPick: false,
|
|
91
|
+
localActionSeen: false,
|
|
92
|
+
localActionActive: false,
|
|
93
|
+
localActionDeadline: 0,
|
|
94
|
+
pageHiding: false,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
function unwrap(raw) {
|
|
98
|
+
if (typeof raw === "string") {
|
|
99
|
+
const text = raw.trim();
|
|
100
|
+
if (text.startsWith("{") || text.startsWith("[")) {
|
|
101
|
+
try {
|
|
102
|
+
return unwrap(JSON.parse(text));
|
|
103
|
+
} catch {
|
|
104
|
+
return raw;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return raw;
|
|
108
|
+
}
|
|
109
|
+
if (Array.isArray(raw) && raw.length >= 3 && typeof raw[1] === "string") {
|
|
110
|
+
return unwrap(raw[2]?.data ?? raw[2]);
|
|
111
|
+
}
|
|
112
|
+
if (raw && typeof raw === "object" && "data" in raw) {
|
|
113
|
+
const keys = Object.keys(raw);
|
|
114
|
+
if (keys.includes("uri") || keys.includes("eventType") || keys.length <= 2) return unwrap(raw.data);
|
|
115
|
+
}
|
|
116
|
+
return raw;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function readStore() {
|
|
120
|
+
try {
|
|
121
|
+
const parsed = JSON.parse(localStorage.getItem(STORE_KEY) || "{}");
|
|
122
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
123
|
+
} catch {
|
|
124
|
+
return {};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function writeStore(store) {
|
|
129
|
+
try {
|
|
130
|
+
localStorage.setItem(STORE_KEY, JSON.stringify(store));
|
|
131
|
+
} catch {}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function readPending() {
|
|
135
|
+
try {
|
|
136
|
+
const parsed = JSON.parse(localStorage.getItem(PENDING_KEY) || "null");
|
|
137
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
138
|
+
} catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function writePending(pending) {
|
|
144
|
+
try {
|
|
145
|
+
localStorage.setItem(PENDING_KEY, JSON.stringify(pending));
|
|
146
|
+
} catch {}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function clearPending() {
|
|
150
|
+
pendingWriteAt = 0;
|
|
151
|
+
try {
|
|
152
|
+
localStorage.removeItem(PENDING_KEY);
|
|
153
|
+
} catch {}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function accountKey() {
|
|
157
|
+
const account = state.account;
|
|
158
|
+
if (!account) return null;
|
|
159
|
+
if (account.puuid) return account.puuid;
|
|
160
|
+
if (account.summonerId) return `sid:${account.summonerId}`;
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function currentTier() {
|
|
165
|
+
const key = accountKey();
|
|
166
|
+
if (!key) return 0;
|
|
167
|
+
const record = readStore()[key];
|
|
168
|
+
if (!record || typeof record.tier !== "number" || typeof record.at !== "number") return 0;
|
|
169
|
+
const drops = Math.max(0, Math.floor((Date.now() - record.at) / DAY_MS));
|
|
170
|
+
return Math.max(0, Math.min(record.tier - drops, 3));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function loadAccount() {
|
|
174
|
+
if (state.account) return state.account;
|
|
175
|
+
if (!accountPromise) {
|
|
176
|
+
accountPromise = api.lcu
|
|
177
|
+
.get(SUMMONER_URI)
|
|
178
|
+
.then((summoner) => {
|
|
179
|
+
if (!active) return null;
|
|
180
|
+
if (summoner) {
|
|
181
|
+
state.account = { puuid: summoner.puuid || "", summonerId: summoner.summonerId || 0 };
|
|
182
|
+
}
|
|
183
|
+
return state.account;
|
|
184
|
+
})
|
|
185
|
+
.catch(() => null)
|
|
186
|
+
.finally(() => {
|
|
187
|
+
accountPromise = null;
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
return accountPromise;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function isCustomQueue(queueId = state.queueId) {
|
|
194
|
+
return state.isCustom || state.gameMode === "PRACTICETOOL" || Number(queueId) === 0;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function isAramQueue(queueId = state.queueId) {
|
|
198
|
+
const id = Number(queueId);
|
|
199
|
+
if (RANKED_QUEUES.has(id)) return false;
|
|
200
|
+
return ARAM_QUEUES.has(id) || /ARAM/i.test(state.gameMode);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function isGamePhase(phase) {
|
|
204
|
+
return GAME_PHASES.has(phase);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function recordDodge(options = {}) {
|
|
208
|
+
if (!active) return false;
|
|
209
|
+
const queueId = Number(options.queueId ?? state.queueId) || 0;
|
|
210
|
+
if (!options.force && isCustomQueue(queueId)) return false;
|
|
211
|
+
const now = Date.now();
|
|
212
|
+
if (now - state.lastSelfDodgeAt < 1500) return false;
|
|
213
|
+
state.lastSelfDodgeAt = now;
|
|
214
|
+
await loadAccount();
|
|
215
|
+
const key = accountKey();
|
|
216
|
+
if (!key) return false;
|
|
217
|
+
const store = readStore();
|
|
218
|
+
const tier = Math.min(currentTier() + 1, 3);
|
|
219
|
+
store[key] = { tier, at: now };
|
|
220
|
+
writeStore(store);
|
|
221
|
+
clearPending();
|
|
222
|
+
state.selfDodgeRecorded = true;
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function lockoutText(minutes) {
|
|
227
|
+
if (minutes >= 720) return "12 hr queue";
|
|
228
|
+
if (minutes >= 30) return "30 min queue";
|
|
229
|
+
return `${minutes} min queue`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function computePenalty(queueId = state.queueId) {
|
|
233
|
+
const tier = Math.min(currentTier() + 1, 3);
|
|
234
|
+
const aram = isAramQueue(queueId);
|
|
235
|
+
const lockout = aram && tier === 1 ? 15 : PENALTY_TIERS[tier].lockout;
|
|
236
|
+
const rankedKey = RANKED_QUEUES.get(Number(queueId));
|
|
237
|
+
if (!rankedKey) {
|
|
238
|
+
return Promise.resolve({ ranked: false, tier, lp: 0, lockout, apex: false, aram });
|
|
239
|
+
}
|
|
240
|
+
return api.lcu
|
|
241
|
+
.get(RANKED_STATS_URI)
|
|
242
|
+
.catch(() => null)
|
|
243
|
+
.then((stats) => {
|
|
244
|
+
const tierName = String(stats?.queueMap?.[rankedKey]?.tier ?? "").toUpperCase();
|
|
245
|
+
const rated = Boolean(tierName) && tierName !== "UNRANKED";
|
|
246
|
+
const apex = rated && APEX_TIERS.has(tierName);
|
|
247
|
+
return {
|
|
248
|
+
ranked: true,
|
|
249
|
+
rated,
|
|
250
|
+
tier,
|
|
251
|
+
lp: rated ? (apex && tier === 1 ? 20 : PENALTY_TIERS[tier].lp) : 0,
|
|
252
|
+
lockout,
|
|
253
|
+
apex,
|
|
254
|
+
aram,
|
|
255
|
+
};
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function penaltyLabel(penalty) {
|
|
260
|
+
if (!penalty) return "";
|
|
261
|
+
const parts = [];
|
|
262
|
+
if (penalty.ranked) parts.push(penalty.lp > 0 ? `\u2212${penalty.lp} LP` : "No LP");
|
|
263
|
+
else parts.push("No LP");
|
|
264
|
+
parts.push(lockoutText(penalty.lockout));
|
|
265
|
+
if (penalty.apex) parts.push("MMR impact");
|
|
266
|
+
return parts.join(" \u00b7 ");
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function penaltyTitle(penalty) {
|
|
270
|
+
const lines = [
|
|
271
|
+
"Queue dodge penalty",
|
|
272
|
+
"1st dodge: -5 LP and a 6-minute queue lockout.",
|
|
273
|
+
"2nd dodge: -15 LP and a 30-minute queue lockout.",
|
|
274
|
+
"3rd+ dodge: -15 LP and a 12-hour queue lockout.",
|
|
275
|
+
"Apex (Master+): the first dodge costs 20 LP instead of 5, and Apex dodges affect MMR.",
|
|
276
|
+
"Your dodge tier drops one step 24 hours after your last dodge.",
|
|
277
|
+
];
|
|
278
|
+
if (penalty) {
|
|
279
|
+
lines.unshift(`Next dodge (tier ${penalty.tier}): ${penaltyLabel(penalty)}.`);
|
|
280
|
+
if (!penalty.ranked) lines.push("No LP is lost outside ranked queues; the queue lockout still applies.");
|
|
281
|
+
if (penalty.aram) lines.push("ARAM: the first dodge has a 15-minute queue lockout.");
|
|
282
|
+
if (penalty.ranked && !penalty.rated) lines.push("This ranked queue is still unranked, so no LP is lost.");
|
|
283
|
+
}
|
|
284
|
+
return lines.join("\n");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function render() {
|
|
288
|
+
const button = document.getElementById(BUTTON_ID);
|
|
289
|
+
const cost = document.getElementById(COST_ID);
|
|
290
|
+
if (!button || !cost) return;
|
|
291
|
+
const ranked = RANKED_QUEUES.has(Number(state.queueId));
|
|
292
|
+
const label = ranked ? "DODGE" : "QUIT";
|
|
293
|
+
if (button.textContent !== label) button.textContent = label;
|
|
294
|
+
button.disabled = dodging;
|
|
295
|
+
const penalty = state.penalty;
|
|
296
|
+
const text = penaltyLabel(penalty);
|
|
297
|
+
if (cost.textContent !== text) cost.textContent = text;
|
|
298
|
+
cost.style.display = text ? "" : "none";
|
|
299
|
+
const title = penalty ? penaltyTitle(penalty) : "Loading the current dodge penalty...";
|
|
300
|
+
button.title = title;
|
|
301
|
+
cost.title = title;
|
|
302
|
+
button.setAttribute("aria-label", `${label}. ${title}`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async function refreshPenalty() {
|
|
306
|
+
if (!active || state.phase !== "ChampSelect" || !state.queueId || isCustomQueue() || state.showQuitButton) return;
|
|
307
|
+
const queueId = state.queueId;
|
|
308
|
+
const token = ++penaltyToken;
|
|
309
|
+
const penalty = await computePenalty(queueId);
|
|
310
|
+
if (!active || token !== penaltyToken || queueId !== state.queueId) return;
|
|
311
|
+
state.penalty = penalty;
|
|
312
|
+
render();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function applySession(raw) {
|
|
316
|
+
const session = unwrap(raw);
|
|
317
|
+
if (!session) return;
|
|
318
|
+
const queue = session.gameData?.queue;
|
|
319
|
+
if (queue) {
|
|
320
|
+
const queueId = Number(queue.id ?? 0) || 0;
|
|
321
|
+
const isCustom = Boolean(session.gameData?.isCustomGame || queue.isCustom);
|
|
322
|
+
const gameMode = String(queue.gameMode ?? "");
|
|
323
|
+
if (queueId !== state.queueId || isCustom !== state.isCustom || gameMode !== state.gameMode) {
|
|
324
|
+
state.queueId = queueId;
|
|
325
|
+
state.isCustom = isCustom;
|
|
326
|
+
state.gameMode = gameMode;
|
|
327
|
+
state.penalty = null;
|
|
328
|
+
if (state.phase === "ChampSelect") refreshPenalty();
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
ensureMount();
|
|
332
|
+
syncWatching();
|
|
333
|
+
savePending();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function syncLocalPick(session) {
|
|
337
|
+
const cellId = Number(session?.localPlayerCellId);
|
|
338
|
+
if (!Number.isFinite(cellId)) return;
|
|
339
|
+
const actions = Array.isArray(session.actions) ? session.actions.flat() : [];
|
|
340
|
+
const picks = actions.filter((action) => Number(action?.actorCellId) === cellId && action?.type === "pick");
|
|
341
|
+
if (!picks.length) return;
|
|
342
|
+
const locked = picks.some((action) => action?.completed === true);
|
|
343
|
+
if (locked) {
|
|
344
|
+
state.lockedLocalPick = true;
|
|
345
|
+
state.localActionSeen = false;
|
|
346
|
+
state.localActionActive = false;
|
|
347
|
+
state.localActionDeadline = 0;
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
state.lockedLocalPick = false;
|
|
351
|
+
const active = picks.find((action) => action?.isInProgress === true);
|
|
352
|
+
state.localActionActive = Boolean(active);
|
|
353
|
+
if (active) {
|
|
354
|
+
state.localActionSeen = true;
|
|
355
|
+
const timeLeft = Number(session?.timer?.timeLeft);
|
|
356
|
+
if (Number.isFinite(timeLeft)) {
|
|
357
|
+
state.localActionDeadline = Date.now() + Math.max(0, timeLeft) * 1000;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function syncTimer(raw) {
|
|
363
|
+
const timer = unwrap(raw);
|
|
364
|
+
if (!timer || typeof timer !== "object") return;
|
|
365
|
+
if (!state.localActionActive || state.lockedLocalPick) return;
|
|
366
|
+
const timeLeft = Number(timer.timeLeft);
|
|
367
|
+
if (!Number.isFinite(timeLeft) || timeLeft < 0) return;
|
|
368
|
+
state.localActionSeen = true;
|
|
369
|
+
state.localActionDeadline = Date.now() + timeLeft * 1000;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function applyChampSelect(raw) {
|
|
373
|
+
const session = unwrap(raw);
|
|
374
|
+
if (!session) return;
|
|
375
|
+
state.showQuitButton = Boolean(session.showQuitButton);
|
|
376
|
+
const queueId = Number(session.queueId ?? 0) || 0;
|
|
377
|
+
if (queueId > 0 && queueId !== state.queueId) {
|
|
378
|
+
state.queueId = queueId;
|
|
379
|
+
state.penalty = null;
|
|
380
|
+
}
|
|
381
|
+
if (typeof session.isCustomGame === "boolean") state.isCustom = session.isCustomGame;
|
|
382
|
+
syncLocalPick(session);
|
|
383
|
+
if (state.localActionActive && !state.localActionDeadline) {
|
|
384
|
+
api.lcu
|
|
385
|
+
.get(TIMER_URI)
|
|
386
|
+
.catch(() => null)
|
|
387
|
+
.then((timer) => {
|
|
388
|
+
if (active && timer) syncTimer(timer);
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
ensureMount();
|
|
392
|
+
syncWatching();
|
|
393
|
+
savePending();
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function build() {
|
|
397
|
+
const root = document.createElement("div");
|
|
398
|
+
root.id = ROOT_ID;
|
|
399
|
+
const cost = document.createElement("div");
|
|
400
|
+
cost.id = COST_ID;
|
|
401
|
+
cost.className = "perf-dodge-cost";
|
|
402
|
+
const button = document.createElement("lol-uikit-flat-button");
|
|
403
|
+
button.id = BUTTON_ID;
|
|
404
|
+
bindButton(button);
|
|
405
|
+
root.append(cost, button);
|
|
406
|
+
return root;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function bindButton(button) {
|
|
410
|
+
if (!button) return;
|
|
411
|
+
if (!button.__perfClickBound) {
|
|
412
|
+
button.__perfClickBound = true;
|
|
413
|
+
button.addEventListener("click", onButtonClick, true);
|
|
414
|
+
}
|
|
415
|
+
const inner = button.shadowRoot?.querySelector(".lol-uikit-flat-button");
|
|
416
|
+
if (inner && !inner.__perfClickBound) {
|
|
417
|
+
inner.__perfClickBound = true;
|
|
418
|
+
inner.addEventListener("click", onButtonClick);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function onButtonClick(event) {
|
|
423
|
+
event.preventDefault();
|
|
424
|
+
event.stopPropagation();
|
|
425
|
+
quitOrDodge();
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function ensureLayer() {
|
|
429
|
+
let layer = document.getElementById(LAYER_ID);
|
|
430
|
+
if (!layer) {
|
|
431
|
+
layer = document.createElement("div");
|
|
432
|
+
layer.id = LAYER_ID;
|
|
433
|
+
document.body.append(layer);
|
|
434
|
+
}
|
|
435
|
+
return layer;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function ensureMount() {
|
|
439
|
+
if (state.phase !== "ChampSelect" || isCustomQueue() || state.showQuitButton) {
|
|
440
|
+
unmount();
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
const layer = ensureLayer();
|
|
444
|
+
let root = document.getElementById(ROOT_ID);
|
|
445
|
+
const created = !root;
|
|
446
|
+
if (!root) root = build();
|
|
447
|
+
if (root.parentElement !== layer) layer.append(root);
|
|
448
|
+
bindButton(document.getElementById(BUTTON_ID));
|
|
449
|
+
render();
|
|
450
|
+
schedulePosition();
|
|
451
|
+
if (created) observeLayout();
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function unmount() {
|
|
455
|
+
document.getElementById(ROOT_ID)?.remove();
|
|
456
|
+
document.getElementById(LAYER_ID)?.remove();
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function visibleFooter() {
|
|
460
|
+
const footer = document.querySelector(FOOTER_SELECTOR);
|
|
461
|
+
if (!footer) return null;
|
|
462
|
+
const rect = footer.getBoundingClientRect();
|
|
463
|
+
if (rect.width < 20 || rect.height < 10) return null;
|
|
464
|
+
if (rect.top < window.innerHeight * 0.5) return null;
|
|
465
|
+
return rect;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function positionRoot() {
|
|
469
|
+
positionFrame = 0;
|
|
470
|
+
const root = document.getElementById(ROOT_ID);
|
|
471
|
+
if (!root) return;
|
|
472
|
+
const footer = visibleFooter();
|
|
473
|
+
const width = root.offsetWidth || 125;
|
|
474
|
+
const height = root.offsetHeight || 32;
|
|
475
|
+
if (footer && footer.left > width + 24) {
|
|
476
|
+
root.style.left = `${Math.round(footer.left - width - 10)}px`;
|
|
477
|
+
root.style.top = `${Math.round(Math.min(window.innerHeight - height - 8, Math.max(8, footer.bottom - height)))}px`;
|
|
478
|
+
root.style.right = "auto";
|
|
479
|
+
root.style.bottom = "auto";
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
root.style.left = "auto";
|
|
483
|
+
root.style.top = "auto";
|
|
484
|
+
root.style.right = "145px";
|
|
485
|
+
root.style.bottom = "21px";
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function schedulePosition() {
|
|
489
|
+
if (positionFrame) return;
|
|
490
|
+
positionFrame = requestAnimationFrame(positionRoot);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function scheduleMount() {
|
|
494
|
+
if (mountFrame) return;
|
|
495
|
+
mountFrame = requestAnimationFrame(() => {
|
|
496
|
+
mountFrame = 0;
|
|
497
|
+
ensureMount();
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function observeLayout() {
|
|
502
|
+
if (typeof ResizeObserver !== "function" || !resizeObserver) return;
|
|
503
|
+
resizeObserver.disconnect();
|
|
504
|
+
const footer = document.querySelector(FOOTER_SELECTOR);
|
|
505
|
+
if (footer) resizeObserver.observe(footer);
|
|
506
|
+
const screen = document.querySelector(SCREEN_SELECTOR);
|
|
507
|
+
if (screen) resizeObserver.observe(screen);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function onWindowResize() {
|
|
511
|
+
schedulePosition();
|
|
512
|
+
observeLayout();
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function onPageHide() {
|
|
516
|
+
if (!active || state.phase !== "ChampSelect" || isCustomQueue()) return;
|
|
517
|
+
state.pageHiding = true;
|
|
518
|
+
savePending(true);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function syncWatching() {
|
|
522
|
+
if (state.phase === "ChampSelect" && !isCustomQueue()) startDodgeWatcher();
|
|
523
|
+
else removeDodgeWatcher();
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function isDodgeUrl(url) {
|
|
527
|
+
return /(?:lol-gameflow\/v1\/session\/dodge|lol-(?:champ-select|lobby-team-builder)\/.*\/session\/quit)(?:\?|$)/i.test(
|
|
528
|
+
String(url),
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function startDodgeWatcher() {
|
|
533
|
+
if (networkWatch) return;
|
|
534
|
+
const originalFetch = window.fetch;
|
|
535
|
+
const originalOpen = XMLHttpRequest.prototype.open;
|
|
536
|
+
const originalSend = XMLHttpRequest.prototype.send;
|
|
537
|
+
|
|
538
|
+
const ourFetch = function (...args) {
|
|
539
|
+
const url = String(args[0]?.url ?? args[0] ?? "");
|
|
540
|
+
const method = String(args[1]?.method ?? args[0]?.method ?? "GET").toUpperCase();
|
|
541
|
+
const promise = originalFetch.apply(this, args);
|
|
542
|
+
if (method === "POST" && isDodgeUrl(url)) {
|
|
543
|
+
promise
|
|
544
|
+
.then((response) => {
|
|
545
|
+
if (response.ok) recordDodge();
|
|
546
|
+
})
|
|
547
|
+
.catch(() => {});
|
|
548
|
+
}
|
|
549
|
+
return promise;
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
const ourOpen = function (method, url, ...rest) {
|
|
553
|
+
this.__perfDodgeUrl = String(url);
|
|
554
|
+
this.__perfDodgeMethod = String(method ?? "GET").toUpperCase();
|
|
555
|
+
return originalOpen.call(this, method, url, ...rest);
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
const ourSend = function (body) {
|
|
559
|
+
if (this.__perfDodgeMethod === "POST" && isDodgeUrl(this.__perfDodgeUrl)) {
|
|
560
|
+
this.addEventListener("load", () => {
|
|
561
|
+
if (this.status >= 200 && this.status < 300) recordDodge();
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
return originalSend.call(this, body);
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
window.fetch = ourFetch;
|
|
568
|
+
XMLHttpRequest.prototype.open = ourOpen;
|
|
569
|
+
XMLHttpRequest.prototype.send = ourSend;
|
|
570
|
+
|
|
571
|
+
networkWatch = {
|
|
572
|
+
restore() {
|
|
573
|
+
if (window.fetch === ourFetch) window.fetch = originalFetch;
|
|
574
|
+
if (XMLHttpRequest.prototype.open === ourOpen) XMLHttpRequest.prototype.open = originalOpen;
|
|
575
|
+
if (XMLHttpRequest.prototype.send === ourSend) XMLHttpRequest.prototype.send = originalSend;
|
|
576
|
+
},
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function removeDodgeWatcher() {
|
|
581
|
+
networkWatch?.restore();
|
|
582
|
+
networkWatch = null;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function savePending(closed = false) {
|
|
586
|
+
if (!active || state.phase !== "ChampSelect" || isCustomQueue()) return;
|
|
587
|
+
const now = Date.now();
|
|
588
|
+
if (!closed && now - pendingWriteAt < 1000) return;
|
|
589
|
+
pendingWriteAt = now;
|
|
590
|
+
writePending({
|
|
591
|
+
key: accountKey(),
|
|
592
|
+
seenAt: now,
|
|
593
|
+
queueId: state.queueId,
|
|
594
|
+
locked: state.lockedLocalPick === true,
|
|
595
|
+
closed: Boolean(closed),
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
async function reconcilePending(phase) {
|
|
600
|
+
const pending = readPending();
|
|
601
|
+
if (!pending) return;
|
|
602
|
+
if (phase === "ChampSelect" || isGamePhase(phase)) {
|
|
603
|
+
clearPending();
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
const age = Date.now() - Number(pending.seenAt || 0);
|
|
607
|
+
if (!Number.isFinite(age) || age < 0 || age > PENDING_MAX_AGE_MS) {
|
|
608
|
+
clearPending();
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
const key = accountKey();
|
|
612
|
+
if (pending.key && key && pending.key !== key) {
|
|
613
|
+
clearPending();
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
if (pending.closed === true && pending.locked !== true) {
|
|
617
|
+
await recordDodge({ force: true, queueId: Number(pending.queueId) || 0 });
|
|
618
|
+
}
|
|
619
|
+
clearPending();
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
async function handleChampSelectEnd(nextPhase) {
|
|
623
|
+
if (isGamePhase(nextPhase)) {
|
|
624
|
+
clearPending();
|
|
625
|
+
resetSession();
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
if (!isCustomQueue() && !state.selfDodgeRecorded && !state.lockedLocalPick) {
|
|
629
|
+
const timedOut =
|
|
630
|
+
state.localActionSeen &&
|
|
631
|
+
state.localActionDeadline > 0 &&
|
|
632
|
+
Date.now() >= state.localActionDeadline - 900;
|
|
633
|
+
if (timedOut) await recordDodge();
|
|
634
|
+
}
|
|
635
|
+
clearPending();
|
|
636
|
+
resetSession();
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function resetSession() {
|
|
640
|
+
state.lockedLocalPick = false;
|
|
641
|
+
state.localActionSeen = false;
|
|
642
|
+
state.localActionActive = false;
|
|
643
|
+
state.localActionDeadline = 0;
|
|
644
|
+
state.penalty = null;
|
|
645
|
+
penaltyToken += 1;
|
|
646
|
+
render();
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
async function handlePhase(raw) {
|
|
650
|
+
if (!active) return;
|
|
651
|
+
const previous = state.phase;
|
|
652
|
+
const phase = typeof raw === "string" ? raw : unwrap(raw);
|
|
653
|
+
if (typeof phase !== "string" || !phase) return;
|
|
654
|
+
state.phase = phase;
|
|
655
|
+
|
|
656
|
+
if (previous === "ChampSelect" && phase !== "ChampSelect") {
|
|
657
|
+
await handleChampSelectEnd(phase);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
if (phase === "ChampSelect") {
|
|
661
|
+
if (previous !== "ChampSelect") {
|
|
662
|
+
state.selfDodgeRecorded = false;
|
|
663
|
+
state.pageHiding = false;
|
|
664
|
+
resetSession();
|
|
665
|
+
}
|
|
666
|
+
await loadAccount();
|
|
667
|
+
const [session, champSelect, timer] = await Promise.all([
|
|
668
|
+
api.lcu.get(SESSION_URI).catch(() => null),
|
|
669
|
+
api.lcu.get(CHAMP_SELECT_URI).catch(() => null),
|
|
670
|
+
api.lcu.get(TIMER_URI).catch(() => null),
|
|
671
|
+
]);
|
|
672
|
+
if (!active) return;
|
|
673
|
+
if (session) applySession(session);
|
|
674
|
+
if (champSelect) applyChampSelect(champSelect);
|
|
675
|
+
if (timer) syncTimer(timer);
|
|
676
|
+
ensureMount();
|
|
677
|
+
refreshPenalty();
|
|
678
|
+
savePending();
|
|
679
|
+
} else {
|
|
680
|
+
syncWatching();
|
|
681
|
+
unmount();
|
|
682
|
+
state.penalty = null;
|
|
683
|
+
state.showQuitButton = false;
|
|
684
|
+
if (isGamePhase(phase) || phase === "Lobby" || phase === "None") clearPending();
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function handleSession(raw) {
|
|
689
|
+
applySession(raw);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function handleChampSelect(raw) {
|
|
693
|
+
applyChampSelect(raw);
|
|
694
|
+
if (state.phase === "ChampSelect") refreshPenalty();
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function handleTimer(raw) {
|
|
698
|
+
syncTimer(raw);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
async function quitOrDodge() {
|
|
702
|
+
if (!active || dodging || !state.queueId || isCustomQueue() || state.showQuitButton) return;
|
|
703
|
+
dodging = true;
|
|
704
|
+
render();
|
|
705
|
+
const ranked = RANKED_QUEUES.has(Number(state.queueId));
|
|
706
|
+
const uri = ranked ? DODGE_URI : QUIT_URI;
|
|
707
|
+
try {
|
|
708
|
+
await api.lcu.send("POST", uri);
|
|
709
|
+
await recordDodge();
|
|
710
|
+
} catch (error) {
|
|
711
|
+
console.warn("[Dodge Helper] quit failed:", error);
|
|
712
|
+
} finally {
|
|
713
|
+
dodging = false;
|
|
714
|
+
render();
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function clearLegacyAnchor() {
|
|
719
|
+
for (const element of document.querySelectorAll(".perf-dodge-anchor")) {
|
|
720
|
+
element.classList.remove("perf-dodge-anchor");
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
async function onEnable(utils) {
|
|
725
|
+
api = utils;
|
|
726
|
+
active = true;
|
|
727
|
+
unmount();
|
|
728
|
+
clearLegacyAnchor();
|
|
729
|
+
removeStyle = utils.addStyle(STYLE_ID, CSS);
|
|
730
|
+
resizeObserver = typeof ResizeObserver === "function" ? new ResizeObserver(schedulePosition) : null;
|
|
731
|
+
window.addEventListener("resize", onWindowResize);
|
|
732
|
+
window.addEventListener("pagehide", onPageHide);
|
|
733
|
+
window.addEventListener("beforeunload", onPageHide);
|
|
734
|
+
|
|
735
|
+
const handlers = {
|
|
736
|
+
[PHASE_URI]: handlePhase,
|
|
737
|
+
[SESSION_URI]: handleSession,
|
|
738
|
+
[CHAMP_SELECT_URI]: handleChampSelect,
|
|
739
|
+
[TIMER_URI]: handleTimer,
|
|
740
|
+
};
|
|
741
|
+
for (const [uri, handler] of Object.entries(handlers)) {
|
|
742
|
+
try {
|
|
743
|
+
const off = utils.subscribe(uri, handler);
|
|
744
|
+
if (off) unsubscribes.push(off);
|
|
745
|
+
} catch {}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
for (const selector of [FOOTER_SELECTOR, SCREEN_SELECTOR]) {
|
|
749
|
+
try {
|
|
750
|
+
const off = utils.watch(selector, () => {
|
|
751
|
+
observeLayout();
|
|
752
|
+
scheduleMount();
|
|
753
|
+
});
|
|
754
|
+
if (off) unwatchers.push(off);
|
|
755
|
+
} catch {}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
await loadAccount();
|
|
759
|
+
const phase = unwrap(await utils.lcu.get(PHASE_URI).catch(() => null));
|
|
760
|
+
if (!active || typeof phase !== "string" || !phase) return;
|
|
761
|
+
await reconcilePending(phase);
|
|
762
|
+
if (active) await handlePhase(phase);
|
|
763
|
+
observeLayout();
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function onDisable() {
|
|
767
|
+
active = false;
|
|
768
|
+
for (const off of unsubscribes) {
|
|
769
|
+
try {
|
|
770
|
+
off();
|
|
771
|
+
} catch {}
|
|
772
|
+
}
|
|
773
|
+
unsubscribes = [];
|
|
774
|
+
for (const off of unwatchers) {
|
|
775
|
+
try {
|
|
776
|
+
off();
|
|
777
|
+
} catch {}
|
|
778
|
+
}
|
|
779
|
+
unwatchers = [];
|
|
780
|
+
window.removeEventListener("resize", onWindowResize);
|
|
781
|
+
window.removeEventListener("pagehide", onPageHide);
|
|
782
|
+
window.removeEventListener("beforeunload", onPageHide);
|
|
783
|
+
resizeObserver?.disconnect();
|
|
784
|
+
resizeObserver = null;
|
|
785
|
+
cancelAnimationFrame(mountFrame);
|
|
786
|
+
mountFrame = 0;
|
|
787
|
+
cancelAnimationFrame(positionFrame);
|
|
788
|
+
positionFrame = 0;
|
|
789
|
+
removeDodgeWatcher();
|
|
790
|
+
unmount();
|
|
791
|
+
if (!state.pageHiding) clearPending();
|
|
792
|
+
removeStyle?.();
|
|
793
|
+
removeStyle = null;
|
|
794
|
+
state.phase = null;
|
|
795
|
+
state.queueId = 0;
|
|
796
|
+
state.isCustom = false;
|
|
797
|
+
state.showQuitButton = false;
|
|
798
|
+
state.gameMode = "";
|
|
799
|
+
state.account = null;
|
|
800
|
+
state.penalty = null;
|
|
801
|
+
state.lastSelfDodgeAt = 0;
|
|
802
|
+
state.selfDodgeRecorded = false;
|
|
803
|
+
state.lockedLocalPick = false;
|
|
804
|
+
state.localActionSeen = false;
|
|
805
|
+
state.localActionActive = false;
|
|
806
|
+
state.localActionDeadline = 0;
|
|
807
|
+
state.pageHiding = false;
|
|
808
|
+
dodging = false;
|
|
809
|
+
pendingWriteAt = 0;
|
|
810
|
+
penaltyToken = 0;
|
|
811
|
+
accountPromise = null;
|
|
812
|
+
api = null;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
export function init() {
|
|
816
|
+
const registerWith = (utils) => {
|
|
817
|
+
utils.registerPlugin({
|
|
818
|
+
id: PLUGIN_ID,
|
|
819
|
+
name: "Dodge Helper",
|
|
820
|
+
description:
|
|
821
|
+
"Adds a DODGE/QUIT button to champion select, showing the LP and queue penalty for the next dodge. It also counts accidental dodges from a missed lock-in or closing the client.",
|
|
822
|
+
version,
|
|
823
|
+
onEnable,
|
|
824
|
+
onDisable,
|
|
825
|
+
});
|
|
826
|
+
};
|
|
827
|
+
|
|
828
|
+
if (window.PerfectionistUtils) {
|
|
829
|
+
registerWith(window.PerfectionistUtils);
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
addEventListener("perfectionist:utils-ready", (event) => registerWith(event.detail ?? window.PerfectionistUtils), {
|
|
833
|
+
once: true,
|
|
834
|
+
});
|
|
835
|
+
}
|