74 lines
5.3 KiB
TypeScript
74 lines
5.3 KiB
TypeScript
import { db } from '../db/index.js';
|
|
import { MIN_RECORDED_ROUND } from '../../match-history.js';
|
|
import { getPersonalBestRecords } from './leaderboard-records.js';
|
|
import { countsAsGamePlayed } from './leaderboard-participation.js';
|
|
import type { LeaderboardOptions, 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, SUM(${countsAsGamePlayed}) 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 IN ('t5', 't6') AND m.status = 'completed' 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(guid: string, requestedOffset = 0,
|
|
options: Pick<LeaderboardOptions, 'game' | 'map' | 'players'> = {}): PlayerProfile | null {
|
|
const handle = db();
|
|
// 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 { matchCount, ...stats } = handle.prepare(`SELECT COUNT(*) AS matchCount, ${totals} ${scope}`)
|
|
.get(guid) as PlayerStats & { matchCount: number };
|
|
if (!matchCount) return null;
|
|
const historyParameters: Array<string | number> = [guid];
|
|
let historyScope = scope;
|
|
if (options.game) { historyScope += ' AND m.game = ?'; historyParameters.push(options.game); }
|
|
if (options.map) { historyScope += ' AND m.map_key = ?'; historyParameters.push(options.map); }
|
|
if (options.players) { historyScope += ' AND m.start_players = ?'; historyParameters.push(options.players); }
|
|
const matchesTotal = options.game || options.map || options.players
|
|
? (handle.prepare(`SELECT COUNT(*) AS total ${historyScope}`).get(...historyParameters) as { total: number }).total
|
|
: matchCount;
|
|
const limit = 25;
|
|
const lastOffset = Math.max(0, Math.floor((matchesTotal - 1) / limit) * limit);
|
|
const offset = Number.isSafeInteger(requestedOffset) ? Math.min(lastOffset, Math.max(0, requestedOffset)) : 0;
|
|
const mapStats = 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.game, m.map_key ORDER BY highestRound DESC, kills DESC, name COLLATE NOCASE, m.game, key`)
|
|
.all(guid) as Array<Omit<PlayerMapRecord, 'records' | 'ranks'>>;
|
|
const [records, ...mapRecords] = getPersonalBestRecords(handle, [{ guid, ...stats },
|
|
...mapStats.map(map => ({ guid, ...map, map: map.key }))]);
|
|
// Rank all three bests in one pass over this player's maps. Filter the GUID
|
|
// only after ranking competitors so ties and skipped ranks match the leaderboard.
|
|
const mapRanks = handle.prepare(`WITH totals AS (
|
|
SELECT requested.key AS requestId, mp.guid,
|
|
MAX(mp.highest_round) AS highestRound, MAX(mp.kills) AS bestKills, MAX(mp.best_score) AS bestScore
|
|
FROM json_each(?) requested
|
|
JOIN leaderboard_matches m ON m.game = json_extract(requested.value, '$.game')
|
|
AND m.map_key = json_extract(requested.value, '$.key')
|
|
JOIN leaderboard_match_players mp ON mp.match_id = m.id
|
|
WHERE m.game IN ('t5', 't6') AND m.status = 'completed' AND mp.has_stats = 1
|
|
AND m.highest_round >= ${MIN_RECORDED_ROUND} AND mp.highest_round >= ${MIN_RECORDED_ROUND}
|
|
AND EXISTS (SELECT 1 FROM leaderboard_players p WHERE p.guid = mp.guid AND p.game IN ('t5', 't6'))
|
|
GROUP BY requested.key, mp.guid
|
|
), ranked AS (
|
|
SELECT requestId, guid,
|
|
RANK() OVER (PARTITION BY requestId ORDER BY highestRound DESC) AS highestRound,
|
|
RANK() OVER (PARTITION BY requestId ORDER BY bestKills DESC) AS bestKills,
|
|
RANK() OVER (PARTITION BY requestId ORDER BY bestScore DESC) AS bestScore
|
|
FROM totals
|
|
) SELECT highestRound, bestKills, bestScore FROM ranked WHERE guid = ? ORDER BY requestId`)
|
|
.all(JSON.stringify(mapStats.map(({ game, key }) => ({ game, key }))), guid) as PlayerMapRecord['ranks'][];
|
|
const maps = mapStats.map((map, index) => ({ ...map, records: mapRecords[index], ranks: mapRanks[index] }));
|
|
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
|
|
${historyScope} ORDER BY mp.last_seen_at DESC, m.id DESC LIMIT ? OFFSET ?`)
|
|
.all(...historyParameters, limit, offset) as PlayerMatch[];
|
|
return { ...player, stats, records, maps, matches, matchesTotal, offset, limit };
|
|
}
|