Harmonies : règle "Esprits de la nature" + logo vectorisé
Build and Publish Docker Image / build-and-push-image (push) Successful in 12s
Build and Publish Docker Image / build-and-push-image (push) Successful in 12s
- Harmonies : nouvelle option activable "Extension : Esprits de la nature" qui ajoute une catégorie de score en fin de partie (incluse dans le total) - GameConfig.getScoringCategories(options) : catégories dépendantes des options (PlayGame et GameOver utilisent les catégories effectives) - Nouveau logo vectorisé public/logo.svg + favicon SVG dans index.html Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" href="/favicon.ico" sizes="48x48" />
|
<link rel="icon" href="/favicon.ico" sizes="48x48" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png" />
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png" />
|
||||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||||
<meta
|
<meta
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" fill="none" role="img" aria-label="Skori">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="skori-planet" x1="20" y1="18" x2="78" y2="82">
|
||||||
|
<stop offset="0%" stop-color="#5eead4"/>
|
||||||
|
<stop offset="55%" stop-color="#14b8a6"/>
|
||||||
|
<stop offset="100%" stop-color="#0d9488"/>
|
||||||
|
</linearGradient>
|
||||||
|
<clipPath id="skori-clip"><circle cx="50" cy="50" r="28"/></clipPath>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Orbit ring — back half -->
|
||||||
|
<path d="M 12 66 A 42 14 -18 0 1 88 40" stroke="#a5f3fc" stroke-width="4.5" stroke-linecap="round"/>
|
||||||
|
|
||||||
|
<!-- Planet body -->
|
||||||
|
<circle cx="50" cy="50" r="28" fill="url(#skori-planet)"/>
|
||||||
|
|
||||||
|
<!-- Flat two-tone shading + craters -->
|
||||||
|
<g clip-path="url(#skori-clip)">
|
||||||
|
<circle cx="72" cy="70" r="30" fill="#0f766e" opacity="0.5"/>
|
||||||
|
<circle cx="40" cy="40" r="6" fill="#99f6e4" opacity="0.55"/>
|
||||||
|
<circle cx="58" cy="56" r="4" fill="#0f766e" opacity="0.45"/>
|
||||||
|
<circle cx="46" cy="62" r="3" fill="#0f766e" opacity="0.4"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Orbit ring — front half -->
|
||||||
|
<path d="M 12 66 A 42 14 -18 0 0 88 40" stroke="#22d3ee" stroke-width="4.5" stroke-linecap="round"/>
|
||||||
|
|
||||||
|
<!-- Sparkle -->
|
||||||
|
<path d="M 80 20 q 2 6 8 8 q -6 2 -8 8 q -2 -6 -8 -8 q 6 -2 8 -8 z" fill="#fde68a"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -1,4 +1,18 @@
|
|||||||
import { GameConfig } from "../../types";
|
import { GameConfig, ScoringCategory } from "../../types";
|
||||||
|
|
||||||
|
const BASE_CATEGORIES: ScoringCategory[] = [
|
||||||
|
{ id: "arbres", label: "Arbres" },
|
||||||
|
{ id: "montagnes", label: "Montagnes" },
|
||||||
|
{ id: "champs", label: "Champs" },
|
||||||
|
{ id: "batiments", label: "Bâtiments" },
|
||||||
|
{ id: "eau", label: "Rivière" },
|
||||||
|
{ id: "animaux", label: "Animaux" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const NATURE_SPIRIT_CATEGORY: ScoringCategory = {
|
||||||
|
id: "esprits_nature",
|
||||||
|
label: "Esprits de la nature",
|
||||||
|
};
|
||||||
|
|
||||||
export const harmonieConfig: GameConfig = {
|
export const harmonieConfig: GameConfig = {
|
||||||
imagePath: "/games-assets/harmonies-logo.webp",
|
imagePath: "/games-assets/harmonies-logo.webp",
|
||||||
@@ -15,12 +29,18 @@ export const harmonieConfig: GameConfig = {
|
|||||||
colors: {
|
colors: {
|
||||||
primary: "#065f46", // Vert émeraude sombre tiré de la boite Harmonies
|
primary: "#065f46", // Vert émeraude sombre tiré de la boite Harmonies
|
||||||
},
|
},
|
||||||
scoringCategories: [
|
options: [
|
||||||
{ id: "arbres", label: "Arbres" },
|
{
|
||||||
{ id: "montagnes", label: "Montagnes" },
|
id: "esprits_nature",
|
||||||
{ id: "champs", label: "Champs" },
|
label: "Extension : Esprits de la nature",
|
||||||
{ id: "batiments", label: "Bâtiments" },
|
type: "boolean",
|
||||||
{ id: "eau", label: "Rivière" },
|
defaultValue: false,
|
||||||
{ id: "animaux", label: "Animaux" },
|
},
|
||||||
],
|
],
|
||||||
|
scoringCategories: BASE_CATEGORIES,
|
||||||
|
// When the "Esprits de la nature" rule is on, add a final scoring category.
|
||||||
|
getScoringCategories: (options) =>
|
||||||
|
options?.esprits_nature
|
||||||
|
? [...BASE_CATEGORIES, NATURE_SPIRIT_CATEGORY]
|
||||||
|
: BASE_CATEGORIES,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -52,6 +52,11 @@ export default function GameOver() {
|
|||||||
|
|
||||||
const Icon = gameConfig?.icon ? (Icons as any)[gameConfig.icon] : null;
|
const Icon = gameConfig?.icon ? (Icons as any)[gameConfig.icon] : null;
|
||||||
|
|
||||||
|
// Effective scoring categories (may depend on the session's options).
|
||||||
|
const categories =
|
||||||
|
gameConfig?.getScoringCategories?.(session.options) ??
|
||||||
|
gameConfig?.scoringCategories;
|
||||||
|
|
||||||
const durationInMinutes = session.dateEnd
|
const durationInMinutes = session.dateEnd
|
||||||
? Math.round((session.dateEnd - session.dateStart) / 60000)
|
? Math.round((session.dateEnd - session.dateStart) / 60000)
|
||||||
: 0;
|
: 0;
|
||||||
@@ -68,7 +73,7 @@ export default function GameOver() {
|
|||||||
const totals = session.players.map((p) => {
|
const totals = session.players.map((p) => {
|
||||||
// If it's a categorized game (Harmonies), extract details from the first (and only) round
|
// If it's a categorized game (Harmonies), extract details from the first (and only) round
|
||||||
let details = undefined;
|
let details = undefined;
|
||||||
if (gameConfig?.scoringCategories && session.rounds.length > 0) {
|
if (categories && session.rounds.length > 0) {
|
||||||
const pScore = session.rounds[0].scores.find((s) => s.playerId === p.id);
|
const pScore = session.rounds[0].scores.find((s) => s.playerId === p.id);
|
||||||
details = pScore?.details;
|
details = pScore?.details;
|
||||||
}
|
}
|
||||||
@@ -232,9 +237,9 @@ export default function GameOver() {
|
|||||||
<span className="text-2xl font-bold">{t.score}</span>
|
<span className="text-2xl font-bold">{t.score}</span>
|
||||||
</div>
|
</div>
|
||||||
{/* Display details if it's a categorized game like Harmonies */}
|
{/* Display details if it's a categorized game like Harmonies */}
|
||||||
{t.details && gameConfig?.scoringCategories && (
|
{t.details && categories && (
|
||||||
<div className="mt-3 pt-3 border-t border-border/50 grid grid-cols-3 gap-2">
|
<div className="mt-3 pt-3 border-t border-border/50 grid grid-cols-3 gap-2">
|
||||||
{gameConfig.scoringCategories.map((cat) => {
|
{categories.map((cat) => {
|
||||||
const val = t.details?.[cat.id];
|
const val = t.details?.[cat.id];
|
||||||
if (!val || val === "0") return null;
|
if (!val || val === "0") return null;
|
||||||
return (
|
return (
|
||||||
|
|||||||
+20
-15
@@ -39,6 +39,11 @@ export default function PlayGame() {
|
|||||||
const gameConfig = useGameConfig(activeSession?.gameId);
|
const gameConfig = useGameConfig(activeSession?.gameId);
|
||||||
useGameTheme(gameConfig);
|
useGameTheme(gameConfig);
|
||||||
|
|
||||||
|
// Effective scoring categories (may depend on the session's options).
|
||||||
|
const categories =
|
||||||
|
gameConfig?.getScoringCategories?.(activeSession?.options ?? {}) ??
|
||||||
|
gameConfig?.scoringCategories;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (sessionId) {
|
if (sessionId) {
|
||||||
loadSession(sessionId);
|
loadSession(sessionId);
|
||||||
@@ -109,7 +114,7 @@ export default function PlayGame() {
|
|||||||
|
|
||||||
const getLiveTotalScore = (playerId: string) => {
|
const getLiveTotalScore = (playerId: string) => {
|
||||||
let total = calculateTotalScore(playerId);
|
let total = calculateTotalScore(playerId);
|
||||||
if (gameConfig?.scoringCategories) {
|
if (categories) {
|
||||||
const pDetails = detailedScores[playerId] || {};
|
const pDetails = detailedScores[playerId] || {};
|
||||||
for (const key in pDetails) {
|
for (const key in pDetails) {
|
||||||
const val = parseInt(pDetails[key] || "0", 10);
|
const val = parseInt(pDetails[key] || "0", 10);
|
||||||
@@ -144,7 +149,7 @@ export default function PlayGame() {
|
|||||||
const scores: RoundScore[] = activeSession.players.map((p) => {
|
const scores: RoundScore[] = activeSession.players.map((p) => {
|
||||||
// For games with categories (like Harmonies), we store the sum as the main score
|
// For games with categories (like Harmonies), we store the sum as the main score
|
||||||
// and keep the details in the `details` object
|
// and keep the details in the `details` object
|
||||||
if (gameConfig?.scoringCategories) {
|
if (categories) {
|
||||||
const pDetails = detailedScores[p.id] || {};
|
const pDetails = detailedScores[p.id] || {};
|
||||||
let total = 0;
|
let total = 0;
|
||||||
for (const key in pDetails) {
|
for (const key in pDetails) {
|
||||||
@@ -318,14 +323,14 @@ export default function PlayGame() {
|
|||||||
<section className="relative">
|
<section className="relative">
|
||||||
<div className="flex items-center justify-between px-2 mb-4">
|
<div className="flex items-center justify-between px-2 mb-4">
|
||||||
<h2 className="text-2xl font-black tracking-tighter drop-shadow-sm">
|
<h2 className="text-2xl font-black tracking-tighter drop-shadow-sm">
|
||||||
{gameConfig.scoringCategories
|
{categories
|
||||||
? gameConfig.scoringCategories[currentCategoryIndex].label
|
? categories[currentCategoryIndex].label
|
||||||
: isSingleRound
|
: isSingleRound
|
||||||
? "Saisie des scores"
|
? "Saisie des scores"
|
||||||
: "À vos marques !"}
|
: "À vos marques !"}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{gameConfig.scoringCategories && currentCategoryIndex > 0 && (
|
{categories && currentCategoryIndex > 0 && (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -338,7 +343,7 @@ export default function PlayGame() {
|
|||||||
)}
|
)}
|
||||||
{activeSession.rounds.length > 0 &&
|
{activeSession.rounds.length > 0 &&
|
||||||
!isSingleRound &&
|
!isSingleRound &&
|
||||||
!gameConfig.scoringCategories && (
|
!categories && (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -349,22 +354,22 @@ export default function PlayGame() {
|
|||||||
Annuler manche
|
Annuler manche
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{gameConfig.scoringCategories && (
|
{categories && (
|
||||||
<span className="text-xs font-black bg-primary/10 text-primary px-3 py-1.5 rounded-full">
|
<span className="text-xs font-black bg-primary/10 text-primary px-3 py-1.5 rounded-full">
|
||||||
Étape {currentCategoryIndex + 1} /{" "}
|
Étape {currentCategoryIndex + 1} /{" "}
|
||||||
{gameConfig.scoringCategories.length}
|
{categories.length}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{gameConfig.scoringCategories ? (
|
{categories ? (
|
||||||
<Card className="bg-background/80 backdrop-blur-xl border-0 shadow-xl rounded-[2.5rem] overflow-hidden">
|
<Card className="bg-background/80 backdrop-blur-xl border-0 shadow-xl rounded-[2.5rem] overflow-hidden">
|
||||||
<CardContent className="p-4 space-y-1">
|
<CardContent className="p-4 space-y-1">
|
||||||
{displayPlayers.map((player, idx) => {
|
{displayPlayers.map((player, idx) => {
|
||||||
const pDetails = detailedScores[player.id] || {};
|
const pDetails = detailedScores[player.id] || {};
|
||||||
const catId =
|
const catId =
|
||||||
gameConfig.scoringCategories![currentCategoryIndex].id;
|
categories![currentCategoryIndex].id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -555,8 +560,8 @@ export default function PlayGame() {
|
|||||||
className="w-full text-xl font-black shadow-xl h-16 rounded-[2rem] transition-transform active:scale-95"
|
className="w-full text-xl font-black shadow-xl h-16 rounded-[2rem] transition-transform active:scale-95"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (
|
if (
|
||||||
gameConfig?.scoringCategories &&
|
categories &&
|
||||||
currentCategoryIndex < gameConfig.scoringCategories.length - 1
|
currentCategoryIndex < categories.length - 1
|
||||||
) {
|
) {
|
||||||
setCurrentCategoryIndex((prev) => prev + 1);
|
setCurrentCategoryIndex((prev) => prev + 1);
|
||||||
} else {
|
} else {
|
||||||
@@ -564,13 +569,13 @@ export default function PlayGame() {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{gameConfig?.scoringCategories &&
|
{categories &&
|
||||||
currentCategoryIndex < gameConfig.scoringCategories.length - 1 ? (
|
currentCategoryIndex < categories.length - 1 ? (
|
||||||
"Suivant"
|
"Suivant"
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Check className="w-7 h-7 mr-2" strokeWidth={3} />
|
<Check className="w-7 h-7 mr-2" strokeWidth={3} />
|
||||||
{gameConfig?.scoringCategories || isSingleRound
|
{categories || isSingleRound
|
||||||
? "Valider le score"
|
? "Valider le score"
|
||||||
: "Valider la manche"}
|
: "Valider la manche"}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ export interface GameConfig {
|
|||||||
) => number;
|
) => number;
|
||||||
fixedRounds?: number; // Used if scoreType is 'fixed_rounds'
|
fixedRounds?: number; // Used if scoreType is 'fixed_rounds'
|
||||||
scoringCategories?: ScoringCategory[];
|
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.
|
||||||
|
getScoringCategories?: (options: Record<string, any>) => ScoringCategory[];
|
||||||
description?: string;
|
description?: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
imagePath?: string; // Optional custom logo image (SVG, PNG, WebP...)
|
imagePath?: string; // Optional custom logo image (SVG, PNG, WebP...)
|
||||||
|
|||||||
Reference in New Issue
Block a user