Add player profiles with lifetime stats and map records

This commit is contained in:
2026-09-19 22:24:35 -04:00
parent ce0321a6e9
commit 33f2f123c5
7 changed files with 268 additions and 2 deletions
+39
View File
@@ -13,6 +13,45 @@ export const leaderboardMetrics = [
export type LeaderboardMetric = typeof leaderboardMetrics[number]['key'];
export type PlayerStats = Record<LeaderboardMetric, number>;
export interface PlayerMapRecord extends PlayerStats {
key: string;
name: string;
lastSeen: string;
}
export interface PlayerMatch {
id: number;
server: string;
mapName: string;
startedAt: string;
lastSeen: string;
status: 'active' | 'completed' | 'interrupted';
completed: number;
highestRound: number;
roundsSurvived: number;
kills: number;
headshots: number;
revives: number;
downs: number;
bestScore: number;
}
export interface PlayerProfile {
game: string;
guid: string;
name: string;
firstSeen: string;
lastSeen: string;
stats: PlayerStats;
maps: PlayerMapRecord[];
matches: PlayerMatch[];
offset: number;
limit: number;
}
export function playerProfileHref(game: string, guid: string): string {
return `/leaderboard/players/${encodeURIComponent(game)}/${encodeURIComponent(guid)}`;
}
export interface LeaderboardEntry {
rank: number;
game: string;
+33
View File
@@ -0,0 +1,33 @@
import { db } from '../db/index.js';
import type { PlayerProfile, PlayerStats, PlayerMapRecord, PlayerMatch } from '../../leaderboard.js';
const totals = `MAX(mp.highest_round) AS highestRound, SUM(mp.kills) AS kills,
SUM(mp.rounds_survived) AS roundsSurvived, SUM(mp.headshots) AS headshots,
SUM(mp.revives) AS revives, SUM(mp.downs) AS downs, COUNT(*) AS gamesPlayed,
SUM(mp.completed) AS gamesCompleted, MAX(mp.kills) AS bestKills, MAX(mp.best_score) AS bestScore`;
const scope = `FROM leaderboard_match_players mp JOIN leaderboard_matches m ON m.id = mp.match_id
WHERE m.game = ? AND mp.guid = ? AND mp.has_stats = 1`;
export function getPlayerProfile(game: string, guid: string, requestedOffset = 0): PlayerProfile | null {
const handle = db();
const player = handle.prepare(`SELECT game, guid, name, first_seen_at AS firstSeen,
last_seen_at AS lastSeen FROM leaderboard_players WHERE game = ? AND guid = ?`)
.get(game, guid) as Pick<PlayerProfile, 'game' | 'guid' | 'name' | 'firstSeen' | 'lastSeen'> | undefined;
if (!player) return null;
const stats = handle.prepare(`SELECT ${totals} ${scope}`).get(game, guid) as PlayerStats;
if (!stats.gamesPlayed) return null;
const limit = 25;
const lastOffset = Math.floor((stats.gamesPlayed - 1) / limit) * limit;
const offset = Number.isSafeInteger(requestedOffset) ? Math.min(lastOffset, Math.max(0, requestedOffset)) : 0;
const maps = handle.prepare(`SELECT m.map_key AS key, m.map_name AS name,
MAX(mp.last_seen_at) AS lastSeen, ${totals} ${scope}
GROUP BY m.map_key ORDER BY highestRound DESC, kills DESC, name COLLATE NOCASE, key`)
.all(game, guid) as PlayerMapRecord[];
const matches = handle.prepare(`SELECT m.id, m.server, m.map_name AS mapName,
m.started_at AS startedAt, mp.last_seen_at AS lastSeen, m.status, mp.completed,
mp.highest_round AS highestRound, mp.rounds_survived AS roundsSurvived,
mp.kills, mp.headshots, mp.revives, mp.downs, mp.best_score AS bestScore
${scope} ORDER BY mp.last_seen_at DESC, m.id DESC LIMIT ? OFFSET ?`)
.all(game, guid, limit, offset) as PlayerMatch[];
return { ...player, stats, maps, matches, offset, limit };
}