perfectionist-plugins 2.4.1 → 2.4.3
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 +1 -15
- package/auto-accept.js +341 -0
- package/dodge-helper.js +519 -0
- package/package.json +7 -3
- package/perfectionist-utils.js +14 -0
package/README.md
CHANGED
|
@@ -75,24 +75,10 @@ Built for [Pengu Loader](https://pengu.lol)
|
|
|
75
75
|
</tr>
|
|
76
76
|
<tr>
|
|
77
77
|
<td>
|
|
78
|
-
Debloats the client without changing how it looks or behaves
|
|
79
|
-
duplicate requests are merged, and immutable payloads (game-data assets and rune definitions) are cached
|
|
80
|
-
instead of re-downloaded and re-parsed every time a screen renders.
|
|
81
|
-
<br><br>
|
|
82
|
-
Everything it does on the page runs in idle slices instead of blocking navigation: image decode marking is
|
|
83
|
-
batched, the client phase comes from the client's own events rather than a timer, and the in-game un-gate pass
|
|
84
|
-
reads the page once with a single query and backs off to one check every five seconds once it has nothing left
|
|
85
|
-
to unlock.
|
|
86
|
-
<br><br>
|
|
87
|
-
It also keeps the social panel, friend requests and settings usable while a game is running, and repairs the
|
|
88
|
-
champ-select data race after a client reload so the ban list is still populated.
|
|
78
|
+
Debloats the client without changing how it looks or behaves and fixes bugs.
|
|
89
79
|
<br><br>
|
|
90
80
|
For weaker machines there is one optional switch, <b>Performance mode</b>, which drops background blur,
|
|
91
81
|
decorative animation and looping background video. Leave it off and the client is pixel-identical.
|
|
92
|
-
<br><br>
|
|
93
|
-
<b>Diagnostics</b> is a separate opt-in switch, off by default. It keeps an in-memory tally of long tasks,
|
|
94
|
-
screen settle times, requests and heap samples and has a <b>Copy report</b> button. Nothing is sent over the
|
|
95
|
-
network and the report contains no account, summoner, token or LCU data.
|
|
96
82
|
</td>
|
|
97
83
|
</tr>
|
|
98
84
|
</table>
|
package/auto-accept.js
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
const PLUGIN_ID = "auto-accept";
|
|
2
|
+
export const version = "1.1.0";
|
|
3
|
+
|
|
4
|
+
const PHASE_URI = "/lol-gameflow/v1/gameflow-phase";
|
|
5
|
+
const READY_CHECK_URI = "/lol-matchmaking/v1/ready-check";
|
|
6
|
+
const ACCEPT_URI = "/lol-matchmaking/v1/ready-check/accept";
|
|
7
|
+
const OPTION_KEY = "autoAccept";
|
|
8
|
+
const DELAY_KEY = "perfectionist:auto-accept:delay";
|
|
9
|
+
const MAX_DELAY = 15000;
|
|
10
|
+
const RETRY_MS = 700;
|
|
11
|
+
const MAX_ATTEMPTS = 4;
|
|
12
|
+
const STYLE_ID = "perf-auto-accept-style";
|
|
13
|
+
const ROOT_ID = "perfAutoAccept";
|
|
14
|
+
const TOGGLE_ID = "perfAutoAcceptToggle";
|
|
15
|
+
const DELAY_INPUT_ID = "perfAutoAcceptDelay";
|
|
16
|
+
|
|
17
|
+
const CSS = `#perfAutoAccept { display: flex !important; align-items: center !important; gap: 8px !important; }
|
|
18
|
+
#perfAutoAccept .auto-accept-button { cursor: pointer !important; }
|
|
19
|
+
#perfAutoAccept .auto-accept-button-text {
|
|
20
|
+
font-family: var(--font-body, "LoL Body", Arial, sans-serif) !important;
|
|
21
|
+
font-size: 12px !important; letter-spacing: 0.3px !important; color: #a09b8c !important;
|
|
22
|
+
}
|
|
23
|
+
#perfAutoAccept .auto-accept-button[selected] .auto-accept-button-text { color: #f0e6d2 !important; }
|
|
24
|
+
#perfAutoAccept .auto-accept-delay {
|
|
25
|
+
display: flex !important; align-items: center !important; box-sizing: border-box !important;
|
|
26
|
+
width: 82px !important; height: 24px !important; margin-left: 8px !important;
|
|
27
|
+
background: #1e2328 !important; border: 1px solid #785a28 !important; border-radius: 0 !important;
|
|
28
|
+
transition: border-color 0.15s ease, background-color 0.15s ease !important;
|
|
29
|
+
}
|
|
30
|
+
#perfAutoAccept .auto-accept-delay:hover { border-color: #c89b3c !important; }
|
|
31
|
+
#perfAutoAccept .auto-accept-delay:focus-within { border-color: #c89b3c !important; background: #0a1428 !important; }
|
|
32
|
+
#perfAutoAccept .auto-accept-delay input {
|
|
33
|
+
width: 100% !important; height: 100% !important; box-sizing: border-box !important;
|
|
34
|
+
background: transparent !important; border: none !important; outline: none !important; box-shadow: none !important;
|
|
35
|
+
color: #f0e6d2 !important; font-family: var(--font-body, "LoL Body", Arial, sans-serif) !important;
|
|
36
|
+
font-size: 12px !important; letter-spacing: 0.3px !important; text-align: center !important; padding: 0 6px !important;
|
|
37
|
+
}
|
|
38
|
+
#perfAutoAccept .auto-accept-delay input::placeholder { color: #5b5a56 !important; }
|
|
39
|
+
#perfAutoAccept .auto-accept-delay input::-webkit-outer-spin-button,
|
|
40
|
+
#perfAutoAccept .auto-accept-delay input::-webkit-inner-spin-button { -webkit-appearance: none !important; margin: 0 !important; }`;
|
|
41
|
+
|
|
42
|
+
let api = null;
|
|
43
|
+
let active = false;
|
|
44
|
+
let timer = null;
|
|
45
|
+
let accepted = false;
|
|
46
|
+
let sending = false;
|
|
47
|
+
let attempts = 0;
|
|
48
|
+
let unsubscribes = [];
|
|
49
|
+
let removeStyle = null;
|
|
50
|
+
let domObserver = null;
|
|
51
|
+
let domTimer = null;
|
|
52
|
+
let delayValue = readDelay();
|
|
53
|
+
|
|
54
|
+
function readDelay() {
|
|
55
|
+
try {
|
|
56
|
+
const raw = Number(localStorage.getItem(DELAY_KEY));
|
|
57
|
+
if (!Number.isFinite(raw) || raw < 0) return 0;
|
|
58
|
+
return Math.min(Math.floor(raw), MAX_DELAY);
|
|
59
|
+
} catch {
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function writeDelay(value) {
|
|
65
|
+
delayValue = value;
|
|
66
|
+
try {
|
|
67
|
+
localStorage.setItem(DELAY_KEY, String(value));
|
|
68
|
+
} catch {}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function enabled() {
|
|
72
|
+
try {
|
|
73
|
+
return api?.getOption?.(PLUGIN_ID, OPTION_KEY) === true;
|
|
74
|
+
} catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function unwrap(raw) {
|
|
80
|
+
if (typeof raw === "string") {
|
|
81
|
+
const text = raw.trim();
|
|
82
|
+
if (text.startsWith("{") || text.startsWith("[")) {
|
|
83
|
+
try {
|
|
84
|
+
return unwrap(JSON.parse(text));
|
|
85
|
+
} catch {
|
|
86
|
+
return raw;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return raw;
|
|
90
|
+
}
|
|
91
|
+
if (Array.isArray(raw) && raw.length >= 3 && typeof raw[1] === "string") {
|
|
92
|
+
return unwrap(raw[2]?.data ?? raw[2]);
|
|
93
|
+
}
|
|
94
|
+
if (raw && typeof raw === "object" && "data" in raw) {
|
|
95
|
+
const keys = Object.keys(raw);
|
|
96
|
+
if (keys.includes("uri") || keys.includes("eventType") || keys.length <= 2) return unwrap(raw.data);
|
|
97
|
+
}
|
|
98
|
+
return raw;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function canAccept(check) {
|
|
102
|
+
if (!check || typeof check !== "object") return false;
|
|
103
|
+
if (check.state !== "InProgress") return false;
|
|
104
|
+
if (check.playerResponse && check.playerResponse !== "None") return false;
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function clearTimer() {
|
|
109
|
+
clearTimeout(timer);
|
|
110
|
+
timer = null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function resetReadyCheck() {
|
|
114
|
+
clearTimer();
|
|
115
|
+
accepted = false;
|
|
116
|
+
attempts = 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function acceptNow() {
|
|
120
|
+
timer = null;
|
|
121
|
+
if (!active || !enabled() || accepted || sending) return;
|
|
122
|
+
sending = true;
|
|
123
|
+
try {
|
|
124
|
+
const check = await api.lcu.get(READY_CHECK_URI).catch(() => null);
|
|
125
|
+
if (!active || !enabled() || accepted || !canAccept(check)) return;
|
|
126
|
+
await api.lcu.send("POST", ACCEPT_URI);
|
|
127
|
+
accepted = true;
|
|
128
|
+
attempts = 0;
|
|
129
|
+
} catch {
|
|
130
|
+
attempts += 1;
|
|
131
|
+
if (active && enabled() && attempts < MAX_ATTEMPTS) timer = setTimeout(acceptNow, RETRY_MS);
|
|
132
|
+
} finally {
|
|
133
|
+
sending = false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function queueAccept() {
|
|
138
|
+
if (!active || !enabled() || accepted || sending || timer) return;
|
|
139
|
+
if (delayValue > 0) timer = setTimeout(acceptNow, delayValue);
|
|
140
|
+
else acceptNow();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function onPhase(raw) {
|
|
144
|
+
const phase = unwrap(raw);
|
|
145
|
+
if (typeof phase !== "string") return;
|
|
146
|
+
if (phase === "ReadyCheck") {
|
|
147
|
+
if (!accepted) queueAccept();
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
resetReadyCheck();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function onReadyCheck(raw) {
|
|
154
|
+
const check = unwrap(raw);
|
|
155
|
+
if (!check || typeof check !== "object" || !check.state) return;
|
|
156
|
+
if (check.playerResponse === "Accepted") {
|
|
157
|
+
accepted = true;
|
|
158
|
+
clearTimer();
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (check.playerResponse === "Declined" || check.state !== "InProgress") {
|
|
162
|
+
resetReadyCheck();
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
queueAccept();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function refresh() {
|
|
169
|
+
const phase = unwrap(await api.lcu.get(PHASE_URI).catch(() => null));
|
|
170
|
+
if (!active) return;
|
|
171
|
+
if (phase === "ReadyCheck") queueAccept();
|
|
172
|
+
else resetReadyCheck();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function syncButton() {
|
|
176
|
+
const option = document.getElementById(TOGGLE_ID);
|
|
177
|
+
if (!option) return;
|
|
178
|
+
if (enabled()) option.setAttribute("selected", "true");
|
|
179
|
+
else option.removeAttribute("selected");
|
|
180
|
+
const field = document.getElementById(DELAY_INPUT_ID);
|
|
181
|
+
if (field && document.activeElement !== field) field.value = String(delayValue);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function toggle() {
|
|
185
|
+
const next = !enabled();
|
|
186
|
+
try {
|
|
187
|
+
api.setOption(PLUGIN_ID, OPTION_KEY, next);
|
|
188
|
+
} catch {}
|
|
189
|
+
syncButton();
|
|
190
|
+
if (next) queueAccept();
|
|
191
|
+
else resetReadyCheck();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function onDelayInput(event) {
|
|
195
|
+
const raw = Number(event.target.value);
|
|
196
|
+
const next = Math.min(Math.max(Number.isFinite(raw) ? Math.floor(raw) : 0, 0), MAX_DELAY);
|
|
197
|
+
writeDelay(next);
|
|
198
|
+
if (event.type === "change") event.target.value = String(next);
|
|
199
|
+
if (active && enabled() && timer) {
|
|
200
|
+
clearTimer();
|
|
201
|
+
queueAccept();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function buildButton() {
|
|
206
|
+
const root = document.createElement("div");
|
|
207
|
+
root.id = ROOT_ID;
|
|
208
|
+
|
|
209
|
+
const option = document.createElement("lol-uikit-radio-input-option");
|
|
210
|
+
option.id = TOGGLE_ID;
|
|
211
|
+
option.className = "auto-accept-button";
|
|
212
|
+
const label = document.createElement("div");
|
|
213
|
+
label.className = "auto-accept-button-text";
|
|
214
|
+
label.textContent = "Auto Accept";
|
|
215
|
+
option.append(label);
|
|
216
|
+
option.addEventListener("click", (event) => {
|
|
217
|
+
event.preventDefault();
|
|
218
|
+
event.stopPropagation();
|
|
219
|
+
toggle();
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const input = document.createElement("lol-uikit-flat-input");
|
|
223
|
+
input.className = "auto-accept-delay";
|
|
224
|
+
const field = document.createElement("input");
|
|
225
|
+
field.id = DELAY_INPUT_ID;
|
|
226
|
+
field.type = "number";
|
|
227
|
+
field.min = "0";
|
|
228
|
+
field.max = String(MAX_DELAY);
|
|
229
|
+
field.step = "100";
|
|
230
|
+
field.placeholder = "ms";
|
|
231
|
+
field.title = "Delay before accepting, in milliseconds";
|
|
232
|
+
field.value = String(delayValue);
|
|
233
|
+
field.addEventListener("input", onDelayInput);
|
|
234
|
+
field.addEventListener("change", onDelayInput);
|
|
235
|
+
field.addEventListener("click", (event) => event.stopPropagation());
|
|
236
|
+
input.append(field);
|
|
237
|
+
|
|
238
|
+
root.append(option, input);
|
|
239
|
+
return root;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function footer() {
|
|
243
|
+
return (
|
|
244
|
+
document.querySelector(".v2-footer-notifications.ember-view") ??
|
|
245
|
+
document.querySelector(".parties-footer-notifications.ember-view")
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function ensureButton() {
|
|
250
|
+
if (!active) return;
|
|
251
|
+
const host = footer();
|
|
252
|
+
if (!host) return;
|
|
253
|
+
if (!document.getElementById(ROOT_ID)) host.append(buildButton());
|
|
254
|
+
syncButton();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function scheduleEnsure() {
|
|
258
|
+
if (domTimer) return;
|
|
259
|
+
domTimer = setTimeout(() => {
|
|
260
|
+
domTimer = null;
|
|
261
|
+
ensureButton();
|
|
262
|
+
}, 250);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function onEnable(utils) {
|
|
266
|
+
api = utils;
|
|
267
|
+
active = true;
|
|
268
|
+
removeStyle = utils.addStyle(STYLE_ID, CSS);
|
|
269
|
+
|
|
270
|
+
domObserver = new MutationObserver(scheduleEnsure);
|
|
271
|
+
domObserver.observe(document.documentElement ?? document, { childList: true, subtree: true });
|
|
272
|
+
|
|
273
|
+
for (const uri of [PHASE_URI, READY_CHECK_URI]) {
|
|
274
|
+
try {
|
|
275
|
+
const off = utils.subscribe(uri, uri === PHASE_URI ? onPhase : onReadyCheck);
|
|
276
|
+
if (off) unsubscribes.push(off);
|
|
277
|
+
} catch {}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
ensureButton();
|
|
281
|
+
refresh();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function onDisable() {
|
|
285
|
+
active = false;
|
|
286
|
+
resetReadyCheck();
|
|
287
|
+
for (const off of unsubscribes) {
|
|
288
|
+
try {
|
|
289
|
+
off();
|
|
290
|
+
} catch {}
|
|
291
|
+
}
|
|
292
|
+
unsubscribes = [];
|
|
293
|
+
domObserver?.disconnect();
|
|
294
|
+
domObserver = null;
|
|
295
|
+
clearTimeout(domTimer);
|
|
296
|
+
domTimer = null;
|
|
297
|
+
document.getElementById(ROOT_ID)?.remove();
|
|
298
|
+
removeStyle?.();
|
|
299
|
+
removeStyle = null;
|
|
300
|
+
api = null;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function onOptionChange(key) {
|
|
304
|
+
if (key !== OPTION_KEY) return;
|
|
305
|
+
syncButton();
|
|
306
|
+
if (enabled()) queueAccept();
|
|
307
|
+
else resetReadyCheck();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function init() {
|
|
311
|
+
const registerWith = (utils) => {
|
|
312
|
+
utils.registerPlugin({
|
|
313
|
+
id: PLUGIN_ID,
|
|
314
|
+
name: "Auto Accept",
|
|
315
|
+
description:
|
|
316
|
+
"Accepts the ready check automatically, with an optional delay. The same switch is shown natively in the lobby footer.",
|
|
317
|
+
version,
|
|
318
|
+
defaultEnabled: true,
|
|
319
|
+
options: [
|
|
320
|
+
{
|
|
321
|
+
key: OPTION_KEY,
|
|
322
|
+
label: "Auto accept matches",
|
|
323
|
+
description:
|
|
324
|
+
"Accepts the ready check for you. The Auto Accept switch in the lobby footer toggles this same setting.",
|
|
325
|
+
default: true,
|
|
326
|
+
},
|
|
327
|
+
],
|
|
328
|
+
onEnable,
|
|
329
|
+
onDisable,
|
|
330
|
+
onOptionChange,
|
|
331
|
+
});
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
if (window.PerfectionistUtils) {
|
|
335
|
+
registerWith(window.PerfectionistUtils);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
addEventListener("perfectionist:utils-ready", (event) => registerWith(event.detail ?? window.PerfectionistUtils), {
|
|
339
|
+
once: true,
|
|
340
|
+
});
|
|
341
|
+
}
|
package/dodge-helper.js
ADDED
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
const PLUGIN_ID = "dodge-helper";
|
|
2
|
+
export const version = "1.4.0";
|
|
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 SUMMONER_URI = "/lol-summoner/v1/current-summoner";
|
|
8
|
+
const RANKED_STATS_URI = "/lol-ranked/v1/current-ranked-stats";
|
|
9
|
+
const DODGE_URI = "/lol-gameflow/v1/session/dodge";
|
|
10
|
+
const QUIT_URI = "/lol-lobby-team-builder/champ-select/v1/session/quit";
|
|
11
|
+
const STORE_KEY = "perfectionist:dodge-helper:accounts:v1";
|
|
12
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
13
|
+
const STYLE_ID = "perf-dodge-helper-style";
|
|
14
|
+
const ROOT_ID = "perfDodgeHelper";
|
|
15
|
+
const BUTTON_ID = "perfDodgeHelperButton";
|
|
16
|
+
const LP_ID = "perfDodgeHelperLp";
|
|
17
|
+
|
|
18
|
+
const RANKED_QUEUES = new Map([
|
|
19
|
+
[420, "RANKED_SOLO_5x5"],
|
|
20
|
+
[440, "RANKED_FLEX_SR"],
|
|
21
|
+
]);
|
|
22
|
+
const APEX_TIERS = new Set(["MASTER", "GRANDMASTER", "CHALLENGER"]);
|
|
23
|
+
const PENALTY_TIERS = {
|
|
24
|
+
1: { lp: 5, lockout: "6-minute" },
|
|
25
|
+
2: { lp: 15, lockout: "30-minute" },
|
|
26
|
+
3: { lp: 15, lockout: "12-hour" },
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const CSS = `
|
|
30
|
+
.perf-dodge-anchor { position: relative !important; overflow: visible !important; }
|
|
31
|
+
#${ROOT_ID} {
|
|
32
|
+
position: relative !important; display: flex !important; align-items: flex-end !important;
|
|
33
|
+
justify-content: center !important; width: 125px !important; height: 32px !important; z-index: 20 !important;
|
|
34
|
+
}
|
|
35
|
+
#${ROOT_ID}.perf-dodge-quit { position: absolute !important; left: 0 !important; bottom: 0 !important; }
|
|
36
|
+
#${ROOT_ID}.perf-dodge-bar { margin-left: 12px !important; }
|
|
37
|
+
#${ROOT_ID}.perf-dodge-floating { position: fixed !important; right: 145px !important; bottom: 21px !important; }
|
|
38
|
+
#${ROOT_ID} lol-uikit-flat-button { width: 125px !important; height: 32px !important; }
|
|
39
|
+
#${ROOT_ID} .perf-dodge-lp {
|
|
40
|
+
position: absolute !important; left: 0 !important; right: 0 !important; bottom: calc(100% + 4px) !important;
|
|
41
|
+
text-align: center !important; white-space: nowrap !important;
|
|
42
|
+
font-family: var(--font-body, "LoL Body", Arial, sans-serif) !important;
|
|
43
|
+
font-size: 11px !important; font-weight: 700 !important; letter-spacing: 0.4px !important;
|
|
44
|
+
line-height: 1 !important; color: #cdbe91 !important;
|
|
45
|
+
}`;
|
|
46
|
+
|
|
47
|
+
let api = null;
|
|
48
|
+
let active = false;
|
|
49
|
+
let dodging = false;
|
|
50
|
+
let unsubscribes = [];
|
|
51
|
+
let removeStyle = null;
|
|
52
|
+
let domObserver = null;
|
|
53
|
+
let domTimer = null;
|
|
54
|
+
let networkWatch = null;
|
|
55
|
+
let penaltyToken = 0;
|
|
56
|
+
const state = {
|
|
57
|
+
phase: null,
|
|
58
|
+
queueId: 0,
|
|
59
|
+
isCustom: false,
|
|
60
|
+
showQuitButton: false,
|
|
61
|
+
gameMode: "",
|
|
62
|
+
account: null,
|
|
63
|
+
penalty: null,
|
|
64
|
+
lastSelfDodgeAt: 0,
|
|
65
|
+
anchor: null,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
function unwrap(raw) {
|
|
69
|
+
if (typeof raw === "string") {
|
|
70
|
+
const text = raw.trim();
|
|
71
|
+
if (text.startsWith("{") || text.startsWith("[")) {
|
|
72
|
+
try {
|
|
73
|
+
return unwrap(JSON.parse(text));
|
|
74
|
+
} catch {
|
|
75
|
+
return raw;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return raw;
|
|
79
|
+
}
|
|
80
|
+
if (Array.isArray(raw) && raw.length >= 3 && typeof raw[1] === "string") {
|
|
81
|
+
return unwrap(raw[2]?.data ?? raw[2]);
|
|
82
|
+
}
|
|
83
|
+
if (raw && typeof raw === "object" && "data" in raw) {
|
|
84
|
+
const keys = Object.keys(raw);
|
|
85
|
+
if (keys.includes("uri") || keys.includes("eventType") || keys.length <= 2) return unwrap(raw.data);
|
|
86
|
+
}
|
|
87
|
+
return raw;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function readStore() {
|
|
91
|
+
try {
|
|
92
|
+
const parsed = JSON.parse(localStorage.getItem(STORE_KEY) || "{}");
|
|
93
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
94
|
+
} catch {
|
|
95
|
+
return {};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function accountKey() {
|
|
100
|
+
const account = state.account;
|
|
101
|
+
if (!account) return null;
|
|
102
|
+
if (account.puuid) return account.puuid;
|
|
103
|
+
if (account.summonerId) return `sid:${account.summonerId}`;
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function currentTier() {
|
|
108
|
+
const key = accountKey();
|
|
109
|
+
if (!key) return 0;
|
|
110
|
+
const record = readStore()[key];
|
|
111
|
+
if (!record || typeof record.tier !== "number" || typeof record.at !== "number") return 0;
|
|
112
|
+
const drops = Math.floor((Date.now() - record.at) / DAY_MS);
|
|
113
|
+
return Math.max(0, Math.min(record.tier - drops, 3));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function loadAccount() {
|
|
117
|
+
if (state.account) return state.account;
|
|
118
|
+
const summoner = await api.lcu.get(SUMMONER_URI).catch(() => null);
|
|
119
|
+
if (summoner) {
|
|
120
|
+
state.account = { puuid: summoner.puuid || "", summonerId: summoner.summonerId || 0 };
|
|
121
|
+
}
|
|
122
|
+
return state.account;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function recordDodge() {
|
|
126
|
+
if (!active) return;
|
|
127
|
+
if (hasNativeQuit()) return;
|
|
128
|
+
const now = Date.now();
|
|
129
|
+
if (now - state.lastSelfDodgeAt < 1500) return;
|
|
130
|
+
state.lastSelfDodgeAt = now;
|
|
131
|
+
await loadAccount();
|
|
132
|
+
const key = accountKey();
|
|
133
|
+
if (!key) return;
|
|
134
|
+
const store = readStore();
|
|
135
|
+
const tier = Math.min(currentTier() + 1, 3);
|
|
136
|
+
store[key] = { tier, at: now };
|
|
137
|
+
try {
|
|
138
|
+
localStorage.setItem(STORE_KEY, JSON.stringify(store));
|
|
139
|
+
} catch {}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function computePenalty(queueId) {
|
|
143
|
+
const rankedKey = RANKED_QUEUES.get(queueId);
|
|
144
|
+
if (!rankedKey) return { ranked: false, lp: 0, tier: 0, lockout: "" };
|
|
145
|
+
await loadAccount();
|
|
146
|
+
const stats = await api.lcu.get(RANKED_STATS_URI).catch(() => null);
|
|
147
|
+
const tierName = String(stats?.queueMap?.[rankedKey]?.tier ?? "").toUpperCase();
|
|
148
|
+
if (!tierName || tierName === "UNRANKED") return { ranked: true, lp: 0, tier: 0, lockout: "" };
|
|
149
|
+
const tier = Math.min(currentTier() + 1, 3);
|
|
150
|
+
const apex = APEX_TIERS.has(tierName);
|
|
151
|
+
return {
|
|
152
|
+
ranked: true,
|
|
153
|
+
lp: apex && tier === 1 ? 20 : PENALTY_TIERS[tier].lp,
|
|
154
|
+
lockout: PENALTY_TIERS[tier].lockout,
|
|
155
|
+
tier,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function isCustomQueue() {
|
|
160
|
+
return state.isCustom || state.gameMode === "PRACTICETOOL" || state.queueId === 0;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function hasNativeQuit() {
|
|
164
|
+
return state.showQuitButton || isCustomQueue();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function render() {
|
|
168
|
+
const button = document.getElementById(BUTTON_ID);
|
|
169
|
+
const lp = document.getElementById(LP_ID);
|
|
170
|
+
if (!button || !lp) return;
|
|
171
|
+
const ranked = RANKED_QUEUES.has(state.queueId);
|
|
172
|
+
const label = ranked ? "DODGE" : "QUIT";
|
|
173
|
+
if (button.textContent !== label) button.textContent = label;
|
|
174
|
+
const penalty = state.penalty;
|
|
175
|
+
const amount = ranked && penalty?.ranked ? penalty.lp : 0;
|
|
176
|
+
const text = amount > 0 ? `\u2212${amount} LP` : "";
|
|
177
|
+
if (lp.textContent !== text) lp.textContent = text;
|
|
178
|
+
lp.style.display = text ? "" : "none";
|
|
179
|
+
if (ranked) {
|
|
180
|
+
button.title =
|
|
181
|
+
amount > 0
|
|
182
|
+
? `Dodge champion select. This costs about ${amount} LP and a ${penalty?.lockout ?? "queue"} lockout. Your dodge tier is shared across queues and drops one step every 24 hours.`
|
|
183
|
+
: "Dodge champion select. This queue is unranked, so no LP is lost, but the dodge still advances your shared dodge tier.";
|
|
184
|
+
} else {
|
|
185
|
+
button.title = "Leave champion select. This counts as a dodge and advances your shared dodge tier.";
|
|
186
|
+
}
|
|
187
|
+
lp.title =
|
|
188
|
+
ranked && amount > 0
|
|
189
|
+
? "Estimated LP for this dodge, from this account's shared dodge tier. The tier drops one step every 24 hours."
|
|
190
|
+
: "";
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function refreshPenalty() {
|
|
194
|
+
if (!active || state.phase !== "ChampSelect" || !state.queueId || hasNativeQuit()) return;
|
|
195
|
+
const queueId = state.queueId;
|
|
196
|
+
const token = ++penaltyToken;
|
|
197
|
+
const penalty = await computePenalty(queueId);
|
|
198
|
+
if (!active || token !== penaltyToken || queueId !== state.queueId) return;
|
|
199
|
+
state.penalty = penalty;
|
|
200
|
+
render();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function applySession(raw) {
|
|
204
|
+
const session = unwrap(raw);
|
|
205
|
+
if (!session) return;
|
|
206
|
+
const queue = session.gameData?.queue;
|
|
207
|
+
if (queue) {
|
|
208
|
+
const queueId = Number(queue.id ?? 0) || 0;
|
|
209
|
+
const isCustom = Boolean(session.gameData?.isCustomGame || queue.isCustom);
|
|
210
|
+
const gameMode = String(queue.gameMode ?? "");
|
|
211
|
+
if (queueId !== state.queueId || isCustom !== state.isCustom || gameMode !== state.gameMode) {
|
|
212
|
+
state.queueId = queueId;
|
|
213
|
+
state.isCustom = isCustom;
|
|
214
|
+
state.gameMode = gameMode;
|
|
215
|
+
state.penalty = null;
|
|
216
|
+
if (state.phase === "ChampSelect") refreshPenalty();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
ensureMount();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function applyChampSelect(raw) {
|
|
223
|
+
const session = unwrap(raw);
|
|
224
|
+
if (!session) return;
|
|
225
|
+
state.showQuitButton = Boolean(session.showQuitButton);
|
|
226
|
+
const queueId = Number(session.queueId ?? 0) || 0;
|
|
227
|
+
if (queueId > 0) state.queueId = queueId;
|
|
228
|
+
if (session.isCustomGame) state.isCustom = true;
|
|
229
|
+
ensureMount();
|
|
230
|
+
syncWatching();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function unmount() {
|
|
234
|
+
for (const element of document.querySelectorAll(`#${ROOT_ID}`)) element.remove();
|
|
235
|
+
if (state.anchor) {
|
|
236
|
+
state.anchor.classList.remove("perf-dodge-anchor");
|
|
237
|
+
state.anchor = null;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function build() {
|
|
242
|
+
const root = document.createElement("div");
|
|
243
|
+
root.id = ROOT_ID;
|
|
244
|
+
const lp = document.createElement("span");
|
|
245
|
+
lp.id = LP_ID;
|
|
246
|
+
lp.className = "perf-dodge-lp";
|
|
247
|
+
const button = document.createElement("lol-uikit-flat-button");
|
|
248
|
+
button.id = BUTTON_ID;
|
|
249
|
+
button.addEventListener("click", (event) => {
|
|
250
|
+
event.preventDefault();
|
|
251
|
+
event.stopPropagation();
|
|
252
|
+
quitOrDodge();
|
|
253
|
+
});
|
|
254
|
+
root.append(lp, button);
|
|
255
|
+
return root;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function host() {
|
|
259
|
+
const quitBar = document.querySelector(".bottom-right-buttons");
|
|
260
|
+
if (quitBar && quitBar.getBoundingClientRect().width > 1) return { element: quitBar, kind: "quit" };
|
|
261
|
+
const screen = document.querySelector(".champion-select");
|
|
262
|
+
if (screen) return { element: screen, kind: "floating" };
|
|
263
|
+
const bars = [...document.querySelectorAll(".champ-select-action-content")].filter(
|
|
264
|
+
(element) => element.getBoundingClientRect().width > 1
|
|
265
|
+
);
|
|
266
|
+
return bars[0] ? { element: bars[0], kind: "bar" } : null;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function ensureMount() {
|
|
270
|
+
if (!active || state.phase !== "ChampSelect" || hasNativeQuit()) {
|
|
271
|
+
unmount();
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const target = host();
|
|
275
|
+
if (!target) {
|
|
276
|
+
unmount();
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (state.anchor && state.anchor !== target.element) {
|
|
280
|
+
state.anchor.classList.remove("perf-dodge-anchor");
|
|
281
|
+
state.anchor = null;
|
|
282
|
+
}
|
|
283
|
+
if (target.kind === "quit") {
|
|
284
|
+
target.element.classList.add("perf-dodge-anchor");
|
|
285
|
+
state.anchor = target.element;
|
|
286
|
+
}
|
|
287
|
+
const roots = [...document.querySelectorAll(`#${ROOT_ID}`)];
|
|
288
|
+
for (const extra of roots.slice(1)) extra.remove();
|
|
289
|
+
const root = roots[0] ?? build();
|
|
290
|
+
if (root.parentElement !== target.element) target.element.append(root);
|
|
291
|
+
root.classList.toggle("perf-dodge-quit", target.kind === "quit");
|
|
292
|
+
root.classList.toggle("perf-dodge-bar", target.kind === "bar");
|
|
293
|
+
root.classList.toggle("perf-dodge-floating", target.kind === "floating");
|
|
294
|
+
render();
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function scheduleMount() {
|
|
298
|
+
if (domTimer) return;
|
|
299
|
+
domTimer = setTimeout(() => {
|
|
300
|
+
domTimer = null;
|
|
301
|
+
ensureMount();
|
|
302
|
+
}, 200);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function startDomObserver() {
|
|
306
|
+
if (domObserver) return;
|
|
307
|
+
const target =
|
|
308
|
+
document.querySelector(".champion-select") ?? document.querySelector(".screen-root") ?? document.body;
|
|
309
|
+
domObserver = new MutationObserver(scheduleMount);
|
|
310
|
+
domObserver.observe(target, { childList: true, subtree: true });
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function stopDomObserver() {
|
|
314
|
+
domObserver?.disconnect();
|
|
315
|
+
domObserver = null;
|
|
316
|
+
clearTimeout(domTimer);
|
|
317
|
+
domTimer = null;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function isDodgeUrl(url) {
|
|
321
|
+
return /\/(lol-gameflow\/v1\/session\/dodge|lol-lobby-team-builder\/champ-select\/v1\/session\/quit)(\?|$)/i.test(
|
|
322
|
+
String(url)
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function installDodgeWatcher() {
|
|
327
|
+
if (networkWatch) return;
|
|
328
|
+
const originalFetch = window.fetch;
|
|
329
|
+
const originalOpen = XMLHttpRequest.prototype.open;
|
|
330
|
+
const originalSend = XMLHttpRequest.prototype.send;
|
|
331
|
+
|
|
332
|
+
const ourFetch = function (...args) {
|
|
333
|
+
const url = String(args[0]?.url ?? args[0] ?? "");
|
|
334
|
+
const method = String(args[1]?.method ?? "GET").toUpperCase();
|
|
335
|
+
const promise = originalFetch.apply(this, args);
|
|
336
|
+
if (method === "POST" && isDodgeUrl(url)) {
|
|
337
|
+
promise
|
|
338
|
+
.then((response) => {
|
|
339
|
+
if (response.ok) recordDodge();
|
|
340
|
+
})
|
|
341
|
+
.catch(() => {});
|
|
342
|
+
}
|
|
343
|
+
return promise;
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const ourOpen = function (method, url, ...rest) {
|
|
347
|
+
this.__perfDodgeUrl = String(url);
|
|
348
|
+
this.__perfDodgeMethod = String(method ?? "GET").toUpperCase();
|
|
349
|
+
return originalOpen.call(this, method, url, ...rest);
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
const ourSend = function (body) {
|
|
353
|
+
if (this.__perfDodgeMethod === "POST" && isDodgeUrl(this.__perfDodgeUrl)) {
|
|
354
|
+
this.addEventListener("load", () => {
|
|
355
|
+
if (this.status >= 200 && this.status < 300) recordDodge();
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
return originalSend.call(this, body);
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
window.fetch = ourFetch;
|
|
362
|
+
XMLHttpRequest.prototype.open = ourOpen;
|
|
363
|
+
XMLHttpRequest.prototype.send = ourSend;
|
|
364
|
+
|
|
365
|
+
networkWatch = {
|
|
366
|
+
restore() {
|
|
367
|
+
if (window.fetch === ourFetch) window.fetch = originalFetch;
|
|
368
|
+
if (XMLHttpRequest.prototype.open === ourOpen) XMLHttpRequest.prototype.open = originalOpen;
|
|
369
|
+
if (XMLHttpRequest.prototype.send === ourSend) XMLHttpRequest.prototype.send = originalSend;
|
|
370
|
+
},
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function removeDodgeWatcher() {
|
|
375
|
+
networkWatch?.restore();
|
|
376
|
+
networkWatch = null;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function syncWatching() {
|
|
380
|
+
if (state.phase === "ChampSelect" && !hasNativeQuit()) {
|
|
381
|
+
startDomObserver();
|
|
382
|
+
installDodgeWatcher();
|
|
383
|
+
} else {
|
|
384
|
+
stopDomObserver();
|
|
385
|
+
removeDodgeWatcher();
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async function quitOrDodge() {
|
|
390
|
+
if (dodging || !state.queueId || hasNativeQuit()) return;
|
|
391
|
+
dodging = true;
|
|
392
|
+
const button = document.getElementById(BUTTON_ID);
|
|
393
|
+
if (button) button.disabled = true;
|
|
394
|
+
const ranked = RANKED_QUEUES.has(state.queueId);
|
|
395
|
+
const uri = ranked ? DODGE_URI : QUIT_URI;
|
|
396
|
+
try {
|
|
397
|
+
await api.lcu.send("POST", uri);
|
|
398
|
+
await recordDodge();
|
|
399
|
+
setTimeout(() => {
|
|
400
|
+
const current = document.getElementById(BUTTON_ID);
|
|
401
|
+
if (current) current.disabled = false;
|
|
402
|
+
}, 1500);
|
|
403
|
+
} catch (error) {
|
|
404
|
+
console.warn("[Dodge Helper] quit failed:", error);
|
|
405
|
+
if (button) button.disabled = false;
|
|
406
|
+
} finally {
|
|
407
|
+
dodging = false;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async function handlePhase(raw) {
|
|
412
|
+
const phase = typeof raw === "string" ? raw : unwrap(raw);
|
|
413
|
+
if (typeof phase === "string" && phase) state.phase = phase;
|
|
414
|
+
if (state.phase === "ChampSelect") {
|
|
415
|
+
await loadAccount();
|
|
416
|
+
const [session, champSelect] = await Promise.all([
|
|
417
|
+
api.lcu.get(SESSION_URI).catch(() => null),
|
|
418
|
+
api.lcu.get(CHAMP_SELECT_URI).catch(() => null),
|
|
419
|
+
]);
|
|
420
|
+
if (session) applySession(session);
|
|
421
|
+
if (champSelect) applyChampSelect(champSelect);
|
|
422
|
+
ensureMount();
|
|
423
|
+
refreshPenalty();
|
|
424
|
+
} else {
|
|
425
|
+
stopDomObserver();
|
|
426
|
+
removeDodgeWatcher();
|
|
427
|
+
unmount();
|
|
428
|
+
state.penalty = null;
|
|
429
|
+
state.showQuitButton = false;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function handleSession(raw) {
|
|
434
|
+
applySession(raw);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function handleChampSelect(raw) {
|
|
438
|
+
applyChampSelect(raw);
|
|
439
|
+
if (state.phase === "ChampSelect") {
|
|
440
|
+
ensureMount();
|
|
441
|
+
refreshPenalty();
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function onEnable(utils) {
|
|
446
|
+
api = utils;
|
|
447
|
+
active = true;
|
|
448
|
+
removeStyle = utils.addStyle(STYLE_ID, CSS);
|
|
449
|
+
|
|
450
|
+
const handlers = {
|
|
451
|
+
[PHASE_URI]: handlePhase,
|
|
452
|
+
[SESSION_URI]: handleSession,
|
|
453
|
+
[CHAMP_SELECT_URI]: handleChampSelect,
|
|
454
|
+
};
|
|
455
|
+
for (const [uri, handler] of Object.entries(handlers)) {
|
|
456
|
+
try {
|
|
457
|
+
const off = utils.subscribe(uri, handler);
|
|
458
|
+
if (off) unsubscribes.push(off);
|
|
459
|
+
} catch {}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
loadAccount();
|
|
463
|
+
utils.lcu
|
|
464
|
+
.get(PHASE_URI)
|
|
465
|
+
.then((phase) => handlePhase(unwrap(phase)))
|
|
466
|
+
.catch(() => {});
|
|
467
|
+
utils.lcu
|
|
468
|
+
.get(SESSION_URI)
|
|
469
|
+
.then((session) => session && applySession(session))
|
|
470
|
+
.catch(() => {});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function onDisable() {
|
|
474
|
+
active = false;
|
|
475
|
+
for (const off of unsubscribes) {
|
|
476
|
+
try {
|
|
477
|
+
off();
|
|
478
|
+
} catch {}
|
|
479
|
+
}
|
|
480
|
+
unsubscribes = [];
|
|
481
|
+
stopDomObserver();
|
|
482
|
+
removeDodgeWatcher();
|
|
483
|
+
unmount();
|
|
484
|
+
removeStyle?.();
|
|
485
|
+
removeStyle = null;
|
|
486
|
+
state.phase = null;
|
|
487
|
+
state.queueId = 0;
|
|
488
|
+
state.isCustom = false;
|
|
489
|
+
state.showQuitButton = false;
|
|
490
|
+
state.gameMode = "";
|
|
491
|
+
state.account = null;
|
|
492
|
+
state.penalty = null;
|
|
493
|
+
state.lastSelfDodgeAt = 0;
|
|
494
|
+
penaltyToken = 0;
|
|
495
|
+
api = null;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
export function init() {
|
|
499
|
+
const registerWith = (utils) => {
|
|
500
|
+
utils.registerPlugin({
|
|
501
|
+
id: PLUGIN_ID,
|
|
502
|
+
name: "Dodge Helper",
|
|
503
|
+
description:
|
|
504
|
+
"Adds a DODGE/QUIT button to champion select: DODGE with the estimated LP loss for ranked queues, QUIT for every other mode. Hidden when the client already shows its own QUIT button.",
|
|
505
|
+
version,
|
|
506
|
+
defaultEnabled: true,
|
|
507
|
+
onEnable,
|
|
508
|
+
onDisable,
|
|
509
|
+
});
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
if (window.PerfectionistUtils) {
|
|
513
|
+
registerWith(window.PerfectionistUtils);
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
addEventListener("perfectionist:utils-ready", (event) => registerWith(event.detail ?? window.PerfectionistUtils), {
|
|
517
|
+
once: true,
|
|
518
|
+
});
|
|
519
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "perfectionist-plugins",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.3",
|
|
4
4
|
"description": "Small plugins that fix the annoying parts of League of Legends champ select. Built for Pengu Loader.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "perfectionist-utils.js",
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
"./unlock-profile-skins.js": "./unlock-profile-skins.js",
|
|
13
13
|
"./auto-claimer.js": "./auto-claimer.js",
|
|
14
14
|
"./auto-read.js": "./auto-read.js",
|
|
15
|
-
"./better-client.js": "./better-client.js"
|
|
15
|
+
"./better-client.js": "./better-client.js",
|
|
16
|
+
"./dodge-helper.js": "./dodge-helper.js",
|
|
17
|
+
"./auto-accept.js": "./auto-accept.js"
|
|
16
18
|
},
|
|
17
19
|
"files": [
|
|
18
20
|
"perfectionist-utils.js",
|
|
@@ -21,7 +23,9 @@
|
|
|
21
23
|
"unlock-profile-skins.js",
|
|
22
24
|
"auto-claimer.js",
|
|
23
25
|
"auto-read.js",
|
|
24
|
-
"better-client.js"
|
|
26
|
+
"better-client.js",
|
|
27
|
+
"dodge-helper.js",
|
|
28
|
+
"auto-accept.js"
|
|
25
29
|
],
|
|
26
30
|
"repository": {
|
|
27
31
|
"type": "git",
|
package/perfectionist-utils.js
CHANGED
|
@@ -61,6 +61,20 @@ const BUNDLED_MODULES = [
|
|
|
61
61
|
version: "1.2.0",
|
|
62
62
|
description: "Automatically marks quests, loot, Clash, TFT and client notices as read so no badge or dot stays lit.",
|
|
63
63
|
},
|
|
64
|
+
{
|
|
65
|
+
id: "auto-accept",
|
|
66
|
+
file: "auto-accept.js",
|
|
67
|
+
name: "Auto Accept",
|
|
68
|
+
version: "1.1.0",
|
|
69
|
+
description: "Accepts the ready check automatically, with an optional delay and a native switch in the lobby footer.",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: "dodge-helper",
|
|
73
|
+
file: "dodge-helper.js",
|
|
74
|
+
name: "Dodge Helper",
|
|
75
|
+
version: "1.4.0",
|
|
76
|
+
description: "Adds a DODGE/QUIT button to champion select: DODGE with the estimated LP loss for ranked queues, QUIT for every other mode.",
|
|
77
|
+
},
|
|
64
78
|
];
|
|
65
79
|
|
|
66
80
|
const STYLE_CSS = `
|