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(); 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(); 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(); 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(); 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(); 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; }