All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 2m48s
253 lines
6.3 KiB
TypeScript
253 lines
6.3 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;
|
|
}
|
|
|
|
/**
|
|
* 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,
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* 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();
|