feat: major redesign — ELO system, 4 practice modes, history view, mobile nav
- 4 practice modes: Timed (SVG ring), Endless (3 lives), Category Drill, Speed Run
- Practice settings modal (timer duration, difficulty) with localStorage persistence
- History view: paginated table, SVG score chart, best-times-per-stratagem
- ELO rating system with server-side K=32 calculation and rank tiers PRIVATE–GENERAL
- Post-match result modal with ELO delta and round history
- Challenge modal showing challenger name + ELO (replaces toast)
- Dashboard hero card with ELO rank icon and daily sequence preview
- Leaderboard tabs: Practice Score / ELO / Speed Run
- Mobile hamburger nav drawer with slide-in animation
- DB migration: elo column, mode column, stratagem_stats table
- WS lobby-update now sends {name, elo, rank} objects
- View fade transitions, danger vignette at ≤5s, streak fire glow, combo badge
- Esc/Enter keyboard shortcuts for modals and practice
This commit is contained in:
@@ -1,5 +1,34 @@
|
||||
# Changelog – helldivers-trainer
|
||||
|
||||
## [2.0.0] – 2026-03-30
|
||||
|
||||
### Added
|
||||
- **4 Practice modes**: Timed (configurable 15/30/45s, SVG ring timer), Endless (3 lives), Category Drill (progress bar), Speed Run (all stratagems, total time)
|
||||
- **Settings modal**: timer duration and difficulty (Easy/Normal/Hard) persisted in localStorage
|
||||
- **History view**: paginated session table with mode/category filters, SVG polyline score chart, best-time-per-stratagem table
|
||||
- **ELO rating system**: server-side ELO (K=32) calculated after every match; rank tiers PRIVATE → GENERAL
|
||||
- **Post-match result modal**: ELO delta display (`before → after +N`), round-by-round history
|
||||
- **Challenge modal**: incoming challenge shows challenger name and ELO; replaces toast notification
|
||||
- **Dashboard hero card**: ELO rank badge, rank icon, daily sequence preview arrows, mode column in recent sessions
|
||||
- **Leaderboard tabs**: Practice Score / ELO Rating / Speed Run with correct column headers per tab
|
||||
- **Mobile hamburger nav**: slide-in drawer with overlay backdrop, Esc key closes modals
|
||||
- **DB migration**: `elo` column on users, `mode` column on practice_sessions, `stratagem_stats` table
|
||||
- **Lobby redesign**: ELO + rank shown per player, 2-column layout (Online + Incoming Challenges)
|
||||
- View fade transitions on every view switch
|
||||
- Danger vignette flash at ≤5s timer remaining
|
||||
- Streak fire glow effect (streak ≥ 5), combo multiplier badge (×N, streak ≥ 2)
|
||||
- Keyboard shortcuts: `Esc` stops practice / closes modals, `Enter` starts practice
|
||||
- `aria-label` on all D-pad buttons, modals with `role="dialog"` and `aria-modal="true"`
|
||||
|
||||
### Changed
|
||||
- Full CSS redesign (~700 → ~1200 lines): glassmorphism cards, new design tokens, SVG timer ring, skeleton shimmer, animated modals
|
||||
- Dashboard online list now shows ELO; lobby shows ELO + rank per player
|
||||
- WS `lobby-update` now sends `[{name, elo, rank}]` objects instead of plain usernames
|
||||
- WS `challenge-received` now includes challenger ELO
|
||||
- Practice leaderboard query joined with `users.elo` for rank badge display
|
||||
|
||||
---
|
||||
|
||||
## [1.0.0] – 2026-03-30
|
||||
|
||||
### Added
|
||||
|
||||
@@ -795,5 +795,24 @@ function showToast(msg) {
|
||||
}, 3200);
|
||||
}
|
||||
|
||||
// ── Static button bindings (replaces inline onclick – blocked by CSP script-src-attr) ──
|
||||
document.getElementById('btn-logout') ?.addEventListener('click', logout);
|
||||
document.getElementById('btn-daily-challenge') ?.addEventListener('click', startDailyChallenge);
|
||||
document.getElementById('btn-start-practice') ?.addEventListener('click', startPractice);
|
||||
document.getElementById('btn-stop-practice') ?.addEventListener('click', stopPracticeUI);
|
||||
document.getElementById('match-ready-btn') ?.addEventListener('click', setReady);
|
||||
document.getElementById('btn-leave-match') ?.addEventListener('click', leaveMatch);
|
||||
document.getElementById('btn-create-user') ?.addEventListener('click', createUser);
|
||||
|
||||
// D-pad: practice and match both use data-dir buttons
|
||||
document.getElementById('practice-dpad')?.addEventListener('click', (e) => {
|
||||
const dir = e.target.closest('[data-dir]')?.dataset.dir;
|
||||
if (dir) dpadInput(dir);
|
||||
});
|
||||
document.getElementById('match-dpad')?.addEventListener('click', (e) => {
|
||||
const dir = e.target.closest('[data-dir]')?.dataset.dir;
|
||||
if (dir) dpadInput(dir);
|
||||
});
|
||||
|
||||
// ── Init ──────────────────────────────────────────────────────────────────────
|
||||
document.addEventListener('DOMContentLoaded', checkAuth);
|
||||
|
||||
+17
-17
@@ -26,7 +26,7 @@
|
||||
</div>
|
||||
<div class="nav-user">
|
||||
<span class="nav-username" id="nav-username"></span>
|
||||
<button class="btn btn-muted btn-sm" onclick="logout()">Logout</button>
|
||||
<button class="btn btn-muted btn-sm" id="btn-logout">Logout</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
<div class="daily-best">
|
||||
Best time: <span id="dash-daily-best">—</span>
|
||||
</div>
|
||||
<button class="btn btn-accent" onclick="startDailyChallenge()">Practice this stratagem</button>
|
||||
<button class="btn btn-accent" id="btn-daily-challenge">Practice this stratagem</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -161,7 +161,7 @@
|
||||
<!-- Idle (start screen) -->
|
||||
<div id="practice-idle" class="practice-idle">
|
||||
<div class="idle-hint">Select categories above, then start training</div>
|
||||
<button class="btn btn-accent btn-lg" onclick="startPractice()">⚡ START TRAINING</button>
|
||||
<button class="btn btn-accent btn-lg" id="btn-start-practice">⚡ START TRAINING</button>
|
||||
</div>
|
||||
|
||||
<!-- Active training -->
|
||||
@@ -189,21 +189,21 @@
|
||||
</div>
|
||||
|
||||
<!-- D-Pad (mobile) -->
|
||||
<div class="dpad">
|
||||
<div class="dpad" id="practice-dpad">
|
||||
<div class="dpad-row">
|
||||
<button class="dpad-btn dpad-up" onclick="dpadInput('up')">↑</button>
|
||||
<button class="dpad-btn dpad-up" data-dir="up">↑</button>
|
||||
</div>
|
||||
<div class="dpad-row">
|
||||
<button class="dpad-btn dpad-left" onclick="dpadInput('left')">←</button>
|
||||
<button class="dpad-btn dpad-left" data-dir="left">←</button>
|
||||
<div class="dpad-center"></div>
|
||||
<button class="dpad-btn dpad-right" onclick="dpadInput('right')">→</button>
|
||||
<button class="dpad-btn dpad-right" data-dir="right">→</button>
|
||||
</div>
|
||||
<div class="dpad-row">
|
||||
<button class="dpad-btn dpad-down" onclick="dpadInput('down')">↓</button>
|
||||
<button class="dpad-btn dpad-down" data-dir="down">↓</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-muted" onclick="stopPracticeUI()">Stop Training</button>
|
||||
<button class="btn btn-muted" id="btn-stop-practice">Stop Training</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -257,24 +257,24 @@
|
||||
</div>
|
||||
</div>
|
||||
<!-- D-Pad (mobile) -->
|
||||
<div class="dpad">
|
||||
<div class="dpad" id="match-dpad">
|
||||
<div class="dpad-row">
|
||||
<button class="dpad-btn dpad-up" onclick="dpadInput('up')">↑</button>
|
||||
<button class="dpad-btn dpad-up" data-dir="up">↑</button>
|
||||
</div>
|
||||
<div class="dpad-row">
|
||||
<button class="dpad-btn dpad-left" onclick="dpadInput('left')">←</button>
|
||||
<button class="dpad-btn dpad-left" data-dir="left">←</button>
|
||||
<div class="dpad-center"></div>
|
||||
<button class="dpad-btn dpad-right" onclick="dpadInput('right')">→</button>
|
||||
<button class="dpad-btn dpad-right" data-dir="right">→</button>
|
||||
</div>
|
||||
<div class="dpad-row">
|
||||
<button class="dpad-btn dpad-down" onclick="dpadInput('down')">↓</button>
|
||||
<button class="dpad-btn dpad-down" data-dir="down">↓</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="match-actions">
|
||||
<button class="btn btn-accent hidden" id="match-ready-btn" onclick="setReady()">READY</button>
|
||||
<button class="btn btn-muted btn-sm" onclick="leaveMatch()">Leave Match</button>
|
||||
<button class="btn btn-accent hidden" id="match-ready-btn">READY</button>
|
||||
<button class="btn btn-muted btn-sm" id="btn-leave-match">Leave Match</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -325,7 +325,7 @@
|
||||
</select>
|
||||
</div>
|
||||
<p id="admin-error" class="error hidden"></p>
|
||||
<button class="btn btn-accent" onclick="createUser()">Create User</button>
|
||||
<button class="btn btn-accent" id="btn-create-user">Create User</button>
|
||||
<div id="new-pw-display" class="pw-display hidden"></div>
|
||||
</div>
|
||||
|
||||
|
||||
+992
-172
File diff suppressed because it is too large
Load Diff
@@ -127,10 +127,35 @@ function initDB() {
|
||||
loser_rounds INTEGER NOT NULL,
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS stratagem_stats (
|
||||
username TEXT NOT NULL,
|
||||
stratagem TEXT NOT NULL,
|
||||
attempts INTEGER DEFAULT 0,
|
||||
completions INTEGER DEFAULT 0,
|
||||
best_time INTEGER DEFAULT NULL,
|
||||
PRIMARY KEY (username, stratagem)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_ps_user ON practice_sessions(username);
|
||||
CREATE INDEX IF NOT EXISTS idx_m_winner ON matches(winner);
|
||||
CREATE INDEX IF NOT EXISTS idx_m_loser ON matches(loser);
|
||||
`);
|
||||
|
||||
// Schema migrations (user_version tracks applied version)
|
||||
const version = db.pragma('user_version', { simple: true });
|
||||
|
||||
if (version < 1) {
|
||||
// Add elo column to users if missing
|
||||
const cols = db.pragma('table_info(users)').map(c => c.name);
|
||||
if (!cols.includes('elo')) {
|
||||
db.exec('ALTER TABLE users ADD COLUMN elo INTEGER DEFAULT 1000');
|
||||
}
|
||||
// Add mode column to practice_sessions if missing
|
||||
const psCols = db.pragma('table_info(practice_sessions)').map(c => c.name);
|
||||
if (!psCols.includes('mode')) {
|
||||
db.exec("ALTER TABLE practice_sessions ADD COLUMN mode TEXT DEFAULT 'timed'");
|
||||
}
|
||||
db.pragma('user_version = 1');
|
||||
}
|
||||
}
|
||||
|
||||
async function initUsers() {
|
||||
@@ -143,13 +168,33 @@ async function initUsers() {
|
||||
if (!exists) {
|
||||
const tempPw = crypto.randomBytes(6).toString('hex');
|
||||
const hash = await bcrypt.hash(tempPw, 12);
|
||||
db.prepare('INSERT INTO users (username, hash, role, mustChange) VALUES (?, ?, ?, 1)')
|
||||
db.prepare('INSERT INTO users (username, hash, role, mustChange, elo) VALUES (?, ?, ?, 1, 1000)')
|
||||
.run(username, hash, role);
|
||||
console.log(`[INIT] Created user '${username}' – temp password: ${tempPw}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── ELO calculation ───────────────────────────────────────────────────────────
|
||||
function calcElo(winnerElo, loserElo, k = 32) {
|
||||
const expected = 1 / (1 + 10 ** ((loserElo - winnerElo) / 400));
|
||||
const delta = Math.round(k * (1 - expected));
|
||||
return {
|
||||
winner: Math.max(0, winnerElo + delta),
|
||||
loser: Math.max(0, loserElo - delta),
|
||||
delta,
|
||||
};
|
||||
}
|
||||
|
||||
// ── ELO rank label ─────────────────────────────────────────────────────────────
|
||||
function eloRank(elo) {
|
||||
if (elo >= 1700) return 'GENERAL';
|
||||
if (elo >= 1500) return 'CAPTAIN';
|
||||
if (elo >= 1300) return 'LIEUTENANT';
|
||||
if (elo >= 1100) return 'SERGEANT';
|
||||
return 'PRIVATE';
|
||||
}
|
||||
|
||||
// ── Session secret ────────────────────────────────────────────────────────────
|
||||
function getSessionSecret() {
|
||||
const file = path.join(DATA_DIR, '.session-secret');
|
||||
@@ -313,6 +358,7 @@ function getLeaderboard() {
|
||||
lbCache = db.prepare(`
|
||||
SELECT
|
||||
u.username,
|
||||
COALESCE(u.elo, 1000) AS elo,
|
||||
COALESCE(ps.sessions, 0) AS sessions,
|
||||
COALESCE(ps.totalScore, 0) AS totalScore,
|
||||
COALESCE(ps.fastestTime, 0) AS fastestTime,
|
||||
@@ -356,7 +402,7 @@ app.get('/api/dashboard', requireAuth, (req, res) => {
|
||||
const rank = rankIdx >= 0 ? { position: rankIdx + 1 } : null;
|
||||
|
||||
const recent = db.prepare(`
|
||||
SELECT stratagem, category, score, time_ms, created_at
|
||||
SELECT stratagem, category, score, time_ms, mode, created_at
|
||||
FROM practice_sessions WHERE username = ?
|
||||
ORDER BY created_at DESC LIMIT 5
|
||||
`).all(u);
|
||||
@@ -368,10 +414,20 @@ app.get('/api/dashboard', requireAuth, (req, res) => {
|
||||
WHERE stratagem = ? AND username = ?
|
||||
`).get(dailyStrat.name, u);
|
||||
|
||||
const userRow = db.prepare('SELECT elo FROM users WHERE username = ?').get(u);
|
||||
const elo = userRow?.elo ?? 1000;
|
||||
|
||||
const onlineWithElo = [...userSockets.keys()].map(name => {
|
||||
const row = db.prepare('SELECT elo FROM users WHERE username = ?').get(name);
|
||||
return { name, elo: row?.elo ?? 1000, rank: eloRank(row?.elo ?? 1000) };
|
||||
});
|
||||
|
||||
res.json({
|
||||
stats: { ...stats, ...matchStats },
|
||||
rank,
|
||||
online: [...userSockets.keys()],
|
||||
elo,
|
||||
eloRank: eloRank(elo),
|
||||
online: onlineWithElo,
|
||||
recent,
|
||||
daily: { stratagem: dailyStrat, bestTime: dailyBest?.bestTime ?? null },
|
||||
});
|
||||
@@ -379,15 +435,27 @@ app.get('/api/dashboard', requireAuth, (req, res) => {
|
||||
|
||||
// ── Scores ────────────────────────────────────────────────────────────────────
|
||||
app.post('/api/scores/practice', requireAuth, (req, res) => {
|
||||
const { stratagem, category, time_ms, score } = req.body || {};
|
||||
const { stratagem, category, time_ms, score, mode } = req.body || {};
|
||||
if (!VALID_NAMES.has(stratagem)) return res.status(400).json({ error: 'Invalid stratagem' });
|
||||
if (typeof time_ms !== 'number' || time_ms <= 0 || time_ms > 31_000) return res.status(400).json({ error: 'Invalid time' });
|
||||
if (typeof score !== 'number' || score < 0 || score > 15_000) return res.status(400).json({ error: 'Invalid score' });
|
||||
if (typeof time_ms !== 'number' || time_ms <= 0 || time_ms > 600_000) return res.status(400).json({ error: 'Invalid time' });
|
||||
if (typeof score !== 'number' || score < 0 || score > 50_000) return res.status(400).json({ error: 'Invalid score' });
|
||||
|
||||
const safeMode = ['timed','endless','drill','speedrun'].includes(mode) ? mode : 'timed';
|
||||
|
||||
db.prepare(`
|
||||
INSERT INTO practice_sessions (username, stratagem, category, time_ms, score, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`).run(req.session.user, stratagem, category || '', time_ms, score, new Date().toISOString());
|
||||
INSERT INTO practice_sessions (username, stratagem, category, time_ms, score, mode, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(req.session.user, stratagem, category || '', time_ms, score, safeMode, new Date().toISOString());
|
||||
|
||||
// Update stratagem_stats (upsert)
|
||||
db.prepare(`
|
||||
INSERT INTO stratagem_stats (username, stratagem, attempts, completions, best_time)
|
||||
VALUES (?, ?, 1, 1, ?)
|
||||
ON CONFLICT(username, stratagem) DO UPDATE SET
|
||||
attempts = attempts + 1,
|
||||
completions = completions + 1,
|
||||
best_time = CASE WHEN best_time IS NULL OR ? < best_time THEN ? ELSE best_time END
|
||||
`).run(req.session.user, stratagem, time_ms, time_ms, time_ms);
|
||||
|
||||
invalidateLB();
|
||||
res.json({ ok: true });
|
||||
@@ -397,10 +465,27 @@ app.get('/api/scores/leaderboard', requireAuth, (req, res) => {
|
||||
res.json(getLeaderboard());
|
||||
});
|
||||
|
||||
app.get('/api/scores/leaderboard/elo', requireAuth, (req, res) => {
|
||||
const rows = db.prepare(`
|
||||
SELECT username, elo, role FROM users ORDER BY elo DESC LIMIT 20
|
||||
`).all();
|
||||
res.json(rows.map(r => ({ ...r, rank: eloRank(r.elo) })));
|
||||
});
|
||||
|
||||
app.get('/api/scores/leaderboard/speedrun', requireAuth, (req, res) => {
|
||||
// Speedrun: sum of all time_ms per user where mode = 'speedrun', order by total time ASC
|
||||
const rows = db.prepare(`
|
||||
SELECT username, SUM(time_ms) AS totalTime, COUNT(*) AS stratagems
|
||||
FROM practice_sessions WHERE mode = 'speedrun'
|
||||
GROUP BY username ORDER BY totalTime ASC LIMIT 20
|
||||
`).all();
|
||||
res.json(rows);
|
||||
});
|
||||
|
||||
app.get('/api/scores/me', requireAuth, (req, res) => {
|
||||
const u = req.session.user;
|
||||
const practice = db.prepare(`
|
||||
SELECT stratagem, category, score, time_ms, created_at
|
||||
SELECT stratagem, category, score, time_ms, mode, created_at
|
||||
FROM practice_sessions WHERE username = ?
|
||||
ORDER BY created_at DESC LIMIT 50
|
||||
`).all(u);
|
||||
@@ -411,6 +496,41 @@ app.get('/api/scores/me', requireAuth, (req, res) => {
|
||||
res.json({ practice, matches });
|
||||
});
|
||||
|
||||
// ── History (paginated) ────────────────────────────────────────────────────────
|
||||
app.get('/api/history', requireAuth, (req, res) => {
|
||||
const u = req.session.user;
|
||||
const page = Math.max(1, parseInt(req.query.page) || 1);
|
||||
const limit = Math.min(50, Math.max(1, parseInt(req.query.limit) || 10));
|
||||
const mode = req.query.mode || '';
|
||||
const cat = req.query.cat || '';
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
let where = 'WHERE username = ?';
|
||||
const params = [u];
|
||||
if (mode) { where += ' AND mode = ?'; params.push(mode); }
|
||||
if (cat) { where += ' AND category = ?'; params.push(cat); }
|
||||
|
||||
const total = db.prepare(`SELECT COUNT(*) AS n FROM practice_sessions ${where}`).get(...params).n;
|
||||
const rows = db.prepare(`
|
||||
SELECT stratagem, category, score, time_ms, mode, created_at
|
||||
FROM practice_sessions ${where}
|
||||
ORDER BY created_at DESC LIMIT ? OFFSET ?
|
||||
`).all(...params, limit, offset);
|
||||
|
||||
res.json({ rows, total, page, pages: Math.ceil(total / limit) });
|
||||
});
|
||||
|
||||
// ── Stratagem stats ────────────────────────────────────────────────────────────
|
||||
app.get('/api/stats/stratagems', requireAuth, (req, res) => {
|
||||
const u = req.session.user;
|
||||
const rows = db.prepare(`
|
||||
SELECT stratagem, attempts, completions, best_time
|
||||
FROM stratagem_stats WHERE username = ?
|
||||
ORDER BY best_time ASC NULLS LAST
|
||||
`).all(u);
|
||||
res.json(rows);
|
||||
});
|
||||
|
||||
// ── Stratagems API (authenticated) ────────────────────────────────────────────
|
||||
// Stratagem sequences are served via API – not as a public static file.
|
||||
app.get('/api/stratagems', requireAuth, (req, res) => {
|
||||
@@ -441,7 +561,10 @@ async function main() {
|
||||
}
|
||||
|
||||
function broadcastLobbyUpdate() {
|
||||
const online = [...userSockets.keys()];
|
||||
const online = [...userSockets.keys()].map(name => {
|
||||
const row = db.prepare('SELECT elo FROM users WHERE username = ?').get(name);
|
||||
return { name, elo: row?.elo ?? 1000, rank: eloRank(row?.elo ?? 1000) };
|
||||
});
|
||||
wss.clients.forEach(ws => {
|
||||
if (ws.readyState !== WebSocket.OPEN || !ws.userId) return;
|
||||
const incoming = [...pendingChallenges.entries()]
|
||||
@@ -470,15 +593,37 @@ async function main() {
|
||||
room.matchScores[winnerId]++;
|
||||
const matchScores = { ...room.matchScores };
|
||||
|
||||
// Track round history for post-match screen
|
||||
if (!room.roundHistory) room.roundHistory = [];
|
||||
room.roundHistory.push({ round: room.roundHistory.length + 1, winner: winnerId });
|
||||
|
||||
broadcastToRoom(room, 'round-complete', { winner: winnerId, matchScores });
|
||||
|
||||
if (room.matchScores[winnerId] >= 5) {
|
||||
broadcastToRoom(room, 'match-end', { winner: winnerId, matchScores });
|
||||
// Calculate ELO delta
|
||||
const wRow = db.prepare('SELECT elo FROM users WHERE username = ?').get(winnerId);
|
||||
const lRow = db.prepare('SELECT elo FROM users WHERE username = ?').get(loser.userId);
|
||||
const eloResult = calcElo(wRow?.elo ?? 1000, lRow?.elo ?? 1000);
|
||||
|
||||
db.prepare('UPDATE users SET elo = ? WHERE username = ?').run(eloResult.winner, winnerId);
|
||||
db.prepare('UPDATE users SET elo = ? WHERE username = ?').run(eloResult.loser, loser.userId);
|
||||
|
||||
db.prepare(`
|
||||
INSERT INTO matches (winner, loser, winner_rounds, loser_rounds, created_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`).run(winnerId, loser.userId, room.matchScores[winnerId], room.matchScores[loser.userId], new Date().toISOString());
|
||||
|
||||
invalidateLB();
|
||||
|
||||
broadcastToRoom(room, 'match-end', {
|
||||
winner: winnerId,
|
||||
matchScores,
|
||||
roundHistory: room.roundHistory,
|
||||
eloChanges: {
|
||||
[winnerId]: { old: wRow?.elo ?? 1000, new: eloResult.winner, delta: +eloResult.delta },
|
||||
[loser.userId]: { old: lRow?.elo ?? 1000, new: eloResult.loser, delta: -eloResult.delta },
|
||||
},
|
||||
});
|
||||
rooms.delete(room.roomId);
|
||||
} else {
|
||||
room.state = 'waiting';
|
||||
@@ -501,7 +646,8 @@ async function main() {
|
||||
const { targetUser } = payload;
|
||||
if (!userSockets.has(targetUser) || targetUser === userId) return;
|
||||
pendingChallenges.set(userId, targetUser);
|
||||
send(userSockets.get(targetUser), 'challenge-received', { from: userId });
|
||||
const challengerElo = db.prepare('SELECT elo FROM users WHERE username = ?').get(userId)?.elo ?? 1000;
|
||||
send(userSockets.get(targetUser), 'challenge-received', { from: userId, elo: challengerElo });
|
||||
broadcastLobbyUpdate();
|
||||
break;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user