Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions back/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const debugRoutes = require("./routes/debug.routes");
const activityRoutes = require("./routes/activity.routes");
const weightRoutes = require("./routes/weight.routes");
const workoutLobbyRoutes = require("./routes/workoutLobby.routes");
const profileRoutes = require("./routes/profile.routes");

// --- Importation des middlewares ---
const errorMiddleware = require("./middleware/error.middleware");
Expand Down Expand Up @@ -100,6 +101,7 @@ app.use("/api/debug", debugRoutes);
app.use("/api/activity", activityRoutes);
app.use("/api/weight", weightRoutes);
app.use("/api/lobby", workoutLobbyRoutes);
app.use("/api/profile", profileRoutes);

// --- Gestion des erreurs ---
// Route 404
Expand Down
37 changes: 37 additions & 0 deletions back/controllers/debug.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const WorkoutLobby = require('../models/WorkoutLobby');
const { xpForLevel, getRankForLevel } = require('../utils/levelHelpers');
const { addItemAtomic, addUniqueItemOnce } = require('../services/inventory.service');
const { checkAndUnlockAchievements } = require('./reward.controller');
const { TITLE_CATALOG } = require('../data/titleCatalog');
const { recordActivityEvent } = require('../services/activity.service');
const { sendPushToUser } = require('../services/push.service');
const { SHAKE_TROLL_MESSAGES } = require('../data/shakeMessages');
Expand Down Expand Up @@ -156,6 +157,42 @@ exports.giveAllItems = async (req, res, next) => {
}
};

// ─────────────────────────────────────────────────────────────────────────────
// giveAllTitles POST /api/debug/godmode/give-all-titles
// ─────────────────────────────────────────────────────────────────────────────

/**
* Outil de test : débloque directement TOUS les titres du catalogue (Section X)
* dans unlockedTitles, sans passer par leurs conditions réelles — permet de
* tester le sélecteur de titres et les cosmétiques associés sans enchaîner
* des dizaines d'actions de jeu. Idempotent ($addToSet via Set union).
*/
exports.giveAllTitles = async (req, res, next) => {
try {
const allTitleIds = Object.keys(TITLE_CATALOG);

const user = await User.findById(req.user.id);
if (!user) return next(createError('Utilisateur introuvable.', 404));

const unlockedSet = new Set(user.unlockedTitles);
allTitleIds.forEach((id) => unlockedSet.add(id));
user.unlockedTitles = [...unlockedSet];
await user.save();

// TITLE_FIRST / TITLE_COLLECTOR_5 doivent aussi se débloquer ici.
const newlyUnlocked = await checkAndUnlockAchievements(req.user.id);

return res.status(200).json({
success: true,
message: `${allTitleIds.length} titre(s) débloqué(s).`,
unlockedTitles: user.unlockedTitles,
newlyUnlocked,
});
} catch (err) {
next(err);
}
};

// ─────────────────────────────────────────────────────────────────────────────
// simulateChestsOpened POST /api/debug/godmode/simulate-chests-opened
// ─────────────────────────────────────────────────────────────────────────────
Expand Down
11 changes: 10 additions & 1 deletion back/controllers/friend.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -498,9 +498,17 @@ exports.getFriendProfile = async (req, res, next) => {
}

const friend = await User.findById(friendId)
.select('pseudo level rank xp achievements showcasedAchievements showcasedRecords streakGels totalWorkoutMinutes equippedFrame createdAt');
.select('pseudo level rank xp achievements showcasedAchievements showcasedRecords streakGels totalWorkoutMinutes equippedFrame equippedTitle createdAt');
if (!friend) return next(createError('Utilisateur introuvable.', 404));

// Résolu côté serveur (label + rareté) pour que le front n'ait pas besoin
// de dupliquer le catalogue des titres juste pour afficher un badge —
// voir data/titleCatalog.js.
const { TITLE_CATALOG } = require('../data/titleCatalog');
const equippedTitleInfo = friend.equippedTitle && TITLE_CATALOG[friend.equippedTitle]
? { id: friend.equippedTitle, label: TITLE_CATALOG[friend.equippedTitle].label, rarity: TITLE_CATALOG[friend.equippedTitle].rarity }
: null;

