feat: arbre d'évolution 3 voies — ponte/marais/adaptation

18 nœuds (6/branche), nœuds exclusifs (pick one), reset gratuit.
Nouveaux effets : double_click, auto_click, crit, generator_boost,
cost_reduction, prestige_dna_bonus, offline_boost, threshold_reduction.
UI 3 colonnes colorées, seuil prestige dynamique, coût réduit.
75 tests (tous passent).
This commit is contained in:
2026-03-28 11:52:51 +01:00
parent 3ba10dad5f
commit ae50908bc9
7 changed files with 405 additions and 76 deletions

View File

@@ -1,8 +1,8 @@
// EvolutionTree.tsx — Arbre d'Évolution permanent (jamais reset)
// EvolutionTree.tsx — Arbre d'Évolution 3 voies (permanent jamais reset par prestige)
import { useGameStore } from "../store/useGameStore";
import { canBuyEvolutionNode } from "../core/economy";
import type { EvolutionNode } from "../core/economy";
import { canBuyEvolutionNode, getSpentDna } from "../core/economy";
import type { EvolutionNode, Branch } from "../core/economy";
const EFFECT_LABELS: Record<string, (v: number) => string> = {
click_multiplier: (v) => `x${v} ponte`,
@@ -10,19 +10,37 @@ const EFFECT_LABELS: Record<string, (v: number) => string> = {
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`,
double_click_chance: (v) => `${(v * 100).toFixed(0)}% chance double ponte`,
auto_click: (v) => `${v} auto-ponte/s`,
crit_click_chance: (v) => `${(v * 100).toFixed(0)}% chance crit x10`,
generator_boost: (v) => `x${v} Nid`,
cost_reduction: (v) => `-${(v * 100).toFixed(0)}% coût générateurs`,
prestige_dna_bonus: (v) => `+${(v * 100).toFixed(0)}% ADN prestige`,
offline_boost: (v) => `+${(v * 100).toFixed(0)}% gains offline`,
prestige_threshold_reduction: (v) => `Prestige à ${((1 - v) * 100).toFixed(0)}% du seuil`,
};
const BRANCH_CONFIG: Record<Branch, { label: string; color: string; accent: string }> = {
ponte: { label: "Ponte", color: "border-emerald-500/30", accent: "gp-accent-green" },
marais: { label: "Marais", color: "border-blue-500/30", accent: "text-blue-400" },
adaptation: { label: "Adaptation", color: "border-amber-500/30", accent: "gp-accent-amber" },
};
function NodeRow({
node,
canBuy,
isExcluded,
onBuy,
}: {
node: EvolutionNode;
canBuy: boolean;
isExcluded: boolean;
onBuy: () => void;
}) {
const rowClass = node.unlocked
? "gp-row gp-row--unlocked"
: isExcluded
? "gp-row gp-row--locked opacity-30!"
: canBuy
? "gp-row gp-row--evolution"
: "gp-row gp-row--locked";
@@ -30,45 +48,101 @@ function NodeRow({
return (
<div className={rowClass}>
<div className="flex flex-col min-w-0">
<span className="gp-value">{node.name}</span>
<span className="gp-label">{EFFECT_LABELS[node.effect](node.value)}</span>
<div className="flex items-center gap-1">
<span className="gp-value text-[0.7rem]!">{node.name}</span>
{node.exclusive_with && !node.unlocked && !isExcluded && (
<span className="gp-label text-[0.55rem]!">OU</span>
)}
</div>
<span className="gp-label">{EFFECT_LABELS[node.effect]?.(node.value) ?? node.effect}</span>
</div>
{node.unlocked ? (
<span className="gp-label gp-accent-green">OK</span>
) : isExcluded ? (
<span className="gp-label text-[0.55rem]!">verrouillé</span>
) : (
<button
disabled={!canBuy}
onClick={onBuy}
className={`gp-btn ${canBuy ? "gp-btn--buy bg-amber-600!" : "gp-btn--disabled"}`}
className={`gp-btn ${canBuy ? "gp-btn--buy" : "gp-btn--disabled"}`}
>
{node.cost} ADN
{node.cost}
</button>
)}
</div>
);
}
export function EvolutionTree() {
function BranchColumn({ branch }: { branch: Branch }) {
const state = useGameStore((s) => s.state);
const buyNode = useGameStore((s) => s.buyNode);
const { evolutionTree, prestigeCount } = state;
if (prestigeCount < 1) return null;
const nodes = state.evolutionTree.filter((n) => n.branch === branch);
const config = BRANCH_CONFIG[branch];
return (
<div className="gp">
<div className="flex justify-between items-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 className={`gp flex-1 min-w-0 border-t-2 ${config.color}`}>
<span className={`gp-title text-center ${config.accent}`}>{config.label}</span>
{nodes.map((node) => {
const isExcluded = node.exclusive_with
? state.evolutionTree.find((n) => n.id === node.exclusive_with)?.unlocked ?? false
: false;
return (
<NodeRow
key={node.id}
node={node}
canBuy={canBuyEvolutionNode(state, node.id)}
isExcluded={isExcluded}
onBuy={() => buyNode(node.id)}
/>
);
})}
</div>
);
}
export function EvolutionTree() {
const state = useGameStore((s) => s.state);
const resetTree = useGameStore((s) => s.resetTree);
const { prestigeCount, ancestralDna, evolutionTree } = state;
if (prestigeCount < 1) return null;
const spentDna = getSpentDna(evolutionTree);
const hasUnlocked = spentDna > 0;
const handleReset = () => {
if (!hasUnlocked) return;
const confirmed = window.confirm(
`Réinitialiser l'Arbre d'Évolution ?\n\n` +
`Tu récupères ${spentDna} ADN Ancestral.\n` +
`Tous les nœuds seront verrouillés.\n\n` +
`Confirmer ?`
);
if (confirmed) resetTree();
};
return (
<div className="flex flex-col gap-2">
<div className="flex justify-between items-center px-1">
<span className="gp-title">Évolution</span>
<div className="flex items-center gap-2">
<span className="gp-value gp-accent-amber">{ancestralDna} ADN</span>
{hasUnlocked && (
<button
onClick={handleReset}
className="gp-btn gp-btn--disabled text-[0.55rem]! hover:bg-red-500/20! hover:text-red-400!"
title={`Récupérer ${spentDna} ADN`}
>
Reset
</button>
)}
</div>
</div>
<div className="flex gap-1.5">
<BranchColumn branch="ponte" />
<BranchColumn branch="marais" />
<BranchColumn branch="adaptation" />
</div>
</div>
);
}

View File

@@ -8,7 +8,7 @@ export function GeneratorShop() {
const resources = useGameStore((s) => s.state.resources);
const productionPerSecond = useGameStore((s) => s.productionPerSecond);
const buy = useGameStore((s) => s.buy);
const generatorCost = useGameStore((s) => s.generatorCost);
const generatorCost = useGameStore((s) => s.generatorCostWithTree);
return (
<div className="gp">

View File

@@ -1,23 +1,24 @@
// MilestoneBar.tsx — Progression vers le prochain prestige
import { useGameStore } from "../store/useGameStore";
import { formatNumber } from "../utils/formatNumber";
const PRESTIGE_THRESHOLD = 1_000_000;
import { formatNumber, } from "../utils/formatNumber";
import { getPrestigeThreshold } from "../core/economy";
export function MilestoneBar() {
const resources = useGameStore((s) => s.state.resources);
const state = useGameStore((s) => s.state);
const resources = state.resources;
const threshold = getPrestigeThreshold(state);
const progress = Math.min(resources / PRESTIGE_THRESHOLD, 1);
const progress = Math.min(resources / threshold, 1);
const progressPercent = (progress * 100).toFixed(1);
const remaining = Math.max(PRESTIGE_THRESHOLD - resources, 0);
const remaining = Math.max(threshold - resources, 0);
return (
<div className="gp gap-1">
<div className="flex justify-between">
<span className="gp-label">Prochaine Génération</span>
<span className="gp-label">
{formatNumber(resources)} / {formatNumber(PRESTIGE_THRESHOLD)}
{formatNumber(resources)} / {formatNumber(threshold)}
</span>
</div>
<div className="gp-progress">

View File

@@ -1,14 +1,19 @@
// PrestigePanel.tsx — Nouvelle Génération (prestige)
import { useGameStore } from "../store/useGameStore";
import { computePrestigeDna } from "../core/economy";
import { computePrestigeDna, getPrestigeDnaBonus, getPrestigeThreshold } from "../core/economy";
import { formatNumber } from "../utils/formatNumber";
export function PrestigePanel() {
const { lifetimeTadpoles } = useGameStore((s) => s.state);
const canPrestige = useGameStore((s) => s.canPrestige);
const prestige = useGameStore((s) => s.prestige);
const dnaPreview = computePrestigeDna(lifetimeTadpoles);
const state = useGameStore((s) => s.state);
const baseDna = computePrestigeDna(lifetimeTadpoles);
const dnaBonus = getPrestigeDnaBonus(state.evolutionTree);
const dnaPreview = Math.floor(baseDna * (1 + dnaBonus));
const threshold = getPrestigeThreshold(state);
const handlePrestige = () => {
const confirmed = window.confirm(
@@ -35,7 +40,7 @@ export function PrestigePanel() {
</button>
</div>
) : (
<span className="gp-label">Atteins 1M têtards pour prestige</span>
<span className="gp-label">Atteins {formatNumber(threshold)} têtards pour prestige</span>
)}
</div>
);