Count only completed games in player leaderboards
This commit is contained in:
@@ -375,7 +375,9 @@ and how to interpret missing or rejected reports.
|
||||
The **Leaderboard** records persistent player totals and map records from these
|
||||
same reports, including highest round, cumulative kills, rounds survived,
|
||||
headshots, revives, downs, and games played. See [Leaderboard](docs/leaderboard.md)
|
||||
for counting rules, history import, and the read API.
|
||||
for counting rules, history import, and the read API. Only completed games count;
|
||||
in-progress and interrupted games remain in Match History without contributing
|
||||
to player rankings or profile totals.
|
||||
|
||||
In **Match History**, select **View match** on a completed match to open its
|
||||
shareable results page. It shows the map, server, final round, timing, and every
|
||||
|
||||
+9
-7
@@ -5,9 +5,11 @@ with one entry per GUID combining Black Ops I (T5) and Black Ops II (T6). Game
|
||||
and map filters limit which matches contribute to each player's statistics and
|
||||
rank. Choose a ranking, search a name or GUID, and share the page URL with its
|
||||
filters intact. Results refresh while the page is visible.
|
||||
Matches only appear in server/global history and leaderboard map options once
|
||||
they reach round 2. Player records only contribute to leaderboards, summaries,
|
||||
profiles and player history once that player is observed at round 2 or later.
|
||||
Matches only appear in server/global history once they reach round 2.
|
||||
Leaderboards, summaries, map options, profiles and player history include only
|
||||
completed matches with a received `game_end`. Active and interrupted matches do
|
||||
not contribute. A player must also be observed at round 2 or later; players who
|
||||
left before a completed game's end retain their recorded stats.
|
||||
This also excludes existing round 1 records. Early telemetry is retained so
|
||||
qualifying games keep their start time, roster and cumulative counters.
|
||||
|
||||
@@ -32,7 +34,7 @@ score are the highest reported values within the match, not necessarily its fina
|
||||
scoreboard. Active and interrupted matches remain listed without results links.
|
||||
Match pages remain available after the original server is removed.
|
||||
|
||||
The leaderboard uses the existing three telemetry events; no reporter change is
|
||||
The leaderboard uses the existing telemetry events; no reporter change is
|
||||
required. The leaderboard and profiles identify a player by **GUID**, displaying
|
||||
the latest reported name across both games, regardless of game or map filters.
|
||||
Different names for the same GUID do not split their leaderboard entry or
|
||||
@@ -47,7 +49,7 @@ leaderboard and profiles require no storage migration. GUIDs stay strings.
|
||||
| Highest round | Highest round reported with that player present, including players who joined mid-game. |
|
||||
| Total kills, headshots, revives, downs | Sum of each player's highest reported counter in each game. Round snapshots are cumulative, so they are never added together within a game. |
|
||||
| Total rounds | Round progression during observed participation, added across games. A player observed at game start and then round 8 receives seven rounds. A player first seen at round 8 gets a highest-round record of 8, but zero total rounds; reaching round 9 then adds one. |
|
||||
| Games played | Games in which a stats report at round 2 or later contains the player. Start-only and round 1 participation do not count. |
|
||||
| Games played | Completed games in which a stats report at round 2 or later contains the player, even if they left before the end. Start-only and round 1 participation do not count. |
|
||||
| Games completed | Games whose `game_end` roster contains the player. Disconnected players retain earlier stats without receiving completion credit. |
|
||||
| Best game kills | Highest kill count recorded in any one game. |
|
||||
| Highest reported score | Highest observed score balance. Spending points can lower the score, so this is not cumulative points earned. |
|
||||
@@ -59,8 +61,8 @@ across both games when unfiltered. Summary cards describe the same selection,
|
||||
regardless of the search text. Player counts include each qualifying GUID once,
|
||||
including when they have records in both games. Summary rounds are
|
||||
player-rounds: four players progressing from game start to round 8 contribute 28.
|
||||
Highest-round records always use the round reached, including the final round
|
||||
and players who joined mid-game.
|
||||
Highest-round records always use the round reached in a completed match,
|
||||
including the final round and players who joined mid-game.
|
||||
|
||||
Round totals require the player to appear in consecutive observations. If a
|
||||
report omits them, their return starts a new participation interval and the gap
|
||||
|
||||
@@ -16,7 +16,7 @@ export function getLeaderboard(options: LeaderboardOptions = {}): LeaderboardRes
|
||||
const metric = options.metric && isLeaderboardMetric(options.metric) ? options.metric : 'highestRound';
|
||||
const limit = Number.isSafeInteger(options.limit) ? Math.min(100, Math.max(1, options.limit!)) : 50;
|
||||
const offset = Number.isSafeInteger(options.offset) ? Math.min(MAX_LEADERBOARD_OFFSET, Math.max(0, options.offset!)) : 0;
|
||||
const conditions = ["m.game IN ('t5', 't6')", 'mp.has_stats = 1', `m.highest_round >= ${MIN_RECORDED_ROUND}`,
|
||||
const conditions = ["m.game IN ('t5', 't6')", "m.status = 'completed'", 'mp.has_stats = 1', `m.highest_round >= ${MIN_RECORDED_ROUND}`,
|
||||
`mp.highest_round >= ${MIN_RECORDED_ROUND}`];
|
||||
const parameters: string[] = [];
|
||||
if (options.game) { conditions.push('m.game = ?'); parameters.push(options.game); }
|
||||
@@ -55,7 +55,7 @@ export function getLeaderboard(options: LeaderboardOptions = {}): LeaderboardRes
|
||||
${scoped}`).get(...parameters) as LeaderboardResponse['summary'];
|
||||
// Keep qualifying map variants available, even after their server is removed.
|
||||
const maps = handle.prepare(`SELECT map_key AS key, game, map, map_name AS name
|
||||
FROM leaderboard_matches WHERE game IN ('t5', 't6')
|
||||
FROM leaderboard_matches WHERE game IN ('t5', 't6') AND status = 'completed'
|
||||
AND highest_round >= ${MIN_RECORDED_ROUND}${options.game ? ' AND game = ?' : ''}
|
||||
GROUP BY game, map_key HAVING id = MAX(id)
|
||||
ORDER BY game, name COLLATE NOCASE, map_key`)
|
||||
|
||||
@@ -53,6 +53,7 @@ export function getMatchDetail(id: number): MatchDetail | null {
|
||||
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
|
||||
FROM leaderboard_match_players p WHERE p.match_id = ?
|
||||
|
||||
@@ -7,7 +7,7 @@ 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 IN ('t5', 't6') AND mp.guid = ? AND mp.has_stats = 1
|
||||
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): PlayerProfile | null {
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
headshots: 'Cumulative headshots across recorded games.',
|
||||
revives: 'Cumulative teammates revived across recorded games.',
|
||||
downs: 'Cumulative downs across recorded games, ranked from most to fewest.',
|
||||
gamesPlayed: 'Games with recorded player stats, including games in progress.',
|
||||
gamesPlayed: 'Finished games with recorded player stats, including players who left before the end.',
|
||||
gamesCompleted: 'Games where the player appears in the final game-end report.',
|
||||
bestKills: 'The most kills reported for a player in a single game.',
|
||||
bestScore: 'The highest reported points balance. Spending points reduces this balance; it is not total points earned.'
|
||||
@@ -179,7 +179,7 @@
|
||||
<div class="record"><span>Highest round</span><strong>{number.format(data.summary.highestRound)}</strong><small>Fleet record</small></div>
|
||||
<div><span>Total kills</span><strong>{number.format(data.summary.kills)}</strong><small>Across all players</small></div>
|
||||
<div><span>Total rounds</span><strong>{number.format(data.summary.roundsSurvived)}</strong><small>Total player rounds</small></div>
|
||||
<div><span>Games tracked</span><strong>{number.format(data.summary.matches)}</strong><small>Includes active games</small></div>
|
||||
<div><span>Games tracked</span><strong>{number.format(data.summary.matches)}</strong><small>Completed games only</small></div>
|
||||
<div><span>Players</span><strong>{number.format(data.summary.players)}</strong><small>Each player counted once</small></div>
|
||||
</div>
|
||||
</details>
|
||||
@@ -227,7 +227,7 @@
|
||||
{#snippet actions()}<Button size="sm" onclick={clearFilters}>Show all players</Button>{/snippet}
|
||||
</EmptyState>
|
||||
{:else}
|
||||
<EmptyState spacious title="Your first records start here" description="Player stats will appear as your servers report games and rounds. Active games count toward the leaderboard." />
|
||||
<EmptyState spacious title="Your first records start here" description="Player stats appear after a game ends, once the player has been observed at round 2 or later." />
|
||||
{/if}
|
||||
</section>
|
||||
{:else if loading}
|
||||
@@ -238,7 +238,7 @@
|
||||
<summary>How these records work</summary>
|
||||
<div>
|
||||
<p>Each GUID has one leaderboard entry across Black Ops I and Black Ops II, so name changes keep a player's records together. The latest reported name across both games appears here and on the player's profile, regardless of filters.</p>
|
||||
<p>Records include active and completed games. Game and map filters choose which matches contribute to totals and rankings. Player search filters the ranked entries without changing their ranks. Player totals count each GUID once.</p>
|
||||
<p>Only completed games contribute to records. Games in progress and interrupted games stay out of totals and rankings. Players who left a completed game keep their recorded stats. Game and map filters choose which matches contribute; player search preserves ranks. Player totals count each GUID once.</p>
|
||||
<p>Highest round includes mid-game joins. Total rounds counts round progression during observed participation, excluding earlier rounds. Joining at round 8 records a highest round of 8; reaching round 9 adds one to total rounds. The fleet total adds all players’ rounds.</p>
|
||||
<p>Kills, headshots, revives, and downs accumulate across recorded games. Highest reported score is the largest observed points balance, not the total points a player has earned. Equal values share a rank.</p>
|
||||
<p>Records cover telemetry saved since tracking began; older unrecorded games cannot be included.{#if data?.summary.firstTrackedAt}{' '}Tracking began <time datetime={data.summary.firstTrackedAt}>{$timeFormat.dateTime(data.summary.firstTrackedAt)}</time> for this selection.{/if}{#if data?.summary.updatedAt}{' '}Latest report: <time datetime={data.summary.updatedAt}>{$timeFormat.dateTime(data.summary.updatedAt)}</time>.{/if}</p>
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
<div class="stat"><span>Average rounds per game</span><strong>{ratio(stats.roundsSurvived, stats.gamesPlayed)}</strong></div>
|
||||
<div class="stat"><span>Completion rate</span><strong>{ratio(stats.gamesCompleted, stats.gamesPlayed, true)}</strong></div>
|
||||
</div>
|
||||
<p class="note">Averages include games in progress. A dash means there is no denominator for that metric.</p>
|
||||
<p class="note">Only completed games contribute. Completion rate measures how often the player appeared in the final report. A dash means there is no denominator for that metric.</p>
|
||||
</section>
|
||||
|
||||
<section class="panel" aria-labelledby="maps-title">
|
||||
@@ -72,7 +72,7 @@
|
||||
</section>
|
||||
|
||||
<section class="panel" aria-labelledby="history-title">
|
||||
<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>
|
||||
<div class="panel-heading"><h2 id="history-title">Match history</h2><p>Completed 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>
|
||||
@@ -90,7 +90,7 @@
|
||||
</div>
|
||||
</section>
|
||||
<details class="note"><summary>How these stats work</summary>
|
||||
<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>Lifetime stats combine completed games from Black Ops I and Black Ops II. Games in progress and interrupted games do not contribute. 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>
|
||||
|
||||
@@ -34,7 +34,7 @@ async function matches() {
|
||||
}>;
|
||||
}
|
||||
|
||||
it('starts counting at round 2, preserves early stats, and excludes players who only stayed for round 1', async () => {
|
||||
it('publishes round 2 or higher games only after game end, preserving early stats and excluding round 1 departures', async () => {
|
||||
const { getLeaderboard } = await import('../../src/lib/server/fleet/leaderboard.js');
|
||||
const { getPlayerProfile } = await import('../../src/lib/server/fleet/player-profile.js');
|
||||
const { serverMatchHistory } = await import('../../src/lib/server/fleet/match-history.js');
|
||||
@@ -45,14 +45,18 @@ it('starts counting at round 2, preserves early stats, and excludes players who
|
||||
expect(getPlayerProfile(player().guid)).toBeNull();
|
||||
expect(serverMatchHistory()).toEqual({ total: 0, matches: [] });
|
||||
await report('round_change', 2, [player(undefined, { kills: 6, score: 500 })]);
|
||||
expect(getLeaderboard()).toMatchObject({ total: 1, entries: [expect.objectContaining({
|
||||
highestRound: 2, kills: 6, bestScore: 1000, roundsSurvived: 1, gamesPlayed: 1, gamesCompleted: 0
|
||||
})], summary: { players: 1, matches: 1, kills: 6 } });
|
||||
expect(getLeaderboard()).toMatchObject({ entries: [], maps: [], total: 0, summary: { players: 0, matches: 0, kills: 0 } });
|
||||
expect(getPlayerProfile(player().guid)).toBeNull();
|
||||
expect(serverMatchHistory()).toMatchObject({ total: 1, matches: [{ status: 'active' }] });
|
||||
expect(getPlayerProfile(departed.guid)).toBeNull();
|
||||
await report('game_end', 2, [player(undefined, { kills: 10 })]);
|
||||
const board = getLeaderboard();
|
||||
const profile = getPlayerProfile(player().guid);
|
||||
const history = serverMatchHistory();
|
||||
expect(board).toMatchObject({ total: 1, entries: [expect.objectContaining({
|
||||
highestRound: 2, kills: 10, bestScore: 1000, roundsSurvived: 1, gamesPlayed: 1, gamesCompleted: 1
|
||||
})], summary: { players: 1, matches: 1, kills: 10 } });
|
||||
expect(getPlayerProfile(departed.guid)).toBeNull();
|
||||
expect(history).toMatchObject({ total: 1, matches: [{ startPlayers: 2, uniquePlayers: 2, durationMs: 3000, finalRound: 2 }] });
|
||||
await report('game_start');
|
||||
await report('game_end', 1, [player(undefined, { kills: 5000 })]);
|
||||
@@ -225,12 +229,19 @@ it('credits the highest round without inventing survival before the first midgam
|
||||
});
|
||||
|
||||
it('keeps disconnected player records while completing only the final roster', async () => {
|
||||
const { getLeaderboard } = await import('../../src/lib/server/fleet/leaderboard.js');
|
||||
const { getPlayerProfile } = await import('../../src/lib/server/fleet/player-profile.js');
|
||||
const other = player('4229696', { kills: 40, name: 'Other player' });
|
||||
await report('game_start', 0, [player(), other]);
|
||||
await report('round_change', 4, [player(undefined, { kills: 80 }), other]);
|
||||
expect(getLeaderboard()).toMatchObject({ entries: [], total: 0 });
|
||||
expect(getPlayerProfile(other.guid)).toBeNull();
|
||||
await report('game_end', 5, [player(undefined, { kills: 100 })]);
|
||||
expect(await records(other.guid)).toEqual([expect.objectContaining({ kills: 40, highest_round: 4, rounds_survived: 3, completed: 0 })]);
|
||||
expect(await records()).toEqual([expect.objectContaining({ kills: 100, highest_round: 5, rounds_survived: 4, completed: 1 })]);
|
||||
expect(getLeaderboard()).toMatchObject({ total: 2, summary: { matches: 1, players: 2, kills: 140 } });
|
||||
expect(getPlayerProfile(other.guid)).toMatchObject({ stats: { kills: 40, highestRound: 4,
|
||||
roundsSurvived: 3, gamesPlayed: 1, gamesCompleted: 0 }, matches: [{ status: 'completed', completed: 0 }] });
|
||||
});
|
||||
|
||||
it('starts separate matches for game starts, map changes, round resets, and new rounds after an end', async () => {
|
||||
@@ -329,10 +340,12 @@ it('keeps deleted match identities and starts a separate match when the server n
|
||||
server, server_deleted_at: expect.any(String), status: 'interrupted'
|
||||
});
|
||||
expect(history.find(match => match.id !== original.id)).toMatchObject({ server, server_deleted_at: null, status: 'active' });
|
||||
expect(getPlayerProfile(player().guid)).toBeNull();
|
||||
await report('game_end', 8, [player(undefined, { kills: 120 })]);
|
||||
const profile = getPlayerProfile(player().guid)!;
|
||||
expect(profile.stats.gamesPlayed).toBe(2);
|
||||
expect(profile.matches.find(match => match.id === original.id)?.server_deleted_at).toEqual(expect.any(String));
|
||||
expect(profile.stats.kills).toBe(240);
|
||||
expect(profile.stats).toMatchObject({ gamesPlayed: 1, kills: 120 });
|
||||
expect(profile.matches).toEqual([expect.objectContaining({ id: history.find(match => match.id !== original.id)!.id,
|
||||
server_deleted_at: null, status: 'completed' })]);
|
||||
});
|
||||
|
||||
it('does not fabricate players for an empty match', async () => {
|
||||
|
||||
@@ -20,11 +20,11 @@ async function seedRecords() {
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '2026-09-01T00:00:00.000Z', ?)`);
|
||||
const iso = (day: number) => `2026-09-0${day}T00:00:00.000Z`;
|
||||
match.run(1, 't5', 'zombie_theater', 't5:theater', 'Kino Der Toten', iso(1), iso(2), 'completed', 10);
|
||||
match.run(2, 't5', 'zombie_theater', 't5:theater', 'Kino Der Toten', iso(2), iso(3), 'active', 8);
|
||||
match.run(2, 't5', 'zombie_theater', 't5:theater', 'Kino Der Toten', iso(2), iso(3), 'completed', 8);
|
||||
match.run(3, 't5', 'zombie_coast', 't5:coast', 'Call of the Dead', iso(3), iso(4), 'completed', 4);
|
||||
match.run(4, 't6', 'zm_transit', 't6:town', 'Town Survival', iso(4), iso(5), 'completed', 20);
|
||||
match.run(5, 't6', 'zm_transit', 't6:farm', 'Farm Survival', iso(5), iso(6), 'interrupted', 15);
|
||||
match.run(6, 't5', 'custom_map', 't5:custom_map', 'Custom Map', iso(1), iso(7), 'active', 2);
|
||||
match.run(5, 't6', 'zm_transit', 't6:farm', 'Farm Survival', iso(5), iso(6), 'completed', 15);
|
||||
match.run(6, 't5', 'custom_map', 't5:custom_map', 'Custom Map', iso(1), iso(7), 'completed', 2);
|
||||
player.run('t5', '101', 'Ace');
|
||||
player.run('t5', '202', 'Blake');
|
||||
player.run('t5', '303', 'Casey');
|
||||
@@ -33,6 +33,7 @@ async function seedRecords() {
|
||||
player.run('t6', '505', 'Eve');
|
||||
record.run(1, '101', 100, 10, 9, 50, 2, 1, 1000, 1, 1, iso(2));
|
||||
record.run(1, '202', 100, 10, 9, 40, 3, 2, 2000, 1, 1, iso(2));
|
||||
// An observed match ending also publishes saved stats for players who departed before it.
|
||||
record.run(2, '101', 150, 8, 7, 60, 1, 2, 3000, 0, 1, iso(3));
|
||||
record.run(2, '303', 70, 8, 7, 20, 0, 0, 1200, 0, 1, iso(3));
|
||||
record.run(3, '101', 20, 4, 3, 10, 1, 0, 500, 1, 1, iso(4));
|
||||
@@ -44,6 +45,38 @@ async function seedRecords() {
|
||||
}
|
||||
|
||||
describe('persisted leaderboard queries', () => {
|
||||
it('excludes active and interrupted matches from ranks, summaries, map options and every part of player profiles', async () => {
|
||||
const handle = await seedRecords();
|
||||
const { getLeaderboard } = await import('../../src/lib/server/fleet/leaderboard.js');
|
||||
const { getPlayerProfile } = await import('../../src/lib/server/fleet/player-profile.js');
|
||||
const { leaderboardMetrics } = await import('../../src/lib/leaderboard.js');
|
||||
const boards = leaderboardMetrics.map(({ key }) => getLeaderboard({ metric: key, limit: 2, offset: 1 }));
|
||||
const profile = getPlayerProfile('101');
|
||||
for (const [index, status] of ['active', 'interrupted'].entries()) {
|
||||
const id = index + 10;
|
||||
handle.prepare(`INSERT INTO leaderboard_matches
|
||||
(id, server, game, map, map_key, map_name, started_at, last_received_at,
|
||||
status, highest_round, last_event, last_fingerprint)
|
||||
VALUES (?, 'removed-server', 't5', ?, ?, 'Unfinished Map',
|
||||
'2026-09-20', '2026-09-20', ?, 99, 'heartbeat', 'unfinished')`)
|
||||
.run(id, status, `t5:${status}`, status);
|
||||
handle.prepare(`INSERT INTO leaderboard_players (game, guid, name, first_seen_at, last_seen_at)
|
||||
VALUES ('t5', ?, ?, '2026-09-20', '2026-09-20')`).run(status, status);
|
||||
for (const guid of ['101', status]) {
|
||||
handle.prepare(`INSERT INTO leaderboard_match_players
|
||||
(match_id, guid, highest_round, rounds_survived, kills, headshots, revives, downs, best_score,
|
||||
has_stats, completed, first_seen_at, last_seen_at)
|
||||
VALUES (?, ?, 99, 99, 99999, 99999, 99999, 99999, 99999, 1, 1, '2026-09-20', '2026-09-20')`)
|
||||
.run(id, guid);
|
||||
}
|
||||
expect(getPlayerProfile(status)).toBeNull();
|
||||
expect(getLeaderboard({ map: `t5:${status}` })).toMatchObject({ entries: [], total: 0,
|
||||
summary: { players: 0, matches: 0, kills: 0, highestRound: 0, firstTrackedAt: null, updatedAt: null } });
|
||||
}
|
||||
expect(leaderboardMetrics.map(({ key }) => getLeaderboard({ metric: key, limit: 2, offset: 1 }))).toEqual(boards);
|
||||
expect(getPlayerProfile('101')).toEqual(profile);
|
||||
});
|
||||
|
||||
it('excludes persisted round 0/1 matches from every history, total, ranking and map option', async () => {
|
||||
const handle = await seedRecords();
|
||||
const { getLeaderboard } = await import('../../src/lib/server/fleet/leaderboard.js');
|
||||
@@ -232,7 +265,7 @@ describe('player profiles', () => {
|
||||
expect(profile.maps[0]).toMatchObject({ game: 't6', key: 't6:town', highestRound: 20, kills: 500, gamesPlayed: 1, gamesCompleted: 1 });
|
||||
expect(profile.maps[1]).toMatchObject({ game: 't5', key: 't5:theater', highestRound: 10, kills: 250, gamesPlayed: 2, gamesCompleted: 1 });
|
||||
expect(profile.matches.map(match => [match.id, match.game])).toEqual([[4, 't6'], [3, 't5'], [2, 't5'], [1, 't5']]);
|
||||
expect(profile.matches[2]).toMatchObject({ status: 'active', completed: 0, server: 'removed-server', kills: 150 });
|
||||
expect(profile.matches[2]).toMatchObject({ status: 'completed', completed: 0, server: 'removed-server', kills: 150 });
|
||||
expect(getPlayerProfile('505')).toMatchObject({ name: 'Eve', stats: { kills: 200, gamesPlayed: 1 },
|
||||
maps: [expect.objectContaining({ game: 't6' })], matches: [expect.objectContaining({ game: 't6' })] });
|
||||
expect(getPlayerProfile('404')).toBeNull();
|
||||
|
||||
@@ -117,6 +117,9 @@ describe('completed match details', () => {
|
||||
|
||||
// A start-only participant can still have a valid profile from another match.
|
||||
await report('game_start', 0, [player('303')]);
|
||||
await report('heartbeat', 3, [player('303')]);
|
||||
expect(getMatchDetail(1)!.players.find(entry => entry.guid === '303')).toMatchObject({ canViewProfile: false });
|
||||
expect(getPlayerProfile('303')).toBeNull();
|
||||
await report('game_end', 3, [player('303')]);
|
||||
participants = getMatchDetail(1)!.players;
|
||||
expect(participants.find(entry => entry.guid === '303')).toMatchObject({ hasStats: false, canViewProfile: true });
|
||||
|
||||
Reference in New Issue
Block a user