Harmonies : règle "Esprits de la nature" + logo vectorisé
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:
Zed
2026-07-12 16:05:15 +02:00
parent 1f400b78cb
commit c69a5195f5
6 changed files with 90 additions and 26 deletions
+1
View File
@@ -3,6 +3,7 @@
<head>
<meta charset="UTF-8" />
<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="apple-touch-icon" href="/apple-touch-icon.png" />
<meta
+30
View File
@@ -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

+28 -8
View File
@@ -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 = {
imagePath: "/games-assets/harmonies-logo.webp",
@@ -15,12 +29,18 @@ export const harmonieConfig: GameConfig = {
colors: {
primary: "#065f46", // Vert émeraude sombre tiré de la boite Harmonies
},
scoringCategories: [
{ 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" },
options: [
{
id: "esprits_nature",
label: "Extension : Esprits de la nature",
type: "boolean",
defaultValue: false,
},
],
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,
};
+8 -3
View File
@@ -52,6 +52,11 @@ export default function GameOver() {
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
? Math.round((session.dateEnd - session.dateStart) / 60000)
: 0;
@@ -68,7 +73,7 @@ export default function GameOver() {
const totals = session.players.map((p) => {
// If it's a categorized game (Harmonies), extract details from the first (and only) round
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);
details = pScore?.details;
}
@@ -232,9 +237,9 @@ export default function GameOver() {
<span className="text-2xl font-bold">{t.score}</span>
</div>
{/* 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">
{gameConfig.scoringCategories.map((cat) => {
{categories.map((cat) => {
const val = t.details?.[cat.id];
if (!val || val === "0") return null;
return (
+20 -15
View File
@@ -39,6 +39,11 @@ export default function PlayGame() {
const gameConfig = useGameConfig(activeSession?.gameId);
useGameTheme(gameConfig);
// Effective scoring categories (may depend on the session's options).
const categories =
gameConfig?.getScoringCategories?.(activeSession?.options ?? {}) ??
gameConfig?.scoringCategories;
useEffect(() => {
if (sessionId) {
loadSession(sessionId);
@@ -109,7 +114,7 @@ export default function PlayGame() {
const getLiveTotalScore = (playerId: string) => {
let total = calculateTotalScore(playerId);
if (gameConfig?.scoringCategories) {
if (categories) {
const pDetails = detailedScores[playerId] || {};
for (const key in pDetails) {
const val = parseInt(pDetails[key] || "0", 10);
@@ -144,7 +149,7 @@ export default function PlayGame() {
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
if (gameConfig?.scoringCategories) {
if (categories) {
const pDetails = detailedScores[p.id] || {};
let total = 0;
for (const key in pDetails) {
@@ -318,14 +323,14 @@ export default function PlayGame() {
<section className="relative">
<div className="flex items-center justify-between px-2 mb-4">
<h2 className="text-2xl font-black tracking-tighter drop-shadow-sm">
{gameConfig.scoringCategories
? gameConfig.scoringCategories[currentCategoryIndex].label
{categories
? categories[currentCategoryIndex].label
: isSingleRound
? "Saisie des scores"
: "À vos marques !"}
</h2>
<div className="flex items-center gap-2">
{gameConfig.scoringCategories && currentCategoryIndex > 0 && (
{categories && currentCategoryIndex > 0 && (
<Button
variant="outline"
size="sm"
@@ -338,7 +343,7 @@ export default function PlayGame() {
)}
{activeSession.rounds.length > 0 &&
!isSingleRound &&
!gameConfig.scoringCategories && (
!categories && (
<Button
variant="outline"
size="sm"
@@ -349,22 +354,22 @@ export default function PlayGame() {
Annuler manche
</Button>
)}
{gameConfig.scoringCategories && (
{categories && (
<span className="text-xs font-black bg-primary/10 text-primary px-3 py-1.5 rounded-full">
Étape {currentCategoryIndex + 1} /{" "}
{gameConfig.scoringCategories.length}
{categories.length}
</span>
)}
</div>
</div>
{gameConfig.scoringCategories ? (
{categories ? (
<Card className="bg-background/80 backdrop-blur-xl border-0 shadow-xl rounded-[2.5rem] overflow-hidden">
<CardContent className="p-4 space-y-1">
{displayPlayers.map((player, idx) => {
const pDetails = detailedScores[player.id] || {};
const catId =
gameConfig.scoringCategories![currentCategoryIndex].id;
categories![currentCategoryIndex].id;
return (
<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"
onClick={() => {
if (
gameConfig?.scoringCategories &&
currentCategoryIndex < gameConfig.scoringCategories.length - 1
categories &&
currentCategoryIndex < categories.length - 1
) {
setCurrentCategoryIndex((prev) => prev + 1);
} else {
@@ -564,13 +569,13 @@ export default function PlayGame() {
}
}}
>
{gameConfig?.scoringCategories &&
currentCategoryIndex < gameConfig.scoringCategories.length - 1 ? (
{categories &&
currentCategoryIndex < categories.length - 1 ? (
"Suivant"
) : (
<>
<Check className="w-7 h-7 mr-2" strokeWidth={3} />
{gameConfig?.scoringCategories || isSingleRound
{categories || isSingleRound
? "Valider le score"
: "Valider la manche"}
</>
+3
View File
@@ -22,6 +22,9 @@ export interface GameConfig {
) => number;
fixedRounds?: number; // Used if scoreType is 'fixed_rounds'
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;
icon?: string;
imagePath?: string; // Optional custom logo image (SVG, PNG, WebP...)