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>
|
||||
<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
|
||||
|
||||
@@ -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 = {
|
||||
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,
|
||||
};
|
||||
|
||||
@@ -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
@@ -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"}
|
||||
</>
|
||||
|
||||
@@ -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...)
|
||||
|
||||
Reference in New Issue
Block a user