perfectionist-plugins 1.3.0 → 1.4.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/README.md +14 -0
- package/auto-claimer.js +374 -0
- package/ban-blocker.js +29 -10
- package/hide-unowned-skins.js +12 -5
- package/package.json +5 -3
- package/perfectionist-utils.js +62 -8
- package/unlock-profile-skins.js +2 -1
package/README.md
CHANGED
|
@@ -53,6 +53,20 @@ Built for [Pengu Loader](https://pengu.lol)
|
|
|
53
53
|
Browse the full list and pick any skin you like as your profile background.
|
|
54
54
|
</td>
|
|
55
55
|
</tr>
|
|
56
|
+
<tr>
|
|
57
|
+
<td> </td>
|
|
58
|
+
</tr>
|
|
59
|
+
<tr>
|
|
60
|
+
<th align="left">🎁 Auto Claimer</th>
|
|
61
|
+
</tr>
|
|
62
|
+
<tr>
|
|
63
|
+
<td>
|
|
64
|
+
Claims every reward for you the moment it becomes claimable — event tracks, missions and the TFT Battle Pass.
|
|
65
|
+
<br><br>
|
|
66
|
+
Three optional extras, all <b>off by default</b>: auto-buy shop items Riot lists for 1 BE,
|
|
67
|
+
auto-open mystery loot (icons, ward skins), and auto-open chests with keys.
|
|
68
|
+
</td>
|
|
69
|
+
</tr>
|
|
56
70
|
</table>
|
|
57
71
|
|
|
58
72
|
## Install
|
package/auto-claimer.js
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
const PLUGIN_ID = "auto-claimer";
|
|
2
|
+
const version = "1.0.0";
|
|
3
|
+
|
|
4
|
+
const SWEEP_DEBOUNCE_MS = 400;
|
|
5
|
+
const CLAIM_COOLDOWN_MS = 5 * 60 * 1000;
|
|
6
|
+
const BUY_COOLDOWN_MS = 30 * 60 * 1000;
|
|
7
|
+
const SHOP_THROTTLE_MS = 60 * 1000;
|
|
8
|
+
const BUY_FALLBACK_MS = 15 * 60 * 1000;
|
|
9
|
+
const MAX_ACTIONS_PER_SWEEP = 25;
|
|
10
|
+
|
|
11
|
+
const EVENTS_URI = "/lol-event-hub/v1/events";
|
|
12
|
+
const MISSIONS_URI = "/lol-missions/v1/missions";
|
|
13
|
+
const TFT_PASS_URI = "/lol-tft-pass/v1/battle-pass";
|
|
14
|
+
const TFT_PASSES_URI = "/lol-tft-pass/v1/active-passes";
|
|
15
|
+
const LOOT_URI = "/lol-loot/v1/player-loot";
|
|
16
|
+
const STORE_READY_URI = "/lol-store/v1/store-ready";
|
|
17
|
+
const CATALOG_URI = "/lol-store/v1/catalog";
|
|
18
|
+
const BUY_URI = "/lol-purchase-widget/v2/purchaseItems";
|
|
19
|
+
|
|
20
|
+
let api = null;
|
|
21
|
+
let active = false;
|
|
22
|
+
let claimTimer = null;
|
|
23
|
+
let claimRunning = false;
|
|
24
|
+
let claimQueued = false;
|
|
25
|
+
let shopDebounce = null;
|
|
26
|
+
let shopInterval = null;
|
|
27
|
+
let shopRunning = false;
|
|
28
|
+
let shopQueued = false;
|
|
29
|
+
let unsubscribes = [];
|
|
30
|
+
let sourceInstalled = false;
|
|
31
|
+
const cooldowns = new Map();
|
|
32
|
+
const menus = new Map();
|
|
33
|
+
|
|
34
|
+
function on(key) {
|
|
35
|
+
try {
|
|
36
|
+
return api?.getOption?.(PLUGIN_ID, key) === true;
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function ready(key, wait) {
|
|
43
|
+
return (cooldowns.get(key) ?? 0) + wait <= Date.now();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function stamp(key) {
|
|
47
|
+
cooldowns.set(key, Date.now());
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getJson(path) {
|
|
51
|
+
return fetch(path, { headers: { Accept: "application/json" } }).then((response) => {
|
|
52
|
+
if (!response.ok) throw new Error(`${path} responded ${response.status}`);
|
|
53
|
+
return response.json();
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function sendJson(method, path, body) {
|
|
58
|
+
return fetch(path, {
|
|
59
|
+
method,
|
|
60
|
+
headers: { Accept: "application/json", "Content-Type": "application/json" },
|
|
61
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
62
|
+
}).then((response) => {
|
|
63
|
+
if (!response.ok) throw new Error(`${method} ${path} responded ${response.status}`);
|
|
64
|
+
return response.status === 204 ? null : response.json().catch(() => null);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function sweepEvents() {
|
|
69
|
+
const events = await getJson(EVENTS_URI);
|
|
70
|
+
if (!Array.isArray(events)) return;
|
|
71
|
+
for (const entry of events) {
|
|
72
|
+
const id = entry?.eventId;
|
|
73
|
+
const unclaimed = entry?.eventInfo?.unclaimedRewardCount ?? 0;
|
|
74
|
+
if (!id || unclaimed <= 0 || !ready(`event:${id}`, CLAIM_COOLDOWN_MS)) continue;
|
|
75
|
+
stamp(`event:${id}`);
|
|
76
|
+
await sendJson("POST", `${EVENTS_URI}/${id}/reward-track/claim-all`).catch(() => {});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function sweepMissions() {
|
|
81
|
+
const missions = await getJson(MISSIONS_URI);
|
|
82
|
+
if (!Array.isArray(missions)) return;
|
|
83
|
+
for (const mission of missions) {
|
|
84
|
+
if (mission?.status !== "SELECT_REWARDS") continue;
|
|
85
|
+
const id = mission.id;
|
|
86
|
+
if (!id || !ready(`mission:${id}`, CLAIM_COOLDOWN_MS)) continue;
|
|
87
|
+
const rewards = Array.isArray(mission.rewards) ? mission.rewards : [];
|
|
88
|
+
const groups = [...new Set(rewards.map((reward) => reward?.rewardGroup).filter((group) => group != null && group !== ""))];
|
|
89
|
+
if (groups.length === 0) continue;
|
|
90
|
+
if (groups.length > 1 && (mission.rewardStrategy?.selectMaxGroupCount ?? 0) > 0) continue;
|
|
91
|
+
stamp(`mission:${id}`);
|
|
92
|
+
await sendJson("PUT", `/lol-missions/v1/player/${id}`, { rewardGroups: [groups[0]] }).catch(() => {});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function sweepTftPasses() {
|
|
97
|
+
const seen = new Set();
|
|
98
|
+
for (const uri of [TFT_PASS_URI, TFT_PASSES_URI]) {
|
|
99
|
+
const data = await getJson(uri).catch(() => null);
|
|
100
|
+
const passes = Array.isArray(data) ? data : data ? [data] : [];
|
|
101
|
+
for (const pass of passes) {
|
|
102
|
+
const passId = pass?.info?.passId;
|
|
103
|
+
if (!passId || seen.has(passId)) continue;
|
|
104
|
+
seen.add(passId);
|
|
105
|
+
const milestones = [...(Array.isArray(pass.milestones) ? pass.milestones : []), ...(Array.isArray(pass.bonuses) ? pass.bonuses : [])];
|
|
106
|
+
const claimable = milestones.some(
|
|
107
|
+
(m) => m && !m.isLocked && !m.isClaimRequestPending && (m.status === "REWARDABLE" || m.status === "CLAIMABLE"),
|
|
108
|
+
);
|
|
109
|
+
if (!claimable || !ready(`tftpass:${passId}`, CLAIM_COOLDOWN_MS)) continue;
|
|
110
|
+
stamp(`tftpass:${passId}`);
|
|
111
|
+
await sendJson("PUT", `/lol-tft-pass/v1/pass/${passId}/milestone/claimAllRewards`).catch(() => {});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function menuFor(lootId) {
|
|
117
|
+
if (!menus.has(lootId)) {
|
|
118
|
+
const promise = getJson(`/lol-loot/v1/player-loot/${encodeURIComponent(lootId)}/context-menu`).catch(() => {
|
|
119
|
+
menus.delete(lootId);
|
|
120
|
+
return null;
|
|
121
|
+
});
|
|
122
|
+
menus.set(lootId, promise);
|
|
123
|
+
}
|
|
124
|
+
return menus.get(lootId);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function sweepLoot() {
|
|
128
|
+
const loot = await getJson(LOOT_URI);
|
|
129
|
+
if (!Array.isArray(loot)) return;
|
|
130
|
+
let actions = 0;
|
|
131
|
+
for (const item of loot) {
|
|
132
|
+
if (actions >= MAX_ACTIONS_PER_SWEEP) break;
|
|
133
|
+
const lootId = item?.lootId;
|
|
134
|
+
const count = item?.count ?? 0;
|
|
135
|
+
if (!lootId || count <= 0 || /^(CURRENCY|MATERIAL)_/.test(lootId)) continue;
|
|
136
|
+
if (!ready(`loot:${lootId}`, CLAIM_COOLDOWN_MS)) continue;
|
|
137
|
+
const menu = await menuFor(lootId);
|
|
138
|
+
const open = (Array.isArray(menu) ? menu : []).find((action) => action?.actionType === "OPEN" && action.name && action.enabled !== false);
|
|
139
|
+
if (!open) continue;
|
|
140
|
+
const needsKey = (open.requiredOthersCount ?? 0) > 0;
|
|
141
|
+
if (needsKey ? !on("openChests") : !on("openLoot")) continue;
|
|
142
|
+
stamp(`loot:${lootId}`);
|
|
143
|
+
actions++;
|
|
144
|
+
const repeat = Math.min(count, 25);
|
|
145
|
+
await sendJson("POST", `/lol-loot/v1/recipes/${encodeURIComponent(open.name)}/craft?repeat=${repeat}`, [lootId]).catch(() => {});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function isOwned(inventoryType, itemId) {
|
|
150
|
+
if (!inventoryType || itemId == null) return null;
|
|
151
|
+
const list = await getJson(`/lol-inventory/v2/inventory/${encodeURIComponent(inventoryType)}`).catch(() => null);
|
|
152
|
+
if (!Array.isArray(list)) return null;
|
|
153
|
+
return list.some((entry) => entry?.itemId === itemId);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function runShop() {
|
|
157
|
+
if (!active || shopRunning) {
|
|
158
|
+
if (shopRunning) shopQueued = true;
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (!on("buy1Be") || !ready("shop-catalog", SHOP_THROTTLE_MS)) return;
|
|
162
|
+
shopRunning = true;
|
|
163
|
+
stamp("shop-catalog");
|
|
164
|
+
try {
|
|
165
|
+
const catalog = await getJson(CATALOG_URI);
|
|
166
|
+
if (!Array.isArray(catalog)) return;
|
|
167
|
+
const cheap = catalog.filter(
|
|
168
|
+
(item) =>
|
|
169
|
+
item?.active === true &&
|
|
170
|
+
Array.isArray(item.prices) &&
|
|
171
|
+
item.prices.some((price) => price?.cost === 1 && price?.currency === "IP"),
|
|
172
|
+
);
|
|
173
|
+
for (const item of cheap) {
|
|
174
|
+
const key = `buy:${item.inventoryType}:${item.itemId}`;
|
|
175
|
+
if (!ready(key, BUY_COOLDOWN_MS)) continue;
|
|
176
|
+
const owned = await isOwned(item.inventoryType, item.itemId);
|
|
177
|
+
if (owned === null) continue;
|
|
178
|
+
stamp(key);
|
|
179
|
+
if (owned) continue;
|
|
180
|
+
await sendJson("POST", BUY_URI, {
|
|
181
|
+
items: [
|
|
182
|
+
{
|
|
183
|
+
itemKey: { itemId: item.itemId, inventoryType: item.inventoryType },
|
|
184
|
+
purchaseCurrencyInfo: { currencyType: "IP", price: 1, purchasable: true },
|
|
185
|
+
source: "store",
|
|
186
|
+
featuredSection: "",
|
|
187
|
+
quantity: 1,
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
}).catch(() => {});
|
|
191
|
+
}
|
|
192
|
+
} finally {
|
|
193
|
+
shopRunning = false;
|
|
194
|
+
if (shopQueued) {
|
|
195
|
+
shopQueued = false;
|
|
196
|
+
scheduleShop();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function scheduleClaims() {
|
|
202
|
+
if (!active) return;
|
|
203
|
+
clearTimeout(claimTimer);
|
|
204
|
+
claimTimer = setTimeout(runClaims, SWEEP_DEBOUNCE_MS);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function scheduleShop() {
|
|
208
|
+
if (!active || !on("buy1Be")) return;
|
|
209
|
+
clearTimeout(shopDebounce);
|
|
210
|
+
shopDebounce = setTimeout(runShop, SWEEP_DEBOUNCE_MS);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function runClaims() {
|
|
214
|
+
if (!active) return;
|
|
215
|
+
if (claimRunning) {
|
|
216
|
+
claimQueued = true;
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
claimRunning = true;
|
|
220
|
+
try {
|
|
221
|
+
await sweepEvents().catch(() => {});
|
|
222
|
+
await sweepMissions().catch(() => {});
|
|
223
|
+
await sweepTftPasses().catch(() => {});
|
|
224
|
+
if (on("openLoot") || on("openChests")) await sweepLoot().catch(() => {});
|
|
225
|
+
} finally {
|
|
226
|
+
claimRunning = false;
|
|
227
|
+
if (claimQueued) {
|
|
228
|
+
claimQueued = false;
|
|
229
|
+
scheduleClaims();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function syncShopTimer() {
|
|
235
|
+
if (active && on("buy1Be")) {
|
|
236
|
+
if (!shopInterval) shopInterval = setInterval(scheduleShop, BUY_FALLBACK_MS);
|
|
237
|
+
scheduleShop();
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
clearInterval(shopInterval);
|
|
241
|
+
clearTimeout(shopDebounce);
|
|
242
|
+
shopInterval = null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function noteTraffic(url) {
|
|
246
|
+
if (!active || typeof url !== "string") return;
|
|
247
|
+
if (url.includes("/lol-store/v1/catalog")) scheduleShop();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function installSourceWatch() {
|
|
251
|
+
if (sourceInstalled) return;
|
|
252
|
+
sourceInstalled = true;
|
|
253
|
+
const original = window.fetch;
|
|
254
|
+
if (typeof original === "function" && !original.__perfClaimWatch) {
|
|
255
|
+
const wrapped = function (input, init) {
|
|
256
|
+
noteTraffic(typeof input === "string" ? input : (input && input.url) || "");
|
|
257
|
+
return original.call(this, input, init);
|
|
258
|
+
};
|
|
259
|
+
wrapped.__perfClaimWatch = true;
|
|
260
|
+
window.fetch = wrapped;
|
|
261
|
+
}
|
|
262
|
+
const proto = window.XMLHttpRequest && window.XMLHttpRequest.prototype;
|
|
263
|
+
if (proto && !proto.open.__perfClaimWatch) {
|
|
264
|
+
const origOpen = proto.open;
|
|
265
|
+
const origSend = proto.send;
|
|
266
|
+
proto.open = function (method, url, ...rest) {
|
|
267
|
+
this.__perfClaimUrl = typeof url === "string" ? url : "";
|
|
268
|
+
return origOpen.call(this, method, url, ...rest);
|
|
269
|
+
};
|
|
270
|
+
proto.open.__perfClaimWatch = true;
|
|
271
|
+
proto.send = function (...args) {
|
|
272
|
+
noteTraffic(this.__perfClaimUrl || "");
|
|
273
|
+
return origSend.apply(this, args);
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function onEnable(utils) {
|
|
279
|
+
api = utils;
|
|
280
|
+
active = true;
|
|
281
|
+
cooldowns.clear();
|
|
282
|
+
menus.clear();
|
|
283
|
+
const claimPush = () => scheduleClaims();
|
|
284
|
+
const shopPush = () => {
|
|
285
|
+
if (on("buy1Be")) scheduleShop();
|
|
286
|
+
};
|
|
287
|
+
for (const uri of [EVENTS_URI, MISSIONS_URI, TFT_PASS_URI, TFT_PASSES_URI, LOOT_URI]) {
|
|
288
|
+
try {
|
|
289
|
+
const off = api.subscribe(uri, claimPush);
|
|
290
|
+
if (off) unsubscribes.push(off);
|
|
291
|
+
} catch {}
|
|
292
|
+
}
|
|
293
|
+
try {
|
|
294
|
+
const off = api.subscribe(STORE_READY_URI, shopPush);
|
|
295
|
+
if (off) unsubscribes.push(off);
|
|
296
|
+
} catch {}
|
|
297
|
+
scheduleClaims();
|
|
298
|
+
syncShopTimer();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function onDisable() {
|
|
302
|
+
active = false;
|
|
303
|
+
clearTimeout(claimTimer);
|
|
304
|
+
clearTimeout(shopDebounce);
|
|
305
|
+
clearInterval(shopInterval);
|
|
306
|
+
claimTimer = null;
|
|
307
|
+
shopDebounce = null;
|
|
308
|
+
shopInterval = null;
|
|
309
|
+
claimRunning = false;
|
|
310
|
+
claimQueued = false;
|
|
311
|
+
shopRunning = false;
|
|
312
|
+
shopQueued = false;
|
|
313
|
+
for (const off of unsubscribes) {
|
|
314
|
+
try {
|
|
315
|
+
off();
|
|
316
|
+
} catch {}
|
|
317
|
+
}
|
|
318
|
+
unsubscribes = [];
|
|
319
|
+
cooldowns.clear();
|
|
320
|
+
menus.clear();
|
|
321
|
+
api = null;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function onOptionChange(key) {
|
|
325
|
+
if (!active) return;
|
|
326
|
+
if (key === "buy1Be") {
|
|
327
|
+
syncShopTimer();
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
scheduleClaims();
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function init() {
|
|
334
|
+
installSourceWatch();
|
|
335
|
+
|
|
336
|
+
const registerWith = (utils) => {
|
|
337
|
+
utils.registerPlugin({
|
|
338
|
+
id: PLUGIN_ID,
|
|
339
|
+
name: "Auto Claimer",
|
|
340
|
+
description: "Automatically claims battle pass, event and mission rewards the moment they become claimable.",
|
|
341
|
+
version,
|
|
342
|
+
defaultEnabled: true,
|
|
343
|
+
options: [
|
|
344
|
+
{
|
|
345
|
+
key: "buy1Be",
|
|
346
|
+
label: "Auto-buy 1 BE shop items",
|
|
347
|
+
description: "Instantly buys items Riot lists in the shop for 1 BE (wards, icons, emotes, bundles).",
|
|
348
|
+
default: false,
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
key: "openLoot",
|
|
352
|
+
label: "Auto-open loot (mystery icons, ward skins, ...)",
|
|
353
|
+
description: "Opens free mystery loot the moment you get it. Never touches items that need a key.",
|
|
354
|
+
default: false,
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
key: "openChests",
|
|
358
|
+
label: "Auto-open chests with keys",
|
|
359
|
+
description: "Opens chest + key loot automatically while you have keys.",
|
|
360
|
+
default: false,
|
|
361
|
+
},
|
|
362
|
+
],
|
|
363
|
+
onEnable,
|
|
364
|
+
onDisable,
|
|
365
|
+
onOptionChange,
|
|
366
|
+
});
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
if (window.PerfectionistUtils) {
|
|
370
|
+
registerWith(window.PerfectionistUtils);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
addEventListener("perfectionist:utils-ready", (e) => registerWith(e.detail ?? window.PerfectionistUtils), { once: true });
|
|
374
|
+
}
|
package/ban-blocker.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const version = "3.
|
|
1
|
+
export const version = "3.6.0";
|
|
2
2
|
|
|
3
3
|
const PLUGIN_ID = "ban-blocker";
|
|
4
4
|
const STYLE_ID = "perfectionist-ban-blocker-style";
|
|
@@ -118,7 +118,7 @@ function payloadName(p) {
|
|
|
118
118
|
|
|
119
119
|
function keyOf(p) {
|
|
120
120
|
if (typeof p === "number") return p;
|
|
121
|
-
const strong = p?.summonerId || p?.puuid;
|
|
121
|
+
const strong = p?.summonerId || p?.puuid || p?.obfuscatedPuuid || p?.obfuscatedSummonerId;
|
|
122
122
|
if (strong) return strong;
|
|
123
123
|
const n = payloadName(p);
|
|
124
124
|
if (n) return `name:${n}`;
|
|
@@ -126,7 +126,8 @@ function keyOf(p) {
|
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
function linkPlayer(p, rowName, cellId) {
|
|
129
|
-
const strong =
|
|
129
|
+
const strong =
|
|
130
|
+
typeof p === "number" ? p : p?.summonerId || p?.puuid || p?.obfuscatedPuuid || p?.obfuscatedSummonerId;
|
|
130
131
|
const pn = typeof p === "number" ? "" : payloadName(p);
|
|
131
132
|
const rn = norm(rowName);
|
|
132
133
|
const keys = [];
|
|
@@ -208,7 +209,7 @@ function decide(s) {
|
|
|
208
209
|
|
|
209
210
|
cellToPlayer.clear();
|
|
210
211
|
for (const p of m) {
|
|
211
|
-
const row = rows.find((r) => r.slotId === p.cellId);
|
|
212
|
+
const row = rows.find((r) => (r.cell ?? r.slotId) === p.cellId);
|
|
212
213
|
cellToPlayer.set(p.cellId, linkPlayer(p, row?.name, p.cellId));
|
|
213
214
|
}
|
|
214
215
|
|
|
@@ -310,7 +311,8 @@ function detectRows() {
|
|
|
310
311
|
return found.map((r, i) => ({
|
|
311
312
|
...r,
|
|
312
313
|
index: i,
|
|
313
|
-
player: rows[i]?.player ?? null,
|
|
314
|
+
player: rows[i]?.el === r.el ? (rows[i]?.player ?? null) : null,
|
|
315
|
+
cell: rows[i]?.el === r.el ? (rows[i]?.cell ?? null) : null,
|
|
314
316
|
}));
|
|
315
317
|
} catch (err) {
|
|
316
318
|
logError("row detection failed.", err);
|
|
@@ -362,11 +364,27 @@ function bindRowsToTeam(session) {
|
|
|
362
364
|
if (!list.length || !cells.length) return;
|
|
363
365
|
for (const c of cells) ensureName(c);
|
|
364
366
|
|
|
367
|
+
const selfIdx = list.findIndex((r) => isSelfRow(r));
|
|
368
|
+
const selfCellIdx =
|
|
369
|
+
session?.localPlayerCellId != null ? cells.findIndex((c) => c.cellId === session.localPlayerCellId) : -1;
|
|
370
|
+
let pick;
|
|
371
|
+
if (selfIdx >= 0 && selfCellIdx >= 0) {
|
|
372
|
+
const selfSlot = list[selfIdx].slotId;
|
|
373
|
+
if (selfSlot != null && selfSlot === cells[selfCellIdx].cellId) {
|
|
374
|
+
pick = (r) => (r.slotId != null ? cells.find((c) => c.cellId === r.slotId) : null);
|
|
375
|
+
} else if (selfIdx === selfCellIdx) {
|
|
376
|
+
pick = (r) => (r.slotId != null ? cells[r.slotId] : null) ?? cells[r.index];
|
|
377
|
+
} else {
|
|
378
|
+
pick = () => null;
|
|
379
|
+
}
|
|
380
|
+
} else {
|
|
381
|
+
pick = (r) => (r.slotId != null ? cells.find((c) => c.cellId === r.slotId) : null);
|
|
382
|
+
}
|
|
383
|
+
|
|
365
384
|
for (const r of list) {
|
|
366
|
-
const cell =
|
|
367
|
-
(r.slotId != null ? cells.find((c) => c.cellId === r.slotId) : null) ??
|
|
368
|
-
cells.find((c) => nameMatches(r.name, keyOf(c)));
|
|
385
|
+
const cell = pick(r) ?? cells.find((c) => nameMatches(r.name, keyOf(c)));
|
|
369
386
|
if (cell) {
|
|
387
|
+
r.cell = cell.cellId;
|
|
370
388
|
r.player = linkPlayer(cell, r.name, cell.cellId);
|
|
371
389
|
cellToPlayer.set(cell.cellId, r.player);
|
|
372
390
|
} else {
|
|
@@ -428,7 +446,7 @@ function buildBadge(slot) {
|
|
|
428
446
|
|
|
429
447
|
function isSelfRow(r) {
|
|
430
448
|
if (r.el.classList.contains("is-self")) return true;
|
|
431
|
-
return lastSession?.localPlayerCellId != null && r.slotId === lastSession.localPlayerCellId;
|
|
449
|
+
return lastSession?.localPlayerCellId != null && (r.cell ?? r.slotId) === lastSession.localPlayerCellId;
|
|
432
450
|
}
|
|
433
451
|
|
|
434
452
|
function nativeSwapIn(circle) {
|
|
@@ -455,7 +473,8 @@ function ensureButtons() {
|
|
|
455
473
|
buttons.set(r.index, btn);
|
|
456
474
|
}
|
|
457
475
|
btn.dataset.slot = String(r.index);
|
|
458
|
-
|
|
476
|
+
const cell = r.cell ?? r.slotId;
|
|
477
|
+
btn.dataset.cell = cell == null ? "" : String(cell);
|
|
459
478
|
}
|
|
460
479
|
for (const [slot, btn] of [...buttons]) {
|
|
461
480
|
if (!list[slot] || !btn.isConnected) {
|
package/hide-unowned-skins.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const version = "2.3.
|
|
1
|
+
export const version = "2.3.2";
|
|
2
2
|
|
|
3
3
|
const PLUGIN_ID = "hide-unowned-skins";
|
|
4
4
|
const STYLE_ID = "perfectionist-hide-unowned-skins-style";
|
|
@@ -259,6 +259,7 @@ function ensureSourceFilter() {
|
|
|
259
259
|
let api = null;
|
|
260
260
|
let summonerId = null;
|
|
261
261
|
let ownedSkinIds = null;
|
|
262
|
+
let allSkinIds = null;
|
|
262
263
|
let currentChampionId = null;
|
|
263
264
|
let carouselOrder = [];
|
|
264
265
|
let selectedSkinId = null;
|
|
@@ -379,7 +380,7 @@ function applyAll() {
|
|
|
379
380
|
let hidden = 0;
|
|
380
381
|
items.forEach((item) => {
|
|
381
382
|
const skinId = currentChampionId === null ? null : readSkinId(item);
|
|
382
|
-
const hide = ownedSkinIds !== null && skinId !== null && !ownedSkinIds.has(skinId);
|
|
383
|
+
const hide = ownedSkinIds !== null && skinId !== null && allSkinIds !== null && allSkinIds.has(skinId) && !ownedSkinIds.has(skinId);
|
|
383
384
|
if (hide) hidden++;
|
|
384
385
|
item.classList.toggle(HIDDEN_CLASS, hide);
|
|
385
386
|
hideTargetFor(item).classList.toggle(HIDDEN_CLASS, hide);
|
|
@@ -390,7 +391,7 @@ function applyAll() {
|
|
|
390
391
|
const dotsMapped = carouselOrder.length > 0 && dots.length === carouselOrder.length;
|
|
391
392
|
dots.forEach((dot, index) => {
|
|
392
393
|
const skinId = dotsMapped ? carouselOrder[index] : undefined;
|
|
393
|
-
const hide = ownedSkinIds !== null && skinId !== undefined && !ownedSkinIds.has(skinId);
|
|
394
|
+
const hide = ownedSkinIds !== null && skinId !== undefined && allSkinIds !== null && allSkinIds.has(skinId) && !ownedSkinIds.has(skinId);
|
|
394
395
|
if (hide) dotsHidden++;
|
|
395
396
|
dot.classList.toggle(HIDDEN_CLASS, hide);
|
|
396
397
|
});
|
|
@@ -487,6 +488,7 @@ async function selectSkin(skinId) {
|
|
|
487
488
|
function resetState() {
|
|
488
489
|
requestToken++;
|
|
489
490
|
ownedSkinIds = null;
|
|
491
|
+
allSkinIds = null;
|
|
490
492
|
currentChampionId = null;
|
|
491
493
|
selectedSkinId = null;
|
|
492
494
|
carouselOrder = [];
|
|
@@ -500,6 +502,7 @@ function resetState() {
|
|
|
500
502
|
function loadChampion(championId) {
|
|
501
503
|
currentChampionId = championId;
|
|
502
504
|
ownedSkinIds = null;
|
|
505
|
+
allSkinIds = null;
|
|
503
506
|
failure = null;
|
|
504
507
|
clientSkinOrder = [];
|
|
505
508
|
const token = ++requestToken;
|
|
@@ -508,6 +511,7 @@ function loadChampion(championId) {
|
|
|
508
511
|
([{ owned, order: inventoryOrder }, carouselIds]) => {
|
|
509
512
|
if (token !== requestToken) return;
|
|
510
513
|
ownedSkinIds = owned;
|
|
514
|
+
allSkinIds = new Set(inventoryOrder);
|
|
511
515
|
carouselOrder = carouselIds.length > 0 ? carouselIds : inventoryOrder;
|
|
512
516
|
applyAllGuarded();
|
|
513
517
|
},
|
|
@@ -542,7 +546,8 @@ function updateForSession(session, selection) {
|
|
|
542
546
|
const belongsToChampion = serverSkinId !== null && Math.floor(serverSkinId / 1000) === championId;
|
|
543
547
|
if (!selectInFlight && belongsToChampion) selectedSkinId = serverSkinId;
|
|
544
548
|
|
|
545
|
-
|
|
549
|
+
const knownUnownedSkin = allSkinIds !== null && allSkinIds.has(serverSkinId) && !ownedSkinIds.has(serverSkinId);
|
|
550
|
+
if (!selectInFlight && knownUnownedSkin) {
|
|
546
551
|
const fallback = nextOwnedSkin(ownedSkinIds, 0, 1);
|
|
547
552
|
if (fallback !== null) {
|
|
548
553
|
selectSkin(fallback).catch((error) => logError("could not correct the selected skin.", error));
|
|
@@ -591,8 +596,10 @@ function onEnable(utils) {
|
|
|
591
596
|
removeStyle = api.addStyle(STYLE_ID, CSS);
|
|
592
597
|
|
|
593
598
|
renderObserver = new MutationObserver(() => {
|
|
599
|
+
const item = document.querySelector(SKIN_ITEM_SELECTOR);
|
|
600
|
+
if (!item && !document.querySelector(DOT_SELECTOR)) return;
|
|
594
601
|
applyAllGuarded();
|
|
595
|
-
if (
|
|
602
|
+
if (item) scheduleRefresh();
|
|
596
603
|
});
|
|
597
604
|
renderObserver.observe(document.documentElement ?? document, OBSERVER_OPTIONS);
|
|
598
605
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "perfectionist-plugins",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.1",
|
|
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",
|
|
@@ -9,13 +9,15 @@
|
|
|
9
9
|
"./perfectionist-utils.js": "./perfectionist-utils.js",
|
|
10
10
|
"./ban-blocker.js": "./ban-blocker.js",
|
|
11
11
|
"./hide-unowned-skins.js": "./hide-unowned-skins.js",
|
|
12
|
-
"./unlock-profile-skins.js": "./unlock-profile-skins.js"
|
|
12
|
+
"./unlock-profile-skins.js": "./unlock-profile-skins.js",
|
|
13
|
+
"./auto-claimer.js": "./auto-claimer.js"
|
|
13
14
|
},
|
|
14
15
|
"files": [
|
|
15
16
|
"perfectionist-utils.js",
|
|
16
17
|
"ban-blocker.js",
|
|
17
18
|
"hide-unowned-skins.js",
|
|
18
|
-
"unlock-profile-skins.js"
|
|
19
|
+
"unlock-profile-skins.js",
|
|
20
|
+
"auto-claimer.js"
|
|
19
21
|
],
|
|
20
22
|
"repository": {
|
|
21
23
|
"type": "git",
|
package/perfectionist-utils.js
CHANGED
|
@@ -6,7 +6,7 @@ const PANEL_ID = "perfectionist-plugins-panel";
|
|
|
6
6
|
const STYLE_ID = "perfectionist-plugins-style";
|
|
7
7
|
const TAB_TITLE = "Perfectionism";
|
|
8
8
|
|
|
9
|
-
export const version = "1.
|
|
9
|
+
export const version = "1.2.0";
|
|
10
10
|
|
|
11
11
|
const PKG = "perfectionist-plugins";
|
|
12
12
|
const CDN = "https://cdn.jsdelivr.net/npm";
|
|
@@ -26,23 +26,30 @@ const BUNDLED_MODULES = [
|
|
|
26
26
|
id: "ban-blocker",
|
|
27
27
|
file: "ban-blocker.js",
|
|
28
28
|
name: "Ban Blocker",
|
|
29
|
-
version: "3.
|
|
29
|
+
version: "3.6.0",
|
|
30
30
|
description: "During bans, blocking a teammate instantly claims the champion they hover to ban.",
|
|
31
31
|
},
|
|
32
32
|
{
|
|
33
33
|
id: "hide-unowned-skins",
|
|
34
34
|
file: "hide-unowned-skins.js",
|
|
35
35
|
name: "Hide Unowned Skins",
|
|
36
|
-
version: "2.3.
|
|
36
|
+
version: "2.3.2",
|
|
37
37
|
description: "In champion select, only shows the skins you own for the picked champion.",
|
|
38
38
|
},
|
|
39
39
|
{
|
|
40
40
|
id: "unlock-profile-skins",
|
|
41
41
|
file: "unlock-profile-skins.js",
|
|
42
42
|
name: "Unlock Profile Skins",
|
|
43
|
-
version: "1.2.
|
|
43
|
+
version: "1.2.1",
|
|
44
44
|
description: "Unlocks every skin in the Set profile background window.",
|
|
45
45
|
},
|
|
46
|
+
{
|
|
47
|
+
id: "auto-claimer",
|
|
48
|
+
file: "auto-claimer.js",
|
|
49
|
+
name: "Auto Claimer",
|
|
50
|
+
version: "1.0.0",
|
|
51
|
+
description: "Automatically claims battle pass, event and mission rewards the moment they become claimable.",
|
|
52
|
+
},
|
|
46
53
|
];
|
|
47
54
|
|
|
48
55
|
const STYLE_CSS = `
|
|
@@ -77,6 +84,10 @@ const STYLE_CSS = `
|
|
|
77
84
|
#${PANEL_ID} .perf-uninstall::after { content: ""; width: 11px; height: 11px;
|
|
78
85
|
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2.5 2.5L9.5 9.5M9.5 2.5L2.5 9.5' fill='none' stroke='%23c89b3c' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E") center / 11px 11px no-repeat; }
|
|
79
86
|
#${PANEL_ID} .perf-empty { font-size: 12px; color: #a09b8c; }
|
|
87
|
+
#${PANEL_ID} .perf-opts { margin-top: 11px; display: flex; flex-direction: column; gap: 8px; }
|
|
88
|
+
#${PANEL_ID} .perf-opt { display: flex; align-items: center; gap: 9px; cursor: pointer; }
|
|
89
|
+
#${PANEL_ID} .perf-opt .perf-opt-label { font-size: 11px; letter-spacing: 0.3px; color: #a09b8c; }
|
|
90
|
+
#${PANEL_ID} .perf-opt:hover .perf-opt-label { color: #f0e6d2; }
|
|
80
91
|
|
|
81
92
|
.perf-update { position: fixed; inset: 0; z-index: 100000; display: flex; align-items: center;
|
|
82
93
|
justify-content: center; background: rgba(1, 10, 19, 0.72); }
|
|
@@ -104,7 +115,7 @@ let availableModules = BUNDLED_MODULES;
|
|
|
104
115
|
let pendingReload = false;
|
|
105
116
|
let runningVersion = version;
|
|
106
117
|
|
|
107
|
-
let state = { installed: {}, enabled: {} };
|
|
118
|
+
let state = { installed: {}, enabled: {}, options: {} };
|
|
108
119
|
|
|
109
120
|
async function loadState() {
|
|
110
121
|
let saved = null;
|
|
@@ -117,7 +128,7 @@ async function loadState() {
|
|
|
117
128
|
saved = null;
|
|
118
129
|
}
|
|
119
130
|
if (saved?.installed && typeof saved.installed === "object") {
|
|
120
|
-
state = { installed: saved.installed, enabled: saved.enabled ?? {} };
|
|
131
|
+
state = { installed: saved.installed, enabled: saved.enabled ?? {}, options: saved.options ?? {} };
|
|
121
132
|
return;
|
|
122
133
|
}
|
|
123
134
|
saveState();
|
|
@@ -128,6 +139,7 @@ function saveState() {
|
|
|
128
139
|
version: /^\d/.test(runningVersion) ? runningVersion : undefined,
|
|
129
140
|
installed: state.installed,
|
|
130
141
|
enabled: state.enabled,
|
|
142
|
+
options: state.options,
|
|
131
143
|
};
|
|
132
144
|
try {
|
|
133
145
|
localStorage.setItem(STATE_KEY, JSON.stringify(record));
|
|
@@ -171,7 +183,7 @@ function applyLifecycle(entry, enabled) {
|
|
|
171
183
|
}
|
|
172
184
|
|
|
173
185
|
function registerPlugin(definition) {
|
|
174
|
-
const { id, name, description = "", version = "1.0.0", defaultEnabled = true, onEnable, onDisable } = definition ?? {};
|
|
186
|
+
const { id, name, description = "", version = "1.0.0", defaultEnabled = true, options = [], onEnable, onDisable, onOptionChange } = definition ?? {};
|
|
175
187
|
if (!id || !name) {
|
|
176
188
|
throw new Error("[Perfectionism] registerPlugin needs an id and a name.");
|
|
177
189
|
}
|
|
@@ -179,7 +191,7 @@ function registerPlugin(definition) {
|
|
|
179
191
|
console.warn(`[Perfectionism] ${name} is already registered.`);
|
|
180
192
|
return;
|
|
181
193
|
}
|
|
182
|
-
const entry = { id, name, description, version, defaultEnabled, onEnable, onDisable, active: false };
|
|
194
|
+
const entry = { id, name, description, version, defaultEnabled, options, onEnable, onDisable, onOptionChange, active: false };
|
|
183
195
|
registry.set(id, entry);
|
|
184
196
|
applyLifecycle(entry, isPluginEnabled(id));
|
|
185
197
|
refreshPanel();
|
|
@@ -203,6 +215,26 @@ function setPluginEnabled(id, enabled) {
|
|
|
203
215
|
return true;
|
|
204
216
|
}
|
|
205
217
|
|
|
218
|
+
function getOption(id, key) {
|
|
219
|
+
const entry = registry.get(id);
|
|
220
|
+
const fallback = entry?.options?.find((option) => option.key === key)?.default;
|
|
221
|
+
const stored = state.options?.[id]?.[key];
|
|
222
|
+
return stored === undefined ? Boolean(fallback) : Boolean(stored);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function setOption(id, key, value) {
|
|
226
|
+
if (!state.options[id]) state.options[id] = {};
|
|
227
|
+
state.options[id][key] = Boolean(value);
|
|
228
|
+
saveState();
|
|
229
|
+
const entry = registry.get(id);
|
|
230
|
+
try {
|
|
231
|
+
entry?.onOptionChange?.(key, Boolean(value));
|
|
232
|
+
} catch (error) {
|
|
233
|
+
logError(`${entry?.name ?? id} could not apply the option change.`, error);
|
|
234
|
+
}
|
|
235
|
+
refreshPanel();
|
|
236
|
+
}
|
|
237
|
+
|
|
206
238
|
function getPlugins() {
|
|
207
239
|
return [...registry.values()]
|
|
208
240
|
.map(({ id, name, description, version }) => ({ id, name, description, version, enabled: isPluginEnabled(id) }))
|
|
@@ -510,6 +542,24 @@ function el(tag, className, text) {
|
|
|
510
542
|
return node;
|
|
511
543
|
}
|
|
512
544
|
|
|
545
|
+
function buildOptions(mod) {
|
|
546
|
+
const options = registry.get(mod.id)?.options ?? [];
|
|
547
|
+
if (options.length === 0) return null;
|
|
548
|
+
const wrap = el("div", "perf-opts");
|
|
549
|
+
for (const option of options) {
|
|
550
|
+
const row = el("label", "perf-opt perf-toggle");
|
|
551
|
+
row.title = option.description ?? "";
|
|
552
|
+
const input = el("input");
|
|
553
|
+
input.type = "checkbox";
|
|
554
|
+
input.checked = getOption(mod.id, option.key);
|
|
555
|
+
input.setAttribute("aria-label", `${mod.name}: ${option.label}`);
|
|
556
|
+
input.addEventListener("change", () => setOption(mod.id, option.key, input.checked));
|
|
557
|
+
row.append(input, el("span", "perf-box"), el("span", "perf-opt-label", option.label));
|
|
558
|
+
wrap.append(row);
|
|
559
|
+
}
|
|
560
|
+
return wrap;
|
|
561
|
+
}
|
|
562
|
+
|
|
513
563
|
function buildToggle(mod) {
|
|
514
564
|
const label = el("label", "perf-toggle");
|
|
515
565
|
label.title = `Enable ${mod.name}`;
|
|
@@ -556,6 +606,8 @@ function buildCard(mod) {
|
|
|
556
606
|
name.append(el("span", "perf-version", `v${registry.get(mod.id)?.version ?? mod.version}`));
|
|
557
607
|
info.append(name);
|
|
558
608
|
if (mod.description) info.append(el("div", "perf-desc", mod.description));
|
|
609
|
+
const optionList = buildOptions(mod);
|
|
610
|
+
if (optionList) info.append(optionList);
|
|
559
611
|
|
|
560
612
|
const actions = el("div", "perf-actions");
|
|
561
613
|
if (installed) actions.append(buildToggle(mod));
|
|
@@ -727,6 +779,8 @@ const utils = {
|
|
|
727
779
|
setPluginEnabled,
|
|
728
780
|
isPluginEnabled,
|
|
729
781
|
getPlugins,
|
|
782
|
+
getOption,
|
|
783
|
+
setOption,
|
|
730
784
|
getAvailableModules,
|
|
731
785
|
installModule,
|
|
732
786
|
uninstallModule,
|
package/unlock-profile-skins.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const version = "1.2.
|
|
1
|
+
export const version = "1.2.1";
|
|
2
2
|
|
|
3
3
|
const PLUGIN_ID = "unlock-profile-skins";
|
|
4
4
|
const PICKER_BUTTON_SELECTOR = ".style-profile-skin-picker-button";
|
|
@@ -294,6 +294,7 @@ function applyTierGrouping() {
|
|
|
294
294
|
tierAppliedForOpen = false;
|
|
295
295
|
}
|
|
296
296
|
if (tierAppliedForOpen) return;
|
|
297
|
+
if (!isPickerOpen()) return;
|
|
297
298
|
const dd = tierDropdown ?? findGroupingDropdown();
|
|
298
299
|
if (!dd) {
|
|
299
300
|
tierAppliedForOpen = false;
|