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