import PocketBase from "pocketbase"; import { Badge, BadgeProgress, EarnedBadge } from "@/features/badges/types"; import { transformBadge, transformBadgeProgress, transformEarnedBadge } from "@/lib/pocketbase/util/transform-types"; export interface PlayerStats { player_id: string; matches: number; wins: number; losses: number; total_cups_made: number; total_cups_against: number; margin_of_victory: number; } export function createBadgesService(pb: PocketBase) { return { async getBadge(id: string): Promise { const result = await pb.collection("badges").getOne(id); return transformBadge(result); }, async listBadges(): Promise { const results = await pb.collection("badges").getFullList({ sort: 'name', }); return results.map(transformBadge); }, async getBadgeProgress(id: string): Promise { const result = await pb.collection("badge_progress").getOne(id, { expand: 'badge,player', }); return transformBadgeProgress(result); }, async getPlayerBadgeProgress(playerId: string): Promise { const results = await pb.collection("badge_progress").getFullList({ filter: `player = "${playerId}"`, expand: 'badge', }); return results.map(transformBadgeProgress); }, async listEarnedBadges(): Promise { const results = await pb.collection("badge_progress").getFullList({ filter: `earned = true`, expand: 'player', }); return results.map(transformEarnedBadge); }, async createBadgeProgress(data: { badge: string; player: string; progress: number; earned: boolean; }): Promise { return await pb.collection("badge_progress").create(data); }, async updateBadgeProgress(id: string, data: { progress?: number; earned?: boolean; }): Promise { return await pb.collection("badge_progress").update(id, data); }, async deleteBadgeProgress(id: string): Promise { await pb.collection("badge_progress").delete(id); return true; }, async clearAllBadgeProgress(): Promise { const existingProgress = await pb.collection("badge_progress").getFullList({ filter: 'badge.type != "manual"', }); for (const progress of existingProgress) { await pb.collection("badge_progress").delete(progress.id); } return existingProgress.length; }, async calculateBadgeProgress(playerId: string, badge: Badge): Promise { if (badge.type === "manual") { return 0; } if (badge.type === "match") { return await this.calculateMatchBadgeProgress(playerId, badge); } if (badge.type === "tournament") { return await this.calculateTournamentBadgeProgress(playerId, badge); } return 0; }, async calculateMatchBadgeProgress(playerId: string, badge: Badge): Promise { const criteria = badge.criteria; const stats = await pb.collection("player_mainline_stats").getFirstListItem( `player_id = "${playerId}"` ).catch(() => null); if (!stats) return 0; if (criteria.matches_played !== undefined) { return stats.matches; } if (criteria.overtime_matches !== undefined || criteria.overtime_wins !== undefined) { const matches = await pb.collection("matches").getFullList({ filter: `(home.players.id ?~ "${playerId}" || away.players.id ?~ "${playerId}") && status = "ended" && ot_count > 0 && (tournament.regional = false || tournament.regional = null)`, expand: 'tournament,home,away,home.players,away.players', }); if (criteria.overtime_matches !== undefined) { return matches.length; } if (criteria.overtime_wins !== undefined) { const overtimeWins = matches.filter(m => { const isHome = m.expand?.home?.expand?.players?.some((p: any) => p.id === playerId) || m.expand?.home?.players?.includes(playerId); const isAway = m.expand?.away?.expand?.players?.some((p: any) => p.id === playerId) || m.expand?.away?.players?.includes(playerId); if (isHome) { return m.home_cups > m.away_cups; } else if (isAway) { return m.away_cups > m.home_cups; } return false; }); return overtimeWins.length; } } if (criteria.margin_of_victory !== undefined) { const matches = await pb.collection("matches").getFullList({ filter: `(home.players.id ?~ "${playerId}" || away.players.id ?~ "${playerId}") && status = "ended" && (tournament.regional = false || tournament.regional = null)`, expand: 'tournament,home,away,home.players,away.players', }); const bigWins = matches.filter(m => { const isHome = m.expand?.home?.expand?.players?.some((p: any) => p.id === playerId) || m.expand?.home?.players?.includes(playerId); const isAway = m.expand?.away?.expand?.players?.some((p: any) => p.id === playerId) || m.expand?.away?.players?.includes(playerId); if (isHome && m.home_cups > m.away_cups) { return (m.home_cups - m.away_cups) >= criteria.margin_of_victory; } else if (isAway && m.away_cups > m.home_cups) { return (m.away_cups - m.home_cups) >= criteria.margin_of_victory; } return false; }); return bigWins.length > 0 ? 1 : 0; } return 0; }, async calculateTournamentBadgeProgress(playerId: string, badge: Badge): Promise { const criteria = badge.criteria; const matches = await pb.collection("matches").getFullList({ filter: `(home.players.id ?~ "${playerId}" || away.players.id ?~ "${playerId}") && status = "ended" && (tournament.regional = false || tournament.regional = null)`, expand: 'tournament,home,away,home.players,away.players', }); const tournamentIds = new Set(matches.map(m => m.tournament)); const tournamentsAttended = tournamentIds.size; if (criteria.tournaments_attended !== undefined) { if (tournamentsAttended === 0 && criteria.tournaments_attended === 0) { const teams = await pb.collection("teams").getFullList({ filter: `players.id ?~ "${playerId}"`, expand: 'tournaments', }); const hasEnrollment = teams.some((team: any) => { const tournaments = team.tournaments || []; return tournaments.length > 0; }); return 0; } return tournamentsAttended; } if (criteria.won_tournament !== undefined) { const tournamentId = criteria.won_tournament; try { const tournamentMatches = await pb.collection("matches").getFullList({ filter: `tournament = "${tournamentId}" && status = "ended"`, expand: 'home,away,home.players,away.players', }); const winnersMatches = tournamentMatches.filter(m => !m.is_losers_bracket); const finalsMatch = winnersMatches.reduce((highest: any, current: any) => (!highest || current.lid > highest.lid) ? current : highest, null); if (finalsMatch && finalsMatch.status === 'ended') { const finalsWinnerId = (finalsMatch.home_cups > finalsMatch.away_cups) ? finalsMatch.home : finalsMatch.away; const winningTeam = finalsMatch.expand?.[finalsWinnerId === finalsMatch.home ? 'home' : 'away']; const winningPlayers = winningTeam?.expand?.players || winningTeam?.players || []; const playerWon = winningPlayers.some((p: any) => (typeof p === 'string' ? p : p.id) === playerId ); return playerWon ? 1 : 0; } return 0; } catch (error) { return 0; } } if (criteria.tournament_wins !== undefined) { if (tournamentIds.size === 0) return 0; let tournamentWins = 0; for (const tournamentId of tournamentIds) { const tournamentMatches = await pb.collection("matches").getFullList({ filter: `tournament = "${tournamentId}" && status = "ended" && (tournament.regional = false || tournament.regional = null)`, expand: 'tournament,home,away,home.players,away.players', }); const winnersMatches = tournamentMatches.filter(m => !m.is_losers_bracket); const finalsMatch = winnersMatches.reduce((highest: any, current: any) => (!highest || current.lid > highest.lid) ? current : highest, null); if (finalsMatch && finalsMatch.status === 'ended') { const finalsWinnerId = (finalsMatch.home_cups > finalsMatch.away_cups) ? finalsMatch.home : finalsMatch.away; const winningTeam = finalsMatch.expand?.[finalsWinnerId === finalsMatch.home ? 'home' : 'away']; const winningPlayers = winningTeam?.expand?.players || winningTeam?.players || []; const playerWon = winningPlayers.some((p: any) => (typeof p === 'string' ? p : p.id) === playerId ); if (playerWon) { tournamentWins++; } } } return tournamentWins; } if (criteria.placement !== undefined && typeof criteria.placement === 'number') { let placementCount = 0; for (const tournamentId of tournamentIds) { const tournamentMatches = await pb.collection("matches").getFullList({ filter: `tournament = "${tournamentId}" && status = "ended" && (tournament.regional = false || tournament.regional = null)`, expand: 'tournament,home,away,home.players,away.players', }); if (criteria.placement === 2) { const winnersMatches = tournamentMatches.filter(m => !m.is_losers_bracket); const finalsMatch = winnersMatches.reduce((highest: any, current: any) => (!highest || current.lid > highest.lid) ? current : highest, null); if (finalsMatch && finalsMatch.status === 'ended') { const finalsLoserId = (finalsMatch.home_cups > finalsMatch.away_cups) ? finalsMatch.away : finalsMatch.home; const losingTeam = finalsMatch.expand?.[finalsLoserId === finalsMatch.home ? 'home' : 'away']; const losingPlayers = losingTeam?.expand?.players || losingTeam?.players || []; const playerLost = losingPlayers.some((p: any) => (typeof p === 'string' ? p : p.id) === playerId ); if (playerLost) { placementCount++; } } } if (criteria.placement === 3) { const losersMatches = tournamentMatches.filter(m => m.is_losers_bracket); const losersFinale = losersMatches.reduce((highest: any, current: any) => (!highest || current.lid > highest.lid) ? current : highest, null); if (losersFinale && losersFinale.status === 'ended') { const losersFinaleLoserId = (losersFinale.home_cups > losersFinale.away_cups) ? losersFinale.away : losersFinale.home; const losingTeam = losersFinale.expand?.[losersFinaleLoserId === losersFinale.home ? 'home' : 'away']; const losingPlayers = losingTeam?.expand?.players || losingTeam?.players || []; const playerLost = losingPlayers.some((p: any) => (typeof p === 'string' ? p : p.id) === playerId ); if (playerLost) { placementCount++; } } } } return placementCount; } if (criteria.tournament_record !== undefined) { const tournaments = await pb.collection("tournaments").getFullList({ filter: 'regional = false || regional = null', sort: 'start_time', }); let timesWent02 = 0; for (const tournamentId of tournamentIds) { const tournament = tournaments.find(t => t.id === tournamentId); if (!tournament) continue; const tournamentMatches = matches.filter(m => m.tournament === tournamentId); let wins = 0; let losses = 0; for (const match of tournamentMatches) { const isHome = match.expand?.home?.expand?.players?.some((p: any) => p.id === playerId) || match.expand?.home?.players?.includes(playerId); const isAway = match.expand?.away?.expand?.players?.some((p: any) => p.id === playerId) || match.expand?.away?.players?.includes(playerId); if (isHome && match.home_cups > match.away_cups) { wins++; } else if (isAway && match.away_cups > match.home_cups) { wins++; } else { losses++; } } const record = `${wins}-${losses}`; if (record === criteria.tournament_record) { if (criteria.won_previous !== undefined && criteria.won_previous === true) { const currentIndex = tournaments.findIndex(t => t.id === tournamentId); if (currentIndex > 0) { const previousTournament = tournaments[currentIndex - 1]; if (previousTournament.winner_id === playerId) { timesWent02++; } } } else { timesWent02++; } } } return timesWent02 > 0 ? 1 : 0; } if (criteria.consecutive_wins !== undefined) { const tournaments = await pb.collection("tournaments").getFullList({ filter: 'regional = false || regional = null', sort: 'start_time', }); let consecutiveWins = 0; let maxConsecutiveWins = 0; for (const tournament of tournaments) { if (!tournamentIds.has(tournament.id)) continue; const tournamentMatches = await pb.collection("matches").getFullList({ filter: `tournament = "${tournament.id}" && status = "ended"`, expand: 'home,away,home.players,away.players', }); const winnersMatches = tournamentMatches.filter(m => !m.is_losers_bracket); const finalsMatch = winnersMatches.reduce((highest: any, current: any) => (!highest || current.lid > highest.lid) ? current : highest, null); if (finalsMatch && finalsMatch.status === 'ended') { const finalsWinnerId = (finalsMatch.home_cups > finalsMatch.away_cups) ? finalsMatch.home : finalsMatch.away; const winningTeam = finalsMatch.expand?.[finalsWinnerId === finalsMatch.home ? 'home' : 'away']; const winningPlayers = winningTeam?.expand?.players || winningTeam?.players || []; const playerWon = winningPlayers.some((p: any) => (typeof p === 'string' ? p : p.id) === playerId ); if (playerWon) { consecutiveWins++; maxConsecutiveWins = Math.max(maxConsecutiveWins, consecutiveWins); } else { consecutiveWins = 0; } } else { consecutiveWins = 0; } } return maxConsecutiveWins >= criteria.consecutive_wins ? 1 : 0; } return 0; }, getTargetProgress(badge: Badge): number { if (badge.type === "manual") { return 1; } const criteria = badge.criteria; // Use explicit checks to handle 0 values correctly if (criteria.matches_played !== undefined) return criteria.matches_played; if (criteria.tournament_wins !== undefined) return criteria.tournament_wins; if (criteria.tournaments_attended !== undefined) return criteria.tournaments_attended; if (criteria.overtime_matches !== undefined) return criteria.overtime_matches; if (criteria.overtime_wins !== undefined) return criteria.overtime_wins; if (criteria.consecutive_wins !== undefined) return criteria.consecutive_wins; if (criteria.won_tournament !== undefined) return 1; if (criteria.placement !== undefined) return 1; if (criteria.margin_of_victory !== undefined) return 1; if (criteria.tournament_record !== undefined) return 1; return 1; }, async awardManualBadge(playerId: string, badgeId: string): Promise { const existingProgress = await pb.collection("badge_progress").getFirstListItem( `player = "${playerId}" && badge = "${badgeId}"`, { expand: 'badge' } ).catch(() => null); if (existingProgress) { const updated = await pb.collection("badge_progress").update(existingProgress.id, { progress: 1, earned: true, }, { expand: 'badge' }); return transformBadgeProgress(updated); } const created = await pb.collection("badge_progress").create({ badge: badgeId, player: playerId, progress: 1, earned: true, }, { expand: 'badge' }); return transformBadgeProgress(created); }, async migrateBadgeProgress(): Promise<{ success: boolean; playersProcessed: number; progressRecordsCreated: number; totalBadgesEarned: number; averageBadgesPerPlayer: string; }> { await this.clearAllBadgeProgress(); const badges = await this.listBadges(); const allPlayers = await pb.collection("players").getFullList(); const uniquePlayers = new Set(allPlayers.map((p: any) => p.id)); let totalProgressRecords = 0; let totalBadgesEarned = 0; for (const playerId of uniquePlayers) { for (const badge of badges) { try { const progress = await this.calculateBadgeProgress(playerId, badge); const target = this.getTargetProgress(badge); const isPlacementBadge = badge.criteria.placement !== undefined; const is8YearVeteran = badge.key === 'veteran_8_badge'; const earned = badge.progressive || isPlacementBadge || is8YearVeteran ? progress >= target : progress === target; if (progress > 0 || earned) { await this.createBadgeProgress({ badge: badge.id, player: playerId, progress: progress, earned: earned, }); totalProgressRecords++; if (earned) { totalBadgesEarned++; } } } catch (error: any) { console.error(`Error processing badge "${badge.name}" for player ${playerId}:`, error.message); } } } return { success: true, playersProcessed: uniquePlayers.size, progressRecordsCreated: totalProgressRecords, totalBadgesEarned: totalBadgesEarned, averageBadgesPerPlayer: (totalBadgesEarned / uniquePlayers.size).toFixed(2), }; }, }; }