tangled
alpha
login
or
join now
alpine.girlfag.club
/
refbot
0
fork
atom
SpinShare Referee Bot
refbot.ellite.dev/overlay
0
fork
atom
overview
issues
13
pulls
pipelines
update overlay
alpinesystem
4 weeks ago
c3e06133
c3be8611
+185
-179
2 changed files
expand all
collapse all
unified
split
overlay
script.js
state
http.js
+177
-178
overlay/script.js
···
4
4
5
5
const defaultWsUrl = (() => {
6
6
const scheme = location.protocol === 'https:' ? 'wss' : 'ws';
7
7
-
// In production behind Traefik (HTTPS), prefer connecting to the same host (no port): wss://refbot.ellite.dev
8
7
if (location.protocol === 'https:') return `${scheme}://${location.host}`;
9
9
-
// In dev (HTTP) prefer an explicit WS port when provided (e.g. docker compose mapped port)
10
8
if (typeof window.WS_PORT !== 'undefined' && window.WS_PORT) return `${scheme}://${location.hostname}:${window.WS_PORT}`;
11
11
-
// Fallback to localhost default for simple local dev
12
9
return `${scheme}://localhost:8080`;
13
10
})();
14
11
···
17
14
connect();
18
15
});
19
16
20
20
-
// track full pool so we can infer bans from shrinking currentMapPool
21
21
-
let fullMapPool = [];
22
22
-
// name -> chart data object, populated from any pool data we receive
23
23
-
const chartDataCache = {};
24
24
-
// name of the currently picked/playing chart
17
17
+
let mappool = [];
25
18
let currentChartName = null;
26
26
-
// set of names that have been played (finished with a result)
27
27
-
let playedChartNames = new Set();
28
28
-
// set of names currently in the active map pool (shrinks as bans happen)
29
29
-
let activePoolNames = new Set();
30
30
-
// set of names that have been explicitly banned (from match.ban events)
31
31
-
let bannedChartNames = new Set();
32
32
-
// whether a chart is actively being played right now
33
19
let chartIsLive = false;
34
34
-
// ready state during ready check phase
35
20
let p1Ready = false;
36
21
let p2Ready = false;
37
22
38
38
-
function entryName(e) {
39
39
-
return typeof e === 'string' ? e : (e.csvName ?? e.displayName ?? e.name ?? '?');
23
23
+
function getEntry(csvName) {
24
24
+
return mappool.find(e => e.csvName === csvName) ?? null;
40
25
}
41
26
42
27
function entryDisplay(e) {
43
43
-
return typeof e === 'string' ? e : (e.title ?? e.displayName ?? e.name ?? '?');
44
44
-
}
45
45
-
46
46
-
function cacheChartData(pool) {
47
47
-
(pool ?? []).forEach(e => {
48
48
-
if (typeof e === 'object') {
49
49
-
const n = entryName(e);
50
50
-
if (n && n !== '?') chartDataCache[n] = e;
51
51
-
}
52
52
-
});
28
28
+
if (!e) return '?';
29
29
+
if (typeof e === 'string') return e;
30
30
+
return e.title ?? e.displayName ?? e.csvName ?? '?';
53
31
}
54
32
55
33
function connect() {
56
34
if (ws) ws.close();
57
35
const url = document.getElementById('ws-url').value.trim() || 'ws://localhost:8080';
58
36
setStatus('connecting');
59
59
-
// If the page is served over HTTPS, force a secure WebSocket scheme.
60
37
let connectUrl = url;
61
38
if (location.protocol === 'https:' && connectUrl.startsWith('ws://')) {
62
39
connectUrl = connectUrl.replace(/^ws:/, 'wss:');
···
74
51
setStatus('connected');
75
52
document.getElementById('waiting').innerHTML = '<div>Connected! Waiting for a match to start...</div><div class="sub">No match is currently in progress</div>';
76
53
try {
77
77
-
const httpPort = window.HTTP_PORT ?? '8081';
78
78
-
const res = await fetch(`http://${location.hostname}:${httpPort}/state`);
54
54
+
const stateUrl = location.protocol === 'https:'
55
55
+
? `${location.origin}/state`
56
56
+
: `http://${location.hostname}:${window.HTTP_PORT}/state`;
57
57
+
const res = await fetch(stateUrl);
79
58
const snapshot = await res.json();
80
59
if (snapshot) handleMessage(snapshot);
81
60
}
82
61
catch (error) {
83
83
-
// Silently fail - initial state fetch is optional
84
62
console.debug('Failed to fetch initial state:', error);
85
63
}
86
64
};
···
108
86
dot.className = '';
109
87
if (s === 'connected') {
110
88
dot.classList.add('connected');
111
111
-
// Start the hide timer when connected
112
89
hideHeaderAfterDelay();
113
90
}
114
91
else if (s === 'error') {
115
92
dot.classList.add('error');
116
116
-
// Show header on error
117
93
showHeader();
118
94
}
119
95
else {
120
120
-
// Show header on disconnect
121
96
showHeader();
122
97
}
123
98
label.textContent = s;
···
126
101
function handleMessage({ event, data }) {
127
102
if (!data) return;
128
103
showMatchView();
129
129
-
cacheChartData(data.currentMapPool);
130
130
-
cacheChartData(data.playedCharts);
131
131
-
cacheChartData(data.fullMapPool);
104
104
+
105
105
+
if (data.mappool?.length) mappool = data.mappool;
132
106
133
107
switch (event) {
134
108
case 'match.checkIn':
···
140
114
addFeed('Check-in approved - match starting!', 'feed-pick');
141
115
break;
142
116
143
143
-
case 'match.snapshot':
144
144
-
// restore full state on reconnect
145
145
-
if (data.fullMapPool?.length) {
146
146
-
fullMapPool = [...data.fullMapPool];
147
147
-
cacheChartData(fullMapPool);
148
148
-
}
149
149
-
if (data.bannedCharts) bannedChartNames = new Set(data.bannedCharts.map(b => typeof b === 'string' ? b : b.name));
150
150
-
playedChartNames = new Set((data.playedCharts ?? []).map(c => typeof c === 'string' ? c : c.name));
151
151
-
activePoolNames = new Set((data.currentMapPool ?? []).map(entryName));
152
152
-
currentChartName = data.currentChart ? entryName(data.currentChart) : null;
153
153
-
chartIsLive = !!data.currentChart;
117
117
+
case 'match.snapshot': {
118
118
+
currentChartName = data.currentChart ?? null;
119
119
+
chartIsLive = data.progressLevel === 'playing';
120
120
+
p1Ready = data.players?.[0]?.ready ?? false;
121
121
+
p2Ready = data.players?.[1]?.ready ?? false;
122
122
+
updateReadyState();
154
123
updateScoreboard(data);
155
155
-
if (data.currentChart) updateCurrentChart(data.currentChart);
124
124
+
if (currentChartName) updateCurrentChart(getEntry(currentChartName));
125
125
+
else clearCurrentChart();
156
126
renderMapPool();
127
127
+
updatePhaseBarFromState(data);
128
128
+
showMatchView();
129
129
+
break;
130
130
+
}
131
131
+
132
132
+
case 'match.start':
133
133
+
document.getElementById('checkin-banner').classList.remove('visible');
134
134
+
document.getElementById('end-banner').style.display = 'none';
135
135
+
currentChartName = null;
136
136
+
chartIsLive = false;
137
137
+
p1Ready = false;
138
138
+
p2Ready = false;
139
139
+
updateReadyState();
140
140
+
updateScoreboard(data);
141
141
+
renderMapPool();
142
142
+
clearCurrentChart();
143
143
+
addFeed('Match started - ban phase beginning', 'feed-pick');
157
144
showMatchView();
158
145
break;
159
146
160
147
case 'match.banOrderDecided': {
161
161
-
const firstBanner = data.currentBanner ?? data.players?.find(p => p.discordId === data.firstBannerDiscordId);
162
162
-
const fbn = firstBanner?.displayName ?? firstBanner?.name ?? '?';
148
148
+
const firstBanner = data.players?.find(p => p.discordId === data.firstBannerDiscordId)
149
149
+
?? data.players?.find(p => p.discordId === data.banPhase?.currentBannerDiscordId);
150
150
+
const fbn = firstBanner?.displayName ?? '?';
163
151
addFeed(`${fbn} will ban first`, 'feed-ban');
164
152
updatePhaseBar('banning', `${fbn} is banning...`);
165
153
updateScoreboard(data);
···
168
156
}
169
157
170
158
case 'match.ban': {
171
171
-
activePoolNames = new Set((data.currentMapPool ?? []).map(entryName));
172
172
-
if (data.bannedChart) bannedChartNames.add(data.bannedChart);
173
159
const banner = data.players?.find(p => p.discordId === data.bannedByDiscordId);
174
174
-
const bannerName = banner?.displayName ?? banner?.name ?? 'Someone';
175
175
-
addFeed(`${bannerName} banned ${data.bannedChart}`, 'feed-ban');
176
176
-
const nextBanner = data.currentBanner;
177
177
-
if (nextBanner?.discordId) {
178
178
-
const nbn = nextBanner.displayName ?? nextBanner.name ?? '?';
179
179
-
updatePhaseBar('banning', `${nbn} is banning...`);
160
160
+
const bannerName = banner?.displayName ?? 'Someone';
161
161
+
const bannedEntry = data.mappool?.find(e => e.csvName === data.bannedChart);
162
162
+
const bannedDisplay = bannedEntry ? entryDisplay(bannedEntry) : data.bannedChart;
163
163
+
addFeed(`${bannerName} banned ${bannedDisplay}`, 'feed-ban');
164
164
+
const nextBanner = data.players?.find(p => p.discordId === data.banPhase?.currentBannerDiscordId);
165
165
+
if (nextBanner) {
166
166
+
updatePhaseBar('banning', `${nextBanner.displayName} is banning...`);
180
167
}
181
168
else {
182
169
updatePhaseBar(null);
···
186
173
break;
187
174
}
188
175
189
189
-
case 'match.start':
190
190
-
document.getElementById('checkin-banner').classList.remove('visible');
191
191
-
document.getElementById('end-banner').style.display = 'none';
192
192
-
fullMapPool = [...(data.fullMapPool?.length ? data.fullMapPool : (data.currentMapPool ?? []))];
193
193
-
cacheChartData(fullMapPool);
194
194
-
activePoolNames = new Set((data.currentMapPool ?? []).map(entryName));
195
195
-
playedChartNames = new Set();
196
196
-
bannedChartNames = new Set();
197
197
-
currentChartName = null;
176
176
+
case 'match.firstChartDetermined': {
177
177
+
const entry = getEntry(data.chart ?? data.currentChart);
178
178
+
currentChartName = data.chart ?? data.currentChart ?? null;
198
179
chartIsLive = false;
180
180
+
p1Ready = false;
181
181
+
p2Ready = false;
182
182
+
updateReadyState();
199
183
updateScoreboard(data);
184
184
+
if (entry) updateCurrentChart(entry);
200
185
renderMapPool();
201
201
-
clearCurrentChart();
202
202
-
addFeed('Match started - ban phase beginning', 'feed-pick');
203
203
-
showMatchView();
186
186
+
addFeed(`Last map standing: ${entry ? entryDisplay(entry) : currentChartName}`, 'feed-pick');
187
187
+
updatePhaseBar('playing', `Ready check: ${entry ? entryDisplay(entry) : '...'}`);
204
188
break;
189
189
+
}
205
190
206
191
case 'match.pickPhaseStart': {
207
207
-
activePoolNames = new Set((data.currentMapPool ?? []).map(entryName));
208
208
-
const firstPicker = data.currentPicker;
209
209
-
const fpn = firstPicker?.displayName ?? firstPicker?.name ?? '?';
192
192
+
const firstPicker = data.players?.find(p => p.discordId === data.currentPickerDiscordId);
193
193
+
const fpn = firstPicker?.displayName ?? '?';
210
194
updatePhaseBar('picking', `${fpn} is picking...`);
211
195
updateScoreboard(data);
212
196
renderMapPool();
···
216
200
}
217
201
218
202
case 'match.pick': {
219
219
-
activePoolNames = new Set((data.currentMapPool ?? []).map(entryName));
220
220
-
currentChartName = data.currentChart ? entryName(data.currentChart) : null;
203
203
+
currentChartName = data.currentChart ?? null;
221
204
chartIsLive = false;
222
205
p1Ready = false;
223
206
p2Ready = false;
224
207
updateReadyState();
225
208
updateScoreboard(data);
226
226
-
if (data.currentChart) updateCurrentChart(data.currentChart);
209
209
+
const pickedEntry = currentChartName ? getEntry(currentChartName) : null;
210
210
+
if (pickedEntry) updateCurrentChart(pickedEntry);
227
211
renderMapPool();
228
228
-
if (data.currentChart) {
212
212
+
if (pickedEntry) {
229
213
const picker = data.players?.find(p => p.discordId === data.pickedByDiscordId);
230
230
-
const pn = picker?.displayName ?? picker?.name ?? '?';
231
231
-
const cn = entryDisplay(data.currentChart);
232
232
-
addFeed(`${pn} picked ${cn}`, 'feed-pick');
214
214
+
const pn = picker?.displayName ?? '?';
215
215
+
addFeed(`${pn} picked ${entryDisplay(pickedEntry)}`, 'feed-pick');
233
216
}
234
234
-
updatePhaseBar('playing', `Playing: ${data.currentChart ? entryDisplay(data.currentChart) : '...'}`);
217
217
+
updatePhaseBar('playing', `Ready check: ${pickedEntry ? entryDisplay(pickedEntry) : '...'}`);
235
218
break;
236
219
}
237
220
238
238
-
case 'match.playerReady':
239
239
-
p1Ready = data.p1Ready ?? false;
240
240
-
p2Ready = data.p2Ready ?? false;
221
221
+
case 'match.playerReady': {
222
222
+
const prevP1Ready = p1Ready;
223
223
+
const prevP2Ready = p2Ready;
224
224
+
p1Ready = data.players?.[0]?.ready ?? false;
225
225
+
p2Ready = data.players?.[1]?.ready ?? false;
241
226
updateReadyState();
242
242
-
addFeed(`${data.p1Ready && !data.p2Ready
243
243
-
? (data.players?.[0]?.displayName ?? data.players?.[0]?.name ?? 'P1')
244
244
-
: (data.players?.[1]?.displayName ?? data.players?.[1]?.name ?? 'P2')} is ready!`, 'feed-win');
227
227
+
const newlyReadyName = !prevP1Ready && p1Ready
228
228
+
? (data.players?.[0]?.displayName ?? 'P1')
229
229
+
: !prevP2Ready && p2Ready
230
230
+
? (data.players?.[1]?.displayName ?? 'P2')
231
231
+
: null;
232
232
+
if (newlyReadyName) addFeed(`${newlyReadyName} is ready!`, 'feed-win');
245
233
break;
234
234
+
}
246
235
247
247
-
case 'match.chartStart':
236
236
+
case 'match.chartStart': {
248
237
p1Ready = true;
249
238
p2Ready = true;
250
239
updateReadyState();
251
251
-
// both players readied up
252
252
-
currentChartName = data.currentChart ? entryName(data.currentChart) : currentChartName;
240
240
+
currentChartName = data.currentChart ?? currentChartName;
253
241
chartIsLive = true;
254
242
updateScoreboard(data);
255
255
-
if (data.currentChart) updateCurrentChart(data.currentChart);
243
243
+
const liveEntry = currentChartName ? getEntry(currentChartName) : null;
244
244
+
if (liveEntry) updateCurrentChart(liveEntry);
256
245
renderMapPool();
257
257
-
if (data.currentChart) {
258
258
-
const n = entryDisplay(data.currentChart);
259
259
-
addFeed(`Now playing: ${n}`, 'feed-pick');
260
260
-
}
246
246
+
if (liveEntry) addFeed(`Now playing: ${entryDisplay(liveEntry)}`, 'feed-pick');
247
247
+
updatePhaseBar('playing', `Playing: ${liveEntry ? entryDisplay(liveEntry) : '...'}`);
261
248
break;
249
249
+
}
262
250
263
251
case 'match.chartResult': {
264
252
chartIsLive = false;
···
266
254
p2Ready = false;
267
255
updateReadyState();
268
256
updateScoreboard(data);
269
269
-
if (data.chart) playedChartNames.add(entryName(data.chart));
270
270
-
activePoolNames = new Set((data.currentMapPool ?? []).map(entryName));
257
257
+
const resultEntry = (data.mappool ?? [])
258
258
+
.filter(e => e.status?.played && e.result)
259
259
+
.sort((a, b) => new Date(b.status.playedAt) - new Date(a.status.playedAt))[0] ?? null;
271
260
currentChartName = null;
272
261
clearCurrentChart();
273
262
renderMapPool();
274
274
-
const chartTitle = data.chart ? entryDisplay(data.chart) : 'Chart';
275
275
-
const p1n = data.players?.[0]?.displayName ?? data.players?.[0]?.name ?? 'P1';
276
276
-
const p2n = data.players?.[1]?.displayName ?? data.players?.[1]?.name ?? 'P2';
277
277
-
const s1 = fmtScore(data.score1, data.fc1, data.pfc1);
278
278
-
const s2 = fmtScore(data.score2, data.fc2, data.pfc2);
279
279
-
addFeed(`${chartTitle}: ${p1n} ${s1} vs ${p2n} ${s2} - ${data.winner} wins!`, 'feed-win');
280
280
-
const nextPicker = data.currentPicker;
281
281
-
if (nextPicker?.discordId) {
282
282
-
const npn = nextPicker.displayName ?? nextPicker.name ?? '?';
283
283
-
updatePhaseBar('picking', `${npn} is picking...`);
284
284
-
}
263
263
+
const chartTitle = resultEntry ? entryDisplay(resultEntry) : 'Chart';
264
264
+
const p1n = data.players?.[0]?.displayName ?? 'P1';
265
265
+
const p2n = data.players?.[1]?.displayName ?? 'P2';
266
266
+
const result = resultEntry?.result ?? {};
267
267
+
const s1 = fmtScore(result.score1, result.fc1, result.pfc1);
268
268
+
const s2 = fmtScore(result.score2, result.fc2, result.pfc2);
269
269
+
const winnerPlayer = data.players?.find(p => p.discordId === result.winnerDiscordId);
270
270
+
addFeed(`${chartTitle}: ${p1n} ${s1} vs ${p2n} ${s2} - ${winnerPlayer?.displayName ?? 'someone'} wins!`, 'feed-win');
271
271
+
const nextPicker = data.players?.find(p => p.discordId === data.currentPickerDiscordId);
272
272
+
if (nextPicker) updatePhaseBar('picking', `${nextPicker.displayName} is picking...`);
285
273
break;
286
274
}
287
275
288
276
case 'match.end':
289
277
chartIsLive = false;
278
278
+
p1Ready = false;
279
279
+
p2Ready = false;
280
280
+
updateReadyState();
290
281
updateScoreboard(data);
291
291
-
if (data.chart) playedChartNames.add(entryName(data.chart));
292
282
currentChartName = null;
293
283
clearCurrentChart();
294
284
renderMapPool();
···
301
291
302
292
default:
303
293
updateScoreboard(data);
304
304
-
if (data.currentChart) updateCurrentChart(data.currentChart);
305
305
-
if (data.currentMapPool) activePoolNames = new Set(data.currentMapPool.map(entryName));
294
294
+
if (data.currentChart) {
295
295
+
currentChartName = data.currentChart;
296
296
+
updateCurrentChart(getEntry(currentChartName));
297
297
+
}
306
298
renderMapPool();
307
299
}
308
300
}
309
301
302
302
+
function updatePhaseBarFromState(data) {
303
303
+
const level = data.progressLevel;
304
304
+
if (level === 'ban-phase') {
305
305
+
const banner = data.players?.find(p => p.discordId === data.banPhase?.currentBannerDiscordId);
306
306
+
if (banner) updatePhaseBar('banning', `${banner.displayName} is banning...`);
307
307
+
else updatePhaseBar(null);
308
308
+
}
309
309
+
else if (level === 'picking-post-result') {
310
310
+
const picker = data.players?.find(p => p.discordId === data.currentPickerDiscordId);
311
311
+
if (picker) updatePhaseBar('picking', `${picker.displayName} is picking...`);
312
312
+
else updatePhaseBar(null);
313
313
+
}
314
314
+
else if (level === 'playing') {
315
315
+
const entry = data.currentChart ? getEntry(data.currentChart) : null;
316
316
+
updatePhaseBar('playing', `Playing: ${entry ? entryDisplay(entry) : '...'}`);
317
317
+
}
318
318
+
else {
319
319
+
updatePhaseBar(null);
320
320
+
}
321
321
+
}
322
322
+
310
323
function fmtScore(score, fc, pfc) {
311
324
if (score == null) return '?';
312
325
let s = Number(score).toLocaleString();
···
375
388
function updateScoreboard(data) {
376
389
if (data.players?.[0]) {
377
390
const p = data.players[0];
378
378
-
if (p.displayName ?? p.name) document.getElementById('p1-name').textContent = p.displayName ?? p.name;
379
379
-
if (p.name) document.getElementById('p1-label').textContent = p.name;
380
380
-
if (p.username) document.getElementById('p1-username').textContent = `@${p.username}`;
391
391
+
if (p.displayName) document.getElementById('p1-name').textContent = p.displayName;
392
392
+
if (p.discordDisplayName) document.getElementById('p1-label').textContent = p.discordDisplayName;
393
393
+
if (p.discordUsername) document.getElementById('p1-username').textContent = `@${p.discordUsername}`;
381
394
const av = document.getElementById('p1-avatar');
382
382
-
if (p.avatar) { av.src = p.avatar; av.style.display = 'block'; }
395
395
+
if (p.avatarUrl) { av.src = p.avatarUrl; av.style.display = 'block'; }
383
396
}
384
397
if (data.players?.[1]) {
385
398
const p = data.players[1];
386
386
-
if (p.displayName ?? p.name) document.getElementById('p2-name').textContent = p.displayName ?? p.name;
387
387
-
if (p.name) document.getElementById('p2-label').textContent = p.name;
388
388
-
if (p.username) document.getElementById('p2-username').textContent = `@${p.username}`;
399
399
+
if (p.displayName) document.getElementById('p2-name').textContent = p.displayName;
400
400
+
if (p.discordDisplayName) document.getElementById('p2-label').textContent = p.discordDisplayName;
401
401
+
if (p.discordUsername) document.getElementById('p2-username').textContent = `@${p.discordUsername}`;
389
402
const av = document.getElementById('p2-avatar');
390
390
-
if (p.avatar) { av.src = p.avatar; av.style.display = 'block'; }
403
403
+
if (p.avatarUrl) { av.src = p.avatarUrl; av.style.display = 'block'; }
391
404
}
392
392
-
if (data.score) document.getElementById('score-display').textContent = `${data.score[0]} - ${data.score[1]}`;
393
393
-
if (data.round) document.getElementById('round-label').textContent = data.round;
394
394
-
if (data.bestOf) document.getElementById('bo-label').textContent = `Best of ${data.bestOf}`;
405
405
+
const p0pts = data.players?.[0]?.points ?? 0;
406
406
+
const p1pts = data.players?.[1]?.points ?? 0;
407
407
+
document.getElementById('score-display').textContent = `${p0pts} - ${p1pts}`;
408
408
+
if (data.meta?.round) document.getElementById('round-label').textContent = data.meta.round;
409
409
+
if (data.meta?.bestOf) document.getElementById('bo-label').textContent = `Best of ${data.meta.bestOf}`;
395
410
}
396
411
397
397
-
function updateCurrentChart(chart) {
398
398
-
if (!chart) return;
412
412
+
function updateCurrentChart(entry) {
413
413
+
if (!entry) return;
399
414
const card = document.getElementById('current-chart-card');
400
415
card.classList.add('active');
401
416
const thumb = document.getElementById('chart-thumb');
402
402
-
const imgSrc = chart.thumbnailUrl ?? chart.cover ?? '';
417
417
+
const imgSrc = entry.thumbnailUrl ?? entry.cover ?? '';
403
418
if (imgSrc) { thumb.src = imgSrc; thumb.style.display = 'block'; }
404
419
else { thumb.style.display = 'none'; }
405
405
-
document.getElementById('chart-title').textContent = chart.title ?? chart.displayName ?? 'Unknown';
406
406
-
document.getElementById('chart-artist').textContent = chart.artist ?? '';
407
407
-
document.getElementById('chart-charter').textContent = chart.charter ? `charted by ${chart.charter}` : '';
420
420
+
document.getElementById('chart-title').textContent = entry.title ?? entry.displayName ?? 'Unknown';
421
421
+
document.getElementById('chart-artist').textContent = entry.artist ?? '';
422
422
+
document.getElementById('chart-charter').textContent = entry.charter ? `charted by ${entry.charter}` : '';
408
423
const diff = document.getElementById('chart-difficulty');
409
409
-
if (chart.difficulty != null) { diff.style.display = 'block'; diff.textContent = `Diff ${chart.difficulty}`; }
424
424
+
if (entry.difficulty != null) { diff.style.display = 'block'; diff.textContent = `Diff ${entry.difficulty}`; }
410
425
else { diff.style.display = 'none'; }
411
426
}
412
427
···
423
438
const grid = document.getElementById('mappool-grid');
424
439
grid.innerHTML = '';
425
440
426
426
-
// use fullMapPool as the source of truth for what to show
427
427
-
// fall back to activePoolNames contents if fullMapPool is empty (e.g. mid-session connect)
428
428
-
const source = fullMapPool.length > 0
429
429
-
? fullMapPool
430
430
-
: [...activePoolNames].map(n => chartDataCache[n] ?? n);
431
431
-
432
432
-
source.forEach(entry => {
433
433
-
const name = entryName(entry);
434
434
-
const cached = chartDataCache[name] ?? (typeof entry === 'object' ? entry : null);
435
435
-
const display = cached ? entryDisplay(cached) : name;
436
436
-
const artist = cached?.artist ?? '';
437
437
-
const thumb = cached?.thumbnailUrl ?? cached?.cover ?? '';
441
441
+
if (!mappool.length) return;
438
442
439
439
-
const isPlayed = playedChartNames.has(name);
440
440
-
const isBanned = !isPlayed && (bannedChartNames.has(name) || (!activePoolNames.has(name) && fullMapPool.length > 0));
441
441
-
// during chart play, dim everything except the current chart
442
442
-
const isDimmed = chartIsLive && currentChartName && name !== currentChartName;
443
443
-
const isCurrent = name === currentChartName;
443
443
+
mappool.forEach(entry => {
444
444
+
const { csvName, title, artist, thumbnailUrl, cover, displayName, status, result } = entry;
445
445
+
const display = title ?? displayName ?? csvName ?? '?';
446
446
+
const thumb = thumbnailUrl ?? cover ?? '';
447
447
+
const isBanned = status?.banned ?? false;
448
448
+
const isPlayed = status?.played ?? false;
449
449
+
const isLive = status?.isBeingPlayed ?? false;
450
450
+
const isCurrent = csvName === currentChartName;
444
451
445
452
const chip = document.createElement('div');
446
446
-
const classes = ['map-chip'];
447
447
-
if (isPlayed) classes.push('played');
448
448
-
else if (isBanned) classes.push('banned');
449
449
-
else if (isDimmed) classes.push('dimmed');
450
450
-
if (isCurrent) classes.push('current');
451
451
-
chip.className = classes.join(' ');
453
453
+
chip.className = 'map-chip';
454
454
+
455
455
+
if (isBanned) chip.classList.add('banned');
456
456
+
else if (isPlayed) chip.classList.add('played');
457
457
+
else if (isLive || isCurrent) chip.classList.add('active');
452
458
453
459
if (thumb) {
454
460
const img = document.createElement('img');
455
461
img.className = 'map-chip-thumb';
456
462
img.src = thumb;
457
457
-
img.alt = display;
458
463
chip.appendChild(img);
459
464
}
460
465
461
461
-
if (isBanned) {
462
462
-
const tag = document.createElement('div');
463
463
-
tag.className = 'chip-tag banned-tag';
464
464
-
tag.textContent = 'BANNED';
465
465
-
chip.appendChild(tag);
466
466
+
if (isPlayed && result) {
467
467
+
const res = document.createElement('div');
468
468
+
res.className = 'map-chip-result';
469
469
+
const s1 = fmtScore(result.score1, result.fc1, result.pfc1);
470
470
+
const s2 = fmtScore(result.score2, result.fc2, result.pfc2);
471
471
+
res.textContent = `${s1} / ${s2}`;
472
472
+
chip.appendChild(res);
466
473
}
467
467
-
else if (isPlayed) {
474
474
+
else if (isBanned) {
468
475
const tag = document.createElement('div');
469
469
-
tag.className = 'chip-tag played-tag';
470
470
-
tag.textContent = 'PLAYED';
476
476
+
tag.className = 'map-chip-tag';
477
477
+
tag.textContent = 'BANNED';
471
478
chip.appendChild(tag);
472
479
}
473
473
-
else if (isCurrent) {
480
480
+
else if (isLive || isCurrent) {
474
481
const tag = document.createElement('div');
475
475
-
tag.className = 'chip-tag current-tag';
482
482
+
tag.className = 'map-chip-tag';
476
483
tag.textContent = chartIsLive ? 'LIVE' : 'PICKED';
477
484
chip.appendChild(tag);
478
485
}
···
510
517
}
511
518
512
519
function hideHeaderAfterDelay() {
513
513
-
if (headerHideTimeout) {
514
514
-
clearTimeout(headerHideTimeout);
515
515
-
}
520
520
+
if (headerHideTimeout) clearTimeout(headerHideTimeout);
516
521
headerHideTimeout = setTimeout(() => {
517
522
const header = document.getElementById('main-header');
518
523
const wsDot = document.getElementById('ws-dot');
519
519
-
// Only hide if connected
520
520
-
if (wsDot.classList.contains('connected')) {
521
521
-
header.classList.add('hidden');
522
522
-
}
524
524
+
if (wsDot.classList.contains('connected')) header.classList.add('hidden');
523
525
}, 1000);
524
526
}
525
527
526
528
function showHeader() {
527
529
const header = document.getElementById('main-header');
528
530
header.classList.remove('hidden');
529
529
-
// Reset the hide timer
530
531
hideHeaderAfterDelay();
531
532
}
532
532
-
// Add mouse move listener to show header when mouse moves near top
533
533
+
533
534
document.addEventListener('mousemove', (e) => {
534
534
-
if (e.clientY < 50) {
535
535
-
showHeader();
536
536
-
}
537
537
-
});
535
535
+
if (e.clientY < 50) showHeader();
536
536
+
});
+8
-1
state/http.js
···
63
63
};
64
64
res.setHeader('Content-Type', contentTypes[ext] ?? 'application/octet-stream');
65
65
res.writeHead(200);
66
66
-
fs.createReadStream(overlayPath).pipe(res);
66
66
+
if (ext === '.html') {
67
67
+
const html = fs.readFileSync(overlayPath, 'utf8');
68
68
+
const injected = html.replace('<head>', `<head>\n <script>window.WS_PORT=${process.env.WS_PORT ?? 8080};window.HTTP_PORT=${process.env.HTTP_PORT ?? 8081};</script>`);
69
69
+
res.end(injected);
70
70
+
}
71
71
+
else {
72
72
+
fs.createReadStream(overlayPath).pipe(res);
73
73
+
}
67
74
return;
68
75
}
69
76