import { prisma } from "../database"; import type { User, Role, Prisma } 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: string | null; } /** * 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, })); } /** * 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 { updatedUser = await userService.getUserById(id, selectFields); if (!updatedUser) { throw new NotFoundError("Utilisateur"); } } // 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();