diff --git a/Frontend/src/__tests__/economy.test.ts b/Frontend/src/__tests__/economy.test.ts index 17af8d0..9eea36d 100644 --- a/Frontend/src/__tests__/economy.test.ts +++ b/Frontend/src/__tests__/economy.test.ts @@ -7,12 +7,16 @@ import { buyGenerator, applyPrestige, canPrestige, + getPrestigeThreshold, computePrestigeDna, canBuyEvolutionNode, buyEvolutionNode, + resetEvolutionTree, getClickMultiplierFromTree, getProductionMultiplierFromTree, getStartBonusFromTree, + getPrestigeDnaBonus, + getCostReduction, offlineEfficiency, computeOfflineGains, DEFAULT_STATE, @@ -263,34 +267,33 @@ describe("computePrestigeDna", () => { }); }); -// --- Arbre d'Évolution --- +// --- Arbre d'Évolution 3 voies --- + +describe("Evolution Tree (3 branches)", () => { + it("arbre a 18 nœuds répartis en 3 branches", () => { + const ponte = DEFAULT_EVOLUTION_TREE.filter((n) => n.branch === "ponte"); + const marais = DEFAULT_EVOLUTION_TREE.filter((n) => n.branch === "marais"); + const adaptation = DEFAULT_EVOLUTION_TREE.filter((n) => n.branch === "adaptation"); + expect(ponte.length).toBe(6); + expect(marais.length).toBe(6); + expect(adaptation.length).toBe(6); + }); -describe("Evolution Tree", () => { describe("canBuyEvolutionNode", () => { - it("peut acheter le premier nœud (pas de prérequis) avec assez d'ADN", () => { + it("peut acheter un nœud racine avec assez d'ADN", () => { const state = { ...DEFAULT_STATE, ancestralDna: 5 }; expect(canBuyEvolutionNode(state, "ponte_amelioree")).toBe(true); + expect(canBuyEvolutionNode(state, "instinct_gregaire")).toBe(true); + expect(canBuyEvolutionNode(state, "memoire_genetique")).toBe(true); }); it("ne peut pas acheter sans assez d'ADN", () => { - const state = { ...DEFAULT_STATE, ancestralDna: 0 }; - expect(canBuyEvolutionNode(state, "ponte_amelioree")).toBe(false); - }); - - it("ne peut pas acheter un nœud déjà débloqué", () => { - const state = { - ...DEFAULT_STATE, - ancestralDna: 100, - evolutionTree: DEFAULT_STATE.evolutionTree.map((n) => - n.id === "ponte_amelioree" ? { ...n, unlocked: true } : n - ), - }; - expect(canBuyEvolutionNode(state, "ponte_amelioree")).toBe(false); + expect(canBuyEvolutionNode(DEFAULT_STATE, "ponte_amelioree")).toBe(false); }); it("ne peut pas acheter un nœud dont le prérequis n'est pas débloqué", () => { const state = { ...DEFAULT_STATE, ancestralDna: 100 }; - expect(canBuyEvolutionNode(state, "instinct_gregaire")).toBe(false); + expect(canBuyEvolutionNode(state, "double_ponte")).toBe(false); }); it("peut acheter un nœud si le prérequis est débloqué", () => { @@ -301,7 +304,32 @@ describe("Evolution Tree", () => { n.id === "ponte_amelioree" ? { ...n, unlocked: true } : n ), }; - expect(canBuyEvolutionNode(state, "instinct_gregaire")).toBe(true); + expect(canBuyEvolutionNode(state, "double_ponte")).toBe(true); + }); + + it("ne peut pas acheter un nœud exclusif si l'alternative est débloquée", () => { + const state = { + ...DEFAULT_STATE, + ancestralDna: 100, + evolutionTree: DEFAULT_STATE.evolutionTree.map((n) => + n.id === "double_ponte" || n.id === "ponte_amelioree" ? { ...n, unlocked: true } : + n.id === "ponte_frenetique" ? { ...n, unlocked: true } : n + ), + }; + // auto_ponte exclusive_with ponte_frenetique → locked + expect(canBuyEvolutionNode(state, "auto_ponte")).toBe(false); + }); + + it("peut acheter un nœud exclusif si l'alternative n'est pas débloquée", () => { + const state = { + ...DEFAULT_STATE, + ancestralDna: 100, + evolutionTree: DEFAULT_STATE.evolutionTree.map((n) => + n.id === "double_ponte" || n.id === "ponte_amelioree" ? { ...n, unlocked: true } : n + ), + }; + expect(canBuyEvolutionNode(state, "auto_ponte")).toBe(true); + expect(canBuyEvolutionNode(state, "ponte_frenetique")).toBe(true); }); }); @@ -315,15 +343,30 @@ describe("Evolution Tree", () => { }); it("retourne null si impossible", () => { - const result = buyEvolutionNode(DEFAULT_STATE, "ponte_amelioree"); - expect(result).toBeNull(); + expect(buyEvolutionNode(DEFAULT_STATE, "ponte_amelioree")).toBeNull(); + }); + }); + + describe("resetEvolutionTree", () => { + it("rembourse tout l'ADN dépensé et relock tous les nœuds", () => { + const state = { + ...DEFAULT_STATE, + ancestralDna: 50, + evolutionTree: DEFAULT_STATE.evolutionTree.map((n) => + n.id === "ponte_amelioree" || n.id === "instinct_gregaire" + ? { ...n, unlocked: true } + : n + ), + }; + // ponte_amelioree (1) + instinct_gregaire (1) = 2 ADN spent + const result = resetEvolutionTree(state); + expect(result.ancestralDna).toBe(52); + expect(result.evolutionTree.every((n) => !n.unlocked)).toBe(true); }); - it("ne modifie pas les autres nœuds", () => { - const state = { ...DEFAULT_STATE, ancestralDna: 5 }; - const result = buyEvolutionNode(state, "ponte_amelioree")!; - const otherNodes = result.evolutionTree.filter((n) => n.id !== "ponte_amelioree"); - expect(otherNodes.every((n) => n.unlocked === false)).toBe(true); + it("ne change rien si aucun nœud débloqué", () => { + const result = resetEvolutionTree({ ...DEFAULT_STATE, ancestralDna: 10 }); + expect(result.ancestralDna).toBe(10); }); }); @@ -338,6 +381,13 @@ describe("Evolution Tree", () => { ); expect(getClickMultiplierFromTree(tree)).toBe(2); }); + + it("multiplie si plusieurs nœuds click débloqués (2 × 3 = 6)", () => { + const tree = DEFAULT_EVOLUTION_TREE.map((n) => + n.id === "ponte_amelioree" || n.id === "ponte_frenetique" ? { ...n, unlocked: true } : n + ); + expect(getClickMultiplierFromTree(tree)).toBe(6); + }); }); describe("getProductionMultiplierFromTree", () => { @@ -365,6 +415,57 @@ describe("Evolution Tree", () => { expect(getStartBonusFromTree(tree)).toBe(100); }); }); + + describe("prestige_dna_bonus", () => { + it("ADN Renforcé + Héritage = +75% ADN", () => { + const tree = DEFAULT_EVOLUTION_TREE.map((n) => + n.id === "adn_renforce" || n.id === "heritage" ? { ...n, unlocked: true } : n + ); + expect(getPrestigeDnaBonus(tree)).toBeCloseTo(0.75); + }); + }); + + describe("cost_reduction", () => { + it("Marée Haute = -20% coût générateurs", () => { + const tree = DEFAULT_EVOLUTION_TREE.map((n) => + n.id === "maree_haute" ? { ...n, unlocked: true } : n + ); + expect(getCostReduction(tree)).toBeCloseTo(0.20); + }); + + it("coût réduit appliqué via generatorCost", () => { + const tree = DEFAULT_EVOLUTION_TREE.map((n) => + n.id === "maree_haute" ? { ...n, unlocked: true } : n + ); + const gen = { ...DEFAULT_GENERATORS[0], owned: 0 }; + const baseCost = generatorCost(gen); + const reducedCost = generatorCost(gen, tree); + expect(reducedCost).toBe(Math.floor(baseCost * 0.8)); + }); + }); + + describe("prestige threshold reduction", () => { + it("Transcendance réduit le seuil de 50%", () => { + const state = { + ...DEFAULT_STATE, + evolutionTree: DEFAULT_STATE.evolutionTree.map((n) => + n.id === "transcendance" ? { ...n, unlocked: true } : n + ), + }; + expect(getPrestigeThreshold(state)).toBe(500_000); + }); + + it("canPrestige utilise le seuil réduit", () => { + const state = { + ...DEFAULT_STATE, + resources: 600_000, + evolutionTree: DEFAULT_STATE.evolutionTree.map((n) => + n.id === "transcendance" ? { ...n, unlocked: true } : n + ), + }; + expect(canPrestige(state)).toBe(true); + }); + }); }); // --- Offline gains (courbe inversée) --- diff --git a/Frontend/src/components/EvolutionTree.tsx b/Frontend/src/components/EvolutionTree.tsx index 8d2a99d..5451ac2 100644 --- a/Frontend/src/components/EvolutionTree.tsx +++ b/Frontend/src/components/EvolutionTree.tsx @@ -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> = { click_multiplier: (v) => `x${v} ponte`, @@ -10,19 +10,37 @@ const EFFECT_LABELS: Record 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 = { + 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 (
- {node.name} - {EFFECT_LABELS[node.effect](node.value)} +
+ {node.name} + {node.exclusive_with && !node.unlocked && !isExcluded && ( + OU + )} +
+ {EFFECT_LABELS[node.effect]?.(node.value) ?? node.effect}
{node.unlocked ? ( OK + ) : isExcluded ? ( + verrouillé ) : ( )}
); } -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 ( -
-
- Évolution - {state.ancestralDna} ADN -
- {evolutionTree.map((node) => ( - buyNode(node.id)} - /> - ))} +
+ {config.label} + {nodes.map((node) => { + const isExcluded = node.exclusive_with + ? state.evolutionTree.find((n) => n.id === node.exclusive_with)?.unlocked ?? false + : false; + return ( + buyNode(node.id)} + /> + ); + })} +
+ ); +} + +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 ( +
+
+ Évolution +
+ {ancestralDna} ADN + {hasUnlocked && ( + + )} +
+
+
+ + + +
); } diff --git a/Frontend/src/components/GeneratorShop.tsx b/Frontend/src/components/GeneratorShop.tsx index 70bfa74..40dbc16 100644 --- a/Frontend/src/components/GeneratorShop.tsx +++ b/Frontend/src/components/GeneratorShop.tsx @@ -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 (
diff --git a/Frontend/src/components/MilestoneBar.tsx b/Frontend/src/components/MilestoneBar.tsx index ef7ca62..a8eecdb 100644 --- a/Frontend/src/components/MilestoneBar.tsx +++ b/Frontend/src/components/MilestoneBar.tsx @@ -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 (
Prochaine Génération - {formatNumber(resources)} / {formatNumber(PRESTIGE_THRESHOLD)} + {formatNumber(resources)} / {formatNumber(threshold)}
diff --git a/Frontend/src/components/PrestigePanel.tsx b/Frontend/src/components/PrestigePanel.tsx index 8a0c2ea..ed0bb03 100644 --- a/Frontend/src/components/PrestigePanel.tsx +++ b/Frontend/src/components/PrestigePanel.tsx @@ -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() {
) : ( - Atteins 1M têtards pour prestige + Atteins {formatNumber(threshold)} têtards pour prestige )}
); diff --git a/Frontend/src/core/economy.ts b/Frontend/src/core/economy.ts index 3dd3c1e..a9580de 100644 --- a/Frontend/src/core/economy.ts +++ b/Frontend/src/core/economy.ts @@ -9,16 +9,33 @@ export interface Generator { owned: number; } -export type EffectType = "click_multiplier" | "production_multiplier" | "start_bonus" | "unlock_generator" | "achievement_scaling"; +export type EffectType = + | "click_multiplier" + | "production_multiplier" + | "start_bonus" + | "unlock_generator" + | "achievement_scaling" + | "double_click_chance" + | "auto_click" + | "crit_click_chance" + | "generator_boost" + | "cost_reduction" + | "prestige_dna_bonus" + | "offline_boost" + | "prestige_threshold_reduction"; + +export type Branch = "ponte" | "marais" | "adaptation"; export interface EvolutionNode { id: string; name: string; - cost: number; // en ADN Ancestral + cost: number; // en ADN Ancestral effect: EffectType; value: number; unlocked: boolean; - requires: string | null; // id du nœud prérequis (null = racine) + requires: string | null; // id du nœud prérequis (null = racine) + branch: Branch; + exclusive_with?: string; // id du nœud alternatif (pick one) } export interface GameState { @@ -37,11 +54,29 @@ export interface GameState { // --- Arbre d'Évolution --- export const DEFAULT_EVOLUTION_TREE: EvolutionNode[] = [ - { id: "ponte_amelioree", name: "Ponte Améliorée", cost: 1, effect: "click_multiplier", value: 2, unlocked: false, requires: null }, - { id: "instinct_gregaire", name: "Instinct Grégaire", cost: 3, effect: "production_multiplier", value: 1.5, unlocked: false, requires: "ponte_amelioree" }, - { id: "memoire_genetique", name: "Mémoire Génétique", cost: 10, effect: "start_bonus", value: 100, unlocked: false, requires: "instinct_gregaire" }, - { id: "mutation_alpha", name: "Mutation Alpha", cost: 25, effect: "unlock_generator", value: 0, unlocked: false, requires: "memoire_genetique" }, - { id: "symbiose", name: "Symbiose", cost: 50, effect: "achievement_scaling", value: 0.01, unlocked: false, requires: "mutation_alpha" }, + // --- Ponte (click) --- + { id: "ponte_amelioree", name: "Ponte Améliorée", cost: 1, effect: "click_multiplier", value: 2, unlocked: false, requires: null, branch: "ponte" }, + { id: "double_ponte", name: "Double Ponte", cost: 3, effect: "double_click_chance", value: 0.10, unlocked: false, requires: "ponte_amelioree", branch: "ponte" }, + { id: "ponte_frenetique", name: "Ponte Frénétique", cost: 8, effect: "click_multiplier", value: 3, unlocked: false, requires: "double_ponte", branch: "ponte", exclusive_with: "auto_ponte" }, + { id: "auto_ponte", name: "Auto-Ponte", cost: 8, effect: "auto_click", value: 1, unlocked: false, requires: "double_ponte", branch: "ponte", exclusive_with: "ponte_frenetique" }, + { id: "ponte_critique", name: "Ponte Critique", cost: 20, effect: "crit_click_chance", value: 0.05, unlocked: false, requires: "ponte_frenetique", branch: "ponte" }, + { id: "maitre_pondeur", name: "Maître Pondeur", cost: 40, effect: "click_multiplier", value: 5, unlocked: false, requires: "ponte_critique", branch: "ponte" }, + + // --- Marais (production) --- + { id: "instinct_gregaire", name: "Instinct Grégaire", cost: 1, effect: "production_multiplier", value: 1.5, unlocked: false, requires: null, branch: "marais" }, + { id: "symbiose_algale", name: "Symbiose Algale", cost: 3, effect: "generator_boost", value: 2, unlocked: false, requires: "instinct_gregaire", branch: "marais" }, + { id: "courant_profond", name: "Courant Profond", cost: 8, effect: "production_multiplier", value: 2, unlocked: false, requires: "symbiose_algale", branch: "marais", exclusive_with: "maree_haute" }, + { id: "maree_haute", name: "Marée Haute", cost: 8, effect: "cost_reduction", value: 0.20, unlocked: false, requires: "symbiose_algale", branch: "marais", exclusive_with: "courant_profond" }, + { id: "ecosysteme_mature", name: "Écosystème Mature", cost: 20, effect: "production_multiplier", value: 3, unlocked: false, requires: "courant_profond", branch: "marais" }, + { id: "marais_eternel", name: "Marais Éternel", cost: 40, effect: "production_multiplier", value: 5, unlocked: false, requires: "ecosysteme_mature", branch: "marais" }, + + // --- Adaptation (utility) --- + { id: "memoire_genetique", name: "Mémoire Génétique", cost: 1, effect: "start_bonus", value: 100, unlocked: false, requires: null, branch: "adaptation" }, + { id: "adn_renforce", name: "ADN Renforcé", cost: 3, effect: "prestige_dna_bonus", value: 0.25, unlocked: false, requires: "memoire_genetique", branch: "adaptation" }, + { id: "eveil_rapide", name: "Éveil Rapide", cost: 8, effect: "offline_boost", value: 0.50, unlocked: false, requires: "adn_renforce", branch: "adaptation", exclusive_with: "resilience" }, + { id: "resilience", name: "Résilience", cost: 8, effect: "unlock_generator", value: 0, unlocked: false, requires: "adn_renforce", branch: "adaptation", exclusive_with: "eveil_rapide" }, + { id: "heritage", name: "Héritage", cost: 20, effect: "prestige_dna_bonus", value: 0.50, unlocked: false, requires: "eveil_rapide", branch: "adaptation" }, + { id: "transcendance", name: "Transcendance", cost: 40, effect: "prestige_threshold_reduction", value: 0.50, unlocked: false, requires: "heritage", branch: "adaptation" }, ]; // Calcule l'ADN gagné lors d'un prestige : floor(150 × sqrt(lifetime / 1e9)) @@ -58,6 +93,11 @@ export function canBuyEvolutionNode(state: GameState, nodeId: string): boolean { const prereq = state.evolutionTree.find((n) => n.id === node.requires); if (!prereq || !prereq.unlocked) return false; } + // Exclusive node: can't buy if the alternative is already unlocked + if (node.exclusive_with) { + const alt = state.evolutionTree.find((n) => n.id === node.exclusive_with); + if (alt && alt.unlocked) return false; + } return true; } @@ -75,6 +115,24 @@ export function buyEvolutionNode(state: GameState, nodeId: string): GameState | }; } +// Reset l'arbre — rembourse tout l'ADN dépensé, relock tous les nœuds +export function resetEvolutionTree(state: GameState): GameState { + const spentDna = state.evolutionTree + .filter((n) => n.unlocked) + .reduce((sum, n) => sum + n.cost, 0); + + return { + ...state, + ancestralDna: state.ancestralDna + spentDna, + evolutionTree: state.evolutionTree.map((n) => ({ ...n, unlocked: false })), + }; +} + +// Compte l'ADN total investi dans l'arbre +export function getSpentDna(tree: EvolutionNode[]): number { + return tree.filter((n) => n.unlocked).reduce((sum, n) => sum + n.cost, 0); +} + // Calcule le multiplicateur click total depuis l'arbre export function getClickMultiplierFromTree(tree: EvolutionNode[]): number { return tree @@ -96,6 +154,62 @@ export function getStartBonusFromTree(tree: EvolutionNode[]): number { .reduce((sum, n) => sum + n.value, 0); } +// Chance de double click (0-1) +export function getDoubleClickChance(tree: EvolutionNode[]): number { + return tree + .filter((n) => n.unlocked && n.effect === "double_click_chance") + .reduce((sum, n) => sum + n.value, 0); +} + +// Auto-clicks par seconde depuis l'arbre +export function getAutoClicksPerSecond(tree: EvolutionNode[]): number { + return tree + .filter((n) => n.unlocked && n.effect === "auto_click") + .reduce((sum, n) => sum + n.value, 0); +} + +// Chance de crit click (0-1), crit = x10 +export function getCritClickChance(tree: EvolutionNode[]): number { + return tree + .filter((n) => n.unlocked && n.effect === "crit_click_chance") + .reduce((sum, n) => sum + n.value, 0); +} + +// Multiplicateur boost sur Nid (generator_boost) +export function getGeneratorBoostFromTree(tree: EvolutionNode[]): number { + return tree + .filter((n) => n.unlocked && n.effect === "generator_boost") + .reduce((mult, n) => mult * n.value, 1); +} + +// Réduction de coût générateurs (0-1) +export function getCostReduction(tree: EvolutionNode[]): number { + return tree + .filter((n) => n.unlocked && n.effect === "cost_reduction") + .reduce((sum, n) => sum + n.value, 0); +} + +// Bonus ADN prestige (additif, ex: 0.25 = +25%) +export function getPrestigeDnaBonus(tree: EvolutionNode[]): number { + return tree + .filter((n) => n.unlocked && n.effect === "prestige_dna_bonus") + .reduce((sum, n) => sum + n.value, 0); +} + +// Boost offline (additif, ex: 0.50 = +50% efficacité offline) +export function getOfflineBoost(tree: EvolutionNode[]): number { + return tree + .filter((n) => n.unlocked && n.effect === "offline_boost") + .reduce((sum, n) => sum + n.value, 0); +} + +// Réduction seuil prestige (multiplicatif, ex: 0.50 = seuil divisé par 2) +export function getPrestigeThresholdReduction(tree: EvolutionNode[]): number { + return tree + .filter((n) => n.unlocked && n.effect === "prestige_threshold_reduction") + .reduce((sum, n) => sum + n.value, 0); +} + // --- Offline gains (courbe inversée) --- const OFFLINE_THRESHOLD = 60_000; // 60s — en-dessous = idle normal, au-dessus = offline @@ -131,6 +245,8 @@ export function computeOfflineGains(state: GameState, now: number): number { const pps = totalProductionPerSecond(state); if (pps <= 0) return 0; + const offlineBoost = 1 + getOfflineBoost(state.evolutionTree); + // Intégration par tranches de 60s const STEP = 60_000; let total = 0; @@ -139,20 +255,27 @@ export function computeOfflineGains(state: GameState, now: number): number { const eff = offlineEfficiency(t + chunk / 2); // milieu de la tranche total += pps * (chunk / 1000) * eff; } - return total; + return total * offlineBoost; } // --- Core economy (mis à jour pour intégrer l'arbre) --- -// Coût d'achat du N-ième générateur : baseCost × 1.15^owned -export function generatorCost(gen: Generator): number { - return Math.floor(gen.baseCost * Math.pow(1.15, gen.owned)); +// Coût d'achat du N-ième générateur : baseCost × 1.15^owned × (1 - costReduction) +export function generatorCost(gen: Generator, tree?: EvolutionNode[]): number { + const base = Math.floor(gen.baseCost * Math.pow(1.15, gen.owned)); + if (!tree) return base; + const reduction = getCostReduction(tree); + return Math.max(1, Math.floor(base * (1 - reduction))); } // Production totale par seconde de tous les générateurs export function totalProductionPerSecond(state: GameState): number { + const nidBoost = getGeneratorBoostFromTree(state.evolutionTree); const base = state.generators.reduce( - (sum, gen) => sum + gen.baseProduction * gen.owned, + (sum, gen) => { + const boost = gen.id === "nid" ? nidBoost : 1; + return sum + gen.baseProduction * gen.owned * boost; + }, 0 ); const treeMultiplier = getProductionMultiplierFromTree(state.evolutionTree); @@ -198,7 +321,7 @@ export function buyGenerator(state: GameState, genId: string): GameState | null if (genIndex === -1) return null; const gen = state.generators[genIndex]; - const cost = generatorCost(gen); + const cost = generatorCost(gen, state.evolutionTree); if (state.resources < cost) return null; const updatedGenerators = [...state.generators]; @@ -212,13 +335,22 @@ export function buyGenerator(state: GameState, genId: string): GameState | null } // Prestige : reset run, gain ADN, arbre persiste +const BASE_PRESTIGE_THRESHOLD = 1_000_000; + +export function getPrestigeThreshold(state: GameState): number { + const reduction = getPrestigeThresholdReduction(state.evolutionTree); + return Math.floor(BASE_PRESTIGE_THRESHOLD * (1 - reduction)); +} + export function canPrestige(state: GameState): boolean { - return state.resources >= 1_000_000; + return state.resources >= getPrestigeThreshold(state); } export function applyPrestige(state: GameState): GameState { const newPrestigeCount = state.prestigeCount + 1; - const dnaGained = computePrestigeDna(state.lifetimeTadpoles); + const dnaBonus = getPrestigeDnaBonus(state.evolutionTree); + const baseDna = computePrestigeDna(state.lifetimeTadpoles); + const dnaGained = Math.floor(baseDna * (1 + dnaBonus)); const startBonus = getStartBonusFromTree(state.evolutionTree); return { diff --git a/Frontend/src/store/useGameStore.ts b/Frontend/src/store/useGameStore.ts index 59d87aa..596710d 100644 --- a/Frontend/src/store/useGameStore.ts +++ b/Frontend/src/store/useGameStore.ts @@ -10,6 +10,7 @@ import { applyClick, buyGenerator, buyEvolutionNode, + resetEvolutionTree, applyPrestige, canPrestige as canPrestigeCheck, totalProductionPerSecond, @@ -65,10 +66,12 @@ interface GameStore { buy: (genId: string) => void; buyNode: (nodeId: string) => void; prestige: () => void; + resetTree: () => void; reset: () => void; loadFromServer: (serverState: GameState) => void; initGuest: () => void; generatorCost: typeof genCost; + generatorCostWithTree: (gen: Parameters[0]) => number; } function hydrateWithOffline(saved: GameState, now: number): { state: GameState; report: OfflineReport | null } { @@ -194,6 +197,18 @@ export const useGameStore = create((set, get) => ({ }); }, + resetTree: () => { + if (!get().ready) return; + set((s) => { + const updated = resetEvolutionTree(s.state); + saveLocal(updated); + return { + state: updated, + productionPerSecond: totalProductionPerSecond(updated), + }; + }); + }, + reset: () => { const fresh = { ...DEFAULT_STATE, lastTick: Date.now(), lastOnline: Date.now() }; saveLocal(fresh); @@ -233,4 +248,5 @@ export const useGameStore = create((set, get) => ({ }, generatorCost: genCost, + generatorCostWithTree: (gen) => genCost(gen, get().state.evolutionTree), }));