badges
This commit is contained in:
@@ -5,6 +5,7 @@ import { createTeamsService } from "./services/teams";
|
||||
import { createMatchesService } from "./services/matches";
|
||||
import { createReactionsService } from "./services/reactions";
|
||||
import { createActivitiesService } from "./services/activities";
|
||||
import { createBadgesService } from "./services/badges";
|
||||
import dotenv from 'dotenv';
|
||||
dotenv.config();
|
||||
|
||||
@@ -37,6 +38,7 @@ class PocketBaseAdminClient {
|
||||
Object.assign(this, createMatchesService(this.pb));
|
||||
Object.assign(this, createReactionsService(this.pb));
|
||||
Object.assign(this, createActivitiesService(this.pb));
|
||||
Object.assign(this, createBadgesService(this.pb));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -57,7 +59,8 @@ interface AdminClient
|
||||
ReturnType<typeof createTournamentsService>,
|
||||
ReturnType<typeof createMatchesService>,
|
||||
ReturnType<typeof createReactionsService>,
|
||||
ReturnType<typeof createActivitiesService> {
|
||||
ReturnType<typeof createActivitiesService>,
|
||||
ReturnType<typeof createBadgesService> {
|
||||
authPromise: Promise<void>;
|
||||
}
|
||||
|
||||
|
||||
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),
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { Match } from "@/features/matches/types";
|
||||
import { Player, PlayerInfo } from "@/features/players/types";
|
||||
import { Team, TeamInfo } from "@/features/teams/types";
|
||||
import { Tournament, TournamentInfo } from "@/features/tournaments/types";
|
||||
import { Badge, BadgeInfo, BadgeProgress } from "@/features/badges/types";
|
||||
|
||||
// pocketbase does this weird thing with relations where it puts them under a seperate "expand" field
|
||||
// this file transforms raw pocketbase results to our types
|
||||
@@ -278,3 +279,36 @@ export function transformReaction(record: any) {
|
||||
match: record.match
|
||||
};
|
||||
}
|
||||
|
||||
export function transformBadgeInfo(record: any): BadgeInfo {
|
||||
return {
|
||||
id: record.id,
|
||||
name: record.name,
|
||||
key: record.key,
|
||||
description: record.description,
|
||||
type: record.type,
|
||||
progressive: record.progressive,
|
||||
order: record.order ?? 999,
|
||||
};
|
||||
}
|
||||
|
||||
export function transformBadge(record: any): Badge {
|
||||
return {
|
||||
...transformBadgeInfo(record),
|
||||
criteria: record.criteria,
|
||||
created: record.created,
|
||||
updated: record.updated,
|
||||
};
|
||||
}
|
||||
|
||||
export function transformBadgeProgress(record: any): BadgeProgress {
|
||||
return {
|
||||
id: record.id,
|
||||
badge: record.expand?.badge ? transformBadgeInfo(record.expand.badge) : record.badge,
|
||||
player: record.player,
|
||||
progress: record.progress,
|
||||
earned: record.earned,
|
||||
created: record.created,
|
||||
updated: record.updated,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user