Unify player profiles across T5 and T6
This commit is contained in:
@@ -15,12 +15,14 @@ export type LeaderboardMetric = typeof leaderboardMetrics[number]['key'];
|
||||
|
||||
export type PlayerStats = Record<LeaderboardMetric, number>;
|
||||
export interface PlayerMapRecord extends PlayerStats {
|
||||
game: string;
|
||||
key: string;
|
||||
name: string;
|
||||
lastSeen: string;
|
||||
}
|
||||
export interface PlayerMatch {
|
||||
id: number;
|
||||
game: string;
|
||||
server: string;
|
||||
server_deleted_at?: string | null;
|
||||
mapName: string;
|
||||
@@ -37,7 +39,6 @@ export interface PlayerMatch {
|
||||
bestScore: number;
|
||||
}
|
||||
export interface PlayerProfile {
|
||||
game: string;
|
||||
guid: string;
|
||||
name: string;
|
||||
firstSeen: string;
|
||||
@@ -49,8 +50,8 @@ export interface PlayerProfile {
|
||||
limit: number;
|
||||
}
|
||||
|
||||
export function playerProfileHref(game: string, guid: string): string {
|
||||
return `/leaderboard/players/${encodeURIComponent(game)}/${encodeURIComponent(guid)}`;
|
||||
export function playerProfileHref(guid: string): string {
|
||||
return `/leaderboard/players/${encodeURIComponent(guid)}`;
|
||||
}
|
||||
|
||||
export interface LeaderboardEntry {
|
||||
|
||||
@@ -7,29 +7,31 @@ const totals = `MAX(mp.highest_round) AS highestRound, SUM(mp.kills) AS kills,
|
||||
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
|
||||
WHERE m.game IN ('t5', 't6') AND mp.guid = ? AND mp.has_stats = 1
|
||||
AND m.highest_round >= ${MIN_RECORDED_ROUND} AND mp.highest_round >= ${MIN_RECORDED_ROUND}`;
|
||||
|
||||
export function getPlayerProfile(game: string, guid: string, requestedOffset = 0): PlayerProfile | null {
|
||||
export function getPlayerProfile(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;
|
||||
// Keep game-specific ingestion identities, but show one identity with the latest reported name.
|
||||
const player = handle.prepare(`SELECT guid, name, MIN(first_seen_at) OVER () AS firstSeen,
|
||||
MAX(last_seen_at) OVER () AS lastSeen FROM leaderboard_players
|
||||
WHERE game IN ('t5', 't6') AND guid = ? ORDER BY last_seen_at DESC, game LIMIT 1`)
|
||||
.get(guid) as Pick<PlayerProfile, 'guid' | 'name' | 'firstSeen' | 'lastSeen'> | undefined;
|
||||
if (!player) return null;
|
||||
const stats = handle.prepare(`SELECT ${totals} ${scope}`).get(game, guid) as PlayerStats;
|
||||
const stats = handle.prepare(`SELECT ${totals} ${scope}`).get(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,
|
||||
const maps = handle.prepare(`SELECT m.game, 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.server_deleted_at, m.map_name AS mapName,
|
||||
GROUP BY m.game, m.map_key ORDER BY highestRound DESC, kills DESC, name COLLATE NOCASE, m.game, key`)
|
||||
.all(guid) as PlayerMapRecord[];
|
||||
const matches = handle.prepare(`SELECT m.id, m.game, m.server, m.server_deleted_at, 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[];
|
||||
.all(guid, limit, offset) as PlayerMatch[];
|
||||
return { ...player, stats, maps, matches, offset, limit };
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
import Badge from './primitives/Badge.svelte';
|
||||
import EmptyState from './primitives/EmptyState.svelte';
|
||||
|
||||
let { report, mapLabel, game, current = true, after = null }: { report: GameTelemetry | undefined; mapLabel?: string; game: string; current?: boolean; after?: string | null } = $props();
|
||||
let { report, mapLabel, current = true, after = null }: { report: GameTelemetry | undefined; mapLabel?: string; current?: boolean; after?: string | null } = $props();
|
||||
const fresh = $derived(current && freshTelemetry(report, $uiNow, after));
|
||||
const timingNow = $derived(report && (report.event === 'game_end' || !fresh)
|
||||
? Date.parse(report.receivedAt) : $uiNow);
|
||||
@@ -89,7 +89,7 @@
|
||||
{@const timing = report.timing?.players[player.guid]}
|
||||
<tr>
|
||||
<th scope="row" class="player">
|
||||
<a class="player-name" href={playerProfileHref(game.toLowerCase(), player.guid)}>{player.name}</a>
|
||||
<a class="player-name" href={playerProfileHref(player.guid)}>{player.name}</a>
|
||||
<div class="guid"><span>GUID</span><CopyValue value={player.guid} label={`GUID for ${player.name}`} /></div>
|
||||
</th>
|
||||
<td class="stat">{timing ? elapsedSince(timing.serverJoinedAt, timingNow) : '—'}</td>
|
||||
|
||||
Reference in New Issue
Block a user