perfectionist-plugins 2.4.7 → 2.5.0
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 +52 -52
- package/auto-accept.js +296 -294
- package/auto-claimer.js +1 -37
- package/auto-read.js +107 -42
- package/ban-blocker.js +483 -677
- package/better-client.js +362 -793
- package/dodge-helper.js +375 -673
- package/hide-unowned-skins.js +63 -686
- package/package.json +40 -40
- package/perfectionist-utils.js +540 -301
- package/unlock-profile-skins.js +174 -620
package/dodge-helper.js
CHANGED
|
@@ -1,128 +1,79 @@
|
|
|
1
1
|
const PLUGIN_ID = "dodge-helper";
|
|
2
|
-
export const version = "
|
|
2
|
+
export const version = "2.0.0";
|
|
3
3
|
|
|
4
4
|
const PHASE_URI = "/lol-gameflow/v1/gameflow-phase";
|
|
5
|
-
const
|
|
5
|
+
const GAMEFLOW_URI = "/lol-gameflow/v1/session";
|
|
6
6
|
const CHAMP_SELECT_URI = "/lol-champ-select/v1/session";
|
|
7
|
-
const
|
|
7
|
+
const SEARCH_STATE_URI = "/lol-lobby/v2/lobby/matchmaking/search-state";
|
|
8
8
|
const SUMMONER_URI = "/lol-summoner/v1/current-summoner";
|
|
9
9
|
const RANKED_STATS_URI = "/lol-ranked/v1/current-ranked-stats";
|
|
10
|
-
|
|
10
|
+
// The same endpoint the client's own QUIT button uses. It leaves champ select on the server and moves the
|
|
11
|
+
// client back to the lobby properly.
|
|
11
12
|
const QUIT_URI = "/lol-lobby-team-builder/champ-select/v1/session/quit";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
13
|
+
// Riot refuses that endpoint in ranked and in the last seconds before a game starts. Closing the client is then
|
|
14
|
+
// the only way out (it counts as a dodge), so it is offered as an explicitly labelled confirm step.
|
|
15
|
+
const CLOSE_CLIENT_URI = "/process-control/v1/process/quit";
|
|
16
|
+
|
|
17
|
+
const STORE_KEY = "perfectionist:dodge-helper:accounts:v2";
|
|
18
|
+
const LEGACY_STORE_KEY = "perfectionist:dodge-helper:accounts:v1";
|
|
16
19
|
const STYLE_ID = "perf-dodge-helper-style";
|
|
17
|
-
const
|
|
18
|
-
const ROOT_ID = "perfDodgeHelper";
|
|
19
|
-
const BUTTON_ID = "perfDodgeHelperButton";
|
|
20
|
-
const COST_ID = "perfDodgeHelperCost";
|
|
20
|
+
const ROOT_CLASS = "perf-dodge-quit";
|
|
21
21
|
const FOOTER_SELECTOR = ".bottom-right-buttons";
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
22
|
+
const HOUR_MS = 60 * 60 * 1000;
|
|
23
|
+
const DAY_MS = 24 * HOUR_MS;
|
|
24
|
+
const CONFIRM_MS = 3000;
|
|
25
|
+
|
|
26
|
+
// Riot's published queue dodge penalties (support article "Queue Dodging"). The tier goes up by one per
|
|
27
|
+
// dodge and drops by one for every 24 hours without a dodge. Dodges count across all queues.
|
|
28
|
+
const LOCKOUT_MINUTES = { 1: 6, 2: 30, 3: 720 };
|
|
29
|
+
const ARAM_LOCKOUT_MINUTES = { 1: 15, 2: 30, 3: 720 };
|
|
30
|
+
const RANKED_LP = { 1: 5, 2: 15, 3: 15 };
|
|
31
|
+
const APEX_FIRST_DODGE_LP = 20;
|
|
29
32
|
const APEX_TIERS = new Set(["MASTER", "GRANDMASTER", "CHALLENGER"]);
|
|
30
|
-
const PENALTY_TIERS = {
|
|
31
|
-
1: { lp: 5, lockout: 6 },
|
|
32
|
-
2: { lp: 15, lockout: 30 },
|
|
33
|
-
3: { lp: 15, lockout: 720 },
|
|
34
|
-
};
|
|
35
|
-
const GAME_PHASES = new Set([
|
|
36
|
-
"GameStart",
|
|
37
|
-
"InProgress",
|
|
38
|
-
"Reconnect",
|
|
39
|
-
"WaitingForStats",
|
|
40
|
-
"PreEndOfGame",
|
|
41
|
-
"EndOfGame",
|
|
42
|
-
]);
|
|
43
33
|
|
|
44
34
|
const CSS = `
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
#${ROOT_ID} lol-uikit-flat-button { width: 125px !important; height: 32px !important; }
|
|
55
|
-
#${ROOT_ID} .perf-dodge-cost {
|
|
56
|
-
position: absolute !important; left: 50% !important; bottom: calc(100% + 4px) !important;
|
|
57
|
-
transform: translateX(-50%) !important; box-sizing: border-box !important;
|
|
58
|
-
max-width: 245px !important; padding: 3px 7px !important;
|
|
59
|
-
background: rgba(1, 10, 19, 0.88) !important; border: 1px solid #785a28 !important;
|
|
60
|
-
color: #cdbe91 !important; font-family: var(--font-body, "LoL Body", Arial, sans-serif) !important;
|
|
61
|
-
font-size: 10px !important; font-weight: 700 !important; letter-spacing: 0.3px !important;
|
|
62
|
-
line-height: 1.25 !important; text-align: center !important; white-space: nowrap !important;
|
|
63
|
-
pointer-events: none !important;
|
|
64
|
-
}
|
|
35
|
+
.${ROOT_CLASS} { position: fixed; z-index: 100; height: 32px; box-sizing: border-box; }
|
|
36
|
+
.${ROOT_CLASS} lol-uikit-flat-button { display: block; width: 100%; height: 32px; }
|
|
37
|
+
.${ROOT_CLASS} .perf-dodge-cost {
|
|
38
|
+
position: absolute; left: 50%; bottom: calc(100% + 5px); transform: translateX(-50%);
|
|
39
|
+
white-space: nowrap; pointer-events: none; text-align: center;
|
|
40
|
+
font-family: var(--font-body, "LoL Body", Arial, sans-serif); font-size: 10px; font-weight: 700;
|
|
41
|
+
letter-spacing: 0.25px; line-height: 14px; color: #c89b3c; text-shadow: 0 1px 2px #000;
|
|
42
|
+
}
|
|
43
|
+
.${ROOT_CLASS}.perf-dodge-armed lol-uikit-flat-button { filter: hue-rotate(-28deg) saturate(1.6); }
|
|
65
44
|
`;
|
|
66
45
|
|
|
67
46
|
let api = null;
|
|
68
47
|
let active = false;
|
|
69
|
-
let dodging = false;
|
|
70
48
|
let unsubscribes = [];
|
|
71
|
-
let
|
|
49
|
+
let unwatchFooter = null;
|
|
50
|
+
let removeFrameHook = null;
|
|
72
51
|
let removeStyle = null;
|
|
73
|
-
let
|
|
74
|
-
let mountFrame = 0;
|
|
75
|
-
let positionFrame = 0;
|
|
76
|
-
let networkWatch = null;
|
|
52
|
+
let armedTimer = null;
|
|
77
53
|
let penaltyToken = 0;
|
|
78
|
-
|
|
79
|
-
let accountPromise = null;
|
|
54
|
+
|
|
80
55
|
const state = {
|
|
81
56
|
phase: null,
|
|
82
|
-
|
|
83
|
-
isCustom: false,
|
|
57
|
+
queue: null,
|
|
84
58
|
showQuitButton: false,
|
|
85
|
-
|
|
86
|
-
account: null,
|
|
59
|
+
summoner: null,
|
|
87
60
|
penalty: null,
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
localActionActive: false,
|
|
93
|
-
localActionDeadline: 0,
|
|
94
|
-
pageHiding: false,
|
|
61
|
+
armed: false,
|
|
62
|
+
busy: false,
|
|
63
|
+
quitSent: false,
|
|
64
|
+
closeOffered: false,
|
|
95
65
|
};
|
|
96
66
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (text.startsWith("{") || text.startsWith("[")) {
|
|
101
|
-
try {
|
|
102
|
-
return unwrap(JSON.parse(text));
|
|
103
|
-
} catch {
|
|
104
|
-
return raw;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
return raw;
|
|
108
|
-
}
|
|
109
|
-
if (Array.isArray(raw) && raw.length >= 3 && typeof raw[1] === "string") {
|
|
110
|
-
return unwrap(raw[2]?.data ?? raw[2]);
|
|
111
|
-
}
|
|
112
|
-
if (raw && typeof raw === "object" && "data" in raw) {
|
|
113
|
-
const keys = Object.keys(raw);
|
|
114
|
-
if (keys.includes("uri") || keys.includes("eventType") || keys.length <= 2) return unwrap(raw.data);
|
|
115
|
-
}
|
|
116
|
-
return raw;
|
|
117
|
-
}
|
|
67
|
+
/* ---------------------------------------------------------------------------------------------
|
|
68
|
+
* Dodge tier bookkeeping (per account, local).
|
|
69
|
+
* ------------------------------------------------------------------------------------------- */
|
|
118
70
|
|
|
119
71
|
function readStore() {
|
|
120
72
|
try {
|
|
121
|
-
const parsed = JSON.parse(localStorage.getItem(STORE_KEY) || "
|
|
122
|
-
|
|
123
|
-
} catch {
|
|
124
|
-
|
|
125
|
-
}
|
|
73
|
+
const parsed = JSON.parse(localStorage.getItem(STORE_KEY) || "null");
|
|
74
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
|
|
75
|
+
} catch {}
|
|
76
|
+
return {};
|
|
126
77
|
}
|
|
127
78
|
|
|
128
79
|
function writeStore(store) {
|
|
@@ -131,636 +82,406 @@ function writeStore(store) {
|
|
|
131
82
|
} catch {}
|
|
132
83
|
}
|
|
133
84
|
|
|
134
|
-
function
|
|
135
|
-
|
|
136
|
-
const parsed = JSON.parse(localStorage.getItem(PENDING_KEY) || "null");
|
|
137
|
-
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
138
|
-
} catch {
|
|
139
|
-
return null;
|
|
140
|
-
}
|
|
85
|
+
function accountKey() {
|
|
86
|
+
return state.summoner?.puuid || (state.summoner?.summonerId ? `sid:${state.summoner.summonerId}` : null);
|
|
141
87
|
}
|
|
142
88
|
|
|
143
|
-
function
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
89
|
+
function storedRecord() {
|
|
90
|
+
const key = accountKey();
|
|
91
|
+
if (!key) return null;
|
|
92
|
+
const record = readStore()[key];
|
|
93
|
+
return record && typeof record.tier === "number" && typeof record.at === "number" ? record : null;
|
|
147
94
|
}
|
|
148
95
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
96
|
+
// Current tier after decay: one step down per full 24 hours since the last dodge.
|
|
97
|
+
function currentTier(now = Date.now()) {
|
|
98
|
+
const record = storedRecord();
|
|
99
|
+
if (!record) return 0;
|
|
100
|
+
const drops = Math.floor(Math.max(0, now - record.at) / DAY_MS);
|
|
101
|
+
return Math.max(0, Math.min(3, record.tier - drops));
|
|
154
102
|
}
|
|
155
103
|
|
|
156
|
-
function
|
|
157
|
-
const
|
|
158
|
-
if (!
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
104
|
+
function saveTier(tier, at) {
|
|
105
|
+
const key = accountKey();
|
|
106
|
+
if (!key) return;
|
|
107
|
+
const store = readStore();
|
|
108
|
+
const previous = store[key];
|
|
109
|
+
// The same dodge can be observed several times (search state, gameflow); keep the first timestamp.
|
|
110
|
+
if (previous && previous.tier === tier && Math.abs(previous.at - at) < 10 * 60 * 1000) return;
|
|
111
|
+
store[key] = { tier: Math.max(1, Math.min(3, tier)), at };
|
|
112
|
+
writeStore(store);
|
|
162
113
|
}
|
|
163
114
|
|
|
164
|
-
function
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
115
|
+
function recordOwnDodge() {
|
|
116
|
+
saveTier(Math.min(3, currentTier() + 1), Date.now());
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// The server tells us the lockout it applied. Its length identifies the tier exactly
|
|
120
|
+
// (6 or 15 minutes: tier 1, 30 minutes: tier 2, 12 hours: tier 3), which also corrects the count for
|
|
121
|
+
// dodges this plugin did not see (closing the client, a missed lock-in, another PC).
|
|
122
|
+
// Only readings that can't be ambiguous are used: more than 30 minutes left can only be the 12 hour
|
|
123
|
+
// lockout, and otherwise the reading must be within 3 minutes of a full lockout (a 30 minute lockout with
|
|
124
|
+
// 10 minutes left would look like a fresh 15 minute one).
|
|
125
|
+
const LOCKOUTS = [
|
|
126
|
+
[6, 1],
|
|
127
|
+
[15, 1],
|
|
128
|
+
[30, 2],
|
|
129
|
+
];
|
|
130
|
+
const FRESH_WINDOW_S = 180;
|
|
131
|
+
|
|
132
|
+
function learnFromPenalty(remainingSeconds) {
|
|
133
|
+
const remaining = Number(remainingSeconds);
|
|
134
|
+
if (!Number.isFinite(remaining) || remaining <= 0) return;
|
|
135
|
+
if (remaining > 30 * 60) {
|
|
136
|
+
saveTier(3, Date.now() - Math.max(0, 720 * 60 - remaining) * 1000);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
for (const [minutes, tier] of LOCKOUTS) {
|
|
140
|
+
const full = minutes * 60;
|
|
141
|
+
if (remaining <= full && remaining >= full - FRESH_WINDOW_S) {
|
|
142
|
+
saveTier(tier, Date.now() - (full - remaining) * 1000);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
189
145
|
}
|
|
190
|
-
return accountPromise;
|
|
191
146
|
}
|
|
192
147
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
148
|
+
/* ---------------------------------------------------------------------------------------------
|
|
149
|
+
* Penalty preview
|
|
150
|
+
* ------------------------------------------------------------------------------------------- */
|
|
196
151
|
|
|
197
|
-
function
|
|
198
|
-
const
|
|
199
|
-
if (
|
|
200
|
-
return
|
|
152
|
+
function queueOf(gameflow) {
|
|
153
|
+
const queue = gameflow?.gameData?.queue;
|
|
154
|
+
if (!queue) return null;
|
|
155
|
+
return {
|
|
156
|
+
id: Number(queue.id) || 0,
|
|
157
|
+
type: String(queue.type ?? ""),
|
|
158
|
+
gameMode: String(queue.gameMode ?? ""),
|
|
159
|
+
isRanked: queue.isRanked === true,
|
|
160
|
+
isCustom: Boolean(gameflow?.gameData?.isCustomGame || queue.isCustom),
|
|
161
|
+
};
|
|
201
162
|
}
|
|
202
163
|
|
|
203
|
-
function
|
|
204
|
-
return
|
|
164
|
+
function isAram(queue) {
|
|
165
|
+
return /ARAM/i.test(queue?.type ?? "") || /^ARAM$/i.test(queue?.gameMode ?? "");
|
|
205
166
|
}
|
|
206
167
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
const queueId = Number(options.queueId ?? state.queueId) || 0;
|
|
210
|
-
if (!options.force && isCustomQueue(queueId)) return false;
|
|
211
|
-
const now = Date.now();
|
|
212
|
-
if (now - state.lastSelfDodgeAt < 1500) return false;
|
|
213
|
-
state.lastSelfDodgeAt = now;
|
|
214
|
-
await loadAccount();
|
|
215
|
-
const key = accountKey();
|
|
216
|
-
if (!key) return false;
|
|
217
|
-
const store = readStore();
|
|
218
|
-
const tier = Math.min(currentTier() + 1, 3);
|
|
219
|
-
store[key] = { tier, at: now };
|
|
220
|
-
writeStore(store);
|
|
221
|
-
clearPending();
|
|
222
|
-
state.selfDodgeRecorded = true;
|
|
223
|
-
return true;
|
|
168
|
+
function isRankedQueue(queue) {
|
|
169
|
+
return Boolean(queue?.isRanked) || /^RANKED_/i.test(queue?.type ?? "");
|
|
224
170
|
}
|
|
225
171
|
|
|
226
172
|
function lockoutText(minutes) {
|
|
227
|
-
if (minutes >=
|
|
228
|
-
|
|
229
|
-
return `${minutes} min queue`;
|
|
173
|
+
if (minutes >= 60) return `${Math.round(minutes / 60)} hr lockout`;
|
|
174
|
+
return `${minutes} min lockout`;
|
|
230
175
|
}
|
|
231
176
|
|
|
232
|
-
function computePenalty(
|
|
177
|
+
async function computePenalty(queue) {
|
|
233
178
|
const tier = Math.min(currentTier() + 1, 3);
|
|
234
|
-
const aram =
|
|
235
|
-
const lockout = aram
|
|
236
|
-
const
|
|
237
|
-
if (!
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
rated,
|
|
250
|
-
tier,
|
|
251
|
-
lp: rated ? (apex && tier === 1 ? 20 : PENALTY_TIERS[tier].lp) : 0,
|
|
252
|
-
lockout,
|
|
253
|
-
apex,
|
|
254
|
-
aram,
|
|
255
|
-
};
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function penaltyLabel(penalty) {
|
|
179
|
+
const aram = isAram(queue);
|
|
180
|
+
const lockout = (aram ? ARAM_LOCKOUT_MINUTES : LOCKOUT_MINUTES)[tier];
|
|
181
|
+
const penalty = { tier, lockout, lp: 0, ranked: isRankedQueue(queue), placed: false, apex: false, aram };
|
|
182
|
+
if (!penalty.ranked) return penalty;
|
|
183
|
+
const stats = await api.lcu.get(RANKED_STATS_URI).catch(() => null);
|
|
184
|
+
const entry = stats?.queueMap?.[queue.type] ?? null;
|
|
185
|
+
const tierName = String(entry?.tier ?? "").toUpperCase();
|
|
186
|
+
// No LP is lost before you are placed in that queue.
|
|
187
|
+
penalty.placed = Boolean(tierName) && tierName !== "NONE" && tierName !== "UNRANKED" && entry?.isProvisional !== true;
|
|
188
|
+
penalty.apex = penalty.placed && APEX_TIERS.has(tierName);
|
|
189
|
+
if (penalty.placed) penalty.lp = penalty.apex && tier === 1 ? APEX_FIRST_DODGE_LP : RANKED_LP[tier];
|
|
190
|
+
return penalty;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function costText(penalty) {
|
|
260
194
|
if (!penalty) return "";
|
|
261
195
|
const parts = [];
|
|
262
|
-
if (penalty.ranked) parts.push(penalty.lp > 0 ?
|
|
263
|
-
else parts.push("No LP");
|
|
196
|
+
if (penalty.ranked) parts.push(penalty.lp > 0 ? `−${penalty.lp} LP` : "No LP");
|
|
264
197
|
parts.push(lockoutText(penalty.lockout));
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const lines = [
|
|
271
|
-
"Queue dodge penalty",
|
|
272
|
-
"1st dodge: -5 LP and a 6-minute queue lockout.",
|
|
273
|
-
"2nd dodge: -15 LP and a 30-minute queue lockout.",
|
|
274
|
-
"3rd+ dodge: -15 LP and a 12-hour queue lockout.",
|
|
275
|
-
"Apex (Master+): the first dodge costs 20 LP instead of 5, and Apex dodges affect MMR.",
|
|
276
|
-
"Your dodge tier drops one step 24 hours after your last dodge.",
|
|
277
|
-
];
|
|
198
|
+
return parts.join(" · ");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function costTitle(penalty) {
|
|
202
|
+
const lines = [];
|
|
278
203
|
if (penalty) {
|
|
279
|
-
lines.
|
|
280
|
-
if (!penalty.
|
|
281
|
-
if (penalty.
|
|
282
|
-
if (penalty.
|
|
204
|
+
lines.push(`Your next dodge is dodge #${penalty.tier}: ${costText(penalty)}.`);
|
|
205
|
+
if (penalty.ranked && !penalty.placed) lines.push("You are not placed in this queue yet, so no LP is lost.");
|
|
206
|
+
if (penalty.apex) lines.push("Master+ dodges also affect your MMR.");
|
|
207
|
+
if (penalty.aram) lines.push("ARAM dodges also cost a Blessed Card.");
|
|
283
208
|
}
|
|
209
|
+
lines.push("", "Dodges within 24 hours stack: 6 min, then 30 min, then 12 hours (ranked: -5, -15, -15 LP).");
|
|
210
|
+
lines.push("Each 24 hours without a dodge lowers the count by one.");
|
|
284
211
|
return lines.join("\n");
|
|
285
212
|
}
|
|
286
213
|
|
|
287
|
-
function render() {
|
|
288
|
-
const button = document.getElementById(BUTTON_ID);
|
|
289
|
-
const cost = document.getElementById(COST_ID);
|
|
290
|
-
if (!button || !cost) return;
|
|
291
|
-
const ranked = RANKED_QUEUES.has(Number(state.queueId));
|
|
292
|
-
const label = ranked ? "DODGE" : "QUIT";
|
|
293
|
-
if (button.textContent !== label) button.textContent = label;
|
|
294
|
-
button.disabled = dodging;
|
|
295
|
-
const penalty = state.penalty;
|
|
296
|
-
const text = penaltyLabel(penalty);
|
|
297
|
-
if (cost.textContent !== text) cost.textContent = text;
|
|
298
|
-
cost.style.display = text ? "" : "none";
|
|
299
|
-
const title = penalty ? penaltyTitle(penalty) : "Loading the current dodge penalty...";
|
|
300
|
-
button.title = title;
|
|
301
|
-
cost.title = title;
|
|
302
|
-
button.setAttribute("aria-label", `${label}. ${title}`);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
214
|
async function refreshPenalty() {
|
|
306
|
-
if (!active || state.phase !== "ChampSelect" || !state.
|
|
307
|
-
const queueId = state.queueId;
|
|
215
|
+
if (!active || state.phase !== "ChampSelect" || !state.queue || state.queue.isCustom) return;
|
|
308
216
|
const token = ++penaltyToken;
|
|
309
|
-
const penalty = await computePenalty(
|
|
310
|
-
if (!active || token !== penaltyToken
|
|
217
|
+
const penalty = await computePenalty(state.queue);
|
|
218
|
+
if (!active || token !== penaltyToken) return;
|
|
311
219
|
state.penalty = penalty;
|
|
312
220
|
render();
|
|
313
221
|
}
|
|
314
222
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
const queue = session.gameData?.queue;
|
|
319
|
-
if (queue) {
|
|
320
|
-
const queueId = Number(queue.id ?? 0) || 0;
|
|
321
|
-
const isCustom = Boolean(session.gameData?.isCustomGame || queue.isCustom);
|
|
322
|
-
const gameMode = String(queue.gameMode ?? "");
|
|
323
|
-
if (queueId !== state.queueId || isCustom !== state.isCustom || gameMode !== state.gameMode) {
|
|
324
|
-
state.queueId = queueId;
|
|
325
|
-
state.isCustom = isCustom;
|
|
326
|
-
state.gameMode = gameMode;
|
|
327
|
-
state.penalty = null;
|
|
328
|
-
if (state.phase === "ChampSelect") refreshPenalty();
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
ensureMount();
|
|
332
|
-
syncWatching();
|
|
333
|
-
savePending();
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
function syncLocalPick(session) {
|
|
337
|
-
const cellId = Number(session?.localPlayerCellId);
|
|
338
|
-
if (!Number.isFinite(cellId)) return;
|
|
339
|
-
const actions = Array.isArray(session.actions) ? session.actions.flat() : [];
|
|
340
|
-
const picks = actions.filter((action) => Number(action?.actorCellId) === cellId && action?.type === "pick");
|
|
341
|
-
if (!picks.length) return;
|
|
342
|
-
const locked = picks.some((action) => action?.completed === true);
|
|
343
|
-
if (locked) {
|
|
344
|
-
state.lockedLocalPick = true;
|
|
345
|
-
state.localActionSeen = false;
|
|
346
|
-
state.localActionActive = false;
|
|
347
|
-
state.localActionDeadline = 0;
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
state.lockedLocalPick = false;
|
|
351
|
-
const active = picks.find((action) => action?.isInProgress === true);
|
|
352
|
-
state.localActionActive = Boolean(active);
|
|
353
|
-
if (active) {
|
|
354
|
-
state.localActionSeen = true;
|
|
355
|
-
const timeLeft = Number(session?.timer?.timeLeft);
|
|
356
|
-
if (Number.isFinite(timeLeft)) {
|
|
357
|
-
state.localActionDeadline = Date.now() + Math.max(0, timeLeft) * 1000;
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
function syncTimer(raw) {
|
|
363
|
-
const timer = unwrap(raw);
|
|
364
|
-
if (!timer || typeof timer !== "object") return;
|
|
365
|
-
if (!state.localActionActive || state.lockedLocalPick) return;
|
|
366
|
-
const timeLeft = Number(timer.timeLeft);
|
|
367
|
-
if (!Number.isFinite(timeLeft) || timeLeft < 0) return;
|
|
368
|
-
state.localActionSeen = true;
|
|
369
|
-
state.localActionDeadline = Date.now() + timeLeft * 1000;
|
|
370
|
-
}
|
|
223
|
+
/* ---------------------------------------------------------------------------------------------
|
|
224
|
+
* Button: the client's own flat button, on its own row above the chat / missions / voice buttons.
|
|
225
|
+
* ------------------------------------------------------------------------------------------- */
|
|
371
226
|
|
|
372
|
-
function
|
|
373
|
-
|
|
374
|
-
if (!session) return;
|
|
375
|
-
state.showQuitButton = Boolean(session.showQuitButton);
|
|
376
|
-
const queueId = Number(session.queueId ?? 0) || 0;
|
|
377
|
-
if (queueId > 0 && queueId !== state.queueId) {
|
|
378
|
-
state.queueId = queueId;
|
|
379
|
-
state.penalty = null;
|
|
380
|
-
}
|
|
381
|
-
if (typeof session.isCustomGame === "boolean") state.isCustom = session.isCustomGame;
|
|
382
|
-
syncLocalPick(session);
|
|
383
|
-
if (state.localActionActive && !state.localActionDeadline) {
|
|
384
|
-
api.lcu
|
|
385
|
-
.get(TIMER_URI)
|
|
386
|
-
.catch(() => null)
|
|
387
|
-
.then((timer) => {
|
|
388
|
-
if (active && timer) syncTimer(timer);
|
|
389
|
-
});
|
|
390
|
-
}
|
|
391
|
-
ensureMount();
|
|
392
|
-
syncWatching();
|
|
393
|
-
savePending();
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
function build() {
|
|
397
|
-
const root = document.createElement("div");
|
|
398
|
-
root.id = ROOT_ID;
|
|
399
|
-
const cost = document.createElement("div");
|
|
400
|
-
cost.id = COST_ID;
|
|
401
|
-
cost.className = "perf-dodge-cost";
|
|
402
|
-
const button = document.createElement("lol-uikit-flat-button");
|
|
403
|
-
button.id = BUTTON_ID;
|
|
404
|
-
bindButton(button);
|
|
405
|
-
root.append(cost, button);
|
|
406
|
-
return root;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
function bindButton(button) {
|
|
410
|
-
if (!button) return;
|
|
411
|
-
if (!button.__perfClickBound) {
|
|
412
|
-
button.__perfClickBound = true;
|
|
413
|
-
button.addEventListener("click", onButtonClick, true);
|
|
414
|
-
}
|
|
415
|
-
const inner = button.shadowRoot?.querySelector(".lol-uikit-flat-button");
|
|
416
|
-
if (inner && !inner.__perfClickBound) {
|
|
417
|
-
inner.__perfClickBound = true;
|
|
418
|
-
inner.addEventListener("click", onButtonClick);
|
|
419
|
-
}
|
|
227
|
+
function shouldShow() {
|
|
228
|
+
return active && state.phase === "ChampSelect" && state.queue && !state.queue.isCustom && !state.showQuitButton;
|
|
420
229
|
}
|
|
421
230
|
|
|
422
|
-
function
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
231
|
+
function buttonLabel() {
|
|
232
|
+
if (state.busy) return "Leaving...";
|
|
233
|
+
if (state.closeOffered) return state.armed ? "Confirm close" : "Close client";
|
|
234
|
+
if (state.armed) return "Confirm";
|
|
235
|
+
return isRankedQueue(state.queue) ? "Dodge" : "Quit";
|
|
426
236
|
}
|
|
427
237
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
238
|
+
// Sits on its own row right above the chat / missions / voice buttons, exactly as wide as that group.
|
|
239
|
+
function place(root, footer) {
|
|
240
|
+
const chat = footer.querySelector("lol-social-chat-toggle-button");
|
|
241
|
+
const voice = footer.querySelector(".champ-select-voice-button-wrapper");
|
|
242
|
+
const box = footer.getBoundingClientRect();
|
|
243
|
+
const left = Math.round((chat ?? footer).getBoundingClientRect().left);
|
|
244
|
+
const right = Math.round((voice ?? footer).getBoundingClientRect().right);
|
|
245
|
+
const top = Math.round(box.top - 32 - 8);
|
|
246
|
+
const width = Math.max(96, right - left);
|
|
247
|
+
const key = `${left}:${top}:${width}`;
|
|
248
|
+
if (root.dataset.place === key) return;
|
|
249
|
+
root.dataset.place = key;
|
|
250
|
+
root.style.left = `${left}px`;
|
|
251
|
+
root.style.top = `${top}px`;
|
|
252
|
+
root.style.width = `${width}px`;
|
|
436
253
|
}
|
|
437
254
|
|
|
438
|
-
function
|
|
439
|
-
|
|
440
|
-
|
|
255
|
+
function render() {
|
|
256
|
+
const footer = document.querySelector(FOOTER_SELECTOR);
|
|
257
|
+
let root = document.querySelector(`.${ROOT_CLASS}`);
|
|
258
|
+
if (!shouldShow() || !footer) {
|
|
259
|
+
root?.remove();
|
|
441
260
|
return;
|
|
442
261
|
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
262
|
+
if (!root || !root.isConnected) {
|
|
263
|
+
root?.remove();
|
|
264
|
+
root = document.createElement("div");
|
|
265
|
+
root.className = ROOT_CLASS;
|
|
266
|
+
const cost = document.createElement("div");
|
|
267
|
+
cost.className = "perf-dodge-cost";
|
|
268
|
+
const button = document.createElement("lol-uikit-flat-button");
|
|
269
|
+
button.addEventListener("click", onClick);
|
|
270
|
+
root.append(cost, button);
|
|
271
|
+
document.body.append(root);
|
|
272
|
+
}
|
|
273
|
+
place(root, footer);
|
|
274
|
+
const button = root.querySelector("lol-uikit-flat-button");
|
|
275
|
+
const cost = root.querySelector(".perf-dodge-cost");
|
|
276
|
+
const label = buttonLabel();
|
|
277
|
+
if (button.textContent !== label) button.textContent = label;
|
|
278
|
+
if (state.busy) button.setAttribute("disabled", "true");
|
|
279
|
+
else button.removeAttribute("disabled");
|
|
280
|
+
root.classList.toggle("perf-dodge-armed", state.armed);
|
|
281
|
+
const text = costText(state.penalty);
|
|
282
|
+
if (cost.textContent !== text) cost.textContent = text;
|
|
283
|
+
const title = state.closeOffered
|
|
284
|
+
? "Riot doesn't let the client leave this champ select (ranked, or the game is about to start). Closing the client dodges; reopen League afterwards."
|
|
285
|
+
: state.armed
|
|
286
|
+
? "Click again to leave champ select."
|
|
287
|
+
: costTitle(state.penalty);
|
|
288
|
+
if (root.title !== title) root.title = title;
|
|
452
289
|
}
|
|
453
290
|
|
|
454
|
-
function
|
|
455
|
-
|
|
456
|
-
|
|
291
|
+
function disarm() {
|
|
292
|
+
clearTimeout(armedTimer);
|
|
293
|
+
armedTimer = null;
|
|
294
|
+
state.armed = false;
|
|
457
295
|
}
|
|
458
296
|
|
|
459
|
-
function
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
if (
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
const footer = visibleFooter();
|
|
473
|
-
const width = root.offsetWidth || 125;
|
|
474
|
-
const height = root.offsetHeight || 32;
|
|
475
|
-
if (footer && footer.left > width + 24) {
|
|
476
|
-
root.style.left = `${Math.round(footer.left - width - 10)}px`;
|
|
477
|
-
root.style.top = `${Math.round(Math.min(window.innerHeight - height - 8, Math.max(8, footer.bottom - height)))}px`;
|
|
478
|
-
root.style.right = "auto";
|
|
479
|
-
root.style.bottom = "auto";
|
|
297
|
+
function onClick(event) {
|
|
298
|
+
event.preventDefault();
|
|
299
|
+
event.stopPropagation();
|
|
300
|
+
if (state.busy || !shouldShow()) return;
|
|
301
|
+
// Two clicks: a stray click can't cost LP.
|
|
302
|
+
if (!state.armed) {
|
|
303
|
+
state.armed = true;
|
|
304
|
+
clearTimeout(armedTimer);
|
|
305
|
+
armedTimer = setTimeout(() => {
|
|
306
|
+
disarm();
|
|
307
|
+
render();
|
|
308
|
+
}, CONFIRM_MS);
|
|
309
|
+
render();
|
|
480
310
|
return;
|
|
481
311
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
root.style.bottom = "21px";
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
function schedulePosition() {
|
|
489
|
-
if (positionFrame) return;
|
|
490
|
-
positionFrame = requestAnimationFrame(positionRoot);
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
function scheduleMount() {
|
|
494
|
-
if (mountFrame) return;
|
|
495
|
-
mountFrame = requestAnimationFrame(() => {
|
|
496
|
-
mountFrame = 0;
|
|
497
|
-
ensureMount();
|
|
498
|
-
});
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
function observeLayout() {
|
|
502
|
-
if (typeof ResizeObserver !== "function" || !resizeObserver) return;
|
|
503
|
-
resizeObserver.disconnect();
|
|
504
|
-
const footer = document.querySelector(FOOTER_SELECTOR);
|
|
505
|
-
if (footer) resizeObserver.observe(footer);
|
|
506
|
-
const screen = document.querySelector(SCREEN_SELECTOR);
|
|
507
|
-
if (screen) resizeObserver.observe(screen);
|
|
312
|
+
disarm();
|
|
313
|
+
if (state.closeOffered) closeClient();
|
|
314
|
+
else leave();
|
|
508
315
|
}
|
|
509
316
|
|
|
510
|
-
function
|
|
511
|
-
|
|
512
|
-
|
|
317
|
+
function closeClient() {
|
|
318
|
+
state.busy = true;
|
|
319
|
+
render();
|
|
320
|
+
recordOwnDodge();
|
|
321
|
+
fetch(CLOSE_CLIENT_URI, { method: "POST" }).catch(() => {});
|
|
513
322
|
}
|
|
514
323
|
|
|
515
|
-
function
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
324
|
+
async function leave() {
|
|
325
|
+
state.busy = true;
|
|
326
|
+
render();
|
|
327
|
+
try {
|
|
328
|
+
const response = await fetch(QUIT_URI, { method: "POST" });
|
|
329
|
+
if (!response.ok) {
|
|
330
|
+
state.closeOffered = true;
|
|
331
|
+
state.armed = true;
|
|
332
|
+
clearTimeout(armedTimer);
|
|
333
|
+
armedTimer = setTimeout(() => {
|
|
334
|
+
disarm();
|
|
335
|
+
render();
|
|
336
|
+
}, CONFIRM_MS * 2);
|
|
337
|
+
throw new Error(`quit responded ${response.status}`);
|
|
338
|
+
}
|
|
339
|
+
state.quitSent = true;
|
|
340
|
+
if (!state.queue?.isCustom) recordOwnDodge();
|
|
341
|
+
} catch (error) {
|
|
342
|
+
console.warn("[Dodge Helper] Could not leave champ select:", error);
|
|
343
|
+
} finally {
|
|
344
|
+
state.busy = false;
|
|
345
|
+
render();
|
|
346
|
+
}
|
|
519
347
|
}
|
|
520
348
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
}
|
|
349
|
+
/* ---------------------------------------------------------------------------------------------
|
|
350
|
+
* Gameflow
|
|
351
|
+
* ------------------------------------------------------------------------------------------- */
|
|
525
352
|
|
|
526
|
-
function
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
);
|
|
353
|
+
async function loadSummoner() {
|
|
354
|
+
if (state.summoner) return state.summoner;
|
|
355
|
+
const summoner = await api.lcu.get(SUMMONER_URI).catch(() => null);
|
|
356
|
+
if (summoner && active) state.summoner = { puuid: summoner.puuid || "", summonerId: summoner.summonerId || 0 };
|
|
357
|
+
return state.summoner;
|
|
530
358
|
}
|
|
531
359
|
|
|
532
|
-
function
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
const url = String(args[0]?.url ?? args[0] ?? "");
|
|
540
|
-
const method = String(args[1]?.method ?? args[0]?.method ?? "GET").toUpperCase();
|
|
541
|
-
const promise = originalFetch.apply(this, args);
|
|
542
|
-
if (method === "POST" && isDodgeUrl(url)) {
|
|
543
|
-
promise
|
|
544
|
-
.then((response) => {
|
|
545
|
-
if (response.ok) recordDodge();
|
|
546
|
-
})
|
|
547
|
-
.catch(() => {});
|
|
548
|
-
}
|
|
549
|
-
return promise;
|
|
550
|
-
};
|
|
551
|
-
|
|
552
|
-
const ourOpen = function (method, url, ...rest) {
|
|
553
|
-
this.__perfDodgeUrl = String(url);
|
|
554
|
-
this.__perfDodgeMethod = String(method ?? "GET").toUpperCase();
|
|
555
|
-
return originalOpen.call(this, method, url, ...rest);
|
|
556
|
-
};
|
|
557
|
-
|
|
558
|
-
const ourSend = function (body) {
|
|
559
|
-
if (this.__perfDodgeMethod === "POST" && isDodgeUrl(this.__perfDodgeUrl)) {
|
|
560
|
-
this.addEventListener("load", () => {
|
|
561
|
-
if (this.status >= 200 && this.status < 300) recordDodge();
|
|
562
|
-
});
|
|
360
|
+
function migrateLegacyStore() {
|
|
361
|
+
try {
|
|
362
|
+
const legacy = JSON.parse(localStorage.getItem(LEGACY_STORE_KEY) || "null");
|
|
363
|
+
if (!legacy || typeof legacy !== "object") return;
|
|
364
|
+
const store = readStore();
|
|
365
|
+
for (const [key, record] of Object.entries(legacy)) {
|
|
366
|
+
if (!store[key] && typeof record?.tier === "number" && typeof record?.at === "number") store[key] = record;
|
|
563
367
|
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
XMLHttpRequest.prototype.open = ourOpen;
|
|
569
|
-
XMLHttpRequest.prototype.send = ourSend;
|
|
570
|
-
|
|
571
|
-
networkWatch = {
|
|
572
|
-
restore() {
|
|
573
|
-
if (window.fetch === ourFetch) window.fetch = originalFetch;
|
|
574
|
-
if (XMLHttpRequest.prototype.open === ourOpen) XMLHttpRequest.prototype.open = originalOpen;
|
|
575
|
-
if (XMLHttpRequest.prototype.send === ourSend) XMLHttpRequest.prototype.send = originalSend;
|
|
576
|
-
},
|
|
577
|
-
};
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
function removeDodgeWatcher() {
|
|
581
|
-
networkWatch?.restore();
|
|
582
|
-
networkWatch = null;
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
function savePending(closed = false) {
|
|
586
|
-
if (!active || state.phase !== "ChampSelect" || isCustomQueue()) return;
|
|
587
|
-
const now = Date.now();
|
|
588
|
-
if (!closed && now - pendingWriteAt < 1000) return;
|
|
589
|
-
pendingWriteAt = now;
|
|
590
|
-
writePending({
|
|
591
|
-
key: accountKey(),
|
|
592
|
-
seenAt: now,
|
|
593
|
-
queueId: state.queueId,
|
|
594
|
-
locked: state.lockedLocalPick === true,
|
|
595
|
-
closed: Boolean(closed),
|
|
596
|
-
});
|
|
368
|
+
writeStore(store);
|
|
369
|
+
localStorage.removeItem(LEGACY_STORE_KEY);
|
|
370
|
+
localStorage.removeItem("perfectionist:dodge-helper:pending:v1");
|
|
371
|
+
} catch {}
|
|
597
372
|
}
|
|
598
373
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
const age = Date.now() - Number(pending.seenAt || 0);
|
|
607
|
-
if (!Number.isFinite(age) || age < 0 || age > PENDING_MAX_AGE_MS) {
|
|
608
|
-
clearPending();
|
|
609
|
-
return;
|
|
610
|
-
}
|
|
611
|
-
const key = accountKey();
|
|
612
|
-
if (pending.key && key && pending.key !== key) {
|
|
613
|
-
clearPending();
|
|
614
|
-
return;
|
|
615
|
-
}
|
|
616
|
-
if (pending.closed === true && pending.locked !== true) {
|
|
617
|
-
await recordDodge({ force: true, queueId: Number(pending.queueId) || 0 });
|
|
374
|
+
function applySearchState(search) {
|
|
375
|
+
if (!search || !state.summoner) return;
|
|
376
|
+
for (const error of Array.isArray(search.errors) ? search.errors : []) {
|
|
377
|
+
if (!/DODGE/i.test(String(error?.errorType ?? ""))) continue;
|
|
378
|
+
const who = Number(error.penalizedSummonerId) || 0;
|
|
379
|
+
if (who && state.summoner.summonerId && who !== state.summoner.summonerId) continue;
|
|
380
|
+
learnFromPenalty(error.penaltyTimeRemaining);
|
|
618
381
|
}
|
|
619
|
-
clearPending();
|
|
620
382
|
}
|
|
621
383
|
|
|
622
|
-
async function
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
resetSession();
|
|
626
|
-
return;
|
|
627
|
-
}
|
|
628
|
-
if (!isCustomQueue() && !state.selfDodgeRecorded && !state.lockedLocalPick) {
|
|
629
|
-
const timedOut =
|
|
630
|
-
state.localActionSeen &&
|
|
631
|
-
state.localActionDeadline > 0 &&
|
|
632
|
-
Date.now() >= state.localActionDeadline - 900;
|
|
633
|
-
if (timedOut) await recordDodge();
|
|
634
|
-
}
|
|
635
|
-
clearPending();
|
|
636
|
-
resetSession();
|
|
384
|
+
async function checkPenaltyFromServer() {
|
|
385
|
+
const search = await api.lcu.get(SEARCH_STATE_URI).catch(() => null);
|
|
386
|
+
if (active) applySearchState(search);
|
|
637
387
|
}
|
|
638
388
|
|
|
639
|
-
function
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
state.
|
|
643
|
-
|
|
644
|
-
state.
|
|
645
|
-
|
|
646
|
-
|
|
389
|
+
async function onChampSelectEnded(nextPhase) {
|
|
390
|
+
const gameflow = await api.lcu.get(GAMEFLOW_URI).catch(() => null);
|
|
391
|
+
const dodgers = (gameflow?.gameDodge?.dodgeIds ?? []).map(Number);
|
|
392
|
+
const me = Number(state.summoner?.summonerId) || 0;
|
|
393
|
+
// A missed lock-in or a closed client is a dodge too; the gameflow names who dodged.
|
|
394
|
+
if (me && dodgers.includes(me) && !state.quitSent && !state.queue?.isCustom) recordOwnDodge();
|
|
395
|
+
state.quitSent = false;
|
|
396
|
+
if (nextPhase === "Lobby" || nextPhase === "None") setTimeout(() => active && checkPenaltyFromServer(), 1500);
|
|
647
397
|
}
|
|
648
398
|
|
|
649
|
-
async function
|
|
399
|
+
async function onPhase(raw) {
|
|
650
400
|
if (!active) return;
|
|
651
|
-
const
|
|
652
|
-
const phase = typeof raw === "string" ? raw : unwrap(raw);
|
|
401
|
+
const phase = typeof raw === "string" ? raw : raw?.data;
|
|
653
402
|
if (typeof phase !== "string" || !phase) return;
|
|
403
|
+
const previous = state.phase;
|
|
654
404
|
state.phase = phase;
|
|
655
|
-
|
|
656
|
-
if (
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
state.pageHiding = false;
|
|
664
|
-
resetSession();
|
|
665
|
-
}
|
|
666
|
-
await loadAccount();
|
|
667
|
-
const [session, champSelect, timer] = await Promise.all([
|
|
668
|
-
api.lcu.get(SESSION_URI).catch(() => null),
|
|
405
|
+
if (previous === "ChampSelect" && phase !== "ChampSelect") onChampSelectEnded(phase);
|
|
406
|
+
if (phase === "ChampSelect" && previous !== "ChampSelect") {
|
|
407
|
+
disarm();
|
|
408
|
+
state.penalty = null;
|
|
409
|
+
state.quitSent = false;
|
|
410
|
+
await loadSummoner();
|
|
411
|
+
const [gameflow, champSelect] = await Promise.all([
|
|
412
|
+
api.lcu.get(GAMEFLOW_URI).catch(() => null),
|
|
669
413
|
api.lcu.get(CHAMP_SELECT_URI).catch(() => null),
|
|
670
|
-
api.lcu.get(TIMER_URI).catch(() => null),
|
|
671
414
|
]);
|
|
672
|
-
if (!active) return;
|
|
673
|
-
if (
|
|
674
|
-
if (champSelect)
|
|
675
|
-
if (timer) syncTimer(timer);
|
|
676
|
-
ensureMount();
|
|
415
|
+
if (!active || state.phase !== "ChampSelect") return;
|
|
416
|
+
if (gameflow) state.queue = queueOf(gameflow) ?? state.queue;
|
|
417
|
+
if (champSelect) state.showQuitButton = Boolean(champSelect.showQuitButton);
|
|
677
418
|
refreshPenalty();
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
unmount();
|
|
682
|
-
state.penalty = null;
|
|
419
|
+
}
|
|
420
|
+
if (phase !== "ChampSelect") {
|
|
421
|
+
disarm();
|
|
683
422
|
state.showQuitButton = false;
|
|
684
|
-
|
|
423
|
+
state.closeOffered = false;
|
|
685
424
|
}
|
|
425
|
+
render();
|
|
686
426
|
}
|
|
687
427
|
|
|
688
|
-
function
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
function handleTimer(raw) {
|
|
698
|
-
syncTimer(raw);
|
|
428
|
+
function onGameflow(message) {
|
|
429
|
+
const queue = queueOf(message?.data ?? message);
|
|
430
|
+
if (!queue) return;
|
|
431
|
+
const changed = !state.queue || queue.id !== state.queue.id || queue.type !== state.queue.type;
|
|
432
|
+
state.queue = queue;
|
|
433
|
+
if (changed && state.phase === "ChampSelect") refreshPenalty();
|
|
434
|
+
render();
|
|
699
435
|
}
|
|
700
436
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
try {
|
|
708
|
-
await api.lcu.send("POST", uri);
|
|
709
|
-
await recordDodge();
|
|
710
|
-
} catch (error) {
|
|
711
|
-
console.warn("[Dodge Helper] quit failed:", error);
|
|
712
|
-
} finally {
|
|
713
|
-
dodging = false;
|
|
437
|
+
function onChampSelect(message) {
|
|
438
|
+
const session = message?.data ?? message;
|
|
439
|
+
if (!session || typeof session !== "object" || message?.eventType === "Delete") return;
|
|
440
|
+
const show = Boolean(session.showQuitButton);
|
|
441
|
+
if (show !== state.showQuitButton) {
|
|
442
|
+
state.showQuitButton = show;
|
|
714
443
|
render();
|
|
715
444
|
}
|
|
716
445
|
}
|
|
717
446
|
|
|
718
|
-
function
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
447
|
+
function onSearchState(message) {
|
|
448
|
+
applySearchState(message?.data ?? message);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function onFrame() {
|
|
452
|
+
// Ember can re-render the footer; put the button back if it went missing.
|
|
453
|
+
if (state.phase !== "ChampSelect") return;
|
|
454
|
+
const footer = document.querySelector(FOOTER_SELECTOR);
|
|
455
|
+
const root = document.querySelector(`.${ROOT_CLASS}`);
|
|
456
|
+
if (shouldShow() ? !(root && footer) : root) render();
|
|
457
|
+
else if (root && footer) place(root, footer);
|
|
722
458
|
}
|
|
723
459
|
|
|
724
460
|
async function onEnable(utils) {
|
|
725
461
|
api = utils;
|
|
726
462
|
active = true;
|
|
727
|
-
|
|
728
|
-
clearLegacyAnchor();
|
|
463
|
+
migrateLegacyStore();
|
|
729
464
|
removeStyle = utils.addStyle(STYLE_ID, CSS);
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
[TIMER_URI]: handleTimer,
|
|
740
|
-
};
|
|
741
|
-
for (const [uri, handler] of Object.entries(handlers)) {
|
|
742
|
-
try {
|
|
743
|
-
const off = utils.subscribe(uri, handler);
|
|
744
|
-
if (off) unsubscribes.push(off);
|
|
745
|
-
} catch {}
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
for (const selector of [FOOTER_SELECTOR, SCREEN_SELECTOR]) {
|
|
749
|
-
try {
|
|
750
|
-
const off = utils.watch(selector, () => {
|
|
751
|
-
observeLayout();
|
|
752
|
-
scheduleMount();
|
|
753
|
-
});
|
|
754
|
-
if (off) unwatchers.push(off);
|
|
755
|
-
} catch {}
|
|
465
|
+
const handlers = [
|
|
466
|
+
[PHASE_URI, onPhase],
|
|
467
|
+
[GAMEFLOW_URI, onGameflow],
|
|
468
|
+
[CHAMP_SELECT_URI, onChampSelect],
|
|
469
|
+
[SEARCH_STATE_URI, onSearchState],
|
|
470
|
+
];
|
|
471
|
+
for (const [uri, handler] of handlers) {
|
|
472
|
+
const off = utils.subscribe(uri, handler);
|
|
473
|
+
if (off) unsubscribes.push(off);
|
|
756
474
|
}
|
|
475
|
+
unwatchFooter = utils.watch(FOOTER_SELECTOR, () => render());
|
|
476
|
+
removeFrameHook = utils.onFrame ? utils.onFrame(onFrame) : null;
|
|
477
|
+
window.addEventListener("resize", render);
|
|
757
478
|
|
|
758
|
-
await
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
if (
|
|
763
|
-
|
|
479
|
+
await loadSummoner();
|
|
480
|
+
if (!active) return;
|
|
481
|
+
const phase = await utils.lcu.get(PHASE_URI).catch(() => null);
|
|
482
|
+
if (!active) return;
|
|
483
|
+
if (phase === "Lobby" || phase === "None") checkPenaltyFromServer();
|
|
484
|
+
if (typeof phase === "string") await onPhase(phase);
|
|
764
485
|
}
|
|
765
486
|
|
|
766
487
|
function onDisable() {
|
|
@@ -771,44 +492,27 @@ function onDisable() {
|
|
|
771
492
|
} catch {}
|
|
772
493
|
}
|
|
773
494
|
unsubscribes = [];
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
window.removeEventListener("pagehide", onPageHide);
|
|
782
|
-
window.removeEventListener("beforeunload", onPageHide);
|
|
783
|
-
resizeObserver?.disconnect();
|
|
784
|
-
resizeObserver = null;
|
|
785
|
-
cancelAnimationFrame(mountFrame);
|
|
786
|
-
mountFrame = 0;
|
|
787
|
-
cancelAnimationFrame(positionFrame);
|
|
788
|
-
positionFrame = 0;
|
|
789
|
-
removeDodgeWatcher();
|
|
790
|
-
unmount();
|
|
791
|
-
if (!state.pageHiding) clearPending();
|
|
495
|
+
unwatchFooter?.();
|
|
496
|
+
unwatchFooter = null;
|
|
497
|
+
removeFrameHook?.();
|
|
498
|
+
removeFrameHook = null;
|
|
499
|
+
window.removeEventListener("resize", render);
|
|
500
|
+
disarm();
|
|
501
|
+
document.querySelector(`.${ROOT_CLASS}`)?.remove();
|
|
792
502
|
removeStyle?.();
|
|
793
503
|
removeStyle = null;
|
|
794
|
-
state
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
state.localActionDeadline = 0;
|
|
807
|
-
state.pageHiding = false;
|
|
808
|
-
dodging = false;
|
|
809
|
-
pendingWriteAt = 0;
|
|
810
|
-
penaltyToken = 0;
|
|
811
|
-
accountPromise = null;
|
|
504
|
+
Object.assign(state, {
|
|
505
|
+
phase: null,
|
|
506
|
+
queue: null,
|
|
507
|
+
showQuitButton: false,
|
|
508
|
+
summoner: null,
|
|
509
|
+
penalty: null,
|
|
510
|
+
armed: false,
|
|
511
|
+
busy: false,
|
|
512
|
+
quitSent: false,
|
|
513
|
+
closeOffered: false,
|
|
514
|
+
});
|
|
515
|
+
penaltyToken += 1;
|
|
812
516
|
api = null;
|
|
813
517
|
}
|
|
814
518
|
|
|
@@ -818,7 +522,7 @@ export function init() {
|
|
|
818
522
|
id: PLUGIN_ID,
|
|
819
523
|
name: "Dodge Helper",
|
|
820
524
|
description:
|
|
821
|
-
"Adds a DODGE
|
|
525
|
+
"Adds a DODGE button to champ select (QUIT outside ranked) that leaves cleanly, showing the LP and queue lockout your next dodge will cost.",
|
|
822
526
|
version,
|
|
823
527
|
onEnable,
|
|
824
528
|
onDisable,
|
|
@@ -829,7 +533,5 @@ export function init() {
|
|
|
829
533
|
registerWith(window.PerfectionistUtils);
|
|
830
534
|
return;
|
|
831
535
|
}
|
|
832
|
-
addEventListener("perfectionist:utils-ready", (event) => registerWith(event.detail ?? window.PerfectionistUtils), {
|
|
833
|
-
once: true,
|
|
834
|
-
});
|
|
536
|
+
addEventListener("perfectionist:utils-ready", (event) => registerWith(event.detail ?? window.PerfectionistUtils), { once: true });
|
|
835
537
|
}
|