const friendObjectId = new mongoose.Types.ObjectId(friendId);

// Records mis en avant par l'ami lui-même (max 6, ordre choisi) — voir
Expand Down Expand Up @@ -543,6 +551,7 @@ exports.getFriendProfile = async (req, res, next) => {
success: true,
profile: {
user: friend,
equippedTitleInfo,
friendshipLevel: friendship.friendshipLevel,
friendshipXp: friendship.friendshipXp,
stats: {
Expand Down
57 changes: 56 additions & 1 deletion back/controllers/groupStreak.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const User = require('../models/User');
const Workout = require('../models/Workout');
const { addUniqueItemOnce } = require('../services/inventory.service');
const { checkAndUnlockAchievements } = require('./reward.controller');
const { checkAndUnlockTitles } = require('./title.controller');
const { levelFromXP, getRankForLevel } = require('../utils/levelHelpers');
const { sendPushToUser } = require('../services/push.service');
const { SHAKE_TROLL_MESSAGES } = require('../data/shakeMessages');
Expand Down Expand Up @@ -258,25 +259,56 @@ async function detectAndApplyStreakBreak(group) {
}

const breakers = [];
const coveredMemberIds = [];
for (const memberId of absentMemberIds) {
const covered = await User.findOneAndUpdate(
{ _id: memberId, streakGels: { $gt: 0 } },
{ $inc: { streakGels: -1 } },
);
if (!covered) breakers.push(memberId);
if (covered) {
coveredMemberIds.push(memberId);
} else {
breakers.push(memberId);
}
}

// Titre STREAK_INSUBMERSIBLE ("sauver sa streak 3 fois de suite avec un Gel
// de Streak") : incrémente pour chaque sauvetage, remis à 0 pour un membre
// dès qu'il devient lui-même breaker (rupture non couverte, voir plus bas).
if (coveredMemberIds.length > 0) {
await User.updateMany(
{ _id: { $in: coveredMemberIds } },
{ $inc: { consecutiveStreakGelSaves: 1 } },
);
}

if (breakers.length === 0) {
// Tous les absents couverts par un Gel de Streak — pareil, on avance le
// curseur pour ne pas re-consommer un gel supplémentaire au prochain appel.
group.lastValidatedDate = missedDayStart;
await group.save();
try {
await Promise.all(coveredMemberIds.map((id) => checkAndUnlockTitles(id)));
} catch (_) {
// best-effort — ne doit jamais bloquer la détection de rupture.
}
return group;
}

await User.updateMany(
{ _id: { $in: breakers } },
{ $set: { consecutiveStreakGelSaves: 0 } },
);

group.currentStreak = 0;
group.shameBreakers = breakers;
group.shameBreakersShamedAt = new Date();
await group.save();
try {
await Promise.all(coveredMemberIds.map((id) => checkAndUnlockTitles(id)));
} catch (_) {
// best-effort
}
return group;
}

Expand Down Expand Up @@ -526,10 +558,22 @@ exports.shakeMember = async (req, res, next) => {
group.shakes.push({ from: myId, to: memberId, date: new Date() });
await group.save();

// Titres SHAME_BURNING (>15 secousses au total) / SOCIAL_POKE_STRIKER
// (3 cibles différentes le même jour) — best-effort, ne bloque jamais
// l'envoi de la notification déjà effectué ci-dessus.
let newlyUnlockedTitles = [];
try {
await User.updateOne({ _id: myId }, { $inc: { totalShakesSent: 1 } });
newlyUnlockedTitles = await checkAndUnlockTitles(myId);
} catch (_) {
// ignore
}

return res.status(200).json({
success: true,
message: `Notification envoyée à ${targetPseudo} !`,
targetId: memberId,
newlyUnlockedTitles,
});
} catch (err) {
next(err);
Expand Down Expand Up @@ -642,6 +686,16 @@ exports.checkAndUpdateGroupStreaks = async (req, res, next) => {
await Promise.all(memberIds.map((id) => checkAndUnlockAchievements(id)));
}

// Titres GROUP_GUILD_MASTER / SHAME_REPENTANCE — best-effort, ne doit
// jamais faire échouer la validation de streak déjà actée ci-dessus.
let newlyUnlockedTitlesByUser = {};
try {
const titleResults = await Promise.all(memberIds.map((id) => checkAndUnlockTitles(id)));
memberIds.forEach((id, i) => { newlyUnlockedTitlesByUser[id] = titleResults[i]; });
} catch (_) {
// ignore
}

return res.status(200).json({
success: true,
allValidated: true,
Expand All @@ -651,6 +705,7 @@ exports.checkAndUpdateGroupStreaks = async (req, res, next) => {
xpUpdates,
groupBonus: { ...xpBonus, memberCount: memberIds.length },
bloodSangUnlocked,
newlyUnlockedTitles: newlyUnlockedTitlesByUser[myId] ?? [],
});
} catch (err) {
next(err);
Expand Down
5 changes: 5 additions & 0 deletions back/controllers/inventory.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const { drawChestItem } = require('../services/chest.service');
const { consumeItemAtomic, addItemAtomic, addUniqueItemOnce, purgeEmptyEntries } = require('../services/inventory.service');
const { levelFromXP, getRankForLevel } = require('../utils/levelHelpers');
const { checkAndUnlockAchievements } = require('./reward.controller');
const { checkAndUnlockTitles } = require('./title.controller');
const { recordActivityEvent } = require('../services/activity.service');

// ─── Helpers ──────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -171,6 +172,7 @@ exports.openChest = async (req, res, next) => {

const finalUser = await purgeEmptyEntries(req.user.id);
const newlyUnlocked = await checkAndUnlockAchievements(req.user.id);
const newlyUnlockedTitles = await checkAndUnlockTitles(req.user.id).catch(() => []);

return res.status(200).json({
success: true,
Expand All @@ -180,6 +182,7 @@ exports.openChest = async (req, res, next) => {
totalChestsOpened: afterCount ? afterCount.totalChestsOpened : null,
themeUnlockGranted,
newlyUnlocked,
newlyUnlockedTitles,
});
} catch (err) {
next(err);
Expand Down Expand Up @@ -233,6 +236,7 @@ exports.claimUniqueItem = async (req, res, next) => {
).select('unlockedCosmetics equippedFrame inventory');

const finalUser = await purgeEmptyEntries(req.user.id);
const newlyUnlockedTitles = await checkAndUnlockTitles(req.user.id).catch(() => []);

return res.status(200).json({
success: true,
Expand All @@ -241,6 +245,7 @@ exports.claimUniqueItem = async (req, res, next) => {
equippedFrame: updated.equippedFrame,
unlockedCosmetics: updated.unlockedCosmetics,
inventory: finalUser.inventory,
newlyUnlockedTitles,
});
} catch (err) {
next(err);
Expand Down
28 changes: 28 additions & 0 deletions back/controllers/reward.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,22 @@ const ACHIEVEMENT_CATALOG = {
category: 'social',
hidden: false,
},

// ── Titres (Section X) ──────────────────────────────────────────────────────
TITLE_FIRST: {
id: 'TITLE_FIRST',
name: 'Nouvelle Identité',
description: "Vous avez débloqué votre premier titre.",
category: 'social',
hidden: false,
},
TITLE_COLLECTOR_5: {
id: 'TITLE_COLLECTOR_5',
name: 'Homme aux Mille Visages',
description: "Vous avez débloqué 5 titres différents.",
category: 'social',
hidden: false,
},
};

// Nombre total de trophées dans le catalogue (utile pour les stats)
Expand Down Expand Up @@ -282,6 +298,18 @@ async function checkAndUnlockAchievements(userId) {
}
}

// Trophées gradués sur le nombre de titres débloqués (Section X).
const TITLE_COUNT_ACHIEVEMENTS = [
[1, 'TITLE_FIRST'],
[5, 'TITLE_COLLECTOR_5'],
];
const unlockedTitleCount = (user.unlockedTitles || []).length;
for (const [threshold, achievementId] of TITLE_COUNT_ACHIEVEMENTS) {
if (!unlockedIds.has(achievementId) && unlockedTitleCount >= threshold) {
tryUnlock(achievementId);
}
}

if (newlyUnlocked.length > 0) {
user.markModified('achievements');
await user.save();
Expand Down
Loading
Loading