diff --git a/public/games-assets/moustache-banner.webp b/public/games-assets/moustache-banner.webp new file mode 100644 index 0000000..73249ef Binary files /dev/null and b/public/games-assets/moustache-banner.webp differ diff --git a/public/games-assets/moustache-logo.webp b/public/games-assets/moustache-logo.webp new file mode 100644 index 0000000..e9ef669 Binary files /dev/null and b/public/games-assets/moustache-logo.webp differ diff --git a/public/games-assets/six-qui-prend-banner.webp b/public/games-assets/six-qui-prend-banner.webp new file mode 100644 index 0000000..31e05a7 Binary files /dev/null and b/public/games-assets/six-qui-prend-banner.webp differ diff --git a/public/games-assets/six-qui-prend-logo.webp b/public/games-assets/six-qui-prend-logo.webp new file mode 100644 index 0000000..b6f3317 Binary files /dev/null and b/public/games-assets/six-qui-prend-logo.webp differ diff --git a/src/games/index.ts b/src/games/index.ts index 2b06f7a..532b8b5 100644 --- a/src/games/index.ts +++ b/src/games/index.ts @@ -8,7 +8,9 @@ import { azulConfig } from "./azul/config"; import { caboConfig } from "./cabo/config"; import { chouineursConfig } from "./chouineurs/config"; import { harmonieConfig } from "./harmonie/config"; +import { moustacheConfig } from "./moustache/config"; import { odinCardsConfig } from "./odin_cards/config"; +import { sixQuiPrendConfig } from "./six_qui_prend/config"; import { skyjoConfig } from "./skyjo/config"; // Built-in games (compiled into the app). @@ -19,6 +21,8 @@ export const games: GameConfig[] = [ harmonieConfig, odinCardsConfig, chouineursConfig, + sixQuiPrendConfig, + moustacheConfig, ]; // Imported games for the active profile, kept in sync in-memory so the diff --git a/src/games/moustache/config.ts b/src/games/moustache/config.ts new file mode 100644 index 0000000..48761a3 --- /dev/null +++ b/src/games/moustache/config.ts @@ -0,0 +1,28 @@ +import { GameConfig } from "../../types"; + +// Les équipes sont retirées au sort à chaque manche : on ne note donc que +// l'équipe gagnante des 4 manches, puis chacun révèle ses points au décompte +// final (5e et dernière manche). Le plus de points l'emporte. +export const moustacheConfig: GameConfig = { + imagePath: "/games-assets/moustache-logo.webp", + bannerPath: "/games-assets/moustache-banner.webp", + id: "moustache", + name: "Moustache", + minPlayers: 3, + maxPlayers: 6, + scoreType: "positive", + teamRounds: 4, + fixedRounds: 5, // 4 manches en équipes + le décompte final + teams: [ + { id: "bleu", label: "Équipe bleue", color: "#2563eb" }, + { id: "rouge", label: "Équipe rouge", color: "#dc2626" }, + ], + description: + "Les équipes changent à chaque manche : notez l'équipe gagnante des 4 manches, puis chacun révèle ses points. Le plus de points gagne.", + icon: "Drama", + colors: { + // Le rose de la moustache du morse : distinctif, et sans confusion + // possible avec le bleu / rouge des équipes. + primary: "#e5709b", + }, +}; diff --git a/src/games/six_qui_prend/config.ts b/src/games/six_qui_prend/config.ts new file mode 100644 index 0000000..fcce0e7 --- /dev/null +++ b/src/games/six_qui_prend/config.ts @@ -0,0 +1,19 @@ +import { GameConfig } from "../../types"; + +export const sixQuiPrendConfig: GameConfig = { + imagePath: "/games-assets/six-qui-prend-logo.webp", + bannerPath: "/games-assets/six-qui-prend-banner.webp", + id: "six_qui_prend", + name: "6 qui prend !", + minPlayers: 2, + maxPlayers: 10, + scoreType: "negative", + targetScore: 66, + targetScoreCondition: "reaches", // la partie s'arrête dès qu'un joueur atteint 66 + description: + "Évitez de ramasser les têtes de bœuf ! La partie s'arrête quand un joueur atteint 66 points. Le plus petit score gagne.", + icon: "Beef", + colors: { + primary: "#d81f26", // Rouge vif de la boite + }, +}; diff --git a/src/pages/GameOver.tsx b/src/pages/GameOver.tsx index 34777ad..5236b49 100644 --- a/src/pages/GameOver.tsx +++ b/src/pages/GameOver.tsx @@ -313,7 +313,38 @@ export default function GameOver() { - {session.rounds.map((round) => ( + {session.rounds.map((round) => { + // Manche jouée en équipes : une seule ligne avec l'équipe + // gagnante, plutôt qu'une rangée de zéros. + const team = round.winningTeamId + ? gameConfig?.teams?.find( + (t) => t.id === round.winningTeamId, + ) + : undefined; + if (team) { + return ( + + + #{round.roundNumber} + + + + {team.label} + + + + ); + } + return ( - ))} + ); + })} Total diff --git a/src/pages/PlayGame.tsx b/src/pages/PlayGame.tsx index ddbd9db..66578bb 100644 --- a/src/pages/PlayGame.tsx +++ b/src/pages/PlayGame.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; import { useParams, useNavigate } from "react-router-dom"; -import { Check, Undo2, Award, Trash2, BookOpen } from "lucide-react"; +import { Check, Undo2, Award, Trash2, BookOpen, Users } from "lucide-react"; import * as Icons from "lucide-react"; import { useLiveQuery } from "dexie-react-hooks"; import { db } from "../database/db"; @@ -32,6 +32,7 @@ export default function PlayGame() { >({}); const [currentCategoryIndex, setCurrentCategoryIndex] = useState(0); + const [selectedTeamId, setSelectedTeamId] = useState(null); // Keep active session players updated if a global player rename happens const playersInDb = useLiveQuery(() => db.players.toArray()); @@ -103,6 +104,17 @@ export default function PlayGame() { return dbPlayer ? { ...p, name: dbPlayer.name } : p; }); + // Jeux en équipes (ex: Moustache) : les premières manches ne notent que + // l'équipe gagnante, la dernière sert au décompte final des points. + const teams = gameConfig.teams; + const isTeamGame = Boolean(teams?.length && gameConfig.teamRounds); + const isTeamRound = + isTeamGame && activeSession.rounds.length < (gameConfig.teamRounds ?? 0); + const isFinalTally = isTeamGame && !isTeamRound; + + const teamWins = (teamId: string) => + activeSession.rounds.filter((r) => r.winningTeamId === teamId).length; + const calculateTotalScore = (playerId: string) => { return calculatePlayerTotalScore( playerId, @@ -146,6 +158,17 @@ export default function PlayGame() { }; const handleValidateRound = async () => { + // Manche jouée en équipes : on n'enregistre que l'équipe gagnante. + if (isTeamRound) { + if (!selectedTeamId) return; + await addRound( + activeSession.players.map((p) => ({ playerId: p.id, score: 0 })), + selectedTeamId, + ); + setSelectedTeamId(null); + return; + } + const scores: RoundScore[] = activeSession.players.map((p) => { // For games with categories (like Harmonies), we store the sum as the main score // and keep the details in the `details` object @@ -305,8 +328,30 @@ export default function PlayGame() {
+ {/* Manches gagnées par équipe (les points restent secrets jusqu'au bout) */} + {isTeamGame && teams && ( +
+ {teams.map((team) => ( +
+ + {teamWins(team.id)} + + + {team.label} + +
+ ))} +
+ )} + {/* Leaderboard Summary (Floating Chips) */} -
+
{displayPlayers.map((player) => { const total = getLiveTotalScore(player.id); return ( @@ -350,9 +395,13 @@ export default function PlayGame() {

{categories ? categories[currentCategoryIndex].label - : isSingleRound - ? "Saisie des scores" - : "À vos marques !"} + : isTeamRound + ? "Qui a gagné ?" + : isFinalTally + ? "Décompte final" + : isSingleRound + ? "Saisie des scores" + : "À vos marques !"}

@@ -389,7 +438,51 @@ export default function PlayGame() {
- {categories ? ( + {isTeamRound ? ( + + +

+ Les équipes changent à chaque manche : notez simplement celle + qui l'emporte. Les points seront révélés à la fin. +

+
+ {teams?.map((team) => { + const selected = selectedTeamId === team.id; + return ( + + ); + })} +
+
+
+ ) : categories ? ( {displayPlayers.map((player, idx) => { @@ -544,7 +637,22 @@ export default function PlayGame() { #{round.roundNumber}
- {round.scores.map((s) => ( + {round.winningTeamId && + (() => { + const t = teams?.find( + (x) => x.id === round.winningTeamId, + ); + return t ? ( + + {t.label} + + ) : null; + })()} + {!round.winningTeamId && + round.scores.map((s) => (
diff --git a/src/stores/gameStore.ts b/src/stores/gameStore.ts index 00d574c..72ebd6c 100644 --- a/src/stores/gameStore.ts +++ b/src/stores/gameStore.ts @@ -17,7 +17,7 @@ interface GameState { profileId: string, locationId?: string, ) => Promise; - addRound: (scores: RoundScore[]) => Promise; + addRound: (scores: RoundScore[], winningTeamId?: string) => Promise; updateRound: (roundId: string, scores: RoundScore[]) => Promise; removeRound: (roundId: string) => Promise; undoLastRound: () => Promise; @@ -63,7 +63,7 @@ export const useGameStore = create((set, get) => ({ return newSession.id; }, - addRound: async (scores) => { + addRound: async (scores, winningTeamId) => { const { activeSession, saveSession } = get(); if (!activeSession) return; @@ -71,6 +71,7 @@ export const useGameStore = create((set, get) => ({ id: generateId(), roundNumber: activeSession.rounds.length + 1, scores, + winningTeamId, }; set({ diff --git a/src/types/gameImport.ts b/src/types/gameImport.ts index b04c742..d05a73c 100644 --- a/src/types/gameImport.ts +++ b/src/types/gameImport.ts @@ -57,4 +57,6 @@ export const RESERVED_GAME_IDS = [ "harmonie", "odin_cards", "chouineurs", + "six_qui_prend", + "moustache", ]; diff --git a/src/types/index.ts b/src/types/index.ts index 662cba8..0c57df8 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -24,6 +24,11 @@ export interface GameConfig { isEndGameCalculation?: boolean, ) => number; fixedRounds?: number; // Used if scoreType is 'fixed_rounds' + // Jeux en équipes tirées au sort à chaque manche (ex: Moustache) : on note + // seulement l'équipe gagnante des `teamRounds` premières manches, puis la + // dernière manche sert au décompte final des points de chaque joueur. + teams?: GameTeam[]; + teamRounds?: number; scoringCategories?: ScoringCategory[]; // Optional: categories that depend on the chosen options (e.g. an extra // scoring category enabled by a rule toggle). Falls back to scoringCategories. @@ -68,6 +73,13 @@ export interface Round { id: string; roundNumber: number; scores: RoundScore[]; + winningTeamId?: string; // manche jouée en équipes : équipe gagnante +} + +export interface GameTeam { + id: string; + label: string; + color: string; } export interface GameSession {