This commit is contained in:
@@ -65,7 +65,7 @@ export interface PlayerProfile {
|
||||
firstSeen: string;
|
||||
lastSeen: string;
|
||||
stats: PlayerStats;
|
||||
records: PersonalBestRecords;
|
||||
records: PersonalBestRecords | null;
|
||||
maps: PlayerMapRecord[];
|
||||
matches: PlayerMatch[];
|
||||
matchesTotal: number;
|
||||
|
||||
@@ -120,14 +120,8 @@ export function getMatchDetail(id: number): MatchDetail | null {
|
||||
CASE WHEN p.has_stats = 1 THEN p.downs END AS downs,
|
||||
CASE WHEN p.has_stats = 1 THEN p.revives END AS revives,
|
||||
CASE WHEN p.has_stats = 1 THEN p.best_score END AS bestScore,
|
||||
(EXISTS (SELECT 1 FROM leaderboard_players identity
|
||||
WHERE identity.guid = p.guid AND identity.game IN ('t5', 't6'))
|
||||
AND EXISTS (SELECT 1 FROM leaderboard_match_players recorded
|
||||
JOIN leaderboard_matches history ON history.id = recorded.match_id
|
||||
WHERE recorded.guid = p.guid AND history.game IN ('t5', 't6') AND recorded.has_stats = 1
|
||||
AND history.status = 'completed'
|
||||
AND history.highest_round >= ${MIN_RECORDED_ROUND}
|
||||
AND recorded.highest_round >= ${MIN_RECORDED_ROUND})) AS canViewProfile
|
||||
EXISTS (SELECT 1 FROM leaderboard_players identity
|
||||
WHERE identity.guid = p.guid AND identity.game IN ('t5', 't6')) AS canViewProfile
|
||||
FROM leaderboard_match_players p WHERE p.match_id = ?
|
||||
ORDER BY hasStats DESC, kills DESC, highestRound DESC, name COLLATE NOCASE, p.guid`
|
||||
)
|
||||
|
||||
@@ -36,7 +36,30 @@ export function getPlayerProfile(
|
||||
const { matchCount, ...stats } = handle
|
||||
.prepare(`SELECT COUNT(*) AS matchCount, ${totals} ${scope}`)
|
||||
.get(guid) as PlayerStats & { matchCount: number };
|
||||
if (!matchCount) return null;
|
||||
const limit = 25;
|
||||
if (!matchCount) {
|
||||
return {
|
||||
...player,
|
||||
stats: {
|
||||
highestRound: 0,
|
||||
kills: 0,
|
||||
roundsSurvived: 0,
|
||||
headshots: 0,
|
||||
revives: 0,
|
||||
downs: 0,
|
||||
gamesPlayed: 0,
|
||||
gamesCompleted: 0,
|
||||
bestKills: 0,
|
||||
bestScore: 0
|
||||
},
|
||||
records: null,
|
||||
maps: [],
|
||||
matches: [],
|
||||
matchesTotal: 0,
|
||||
offset: 0,
|
||||
limit
|
||||
};
|
||||
}
|
||||
const historyParameters: Array<string | number> = [guid];
|
||||
let historyScope = scope;
|
||||
if (options.game) {
|
||||
@@ -59,7 +82,6 @@ export function getPlayerProfile(
|
||||
.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))
|
||||
|
||||
@@ -35,7 +35,7 @@ export async function load({ params, url }: { params: { guid: string }; url: URL
|
||||
Number(url.searchParams.get('offset') ?? 0),
|
||||
options
|
||||
);
|
||||
if (!profile) error(404, 'No recorded stats found for this player');
|
||||
if (!profile) error(404, 'Player not found');
|
||||
return {
|
||||
profile,
|
||||
returnTo: leaderboardReturnHref(url.searchParams.get('returnTo')),
|
||||
|
||||
@@ -156,6 +156,12 @@
|
||||
onRetry={() => void refresh()}
|
||||
/>{/snippet}
|
||||
</SectionHeader>
|
||||
{#if !profile.records}
|
||||
<EmptyState
|
||||
title="No qualifying match history yet"
|
||||
description="Stats appear after a recorded game ends, provided this player was observed at round 2 or later. Games in progress and interrupted games do not contribute."
|
||||
/>
|
||||
{/if}
|
||||
<nav class="jump-links" aria-label="Player sections">
|
||||
<a href="#overview">Overview</a><a href="#maps-title">Maps</a><a href="#history-title"
|
||||
>Matches</a
|
||||
@@ -185,12 +191,13 @@
|
||||
><a href={scopedLeaderboard}>Selected leaderboard</a>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span>Lifetime best round</span><strong>{number.format(stats.highestRound)}</strong
|
||||
><RecordSource
|
||||
evidence={profile.records.highestRound}
|
||||
{returnTo}
|
||||
metric="Lifetime best round"
|
||||
/>
|
||||
<span>Lifetime best round</span><strong
|
||||
>{profile.records ? number.format(stats.highestRound) : '—'}</strong
|
||||
>{#if profile.records}<RecordSource
|
||||
evidence={profile.records.highestRound}
|
||||
{returnTo}
|
||||
metric="Lifetime best round"
|
||||
/>{/if}
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span>Lifetime games played</span><strong>{number.format(stats.gamesPlayed)}</strong
|
||||
@@ -219,54 +226,60 @@
|
||||
map. Tied records share a rank. Each record links to the match that established it.
|
||||
</p>
|
||||
</div>
|
||||
<div class="mobile-records">
|
||||
<PlayerMapCards maps={profile.maps} metric={scope.metric} {returnTo} />
|
||||
</div>
|
||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex (Scrollable records for keyboard users.) -->
|
||||
<div
|
||||
class="table-scroll desktop-records"
|
||||
role="region"
|
||||
aria-label="Per-map player records"
|
||||
tabindex="0"
|
||||
>
|
||||
<table>
|
||||
<thead
|
||||
><tr
|
||||
><th scope="col">Map</th>{#each records as record}<th scope="col"
|
||||
>{record.label}</th
|
||||
>{/each}<th scope="col">Games</th><th scope="col">Total kills</th><th
|
||||
scope="col">Total rounds</th
|
||||
></tr
|
||||
></thead
|
||||
>
|
||||
<tbody
|
||||
>{#each profile.maps as map (`${map.game}:${map.key}`)}<tr
|
||||
><th scope="row"
|
||||
><a
|
||||
href={`/leaderboard?${new URLSearchParams({ game: map.game, map: map.key, metric: scope.metric })}`}
|
||||
>{map.name}</a
|
||||
><small>{gameName(map.game)}</small></th
|
||||
>{#each records as record}<td
|
||||
{#if profile.maps.length}
|
||||
<div class="mobile-records">
|
||||
<PlayerMapCards maps={profile.maps} metric={scope.metric} {returnTo} />
|
||||
</div>
|
||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex (Scrollable records for keyboard users.) -->
|
||||
<div
|
||||
class="table-scroll desktop-records"
|
||||
role="region"
|
||||
aria-label="Per-map player records"
|
||||
tabindex="0"
|
||||
>
|
||||
<table>
|
||||
<thead
|
||||
><tr
|
||||
><th scope="col">Map</th>{#each records as record}<th scope="col"
|
||||
>{record.label}</th
|
||||
>{/each}<th scope="col">Games</th><th scope="col">Total kills</th
|
||||
><th scope="col">Total rounds</th></tr
|
||||
></thead
|
||||
>
|
||||
<tbody
|
||||
>{#each profile.maps as map (`${map.game}:${map.key}`)}<tr
|
||||
><th scope="row"
|
||||
><a
|
||||
href={contextHref(
|
||||
`/matches/${map.records[record.key].matchId}`,
|
||||
$page.url
|
||||
)}
|
||||
aria-label={`${map.name} ${record.label}: ${number.format(map[record.key])}, Match #${map.records[record.key].matchId}`}
|
||||
title={map.records[record.key].endedAt
|
||||
? $timeFormat.dateTime(map.records[record.key].endedAt!)
|
||||
: 'Completed match'}>{number.format(map[record.key])}</a
|
||||
><small
|
||||
aria-label={`${map.name} ${record.label} rank ${map.ranks[record.key]}`}
|
||||
>Rank #{number.format(map.ranks[record.key])}</small
|
||||
></td
|
||||
>{/each}<td>{number.format(map.gamesPlayed)}</td><td
|
||||
>{number.format(map.kills)}</td
|
||||
><td>{number.format(map.roundsSurvived)}</td></tr
|
||||
>{/each}</tbody
|
||||
>
|
||||
</table>
|
||||
</div>
|
||||
href={`/leaderboard?${new URLSearchParams({ game: map.game, map: map.key, metric: scope.metric })}`}
|
||||
>{map.name}</a
|
||||
><small>{gameName(map.game)}</small></th
|
||||
>{#each records as record}<td
|
||||
><a
|
||||
href={contextHref(
|
||||
`/matches/${map.records[record.key].matchId}`,
|
||||
$page.url
|
||||
)}
|
||||
aria-label={`${map.name} ${record.label}: ${number.format(map[record.key])}, Match #${map.records[record.key].matchId}`}
|
||||
title={map.records[record.key].endedAt
|
||||
? $timeFormat.dateTime(
|
||||
map.records[record.key].endedAt!
|
||||
)
|
||||
: 'Completed match'}
|
||||
>{number.format(map[record.key])}</a
|
||||
><small
|
||||
aria-label={`${map.name} ${record.label} rank ${map.ranks[record.key]}`}
|
||||
>Rank #{number.format(map.ranks[record.key])}</small
|
||||
></td
|
||||
>{/each}<td>{number.format(map.gamesPlayed)}</td><td
|
||||
>{number.format(map.kills)}</td
|
||||
><td>{number.format(map.roundsSurvived)}</td></tr
|
||||
>{/each}</tbody
|
||||
>
|
||||
</table>
|
||||
</div>
|
||||
{:else}
|
||||
<EmptyState headingLevel={3} title="No map records yet" />
|
||||
{/if}
|
||||
</section>
|
||||
<section class="panel" aria-labelledby="history-title">
|
||||
<div class="panel-heading">
|
||||
@@ -333,6 +346,10 @@
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{:else if !profile.records}<EmptyState
|
||||
headingLevel={3}
|
||||
title="No qualifying matches recorded"
|
||||
/>
|
||||
{:else}<EmptyState
|
||||
headingLevel={3}
|
||||
title="No completed matches in this selection"
|
||||
@@ -352,7 +369,12 @@
|
||||
<dl class="metric-list">
|
||||
{#each otherMetrics as metric}<div>
|
||||
<dt>{metric.label}</dt>
|
||||
<dd>{number.format(stats[metric.key])}</dd>
|
||||
<dd>
|
||||
{!profile.records &&
|
||||
(metric.key === 'bestKills' || metric.key === 'bestScore')
|
||||
? '—'
|
||||
: number.format(stats[metric.key])}
|
||||
</dd>
|
||||
</div>{/each}{#each efficiencyMetrics as metric}<div>
|
||||
<dt>{metric.label}</dt>
|
||||
<dd>{formatEfficiencyValue(efficiencyValue(stats, metric.key), metric.key)}</dd>
|
||||
|
||||
Reference in New Issue
Block a user