import PocketBase from "pocketbase"; import { Badge, BadgeProgress } from "@/features/badges/types"; import { transformBadge, transformBadgeProgress } 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 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(); 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_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`, expand: '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"`, expand: '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"`, 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) { return tournamentsAttended; } 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"`, 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) { 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"`, expand: '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({ 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({ sort: 'start_time', }); let consecutiveWins = 0; let maxConsecutiveWins = 0; for (const tournament of tournaments) { if (!tournamentIds.has(tournament.id)) continue; if (tournament.winner_id === playerId) { consecutiveWins++; maxConsecutiveWins = Math.max(maxConsecutiveWins, consecutiveWins); } 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; return ( criteria.matches_played || criteria.tournament_wins || criteria.tournaments_attended || criteria.overtime_matches || criteria.overtime_wins || criteria.consecutive_wins || 1 ); }, async awardManualBadge(playerId: string, badgeId: string): Promise { // Get or create badge progress record const existingProgress = await pb.collection("badge_progress").getFirstListItem( `player = "${playerId}" && badge = "${badgeId}"`, { expand: 'badge' } ).catch(() => null); if (existingProgress) { // Update existing progress to mark as earned const updated = await pb.collection("badge_progress").update(existingProgress.id, { progress: 1, earned: true, }, { expand: 'badge' }); return transformBadgeProgress(updated); } // Create new progress record 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 playerStats = await pb.collection("player_stats").getFullList(); const uniquePlayers = new Set(playerStats.map(s => s.player_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 earned = 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), }; }, }; }