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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user