import { prisma } from "../database"; import type { User, Role, Prisma, CharacterClass, } from "@/prisma/generated/prisma/client"; import { NotFoundError } from "../errors"; import { userService } from "./user.service"; export interface UpdateUserStatsInput { hpDelta?: number; xpDelta?: number; score?: number; level?: number; role?: Role; } export interface LeaderboardEntry { rank: number; username: string; email: string; score: number; level: number; avatar: string | null; bio: string | null; characterClass: CharacterClass | null; } export interface HouseMember { id: string; username: string; avatar: string | null; score: number; level: number; role: string; } export interface HouseLeaderboardEntry { rank: number; houseId: string; houseName: string; totalScore: number; memberCount: number; averageScore: number; description: string | null; members: HouseMember[]; } /** * Service de gestion des statistiques utilisateur */ export class UserStatsService { /** * Récupère le leaderboard */ async getLeaderboard(limit: number = 10): Promise { const users = await prisma.user.findMany({ orderBy: { score: "desc", }, take: limit, select: { id: true, username: true, email: true, score: true, level: true, avatar: true, bio: true, characterClass: true, }, }); return users.map((user, index) => ({ rank: index + 1, username: user.username, email: user.email, score: user.score, level: user.level, avatar: user.avatar, bio: user.bio, characterClass: user.characterClass, })); } /** * Récupère le leaderboard par maison */ async getHouseLeaderboard(limit: number = 10): Promise { // Récupérer toutes les maisons avec leurs membres et leurs scores const houses = await prisma.house.findMany({ include: { memberships: { include: { user: { select: { id: true, username: true, avatar: true, score: true, level: true, }, }, }, orderBy: [ { role: "asc" }, // OWNER, ADMIN, MEMBER { user: { score: "desc" } }, // Puis par score décroissant ], }, }, }); // Calculer le score total et la moyenne pour chaque maison const houseStats = houses .map((house) => { const memberScores = house.memberships.map((m) => m.user.score); const totalScore = memberScores.reduce((sum, score) => sum + score, 0); const memberCount = house.memberships.length; const averageScore = memberCount > 0 ? Math.floor(totalScore / memberCount) : 0; // Mapper les membres avec leurs détails const members: HouseMember[] = house.memberships.map((membership) => ({ id: membership.user.id, username: membership.user.username, avatar: membership.user.avatar, score: membership.user.score, level: membership.user.level, role: membership.role, })); return { houseId: house.id, houseName: house.name, totalScore, memberCount, averageScore, description: house.description, members, }; }) .filter((house) => house.memberCount > 0) // Exclure les maisons sans membres .sort((a, b) => b.totalScore - a.totalScore) // Trier par score total décroissant .slice(0, limit) // Limiter le nombre de résultats .map((house, index) => ({ rank: index + 1, ...house, })); return houseStats; } /** * Met à jour les statistiques d'un utilisateur */ async updateUserStats( id: string, stats: UpdateUserStatsInput, select?: Prisma.UserSelect ): Promise { // Récupérer l'utilisateur actuel const user = await prisma.user.findUnique({ where: { id }, }); if (!user) { throw new Error("Utilisateur non trouvé"); } // Calculer les nouvelles valeurs let newHp = user.hp; let newXp = user.xp; let newLevel = user.level; let newMaxXp = user.maxXp; // Appliquer les changements de HP if (stats.hpDelta !== undefined) { newHp = Math.max(0, Math.min(user.maxHp, user.hp + stats.hpDelta)); } // Appliquer les changements de XP if (stats.xpDelta !== undefined) { newXp = user.xp + stats.xpDelta; newLevel = user.level; newMaxXp = user.maxXp; // Gérer le niveau up si nécessaire (quand on ajoute de l'XP) if (newXp >= newMaxXp && newXp > 0) { while (newXp >= newMaxXp) { newXp -= newMaxXp; newLevel += 1; // Augmenter le maxXp pour le prochain niveau (formule simple) newMaxXp = Math.floor(newMaxXp * 1.2); } } // Gérer le niveau down si nécessaire (quand on enlève de l'XP) if (newXp < 0 && newLevel > 1) { while (newXp < 0 && newLevel > 1) { newLevel -= 1; // Calculer le maxXp du niveau précédent newMaxXp = Math.floor(newMaxXp / 1.2); newXp += newMaxXp; } // S'assurer que l'XP ne peut pas être négative newXp = Math.max(0, newXp); } // S'assurer que le niveau minimum est 1 if (newLevel < 1) { newLevel = 1; newXp = 0; } } // Construire les données de mise à jour const updateData: Prisma.UserUpdateInput = { hp: newHp, xp: newXp, level: newLevel, maxXp: newMaxXp, }; // Appliquer les changements directs if (stats.score !== undefined) { updateData.score = Math.max(0, stats.score); } if (stats.level !== undefined) { // Si le niveau est modifié directement, utiliser cette valeur const targetLevel = Math.max(1, stats.level); updateData.level = targetLevel; // Recalculer le maxXp pour le nouveau niveau // Formule: maxXp = 5000 * (1.2 ^ (level - 1)) let calculatedMaxXp = 5000; for (let i = 1; i < targetLevel; i++) { calculatedMaxXp = Math.floor(calculatedMaxXp * 1.2); } updateData.maxXp = calculatedMaxXp; // Réinitialiser l'XP si le niveau change directement (sauf si on modifie aussi l'XP) if (targetLevel !== user.level && stats.xpDelta === undefined) { updateData.xp = 0; } } if (stats.role !== undefined) { if (stats.role === "ADMIN" || stats.role === "USER") { updateData.role = stats.role; } } return prisma.user.update({ where: { id }, data: updateData, select, }); } /** * Met à jour les stats ET le profil d'un utilisateur (pour admin) */ async updateUserStatsAndProfile( id: string, data: { username?: string; avatar?: string | null; hpDelta?: number; xpDelta?: number; score?: number; level?: number; role?: Role; }, select?: Prisma.UserSelect ): Promise { // Vérifier que l'utilisateur existe const user = await userService.getUserById(id); if (!user) { throw new NotFoundError("Utilisateur"); } const selectFields = select || { id: true, username: true, email: true, role: true, score: true, level: true, hp: true, maxHp: true, xp: true, maxXp: true, avatar: true, }; // Mettre à jour les stats si nécessaire const hasStatsChanges = data.hpDelta !== undefined || data.xpDelta !== undefined || data.score !== undefined || data.level !== undefined || data.role !== undefined; let updatedUser: User; if (hasStatsChanges) { updatedUser = await this.updateUserStats( id, { hpDelta: data.hpDelta, xpDelta: data.xpDelta, score: data.score, level: data.level, role: data.role, }, selectFields ); } else { const user = await userService.getUserById(id, selectFields); if (!user) { throw new NotFoundError("Utilisateur"); } updatedUser = user; } // Mettre à jour username/avatar si nécessaire if (data.username !== undefined || data.avatar !== undefined) { updatedUser = await userService.updateUser( id, { username: data.username !== undefined ? data.username.trim() : undefined, avatar: data.avatar, }, selectFields ); } return updatedUser; } } export const userStatsService = new UserStatsService();