import { json, error } from "@sveltejs/kit"; import type { RequestHandler } from "./$types"; import { getSession, getAgent } from "$lib/server/auth"; import { getDb } from "$lib/server/db"; import { fetchGameRecord, fetchCloudGoProfile } from "$lib/atproto-client"; import { calculateNewRating, getDefaultRating } from "$lib/server/elo"; export const POST: RequestHandler = async (event) => { const session = await getSession(event); if (!session) { throw error(401, "Not authenticated"); } const { status, wins, losses } = await event.request.json(); if (status && !["playing", "watching", "offline"].includes(status)) { throw error(400, "Invalid status. Must be: playing, watching, or offline"); } try { const agent = await getAgent(event); if (!agent) { throw error(401, "Failed to get authenticated agent"); } const now = new Date().toISOString(); // Check if profile exists let existingProfile = null; try { const getResult = await (agent as any).get("com.atproto.repo.getRecord", { params: { repo: session.did, collection: "boo.sky.go.profile", rkey: "self", }, }); if (getResult.ok) { existingProfile = getResult.data.value; } } catch (err) { console.error("Failed to get profile:", err); // Profile doesn't exist yet } // Recalculate stats from completed games if status is being updated let calculatedWins = existingProfile?.wins ?? 0; let calculatedLosses = existingProfile?.losses ?? 0; let calculatedRanking = existingProfile?.ranking ?? 1200; if (status) { try { const stats = await recalculatePlayerStats(session.did, event.platform); calculatedWins = stats.wins; calculatedLosses = stats.losses; calculatedRanking = stats.ranking; } catch (err) { console.error("Failed to recalculate stats:", err); // Fall back to existing values } } const record = { $type: "boo.sky.go.profile", wins: wins !== undefined ? wins : calculatedWins, losses: losses !== undefined ? losses : calculatedLosses, ranking: calculatedRanking, status: status || existingProfile?.status || "offline", createdAt: existingProfile?.createdAt || now, updatedAt: now, }; if (existingProfile) { // Update existing profile const result = await (agent as any).post("com.atproto.repo.putRecord", { input: { repo: session.did, collection: "boo.sky.go.profile", rkey: "self", record, }, }); if (!result.ok) { throw new Error(`Failed to update profile: ${result.data.message}`); } } else { // Create new profile const result = await (agent as any).post( "com.atproto.repo.createRecord", { input: { repo: session.did, collection: "boo.sky.go.profile", rkey: "self", record, }, }, ); if (!result.ok) { throw new Error(`Failed to create profile: ${result.data.message}`); } } return json({ success: true, profile: record }); } catch (err) { console.error("Failed to update profile:", err); throw error(500, "Failed to update profile"); } }; /** * Recalculate a player's wins, losses, and ranking from all completed games */ async function recalculatePlayerStats( did: string, platform: App.Platform | undefined, ): Promise<{ wins: number; losses: number; ranking: number }> { const db = getDb(platform); // Fetch all completed games where the player participated const completedGames = await db .selectFrom("games") .selectAll() .where("status", "=", "completed") .where((eb) => eb.or([eb("player_one", "=", did), eb("player_two", "=", did)]), ) .execute(); let wins = 0; let losses = 0; let currentRanking = getDefaultRating(); // Sort by updated_at to process games in chronological order completedGames.sort( (a, b) => new Date(a.updated_at).getTime() - new Date(b.updated_at).getTime(), ); // Process each game to calculate wins/losses and update ranking for (const game of completedGames) { if (!game.player_two) continue; // Skip games without opponent try { // Fetch the game record to get winner const gameRecord = await fetchGameRecord(game.player_one, game.rkey); if (!gameRecord?.winner) continue; const won = gameRecord.winner === did; const opponentDid = game.player_one === did ? game.player_two : game.player_one; // Update win/loss count if (won) { wins++; } else { losses++; } // Fetch opponent's ranking at time of calculation const opponentProfile = await fetchCloudGoProfile(opponentDid); const opponentRanking = opponentProfile?.ranking ?? getDefaultRating(); // Calculate new ranking const actualScore = won ? 1 : 0; currentRanking = calculateNewRating( currentRanking, opponentRanking, actualScore, ); } catch (err) { console.error(`Failed to process game ${game.id}:`, err); // Continue processing other games } } return { wins, losses, ranking: currentRanking }; }