Unify player profiles across T5 and T6
This commit is contained in:
@@ -209,7 +209,7 @@
|
||||
<tr>
|
||||
<td class="rank"><span class:podium={entry.rank <= 3}>{number.format(entry.rank)}</span></td>
|
||||
<th scope="row" class="player">
|
||||
<a class="player-name" href={playerProfileHref(entry.game, entry.guid)}>{entry.name}</a>
|
||||
<a class="player-name" href={playerProfileHref(entry.guid)}>{entry.name}</a>
|
||||
<div class="identity"><span class="game-label">{entry.game.toUpperCase()}</span><CopyValue value={entry.guid} label={`GUID for ${entry.name}`} /></div>
|
||||
</th>
|
||||
{#each columns as key (key)}<td class="stat" class:selected={key === metric}>{number.format(entry[key])}</td>{/each}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { getPlayerProfile } from '$lib/server/fleet/player-profile.js';
|
||||
import { error, redirect } from '@sveltejs/kit';
|
||||
import { playerProfileHref } from '$lib/leaderboard.js';
|
||||
|
||||
export function load({ params, url }: { params: { game: string; guid: string }; url: URL }) {
|
||||
if (params.game !== 't5' && params.game !== 't6') error(404, 'Player not found');
|
||||
const profile = getPlayerProfile(params.game, params.guid, Number(url.searchParams.get('offset') ?? 0));
|
||||
if (!profile) error(404, 'No recorded stats found for this player');
|
||||
return { profile };
|
||||
redirect(308, playerProfileHref(params.guid) + url.search);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { getPlayerProfile } from '$lib/server/fleet/player-profile.js';
|
||||
|
||||
export function load({ params, url }: { params: { guid: string }; url: URL }) {
|
||||
const profile = getPlayerProfile(params.guid, Number(url.searchParams.get('offset') ?? 0));
|
||||
if (!profile) error(404, 'No recorded stats found for this player');
|
||||
return { profile };
|
||||
}
|
||||
+9
-8
@@ -12,6 +12,7 @@
|
||||
const stats = $derived(profile.stats);
|
||||
const number = new Intl.NumberFormat('en-US', { maximumFractionDigits: 1 });
|
||||
const date = (value: string) => $timeFormat.dateTime(value);
|
||||
const gameName = (game: string) => game === 't5' ? 'Black Ops I' : game === 't6' ? 'Black Ops II' : game.toUpperCase();
|
||||
const ratio = (value: number, total: number, percent = false) => total ? `${number.format(value / total * (percent ? 100 : 1))}${percent ? '%' : ''}` : '—';
|
||||
const historyColumns = ['highestRound', 'kills', 'roundsSurvived', 'headshots', 'revives', 'downs', 'bestScore'] as const satisfies readonly LeaderboardMetric[];
|
||||
let refreshing = $state(false);
|
||||
@@ -28,7 +29,7 @@
|
||||
<svelte:head><title>{profile.name} · Player stats · Electric Cherry</title></svelte:head>
|
||||
|
||||
<section class="profile">
|
||||
<SectionHeader title={profile.name} eyebrow="Player profile" subtitle={`${profile.game === 't5' ? 'Black Ops I' : 'Black Ops II'} · All recorded games`}>
|
||||
<SectionHeader title={profile.name} eyebrow="Player profile" subtitle="Black Ops I & Black Ops II · Lifetime Zombies stats">
|
||||
{#snippet crumbs()}<a href="/leaderboard">← Leaderboard</a>{/snippet}
|
||||
{#snippet actions()}<Button size="sm" loading={refreshing} onclick={refresh}>Refresh stats</Button>{/snippet}
|
||||
</SectionHeader>
|
||||
@@ -57,13 +58,13 @@
|
||||
</section>
|
||||
|
||||
<section class="panel" aria-labelledby="maps-title">
|
||||
<div class="panel-heading"><h2 id="maps-title">Per-map records</h2><p>Personal best rounds and every tracked stat, ordered by highest round.</p></div>
|
||||
<div class="panel-heading"><h2 id="maps-title">Per-map records</h2><p>Personal best rounds and every tracked stat across both games, ordered by highest round.</p></div>
|
||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex (Scrollable table for keyboard users.) -->
|
||||
<div class="table-scroll" role="region" aria-label="Per-map player records" tabindex="0">
|
||||
<table>
|
||||
<thead><tr><th scope="col">Map</th>{#each leaderboardMetrics as metric}<th scope="col">{metric.label}</th>{/each}</tr></thead>
|
||||
<tbody>{#each profile.maps as map (map.key)}
|
||||
<tr><th scope="row"><a href={`/leaderboard?${new URLSearchParams({ game: profile.game, map: map.key })}`}>{map.name}</a><small>Last played {date(map.lastSeen)}</small></th>
|
||||
<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 })}`}>{map.name}</a><small>{gameName(map.game)}</small><small>Last played {date(map.lastSeen)}</small></th>
|
||||
{#each leaderboardMetrics as metric}<td class:best={metric.key === 'highestRound'}>{number.format(map[metric.key])}</td>{/each}</tr>
|
||||
{/each}</tbody>
|
||||
</table>
|
||||
@@ -71,13 +72,13 @@
|
||||
</section>
|
||||
|
||||
<section class="panel" aria-labelledby="history-title">
|
||||
<div class="panel-heading"><h2 id="history-title">Match history</h2><p>Most recently observed games first. Stats reflect this player's participation.</p></div>
|
||||
<div class="panel-heading"><h2 id="history-title">Match history</h2><p>Matches from both games, most recently observed first. Stats reflect this player's participation.</p></div>
|
||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex (Scrollable table for keyboard users.) -->
|
||||
<div class="table-scroll" role="region" aria-label="Player match history" tabindex="0">
|
||||
<table>
|
||||
<thead><tr><th scope="col">Map / server</th><th scope="col">Match started</th><th scope="col">Match status</th>{#each historyColumns as key}<th scope="col">{leaderboardMetrics.find(metric => metric.key === key)!.label}</th>{/each}</tr></thead>
|
||||
<tbody>{#each profile.matches as match (match.id)}
|
||||
<tr><th scope="row">{match.mapName}<small>{match.server} {#if match.server_deleted_at}<Badge>Deleted</Badge>{/if}</small></th><td>{date(match.startedAt)}</td>
|
||||
<tr><th scope="row">{match.mapName}<small>{gameName(match.game)}</small><small>{match.server} {#if match.server_deleted_at}<Badge>Deleted</Badge>{/if}</small></th><td>{date(match.startedAt)}</td>
|
||||
<td class="status">{match.status}<small>{match.completed ? 'Completion credited' : 'No completion credit'}</small></td>
|
||||
{#each historyColumns as key}<td class:best={key === 'highestRound'}>{number.format(match[key])}</td>{/each}
|
||||
</tr>
|
||||
@@ -89,8 +90,8 @@
|
||||
</div>
|
||||
</section>
|
||||
<details class="note"><summary>How these stats work</summary>
|
||||
<p>Records cover saved telemetry, including active and interrupted games. Highest round includes mid-game joins; total rounds only counts observed round progression. Kills, headshots, revives and downs use each match's highest reported counters.</p>
|
||||
<p>Highest reported score is the largest observed points balance, not total points earned. Completion requires the player to appear in the final report. Records stay with the same GUID within this game when a player changes names.</p>
|
||||
<p>Lifetime stats combine saved telemetry from Black Ops I and Black Ops II, including active and interrupted games. Highest round includes mid-game joins; total rounds only counts observed round progression. Kills, headshots, revives and downs use each match's highest reported counters.</p>
|
||||
<p>Highest reported score is the largest observed points balance, not total points earned. Completion requires the player to appear in the final report. Records stay with the same GUID across both games when a player changes names, and the latest reported name is displayed.</p>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user