badges
This commit is contained in:
407
src/lib/pocketbase/services/badges.ts
Normal file
407
src/lib/pocketbase/services/badges.ts
Normal file
@@ -0,0 +1,407 @@
|
||||
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 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),
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user