434 lines
15 KiB
TypeScript
434 lines
15 KiB
TypeScript
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<Badge> {
|
|
const result = await pb.collection("badges").getOne(id);
|
|
return transformBadge(result);
|
|
},
|
|
|
|
async listBadges(): Promise<Badge[]> {
|
|
const results = await pb.collection("badges").getFullList({
|
|
sort: 'name',
|
|
});
|
|
return results.map(transformBadge);
|
|
},
|
|
|
|
async getBadgeProgress(id: string): Promise<BadgeProgress> {
|
|
const result = await pb.collection("badge_progress").getOne(id, {
|
|
expand: 'badge,player',
|
|
});
|
|
return transformBadgeProgress(result);
|
|
},
|
|
|
|
async getPlayerBadgeProgress(playerId: string): Promise<BadgeProgress[]> {
|
|
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<BadgeProgress> {
|
|
return await pb.collection("badge_progress").create<BadgeProgress>(data);
|
|
},
|
|
|
|
async updateBadgeProgress(id: string, data: {
|
|
progress?: number;
|
|
earned?: boolean;
|
|
}): Promise<BadgeProgress> {
|
|
return await pb.collection("badge_progress").update<BadgeProgress>(id, data);
|
|
},
|
|
|
|
async deleteBadgeProgress(id: string): Promise<boolean> {
|
|
await pb.collection("badge_progress").delete(id);
|
|
return true;
|
|
},
|
|
|
|
async clearAllBadgeProgress(): Promise<number> {
|
|
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<number> {
|
|
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<number> {
|
|
const criteria = badge.criteria;
|
|
|
|
const stats = await pb.collection("player_stats").getFirstListItem<PlayerStats>(
|
|
`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<number> {
|
|
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<BadgeProgress> {
|
|
// 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<PlayerStats>();
|
|
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),
|
|
};
|
|
},
|
|
};
|
|
}
|