perfectionist-plugins 1.0.0 → 1.0.1
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/ban-blocker.js +552 -617
- package/hide-unowned-skins.js +578 -640
- package/package.json +1 -1
- package/perfectionist-utils.js +754 -819
package/ban-blocker.js
CHANGED
|
@@ -1,617 +1,552 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
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
|
-
let
|
|
72
|
-
let
|
|
73
|
-
let
|
|
74
|
-
let
|
|
75
|
-
|
|
76
|
-
let
|
|
77
|
-
let
|
|
78
|
-
let
|
|
79
|
-
let
|
|
80
|
-
let
|
|
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
|
-
function
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function
|
|
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
|
-
const
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
if (
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
for (const
|
|
339
|
-
|
|
340
|
-
(
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
if (
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
btn.
|
|
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
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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
|
-
|
|
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
|
-
try {
|
|
554
|
-
off();
|
|
555
|
-
} catch (err) {
|
|
556
|
-
logError("unsubscribe failed.", err);
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
};
|
|
560
|
-
|
|
561
|
-
poll();
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
function onDisable() {
|
|
565
|
-
unsubscribe?.();
|
|
566
|
-
unsubscribe = null;
|
|
567
|
-
clearInterval(pollTimer);
|
|
568
|
-
clearInterval(ensureTimer);
|
|
569
|
-
clearTimeout(refreshTimer);
|
|
570
|
-
clearTimeout(detectTimer);
|
|
571
|
-
clearTimeout(pumpTimer);
|
|
572
|
-
pollTimer = ensureTimer = refreshTimer = detectTimer = pumpTimer = null;
|
|
573
|
-
observer?.disconnect();
|
|
574
|
-
observer = null;
|
|
575
|
-
|
|
576
|
-
uiVisible = false;
|
|
577
|
-
goneAway = true;
|
|
578
|
-
target = undefined;
|
|
579
|
-
sent = 0;
|
|
580
|
-
failedUntil.clear();
|
|
581
|
-
failCount.clear();
|
|
582
|
-
cellToPlayer.clear();
|
|
583
|
-
enabled.clear();
|
|
584
|
-
playerNames.clear();
|
|
585
|
-
lastSession = null;
|
|
586
|
-
rows = [];
|
|
587
|
-
removeButtons();
|
|
588
|
-
|
|
589
|
-
removeStyle?.();
|
|
590
|
-
removeStyle = null;
|
|
591
|
-
api = null;
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
/** Pengu entry point. Registers with the Perfectionism core. */
|
|
595
|
-
export function init() {
|
|
596
|
-
const registerWith = (utils) => {
|
|
597
|
-
utils.registerPlugin({
|
|
598
|
-
id: PLUGIN_ID,
|
|
599
|
-
name: "Ban Blocker",
|
|
600
|
-
description: "During bans, blocking a teammate instantly claims the champion they hover to ban.",
|
|
601
|
-
version,
|
|
602
|
-
defaultEnabled: true,
|
|
603
|
-
onEnable,
|
|
604
|
-
onDisable,
|
|
605
|
-
});
|
|
606
|
-
};
|
|
607
|
-
|
|
608
|
-
if (window.PerfectionistUtils) {
|
|
609
|
-
registerWith(window.PerfectionistUtils);
|
|
610
|
-
return;
|
|
611
|
-
}
|
|
612
|
-
addEventListener(
|
|
613
|
-
"perfectionist:utils-ready",
|
|
614
|
-
(e) => registerWith(e.detail ?? window.PerfectionistUtils),
|
|
615
|
-
{ once: true },
|
|
616
|
-
);
|
|
617
|
-
}
|
|
1
|
+
export const version = "3.4.0";
|
|
2
|
+
|
|
3
|
+
const PLUGIN_ID = "ban-blocker";
|
|
4
|
+
const STYLE_ID = "perfectionist-ban-blocker-style";
|
|
5
|
+
const BTN_CLASS = "bb-toggle";
|
|
6
|
+
const SESSION_URI = "/lol-champ-select/v1/session";
|
|
7
|
+
|
|
8
|
+
const RESEND_AFTER_MS = 150;
|
|
9
|
+
const RESCHEDULE_MS = RESEND_AFTER_MS + 20;
|
|
10
|
+
const MAX_IN_FLIGHT = 3;
|
|
11
|
+
const FAIL_COOLDOWN_MS = 1200;
|
|
12
|
+
const FAIL_COOLDOWN_MAX_MS = 30000;
|
|
13
|
+
const POLL_MS = 40;
|
|
14
|
+
const ENSURE_MS = 300;
|
|
15
|
+
const DETECT_RETRY_MS = 2000;
|
|
16
|
+
|
|
17
|
+
const BAN_GLYPH_SVG =
|
|
18
|
+
"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'>" +
|
|
19
|
+
"<g fill='none' stroke='white' stroke-width='2.4'>" +
|
|
20
|
+
"<circle cx='10' cy='10' r='7'/><path d='M5.1 14.9 14.9 5.1'/></g></svg>";
|
|
21
|
+
const BAN_GLYPH = `data:image/svg+xml,${encodeURIComponent(BAN_GLYPH_SVG)}`;
|
|
22
|
+
|
|
23
|
+
const CSS = `
|
|
24
|
+
.swap-button-component.${BTN_CLASS} {
|
|
25
|
+
position: absolute !important;
|
|
26
|
+
top: -2px !important;
|
|
27
|
+
right: -2px !important;
|
|
28
|
+
left: auto !important;
|
|
29
|
+
bottom: auto !important;
|
|
30
|
+
z-index: 2 !important;
|
|
31
|
+
}
|
|
32
|
+
.swap-button-component.${BTN_CLASS} .icon {
|
|
33
|
+
-webkit-mask-image: url("${BAN_GLYPH}") !important;
|
|
34
|
+
mask-image: url("${BAN_GLYPH}") !important;
|
|
35
|
+
-webkit-mask-size: 70% 70% !important;
|
|
36
|
+
mask-size: 70% 70% !important;
|
|
37
|
+
}
|
|
38
|
+
.swap-button-component.${BTN_CLASS}.on .frame-color {
|
|
39
|
+
background-color: rgb(200, 170, 110) !important;
|
|
40
|
+
}
|
|
41
|
+
.swap-button-component.${BTN_CLASS}.on .icon {
|
|
42
|
+
background-color: rgb(1, 10, 19) !important;
|
|
43
|
+
}
|
|
44
|
+
.swap-button-component.${BTN_CLASS}.on .frame-color:hover {
|
|
45
|
+
background-color: rgb(240, 230, 210) !important;
|
|
46
|
+
}
|
|
47
|
+
.swap-button-component.${BTN_CLASS}.unresolved {
|
|
48
|
+
opacity: 0.35 !important;
|
|
49
|
+
cursor: default !important;
|
|
50
|
+
}
|
|
51
|
+
`;
|
|
52
|
+
|
|
53
|
+
let api = null;
|
|
54
|
+
let removeStyle = null;
|
|
55
|
+
let unsubscribe = null;
|
|
56
|
+
let pollTimer = null;
|
|
57
|
+
let ensureTimer = null;
|
|
58
|
+
let detectTimer = null;
|
|
59
|
+
let refreshTimer = null;
|
|
60
|
+
let pumpTimer = null;
|
|
61
|
+
let observer = null;
|
|
62
|
+
let pollInFlight = false;
|
|
63
|
+
let uiScheduled = false;
|
|
64
|
+
|
|
65
|
+
const enabled = new Map();
|
|
66
|
+
const playerNames = new Map();
|
|
67
|
+
const cellToPlayer = new Map();
|
|
68
|
+
const buttons = new Map();
|
|
69
|
+
const failedUntil = new Map();
|
|
70
|
+
const failCount = new Map();
|
|
71
|
+
let rows = [];
|
|
72
|
+
let uiVisible = false;
|
|
73
|
+
let goneAway = true;
|
|
74
|
+
let lastSession = null;
|
|
75
|
+
|
|
76
|
+
let myActionId = null;
|
|
77
|
+
let target = undefined;
|
|
78
|
+
let sent = 0;
|
|
79
|
+
let sentAt = -Infinity;
|
|
80
|
+
let inFlight = 0;
|
|
81
|
+
|
|
82
|
+
function logError(scope, error) {
|
|
83
|
+
console.error(`[Ban Blocker] ${scope}`, error);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function norm(v) {
|
|
87
|
+
return String(v ?? "").toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function champOf(a) {
|
|
91
|
+
return a && a.championId > 0 ? a.championId : 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function isPicked(members, champ) {
|
|
95
|
+
if (!champ) return false;
|
|
96
|
+
return (members ?? []).some((p) => p.championId === champ && p.completed);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function isBlocked(id) {
|
|
100
|
+
return enabled.get(id) === true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function playerId(p) {
|
|
104
|
+
if (typeof p === "number") return p;
|
|
105
|
+
return p?.summonerId ?? p?.puuid ?? null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function banPending(s) {
|
|
109
|
+
const bans = (s?.actions?.flat?.() ?? []).filter((a) => a.type === "ban");
|
|
110
|
+
if (!bans.length) return true;
|
|
111
|
+
return bans.some((a) => !a.completed);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function pump() {
|
|
115
|
+
if (target === undefined || myActionId == null) return;
|
|
116
|
+
if (inFlight >= MAX_IN_FLIGHT) return;
|
|
117
|
+
if (sent === target && performance.now() - sentAt < RESEND_AFTER_MS) return;
|
|
118
|
+
fire(myActionId, target);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function fire(id, champ) {
|
|
122
|
+
const send = api?.lcu?.send;
|
|
123
|
+
if (!send || id == null) return;
|
|
124
|
+
sent = champ;
|
|
125
|
+
sentAt = performance.now();
|
|
126
|
+
inFlight++;
|
|
127
|
+
clearTimeout(pumpTimer);
|
|
128
|
+
pumpTimer = setTimeout(pump, RESCHEDULE_MS);
|
|
129
|
+
send("PATCH", `${SESSION_URI}/actions/${id}`, { championId: champ })
|
|
130
|
+
.then(() => {
|
|
131
|
+
failCount.delete(champ);
|
|
132
|
+
})
|
|
133
|
+
.catch(() => {
|
|
134
|
+
const n = failCount.get(champ) ?? 0;
|
|
135
|
+
failCount.set(champ, n + 1);
|
|
136
|
+
failedUntil.set(champ, performance.now() + Math.min(FAIL_COOLDOWN_MS * 2 ** n, FAIL_COOLDOWN_MAX_MS));
|
|
137
|
+
if (target === champ) target = undefined;
|
|
138
|
+
if (sent === champ) sent = 0;
|
|
139
|
+
})
|
|
140
|
+
.finally(() => {
|
|
141
|
+
inFlight--;
|
|
142
|
+
pump();
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function decide(s) {
|
|
147
|
+
const m = s?.myTeam ?? [];
|
|
148
|
+
const actions = s?.actions?.flat?.() ?? [];
|
|
149
|
+
const me = s?.localPlayerCellId;
|
|
150
|
+
|
|
151
|
+
cellToPlayer.clear();
|
|
152
|
+
for (const p of m) cellToPlayer.set(p.cellId, playerId(p));
|
|
153
|
+
|
|
154
|
+
let myPick = null;
|
|
155
|
+
for (const a of actions) {
|
|
156
|
+
if (a.type === "pick" && a.actorCellId === me && !a.completed) {
|
|
157
|
+
myPick = a;
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
myActionId = myPick ? myPick.id : null;
|
|
162
|
+
|
|
163
|
+
const blockedCells = new Set(m.filter((p) => isBlocked(playerId(p))).map((p) => p.cellId));
|
|
164
|
+
|
|
165
|
+
let threat = 0;
|
|
166
|
+
for (const a of actions) {
|
|
167
|
+
if (a.type !== "ban" || a.completed || a.actorCellId === me) continue;
|
|
168
|
+
if (!blockedCells.has(a.actorCellId)) continue;
|
|
169
|
+
const champ = champOf(a);
|
|
170
|
+
if (!champ || isPicked(m, champ)) continue;
|
|
171
|
+
if ((failedUntil.get(champ) ?? 0) > performance.now()) continue;
|
|
172
|
+
threat = champ;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (threat && champOf(myPick) === threat) {
|
|
176
|
+
target = undefined;
|
|
177
|
+
sent = threat;
|
|
178
|
+
} else if (threat) {
|
|
179
|
+
target = threat;
|
|
180
|
+
} else {
|
|
181
|
+
target = undefined;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function onSessionGone() {
|
|
186
|
+
sent = 0;
|
|
187
|
+
target = undefined;
|
|
188
|
+
clearTimeout(pumpTimer);
|
|
189
|
+
failedUntil.clear();
|
|
190
|
+
failCount.clear();
|
|
191
|
+
cellToPlayer.clear();
|
|
192
|
+
lastSession = null;
|
|
193
|
+
goneAway = true;
|
|
194
|
+
uiVisible = false;
|
|
195
|
+
removeButtons();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function onSession(raw) {
|
|
199
|
+
try {
|
|
200
|
+
const msg = raw ?? {};
|
|
201
|
+
if (msg.eventType === "Delete") {
|
|
202
|
+
onSessionGone();
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
let s = msg.data ?? msg;
|
|
206
|
+
if (s && !s.myTeam && typeof s.phase === "string") {
|
|
207
|
+
pump();
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (!s?.myTeam) return;
|
|
211
|
+
goneAway = false;
|
|
212
|
+
lastSession = s;
|
|
213
|
+
|
|
214
|
+
decide(s);
|
|
215
|
+
pump();
|
|
216
|
+
scheduleUi();
|
|
217
|
+
} catch (err) {
|
|
218
|
+
logError("session handling failed.", err);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function detectRows() {
|
|
223
|
+
try {
|
|
224
|
+
const nodes = document.querySelectorAll("div.summoner-array.your-party div.summoner-object");
|
|
225
|
+
const list = nodes.length ? nodes : document.querySelectorAll("div.summoner-object.left");
|
|
226
|
+
const found = [];
|
|
227
|
+
for (const e of list) {
|
|
228
|
+
const slot = /slot-id-(\d+)/.exec(String(e.className));
|
|
229
|
+
const nameNode = e.querySelector(".summoner-name");
|
|
230
|
+
found.push({
|
|
231
|
+
el: e,
|
|
232
|
+
slotId: slot ? Number(slot[1]) : null,
|
|
233
|
+
name: (nameNode?.textContent ?? "").trim(),
|
|
234
|
+
circle: e.querySelector(".champion-icon-container"),
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
if (!found.length) return [];
|
|
238
|
+
return found.map((r, i) => ({
|
|
239
|
+
...r,
|
|
240
|
+
index: i,
|
|
241
|
+
player: rows[i]?.player ?? null,
|
|
242
|
+
}));
|
|
243
|
+
} catch (err) {
|
|
244
|
+
logError("row detection failed.", err);
|
|
245
|
+
return [];
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function getRows() {
|
|
250
|
+
const found = detectRows();
|
|
251
|
+
if (found.length) rows = found;
|
|
252
|
+
else if (!document.querySelector("div.summoner-object")) rows = [];
|
|
253
|
+
clearTimeout(detectTimer);
|
|
254
|
+
detectTimer = setTimeout(getRows, DETECT_RETRY_MS);
|
|
255
|
+
return rows;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function ensureName(cell) {
|
|
259
|
+
const id = playerId(cell);
|
|
260
|
+
if (id == null || playerNames.has(id)) return;
|
|
261
|
+
playerNames.set(id, []);
|
|
262
|
+
const get = api?.lcu?.get;
|
|
263
|
+
if (!get) {
|
|
264
|
+
playerNames.delete(id);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const byPuuid = typeof id === "string" && id.length > 20;
|
|
268
|
+
const path = byPuuid ? `/lol-summoner/v2/summoners/puuid/${id}` : `/lol-summoner/v1/summoners/${id}`;
|
|
269
|
+
get(path)
|
|
270
|
+
.then((data) => {
|
|
271
|
+
const names = [norm(data?.displayName), norm(data?.gameName)].filter(Boolean);
|
|
272
|
+
if (names.length) playerNames.set(id, names);
|
|
273
|
+
else playerNames.delete(id);
|
|
274
|
+
})
|
|
275
|
+
.catch(() => playerNames.delete(id));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function nameMatches(rowName, id) {
|
|
279
|
+
const rn = norm(rowName);
|
|
280
|
+
if (!rn) return false;
|
|
281
|
+
return (playerNames.get(id) ?? []).some(
|
|
282
|
+
(n) => n === rn || (n.length >= 3 && rn.length >= 3 && (n.startsWith(rn) || rn.startsWith(n))),
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function bindRowsToTeam(session) {
|
|
287
|
+
const cells = session?.myTeam ?? [];
|
|
288
|
+
const list = getRows();
|
|
289
|
+
if (!list.length || !cells.length) return;
|
|
290
|
+
for (const c of cells) ensureName(c);
|
|
291
|
+
|
|
292
|
+
for (const r of list) {
|
|
293
|
+
const cell =
|
|
294
|
+
(r.slotId != null ? cells.find((c) => c.cellId === r.slotId) : null) ??
|
|
295
|
+
cells.find((c) => nameMatches(r.name, playerId(c)));
|
|
296
|
+
r.player = cell ? playerId(cell) : null;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function stopEvent(e) {
|
|
301
|
+
if (!e) return;
|
|
302
|
+
e.stopPropagation();
|
|
303
|
+
e.preventDefault();
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function toggle(btn) {
|
|
307
|
+
let id = btn.dataset.cell ? (cellToPlayer.get(Number(btn.dataset.cell)) ?? null) : null;
|
|
308
|
+
if (id == null) {
|
|
309
|
+
if (lastSession) bindRowsToTeam(lastSession);
|
|
310
|
+
const list = getRows();
|
|
311
|
+
const rowEl = btn.closest(".summoner-object");
|
|
312
|
+
const r = list.find((x) => x.el === rowEl) ?? list[Number(btn.dataset.slot)];
|
|
313
|
+
id = r?.player ?? null;
|
|
314
|
+
}
|
|
315
|
+
if (id == null) return;
|
|
316
|
+
const myCell = lastSession?.localPlayerCellId;
|
|
317
|
+
if (myCell != null && btn.dataset.cell && Number(btn.dataset.cell) === myCell) return;
|
|
318
|
+
enabled.set(id, !isBlocked(id));
|
|
319
|
+
renderButtons();
|
|
320
|
+
if (lastSession) {
|
|
321
|
+
decide(lastSession);
|
|
322
|
+
pump();
|
|
323
|
+
}
|
|
324
|
+
queueRefresh();
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function buildBadge(slot) {
|
|
328
|
+
const btn = document.createElement("div");
|
|
329
|
+
btn.className = `swap-button-component ${BTN_CLASS}`;
|
|
330
|
+
btn.dataset.slot = String(slot);
|
|
331
|
+
btn.dataset.bb = "1";
|
|
332
|
+
btn.innerHTML =
|
|
333
|
+
'<div class="swap-button--btn uikit-framed-icon">' +
|
|
334
|
+
'<div class="outer-mask interactive">' +
|
|
335
|
+
'<div class="frame-color" style="padding: 2px;">' +
|
|
336
|
+
'<div class="icon"></div>' +
|
|
337
|
+
"</div></div></div>";
|
|
338
|
+
for (const type of ["mouseenter", "mouseleave", "mousedown", "mouseup", "click"]) {
|
|
339
|
+
btn.addEventListener(type, (e) => {
|
|
340
|
+
stopEvent(e);
|
|
341
|
+
if (type === "click") toggle(btn);
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
return btn;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function isSelfRow(r) {
|
|
348
|
+
if (r.el.classList.contains("is-self")) return true;
|
|
349
|
+
return lastSession?.localPlayerCellId != null && r.slotId === lastSession.localPlayerCellId;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function nativeSwapIn(circle) {
|
|
353
|
+
return !!circle.querySelector(".swap-button-component:not([data-bb])");
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function ensureButtons() {
|
|
357
|
+
const list = getRows();
|
|
358
|
+
for (const r of list) {
|
|
359
|
+
if (!r.circle || !r.circle.isConnected) continue;
|
|
360
|
+
let btn = buttons.get(r.index);
|
|
361
|
+
if (isSelfRow(r) || !nativeSwapIn(r.circle)) {
|
|
362
|
+
if (btn) {
|
|
363
|
+
btn.remove();
|
|
364
|
+
buttons.delete(r.index);
|
|
365
|
+
}
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
if (!btn || !btn.isConnected || btn.parentElement !== r.circle) {
|
|
369
|
+
if (btn) btn.remove();
|
|
370
|
+
for (const orphan of r.circle.querySelectorAll(`:scope > .${BTN_CLASS}`)) orphan.remove();
|
|
371
|
+
btn = buildBadge(r.index);
|
|
372
|
+
r.circle.appendChild(btn);
|
|
373
|
+
buttons.set(r.index, btn);
|
|
374
|
+
}
|
|
375
|
+
btn.dataset.slot = String(r.index);
|
|
376
|
+
btn.dataset.cell = r.slotId == null ? "" : String(r.slotId);
|
|
377
|
+
}
|
|
378
|
+
for (const [slot, btn] of [...buttons]) {
|
|
379
|
+
if (!list[slot] || !btn.isConnected) {
|
|
380
|
+
btn.remove();
|
|
381
|
+
buttons.delete(slot);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function renderButtons() {
|
|
387
|
+
const hasSession = !!lastSession?.myTeam;
|
|
388
|
+
for (const [, btn] of buttons) {
|
|
389
|
+
const id = btn.dataset.cell ? (cellToPlayer.get(Number(btn.dataset.cell)) ?? null) : null;
|
|
390
|
+
const on = id != null && isBlocked(id);
|
|
391
|
+
btn.classList.toggle("on", on);
|
|
392
|
+
btn.classList.toggle("unresolved", id == null && hasSession);
|
|
393
|
+
const title = on ? "Blocked — click to allow their bans" : "Block this teammate's bans";
|
|
394
|
+
if (btn.title !== title) btn.title = title;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function removeButtons() {
|
|
399
|
+
for (const btn of buttons.values()) btn.remove();
|
|
400
|
+
buttons.clear();
|
|
401
|
+
for (const el of document.querySelectorAll(`.${BTN_CLASS}[data-bb]`)) el.remove();
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function queueRefresh() {
|
|
405
|
+
clearTimeout(refreshTimer);
|
|
406
|
+
refreshTimer = setTimeout(poll, 80);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function scheduleUi() {
|
|
410
|
+
if (uiScheduled) return;
|
|
411
|
+
uiScheduled = true;
|
|
412
|
+
queueMicrotask(() => {
|
|
413
|
+
uiScheduled = false;
|
|
414
|
+
try {
|
|
415
|
+
update();
|
|
416
|
+
} catch (err) {
|
|
417
|
+
logError("ui update failed.", err);
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function update() {
|
|
423
|
+
if (goneAway) return;
|
|
424
|
+
const s = lastSession;
|
|
425
|
+
if (s?.myTeam) bindRowsToTeam(s);
|
|
426
|
+
const visible = !s?.myTeam ? true : banPending(s);
|
|
427
|
+
if (visible) {
|
|
428
|
+
uiVisible = true;
|
|
429
|
+
ensureButtons();
|
|
430
|
+
renderButtons();
|
|
431
|
+
} else if (uiVisible) {
|
|
432
|
+
uiVisible = false;
|
|
433
|
+
removeButtons();
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async function poll() {
|
|
438
|
+
const get = api?.lcu?.get;
|
|
439
|
+
if (!get || pollInFlight) return;
|
|
440
|
+
if (goneAway && !document.querySelector(".summoner-object")) return;
|
|
441
|
+
pollInFlight = true;
|
|
442
|
+
try {
|
|
443
|
+
onSession(await get(SESSION_URI));
|
|
444
|
+
} catch {
|
|
445
|
+
onSessionGone();
|
|
446
|
+
} finally {
|
|
447
|
+
pollInFlight = false;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function onEnable(utils) {
|
|
452
|
+
api = utils;
|
|
453
|
+
try {
|
|
454
|
+
removeStyle = api.addStyle(STYLE_ID, CSS);
|
|
455
|
+
} catch (err) {
|
|
456
|
+
logError("style setup failed.", err);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
pollTimer = setInterval(poll, POLL_MS);
|
|
460
|
+
ensureTimer = setInterval(() => {
|
|
461
|
+
if (uiVisible) update();
|
|
462
|
+
}, ENSURE_MS);
|
|
463
|
+
|
|
464
|
+
try {
|
|
465
|
+
observer = new MutationObserver(() => {
|
|
466
|
+
try {
|
|
467
|
+
update();
|
|
468
|
+
} catch (err) {
|
|
469
|
+
logError("ui update failed.", err);
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
473
|
+
} catch (err) {
|
|
474
|
+
logError("observer setup failed.", err);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const onPush = (msg) => onSession(msg);
|
|
478
|
+
let offs = [];
|
|
479
|
+
for (const uri of [SESSION_URI, `${SESSION_URI}/timer`]) {
|
|
480
|
+
try {
|
|
481
|
+
const off = api.subscribe(uri, onPush);
|
|
482
|
+
if (off) offs.push(off);
|
|
483
|
+
} catch (err) {
|
|
484
|
+
logError(`subscribe failed for ${uri}.`, err);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
unsubscribe = () => {
|
|
488
|
+
for (const off of offs) {
|
|
489
|
+
try {
|
|
490
|
+
off();
|
|
491
|
+
} catch (err) {
|
|
492
|
+
logError("unsubscribe failed.", err);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
poll();
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function onDisable() {
|
|
501
|
+
unsubscribe?.();
|
|
502
|
+
unsubscribe = null;
|
|
503
|
+
clearInterval(pollTimer);
|
|
504
|
+
clearInterval(ensureTimer);
|
|
505
|
+
clearTimeout(refreshTimer);
|
|
506
|
+
clearTimeout(detectTimer);
|
|
507
|
+
clearTimeout(pumpTimer);
|
|
508
|
+
pollTimer = ensureTimer = refreshTimer = detectTimer = pumpTimer = null;
|
|
509
|
+
observer?.disconnect();
|
|
510
|
+
observer = null;
|
|
511
|
+
|
|
512
|
+
uiVisible = false;
|
|
513
|
+
goneAway = true;
|
|
514
|
+
target = undefined;
|
|
515
|
+
sent = 0;
|
|
516
|
+
failedUntil.clear();
|
|
517
|
+
failCount.clear();
|
|
518
|
+
cellToPlayer.clear();
|
|
519
|
+
enabled.clear();
|
|
520
|
+
playerNames.clear();
|
|
521
|
+
lastSession = null;
|
|
522
|
+
rows = [];
|
|
523
|
+
removeButtons();
|
|
524
|
+
|
|
525
|
+
removeStyle?.();
|
|
526
|
+
removeStyle = null;
|
|
527
|
+
api = null;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
export function init() {
|
|
531
|
+
const registerWith = (utils) => {
|
|
532
|
+
utils.registerPlugin({
|
|
533
|
+
id: PLUGIN_ID,
|
|
534
|
+
name: "Ban Blocker",
|
|
535
|
+
description: "During bans, blocking a teammate instantly claims the champion they hover to ban.",
|
|
536
|
+
version,
|
|
537
|
+
defaultEnabled: true,
|
|
538
|
+
onEnable,
|
|
539
|
+
onDisable,
|
|
540
|
+
});
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
if (window.PerfectionistUtils) {
|
|
544
|
+
registerWith(window.PerfectionistUtils);
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
addEventListener(
|
|
548
|
+
"perfectionist:utils-ready",
|
|
549
|
+
(e) => registerWith(e.detail ?? window.PerfectionistUtils),
|
|
550
|
+
{ once: true },
|
|
551
|
+
);
|
|
552
|
+
}
|