Files
got-gaming/services/users/user-stats.service.ts

344 lines
8.7 KiB
TypeScript

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<LeaderboardEntry[]> {
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<HouseLeaderboardEntry[]> {
// 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<User> {
// 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<User> {
// 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();