blanchon's picture
Home: POV tab + DuckDB advanced filter
5a67aa1
import type { Match, Round } from '$lib/types';
const TICK_RATE = 64;
const PLAYERS_PER_ROUND = 10;
export type MapRow = Match & {
duration_s: number;
};
export type PovRow = {
match_id: number;
map_name: string;
round: number;
player: number;
duration_s: number;
event: string;
team1: string;
team2: string;
score1: number;
score2: number;
winner: Match['winner'];
format: string;
match_date: string;
uploaded_at: string;
rounds_played: number;
// When this row is materialized from an SQL filter, the timestamp inside
// the POV's video to seek to on open. Otherwise undefined → opens at 0.
start_t?: number;
};
export type RoundRow = {
match_id: number;
map_name: string;
round: number;
demo_round: number;
duration_s: number;
event: string;
team1: string;
team2: string;
score1: number;
score2: number;
winner: Match['winner'];
format: string;
match_date: string;
uploaded_at: string;
rounds_played: number;
};
export type MatchRow = {
match_id: number;
event: string;
team1: string;
team2: string;
score1: number;
score2: number;
winner: Match['winner'] | '';
format: string;
match_date: string;
uploaded_at: string;
maps: string[];
maps_played: number;
first_map: string;
rounds_played: number;
duration_s: number;
};
export function buildMapRows(matches: Match[], rounds: Round[]): MapRow[] {
const durationByMap = new Map<string, number>();
for (const r of rounds) {
const k = `${r.match_id}|${r.map_name}`;
durationByMap.set(k, (durationByMap.get(k) ?? 0) + (r.round_duration_ticks || 0) / TICK_RATE);
}
return matches.map((m) => ({
...m,
duration_s: durationByMap.get(`${m.match_id}|${m.map_name}`) ?? 0
}));
}
export function buildRoundRows(matches: Match[], rounds: Round[]): RoundRow[] {
const matchByMap = new Map<string, Match>();
for (const m of matches) matchByMap.set(`${m.match_id}|${m.map_name}`, m);
const out: RoundRow[] = [];
for (const r of rounds) {
const m = matchByMap.get(`${r.match_id}|${r.map_name}`);
if (!m) continue;
out.push({
match_id: r.match_id,
map_name: r.map_name,
round: r.round,
demo_round: r.demo_round,
duration_s: (r.round_duration_ticks || 0) / TICK_RATE,
event: m.event,
team1: m.team1,
team2: m.team2,
score1: m.score1,
score2: m.score2,
winner: m.winner,
format: m.format,
match_date: m.match_date,
uploaded_at: r.uploaded_at,
rounds_played: m.rounds_played
});
}
return out;
}
/**
* Synthesize 10 rows per round (one per player slot). No per-POV metadata
* is loaded eagerly — side/weapon would require fetching per-(match, map)
* chunks-preview.parquet for every visible match, which doesn't pay back on
* the home page. Per-POV detail surfaces inside the match viewer.
*/
export function buildPovRows(matches: Match[], rounds: Round[]): PovRow[] {
const matchByMap = new Map<string, Match>();
for (const m of matches) matchByMap.set(`${m.match_id}|${m.map_name}`, m);
const out: PovRow[] = [];
for (const r of rounds) {
const m = matchByMap.get(`${r.match_id}|${r.map_name}`);
if (!m) continue;
const duration_s = (r.round_duration_ticks || 0) / TICK_RATE;
for (let player = 0; player < PLAYERS_PER_ROUND; player++) {
out.push({
match_id: r.match_id,
map_name: r.map_name,
round: r.round,
player,
duration_s,
event: m.event,
team1: m.team1,
team2: m.team2,
score1: m.score1,
score2: m.score2,
winner: m.winner,
format: m.format,
match_date: m.match_date,
uploaded_at: r.uploaded_at,
rounds_played: m.rounds_played
});
}
}
return out;
}
export function buildMatchRows(matches: Match[], rounds: Round[]): MatchRow[] {
const durationByMap = new Map<string, number>();
for (const r of rounds) {
const k = `${r.match_id}|${r.map_name}`;
durationByMap.set(k, (durationByMap.get(k) ?? 0) + (r.round_duration_ticks || 0) / TICK_RATE);
}
const grouped = new Map<number, Match[]>();
for (const m of matches) {
const arr = grouped.get(m.match_id);
if (arr) arr.push(m);
else grouped.set(m.match_id, [m]);
}
const rows: MatchRow[] = [];
for (const [matchId, mapsForMatch] of grouped) {
const sorted = [...mapsForMatch].sort((a, b) => (a.map_index ?? 0) - (b.map_index ?? 0));
const head = sorted[0];
const team1MapWins = sorted.filter((m) => m.winner === 'team1').length;
const team2MapWins = sorted.filter((m) => m.winner === 'team2').length;
const totalRounds = sorted.reduce((acc, m) => acc + m.rounds_played, 0);
const totalDuration = sorted.reduce(
(acc, m) => acc + (durationByMap.get(`${m.match_id}|${m.map_name}`) ?? 0),
0
);
// Use the latest upload across the match's maps so the row reflects when
// the whole series finished landing in the dataset.
const latestUpload = sorted.reduce(
(acc, m) => (m.uploaded_at > acc ? m.uploaded_at : acc),
head.uploaded_at
);
rows.push({
match_id: matchId,
event: head.event,
team1: head.team1,
team2: head.team2,
score1: team1MapWins,
score2: team2MapWins,
winner: team1MapWins > team2MapWins ? 'team1' : team2MapWins > team1MapWins ? 'team2' : '',
format: head.format,
match_date: head.match_date,
uploaded_at: latestUpload,
maps: sorted.map((m) => m.map_name),
maps_played: sorted.length,
first_map: head.map_name,
rounds_played: totalRounds,
duration_s: totalDuration
});
}
rows.sort(
(a, b) =>
new Date(b.uploaded_at).getTime() - new Date(a.uploaded_at).getTime() ||
b.match_id - a.match_id
);
return rows;
}