Corrige les joueurs en double dans les statistiques
Build and Publish Docker Image / build-and-push-image (push) Successful in 1m43s
Build and Publish Docker Image / build-and-push-image (push) Successful in 1m43s
Un nom saisi à la main recevait un nouvel id même si le joueur existait déjà : la même personne apparaissait plusieurs fois dans le classement, chaque ligne avec une seule partie. - NewGame : réutilise l'id (et l'avatar) du joueur existant quand le nom correspond, sauf si deux joueurs homonymes sont dans la même partie - Stats : classement agrégé par personne (nom normalisé) et non par id, y compris parties, victoires et compteurs Cabo ; les filtres par joueur suivent la même logique pour retrouver les anciennes parties Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+12
-1
@@ -219,7 +219,18 @@ export default function NewGame() {
|
|||||||
.and((pl) => pl.profileId === activeProfileId && !pl.deletedAt)
|
.and((pl) => pl.profileId === activeProfileId && !pl.deletedAt)
|
||||||
.first();
|
.first();
|
||||||
|
|
||||||
if (!exists) {
|
if (exists) {
|
||||||
|
// Reuse the roster entry so a hand-typed name isn't counted as a new
|
||||||
|
// player in the stats. Guard against the same name twice in one game.
|
||||||
|
const idTaken = playersToSave.some((o, j) => j !== i && o.id === exists.id);
|
||||||
|
if (!idTaken) {
|
||||||
|
playersToSave[i] = {
|
||||||
|
...p,
|
||||||
|
id: exists.id,
|
||||||
|
avatar: p.avatar ?? exists.avatar,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
let finalAvatar = p.avatar;
|
let finalAvatar = p.avatar;
|
||||||
// Generate a random avatar automatically
|
// Generate a random avatar automatically
|
||||||
if (!finalAvatar) {
|
if (!finalAvatar) {
|
||||||
|
|||||||
+46
-40
@@ -66,6 +66,10 @@ export default function Statistics() {
|
|||||||
const isDeletedPlayer = (id: string, name: string) =>
|
const isDeletedPlayer = (id: string, name: string) =>
|
||||||
deletedIds.has(id) || deletedNames.has(norm(name));
|
deletedIds.has(id) || deletedNames.has(norm(name));
|
||||||
|
|
||||||
|
// The same person can carry several ids across sessions (a hand-typed name
|
||||||
|
// gets a fresh id), so statistics are keyed on the name, not the id.
|
||||||
|
const keyOf = (p: { id: string; name: string }) => norm(p.name) || p.id;
|
||||||
|
|
||||||
const [timeFilter, setTimeFilter] = useState<"all" | "7d" | "30d" | "year">(
|
const [timeFilter, setTimeFilter] = useState<"all" | "7d" | "30d" | "year">(
|
||||||
"all",
|
"all",
|
||||||
);
|
);
|
||||||
@@ -97,10 +101,13 @@ export default function Statistics() {
|
|||||||
|
|
||||||
// Filter by Players (Must include ALL selected players to allow "Head to Head" stats)
|
// Filter by Players (Must include ALL selected players to allow "Head to Head" stats)
|
||||||
if (selectedPlayerIds.length > 0) {
|
if (selectedPlayerIds.length > 0) {
|
||||||
|
// Compare on the aggregation key: the same person may carry a different
|
||||||
|
// id from one session to the next.
|
||||||
|
const selectedKeys = players
|
||||||
|
.filter((p) => selectedPlayerIds.includes(p.id))
|
||||||
|
.map(keyOf);
|
||||||
filtered = filtered.filter((s) =>
|
filtered = filtered.filter((s) =>
|
||||||
selectedPlayerIds.every((selectedId) =>
|
selectedKeys.every((key) => s.players.some((p) => keyOf(p) === key)),
|
||||||
s.players.some((p) => p.id === selectedId),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,7 +117,7 @@ export default function Statistics() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return filtered;
|
return filtered;
|
||||||
}, [allSessions, timeFilter, selectedPlayerIds, selectedGameId]);
|
}, [allSessions, timeFilter, selectedPlayerIds, selectedGameId, players]);
|
||||||
|
|
||||||
// 2. Calculate Global Stats
|
// 2. Calculate Global Stats
|
||||||
const totalGames = sessions.length;
|
const totalGames = sessions.length;
|
||||||
@@ -159,41 +166,41 @@ export default function Statistics() {
|
|||||||
}
|
}
|
||||||
> = {};
|
> = {};
|
||||||
|
|
||||||
// Initialize with known players
|
const blankStat = (p: { id: string; name: string; avatar?: string }) => ({
|
||||||
|
id: p.id,
|
||||||
|
name: p.name,
|
||||||
|
played: 0,
|
||||||
|
won: 0,
|
||||||
|
avatar: p.avatar,
|
||||||
|
caboSuccess: 0,
|
||||||
|
caboFails: 0,
|
||||||
|
kamikaze: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize with known players (the roster entry provides the id + avatar)
|
||||||
players.forEach((p) => {
|
players.forEach((p) => {
|
||||||
playerStats[p.id] = {
|
playerStats[keyOf(p)] = blankStat(p);
|
||||||
id: p.id,
|
|
||||||
name: p.name,
|
|
||||||
played: 0,
|
|
||||||
won: 0,
|
|
||||||
avatar: p.avatar,
|
|
||||||
caboSuccess: 0,
|
|
||||||
caboFails: 0,
|
|
||||||
kamikaze: 0,
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
finishedGames.forEach((session) => {
|
finishedGames.forEach((session) => {
|
||||||
|
// Map this session's player ids to their aggregation key
|
||||||
|
const keyById = new Map<string, string>();
|
||||||
|
|
||||||
session.players.forEach((p) => {
|
session.players.forEach((p) => {
|
||||||
if (isDeletedPlayer(p.id, p.name)) return; // joueur supprimé : hors stats
|
if (isDeletedPlayer(p.id, p.name)) return; // joueur supprimé : hors stats
|
||||||
if (!playerStats[p.id]) {
|
const key = keyOf(p);
|
||||||
playerStats[p.id] = {
|
keyById.set(p.id, key);
|
||||||
id: p.id,
|
if (!playerStats[key]) playerStats[key] = blankStat(p);
|
||||||
name: p.name,
|
if (!playerStats[key].avatar && p.avatar) {
|
||||||
played: 0,
|
playerStats[key].avatar = p.avatar;
|
||||||
won: 0,
|
|
||||||
avatar: p.avatar,
|
|
||||||
caboSuccess: 0,
|
|
||||||
caboFails: 0,
|
|
||||||
kamikaze: 0,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
playerStats[p.id].played += 1;
|
playerStats[key].played += 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
session.winnerIds?.forEach((winnerId) => {
|
session.winnerIds?.forEach((winnerId) => {
|
||||||
if (playerStats[winnerId]) {
|
const key = keyById.get(winnerId);
|
||||||
playerStats[winnerId].won += 1;
|
if (key && playerStats[key]) {
|
||||||
|
playerStats[key].won += 1;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -201,15 +208,11 @@ export default function Statistics() {
|
|||||||
if (session.gameId === "cabo") {
|
if (session.gameId === "cabo") {
|
||||||
session.rounds.forEach((round) => {
|
session.rounds.forEach((round) => {
|
||||||
round.scores.forEach((score) => {
|
round.scores.forEach((score) => {
|
||||||
if (score.caboCall === "success" && playerStats[score.playerId]) {
|
const key = keyById.get(score.playerId);
|
||||||
playerStats[score.playerId].caboSuccess += 1;
|
if (!key || !playerStats[key]) return;
|
||||||
}
|
if (score.caboCall === "success") playerStats[key].caboSuccess += 1;
|
||||||
if (score.caboCall === "fail" && playerStats[score.playerId]) {
|
if (score.caboCall === "fail") playerStats[key].caboFails += 1;
|
||||||
playerStats[score.playerId].caboFails += 1;
|
if (score.kamikaze) playerStats[key].kamikaze += 1;
|
||||||
}
|
|
||||||
if (score.kamikaze && playerStats[score.playerId]) {
|
|
||||||
playerStats[score.playerId].kamikaze += 1;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -224,9 +227,12 @@ export default function Statistics() {
|
|||||||
.sort((a, b) => b.winrate - a.winrate || b.played - a.played); // Sort by winrate, then games played
|
.sort((a, b) => b.winrate - a.winrate || b.played - a.played); // Sort by winrate, then games played
|
||||||
|
|
||||||
// 4. Hide unselected players from leaderboard if any are selected
|
// 4. Hide unselected players from leaderboard if any are selected
|
||||||
|
const selectedKeys = players
|
||||||
|
.filter((p) => selectedPlayerIds.includes(p.id))
|
||||||
|
.map(keyOf);
|
||||||
const displayLeaderboard =
|
const displayLeaderboard =
|
||||||
selectedPlayerIds.length > 0
|
selectedKeys.length > 0
|
||||||
? leaderboard.filter((stat) => selectedPlayerIds.includes(stat.id))
|
? leaderboard.filter((stat) => selectedKeys.includes(keyOf(stat)))
|
||||||
: leaderboard;
|
: leaderboard;
|
||||||
|
|
||||||
// 5. Records: best single-game score per game, according to the game's
|
// 5. Records: best single-game score per game, according to the game's
|
||||||
|
|||||||
Reference in New Issue
Block a user