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

@@ -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) ---