feat: cockpit sidebar — design tokens, panels harmonisés, header stats fixe

- Design tokens game dans root.scss (--gp-*) — un seul endroit pour thémiser
- game-panels.scss : classes partagées (gp, gp-row, gp-btn, gp-progress, etc.)
- CockpitHeader : dashboard résumé (prod/s, ponte, mult, ADN, génération)
- Tous les panels refactorisés sur le système gp-* (tailles, couleurs, spacing)
- Sidebar structurée en zones : header → progression → générateurs → prestige → évolution
This commit is contained in:
2026-03-20 16:03:59 +01:00
parent 03b41649ee
commit 9065b1c593
9 changed files with 321 additions and 152 deletions

View File

@@ -0,0 +1,37 @@
// CockpitHeader.tsx — Dashboard résumé toujours visible en haut du cockpit
import { useGameStore } from "../store/useGameStore";
import { formatNumber } from "../utils/formatNumber";
export function CockpitHeader() {
const productionPerSecond = useGameStore((s) => s.productionPerSecond);
const { clickMultiplier, prestigeMultiplier, ancestralDna, prestigeCount } =
useGameStore((s) => s.state);
return (
<div className="gp gp-cockpit-header">
<div className="gp-stat">
<span className="gp-label">Prod/s</span>
<span className="gp-value gp-accent-green">
{formatNumber(productionPerSecond)}
</span>
</div>
<div className="gp-stat">
<span className="gp-label">Ponte</span>
<span className="gp-value">x{clickMultiplier}</span>
</div>
<div className="gp-stat">
<span className="gp-label">Mult</span>
<span className="gp-value">x{prestigeMultiplier.toFixed(1)}</span>
</div>
<div className="gp-stat">
<span className="gp-label">ADN</span>
<span className="gp-value gp-accent-purple">{ancestralDna}</span>
</div>
<div className="gp-stat">
<span className="gp-label">Gén.</span>
<span className="gp-value">{prestigeCount}</span>
</div>
</div>
);
}

View File

@@ -1,20 +1,18 @@
// EvolutionTree.tsx — Arbre d'Évolution permanent (jamais reset)
// Visible après le premier prestige (prestigeCount >= 1)
import React from "react";
import { useGameStore } from "../store/useGameStore";
import { canBuyEvolutionNode } from "../core/economy";
import type { EvolutionNode } from "../core/economy";
const EFFECT_DESCRIPTIONS: Record<string, (value: number) => string> = {
click_multiplier: (v) => `x${v} puissance de Ponte`,
production_multiplier: (v) => `x${v} production tous générateurs`,
start_bonus: (v) => `+${v} têtards au début de chaque run`,
unlock_generator: () => `Débloque le Lac Mystique dès le début`,
achievement_scaling: (v) => `+${(v * 100).toFixed(0)}% production par succès`,
const EFFECT_LABELS: Record<string, (v: number) => string> = {
click_multiplier: (v) => `x${v} ponte`,
production_multiplier: (v) => `x${v} production`,
start_bonus: (v) => `+${v} têtards au départ`,
unlock_generator: () => `Lac Mystique dès le début`,
achievement_scaling: (v) => `+${(v * 100).toFixed(0)}% prod/succès`,
};
function NodeCard({
function NodeRow({
node,
canBuy,
onBuy,
@@ -23,36 +21,28 @@ function NodeCard({
canBuy: boolean;
onBuy: () => void;
}) {
const rowClass = node.unlocked
? "gp-row gp-row--unlocked"
: canBuy
? "gp-row gp-row--evolution"
: "gp-row gp-row--locked";
return (
<div
className={`flex flex-col gap-2 p-3 rounded-lg border text-sm transition-colors ${
node.unlocked
? "border-emerald-500/50 bg-emerald-950/30"
: canBuy
? "border-amber-500/50 bg-amber-950/20"
: "border-gray-700/50 bg-gray-800/30 opacity-50"
}`}
>
<div className="flex justify-between items-center">
<span className="text-white font-semibold">{node.name}</span>
<span className="text-xs text-gray-400">
{node.unlocked ? "Débloqué" : `${node.cost} ADN`}
</span>
<div className={rowClass}>
<div style={{ display: "flex", flexDirection: "column", minWidth: 0 }}>
<span className="gp-value">{node.name}</span>
<span className="gp-label">{EFFECT_LABELS[node.effect](node.value)}</span>
</div>
<p className="text-xs text-gray-300">
{EFFECT_DESCRIPTIONS[node.effect](node.value)}
</p>
{!node.unlocked && (
{node.unlocked ? (
<span className="gp-label gp-accent-green">OK</span>
) : (
<button
disabled={!canBuy}
onClick={onBuy}
className={`px-3 py-1 rounded text-xs font-medium transition-colors cursor-pointer ${
canBuy
? "bg-amber-600 hover:bg-amber-500 text-white"
: "bg-gray-700 text-gray-500 cursor-not-allowed"
}`}
className={`gp-btn ${canBuy ? "gp-btn--buy" : "gp-btn--disabled"}`}
style={canBuy ? { background: "#d97706" } : {}}
>
{canBuy ? "Débloquer" : "Verrouillé"}
{node.cost} ADN
</button>
)}
</div>
@@ -67,33 +57,19 @@ export function EvolutionTree() {
if (prestigeCount < 1) return null;
return (
<div className="flex flex-col gap-3 p-4 rounded-xl bg-gray-900/80 backdrop-blur-sm max-w-md w-full">
<div className="flex justify-between items-center">
<h3 className="text-lg font-bold text-white">Arbre d'Évolution</h3>
<span className="text-sm text-amber-300">{state.ancestralDna} ADN</span>
</div>
<div className="flex flex-col gap-2">
{evolutionTree.map((node, index) => (
<React.Fragment key={node.id}>
{index > 0 && (
<div
className={`text-center text-xs ${
evolutionTree[index - 1].unlocked
? "text-emerald-400"
: "text-gray-600"
}`}
>
|
</div>
)}
<NodeCard
node={node}
canBuy={canBuyEvolutionNode(state, node.id)}
onBuy={() => buyNode(node.id)}
/>
</React.Fragment>
))}
<div className="gp">
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<span className="gp-title">Évolution</span>
<span className="gp-value gp-accent-amber">{state.ancestralDna} ADN</span>
</div>
{evolutionTree.map((node) => (
<NodeRow
key={node.id}
node={node}
canBuy={canBuyEvolutionNode(state, node.id)}
onBuy={() => buyNode(node.id)}
/>
))}
</div>
);
}

View File

@@ -11,12 +11,10 @@ export function GeneratorShop() {
const generatorCost = useGameStore((s) => s.generatorCost);
return (
<div className="flex flex-col gap-2 p-4 rounded-xl bg-gray-900/80 backdrop-blur-sm max-w-md w-full">
<div className="flex justify-between items-center">
<h2 className="text-sm font-bold text-white">Générateurs</h2>
<span className="text-xs text-emerald-400 font-medium">
{formatNumber(productionPerSecond)}/s
</span>
<div className="gp">
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<span className="gp-title">Générateurs</span>
<span className="gp-value gp-accent-green">{formatNumber(productionPerSecond)}/s</span>
</div>
{generators.map((gen) => {
const cost = generatorCost(gen);
@@ -26,32 +24,24 @@ export function GeneratorShop() {
return (
<div
key={gen.id}
className={`flex items-center justify-between gap-2 p-2.5 rounded-lg border transition-colors ${
canAfford
? "border-emerald-500/50 bg-emerald-950/30 hover:bg-emerald-950/50"
: "border-gray-700/50 bg-gray-800/30 opacity-60"
}`}
className={`gp-row ${canAfford ? "gp-row--active" : "gp-row--locked"}`}
>
<div className="flex flex-col min-w-0">
<div className="flex items-center gap-2">
<span className="text-white font-semibold text-sm">{gen.name}</span>
<div style={{ display: "flex", flexDirection: "column", minWidth: 0 }}>
<div style={{ display: "flex", alignItems: "center", gap: "0.3rem" }}>
<span className="gp-value">{gen.name}</span>
{gen.owned > 0 && (
<span className="text-emerald-400 text-xs font-medium">x{gen.owned}</span>
<span className="gp-label gp-accent-green">x{gen.owned}</span>
)}
</div>
<span className="text-gray-400 text-xs">
+{gen.baseProduction}/s chacun
<span className="gp-label">
+{gen.baseProduction}/s
{gen.owned > 0 && ` · ${formatNumber(currentProd)}/s total`}
</span>
</div>
<button
onClick={() => buy(gen.id)}
disabled={!canAfford}
className={`shrink-0 px-3 py-1.5 rounded-md text-xs font-medium transition-colors cursor-pointer ${
canAfford
? "bg-emerald-600 hover:bg-emerald-500 text-white"
: "bg-gray-700 text-gray-500 cursor-not-allowed"
}`}
className={`gp-btn ${canAfford ? "gp-btn--buy" : "gp-btn--disabled"}`}
>
{formatNumber(cost)}
</button>

View File

@@ -1,5 +1,4 @@
// MilestoneBar.tsx — Progression vers le prochain prestige
// Barre visuelle ressources / 1 000 000
import { useGameStore } from "../store/useGameStore";
import { formatNumber } from "../utils/formatNumber";
@@ -14,24 +13,27 @@ export function MilestoneBar() {
const remaining = Math.max(PRESTIGE_THRESHOLD - resources, 0);
return (
<div className="flex flex-col gap-1 max-w-md w-full">
<div className="text-xs text-gray-300 flex justify-between">
<span>Prochaine Génération</span>
<span>
<div className="gp" style={{ gap: "0.25rem" }}>
<div style={{ display: "flex", justifyContent: "space-between" }}>
<span className="gp-label">Prochaine Génération</span>
<span className="gp-label">
{formatNumber(resources)} / {formatNumber(PRESTIGE_THRESHOLD)}
</span>
</div>
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
<div className="gp-progress">
<div
className="h-full bg-gradient-to-r from-purple-600 to-purple-400 transition-all duration-500 rounded-full"
style={{ width: `${progressPercent}%` }}
className="gp-progress-fill"
style={{
width: `${progressPercent}%`,
background: "linear-gradient(90deg, #7c3aed, #a78bfa)",
}}
/>
</div>
<div className="text-xs text-gray-400 text-right">
<span className="gp-label" style={{ textAlign: "right" }}>
{remaining > 0
? `${formatNumber(remaining)} têtards restants`
? `${formatNumber(remaining)} restants`
: "Nouvelle Génération disponible !"}
</div>
</span>
</div>
);
}

View File

@@ -1,12 +1,10 @@
// PrestigePanel.tsx — Nouvelle Génération (prestige)
// Visible uniquement quand canPrestige = true (ressources >= 1 000 000)
import { useGameStore } from "../store/useGameStore";
import { computePrestigeDna } from "../core/economy";
export function PrestigePanel() {
const { prestigeCount, prestigeMultiplier, ancestralDna, lifetimeTadpoles } =
useGameStore((s) => s.state);
const { lifetimeTadpoles } = useGameStore((s) => s.state);
const canPrestige = useGameStore((s) => s.canPrestige);
const prestige = useGameStore((s) => s.prestige);
@@ -18,40 +16,26 @@ export function PrestigePanel() {
`Reset : têtards et générateurs à zéro.\n` +
`Récompense : +${dnaPreview} ADN Ancestral\n` +
` +0.1x multiplicateur permanent\n\n` +
`ADN actuel : ${ancestralDna}\n` +
`ADN après : ${ancestralDna + dnaPreview}\n` +
`Multiplicateur : x${prestigeMultiplier.toFixed(1)} → x${(prestigeMultiplier + 0.1).toFixed(1)}\n\n` +
`L'Arbre d'Évolution persiste.\n\n` +
`Confirmer la Nouvelle Génération ?`
`Confirmer ?`
);
if (confirmed) prestige();
};
return (
<div className="flex flex-col gap-2 p-4 rounded-xl bg-purple-900/60 backdrop-blur-sm max-w-md w-full">
<h2 className="text-sm font-bold text-purple-100">Prestige</h2>
<div className="flex flex-wrap gap-3 text-xs text-purple-200">
<span>Gén. {prestigeCount}</span>
<span>Mult x{prestigeMultiplier.toFixed(1)}</span>
<span>ADN {ancestralDna}</span>
</div>
<div className="gp">
<span className="gp-title">Prestige</span>
{canPrestige ? (
<div className="flex flex-col gap-2 mt-1">
<p className="text-sm text-purple-100">
<div style={{ display: "flex", flexDirection: "column", gap: "0.4rem" }}>
<span className="gp-value gp-accent-purple">
+{dnaPreview} ADN · +0.1x mult
</p>
<button
onClick={handlePrestige}
className="px-4 py-2 rounded-lg bg-purple-600 hover:bg-purple-500 text-white font-semibold text-sm transition-colors cursor-pointer animate-pulse"
>
</span>
<button onClick={handlePrestige} className="gp-btn gp-btn--prestige">
Nouvelle Génération
</button>
</div>
) : (
<p className="text-xs text-purple-300/60">
Atteins 1M têtards pour prestige
</p>
<span className="gp-label">Atteins 1M têtards pour prestige</span>
)}
</div>
);