import { describe, it, expect } from "vitest"; import { generatorCost, totalProductionPerSecond, computeIdleGains, applyClick, buyGenerator, applyPrestige, canPrestige, getPrestigeThreshold, computePrestigeDna, canBuyEvolutionNode, buyEvolutionNode, resetEvolutionTree, getClickMultiplierFromTree, getProductionMultiplierFromTree, getStartBonusFromTree, getPrestigeDnaBonus, getCostReduction, getAutoClicksPerSecond, offlineEfficiency, computeOfflineGains, DEFAULT_STATE, DEFAULT_GENERATORS, DEFAULT_EVOLUTION_TREE, } from "../core/economy"; // --- PrestigePanel visibility --- describe("PrestigePanel visibility (canPrestige guard)", () => { it("canPrestige = false pour resources = 0 → panneau non visible", () => { expect(canPrestige({ ...DEFAULT_STATE, resources: 0 })).toBe(false); }); it("canPrestige = false pour resources = 999 999 → panneau non visible", () => { expect(canPrestige({ ...DEFAULT_STATE, resources: 999_999 })).toBe(false); }); it("canPrestige = true pour resources = 1 000 000 → panneau visible", () => { expect(canPrestige({ ...DEFAULT_STATE, resources: 1_000_000 })).toBe(true); }); }); // --- Prestige reset --- describe("applyPrestige — post-prestige state", () => { const prestigeState = { ...DEFAULT_STATE, resources: 1_500_000, generators: DEFAULT_STATE.generators.map((g) => ({ ...g, owned: 3 })), prestigeCount: 0, prestigeMultiplier: 1, lifetimeTadpoles: 2_000_000_000, }; it("ressources = startBonus (0 sans nœud Mémoire Génétique) après prestige", () => { expect(applyPrestige(prestigeState).resources).toBe(0); }); it("multiplicateur = 1.1 après premier prestige", () => { expect(applyPrestige(prestigeState).prestigeMultiplier).toBeCloseTo(1.1); }); it("tous les générateurs owned = 0 après prestige", () => { const result = applyPrestige(prestigeState); expect(result.generators.every((g) => g.owned === 0)).toBe(true); }); it("prestigeCount incrémenté à 1 après premier prestige", () => { expect(applyPrestige(prestigeState).prestigeCount).toBe(1); }); it("gagne de l'ADN Ancestral au prestige", () => { const result = applyPrestige(prestigeState); const expectedDna = computePrestigeDna(2_000_000_000); expect(result.ancestralDna).toBe(expectedDna); expect(expectedDna).toBeGreaterThan(0); }); it("lifetimeTadpoles reset à 0 après prestige", () => { expect(applyPrestige(prestigeState).lifetimeTadpoles).toBe(0); }); it("arbre d'évolution persiste après prestige", () => { const stateWithNode = { ...prestigeState, evolutionTree: prestigeState.evolutionTree.map((n) => n.id === "ponte_amelioree" ? { ...n, unlocked: true } : n ), }; const result = applyPrestige(stateWithNode); expect(result.evolutionTree.find((n) => n.id === "ponte_amelioree")!.unlocked).toBe(true); }); it("startBonus appliqué si Mémoire Génétique débloquée", () => { const stateWithMemory = { ...prestigeState, evolutionTree: prestigeState.evolutionTree.map((n) => n.id === "memoire_genetique" ? { ...n, unlocked: true } : n ), }; const result = applyPrestige(stateWithMemory); expect(result.resources).toBe(100); }); }); // --- Generator cost --- describe("generatorCost", () => { it("retourne baseCost quand owned = 0", () => { const gen = { ...DEFAULT_GENERATORS[0], owned: 0 }; expect(generatorCost(gen)).toBe(gen.baseCost); }); it("applique la formule base × 1.15^n", () => { const gen = { ...DEFAULT_GENERATORS[0], owned: 2 }; expect(generatorCost(gen)).toBe(Math.floor(gen.baseCost * Math.pow(1.15, 2))); }); }); // --- Production --- describe("totalProductionPerSecond", () => { it("retourne 0 si aucun générateur acheté", () => { expect(totalProductionPerSecond(DEFAULT_STATE)).toBe(0); }); it("somme correctement la production de plusieurs générateurs", () => { const state = { ...DEFAULT_STATE, generators: DEFAULT_STATE.generators.map((g, i) => i === 0 ? { ...g, owned: 2 } : i === 1 ? { ...g, owned: 1 } : g ), }; const expected = (DEFAULT_GENERATORS[0].baseProduction * 2 + DEFAULT_GENERATORS[1].baseProduction * 1) * 1; expect(totalProductionPerSecond(state)).toBeCloseTo(expected); }); it("applique le multiplicateur de prestige", () => { const state = { ...DEFAULT_STATE, generators: DEFAULT_STATE.generators.map((g, i) => i === 0 ? { ...g, owned: 1 } : g), prestigeMultiplier: 1.5 }; expect(totalProductionPerSecond(state)).toBeCloseTo(DEFAULT_GENERATORS[0].baseProduction * 1.5); }); it("applique le multiplicateur arbre d'évolution", () => { const state = { ...DEFAULT_STATE, generators: DEFAULT_STATE.generators.map((g, i) => i === 0 ? { ...g, owned: 1 } : g), evolutionTree: DEFAULT_STATE.evolutionTree.map((n) => n.id === "instinct_gregaire" ? { ...n, unlocked: true } : n ), }; expect(totalProductionPerSecond(state)).toBeCloseTo(DEFAULT_GENERATORS[0].baseProduction * 1.5); }); }); // --- Idle gains --- describe("computeIdleGains (lazy calculation)", () => { it("calcule les gains proportionnellement au temps écoulé", () => { const state = { ...DEFAULT_STATE, generators: DEFAULT_STATE.generators.map((g, i) => i === 0 ? { ...g, owned: 10 } : g), lastTick: 0, }; const gains = computeIdleGains(state, 5000); // 5 secondes const expected = DEFAULT_GENERATORS[0].baseProduction * 10 * 5; expect(gains).toBeCloseTo(expected); }); it("retourne 0 si aucun temps écoulé", () => { const now = Date.now(); const state = { ...DEFAULT_STATE, lastTick: now }; expect(computeIdleGains(state, now)).toBeCloseTo(0); }); }); // --- Click (avec double + crit) --- describe("applyClick", () => { it("augmente les ressources du clickMultiplier × prestigeMultiplier", () => { const state = { ...DEFAULT_STATE, clickMultiplier: 3, prestigeMultiplier: 2 }; const result = applyClick(state, 0.99); // rng high → no double, no crit expect(result.state.resources).toBe(6); expect(result.isDouble).toBe(false); expect(result.isCrit).toBe(false); }); it("applique le multiplicateur click de l'arbre", () => { const state = { ...DEFAULT_STATE, clickMultiplier: 1, prestigeMultiplier: 1, evolutionTree: DEFAULT_STATE.evolutionTree.map((n) => n.id === "ponte_amelioree" ? { ...n, unlocked: true } : n ), }; const result = applyClick(state, 0.99); expect(result.state.resources).toBe(2); }); it("incrémente lifetimeTadpoles", () => { const state = { ...DEFAULT_STATE, clickMultiplier: 5, prestigeMultiplier: 1 }; const result = applyClick(state, 0.99); expect(result.state.lifetimeTadpoles).toBe(5); }); it("double ponte x2 quand rng < doubleClickChance", () => { const state = { ...DEFAULT_STATE, clickMultiplier: 1, prestigeMultiplier: 1, evolutionTree: DEFAULT_STATE.evolutionTree.map((n) => n.id === "double_ponte" ? { ...n, unlocked: true } : n ), }; // double_ponte = 10% chance, rng=0.05 < 0.10 → double const result = applyClick(state, 0.05); expect(result.isDouble).toBe(true); expect(result.gain).toBe(2); }); it("pas de double ponte quand rng > doubleClickChance", () => { const state = { ...DEFAULT_STATE, clickMultiplier: 1, evolutionTree: DEFAULT_STATE.evolutionTree.map((n) => n.id === "double_ponte" ? { ...n, unlocked: true } : n ), }; const result = applyClick(state, 0.50); expect(result.isDouble).toBe(false); expect(result.gain).toBe(1); }); it("crit x10 quand critRng < critClickChance", () => { const state = { ...DEFAULT_STATE, clickMultiplier: 1, prestigeMultiplier: 1, evolutionTree: DEFAULT_STATE.evolutionTree.map((n) => n.id === "ponte_critique" ? { ...n, unlocked: true } : n ), }; // ponte_critique = 5% chance, need critRng = (rng * 7.13) % 1 < 0.05 // rng = 0.007 → critRng = 0.04991 < 0.05 → crit! const result = applyClick(state, 0.007); expect(result.isCrit).toBe(true); expect(result.gain).toBe(10); }); }); // --- Buy generator --- describe("buyGenerator", () => { it("retourne null si fonds insuffisants", () => { const result = buyGenerator(DEFAULT_STATE, "nid"); expect(result).toBeNull(); }); it("achète correctement et déduit le coût", () => { const state = { ...DEFAULT_STATE, resources: 100 }; const result = buyGenerator(state, "nid"); expect(result).not.toBeNull(); expect(result!.generators.find((g) => g.id === "nid")!.owned).toBe(1); expect(result!.resources).toBe(100 - DEFAULT_GENERATORS[0].baseCost); }); }); // --- Prestige legacy --- describe("prestige", () => { it("canPrestige retourne false si < 1 000 000 ressources", () => { expect(canPrestige({ ...DEFAULT_STATE, resources: 999_999 })).toBe(false); }); it("canPrestige retourne true si ≥ 1 000 000 ressources", () => { expect(canPrestige({ ...DEFAULT_STATE, resources: 1_000_000 })).toBe(true); }); it("reset les ressources + générateurs + incrémente le multiplicateur", () => { const state = { ...DEFAULT_STATE, resources: 2_000_000, generators: DEFAULT_STATE.generators.map((g) => ({ ...g, owned: 5 })), prestigeCount: 0, }; const result = applyPrestige(state); expect(result.resources).toBe(0); expect(result.prestigeCount).toBe(1); expect(result.prestigeMultiplier).toBeCloseTo(1.1); expect(result.generators.every((g) => g.owned === 0)).toBe(true); }); }); // --- ADN Ancestral --- describe("computePrestigeDna", () => { it("retourne 0 pour 0 têtards", () => { expect(computePrestigeDna(0)).toBe(0); }); it("retourne 150 pour 1e9 têtards (sqrt(1) = 1)", () => { expect(computePrestigeDna(1e9)).toBe(150); }); it("retourne 212 pour 2e9 têtards (sqrt(2) ≈ 1.414)", () => { expect(computePrestigeDna(2e9)).toBe(Math.floor(150 * Math.sqrt(2))); }); it("scaling sub-linéaire — 10× têtards ≠ 10× ADN", () => { const dna1 = computePrestigeDna(1e9); const dna10 = computePrestigeDna(10e9); expect(dna10 / dna1).toBeCloseTo(Math.sqrt(10), 1); }); }); // --- 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("canBuyEvolutionNode", () => { 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", () => { 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, "double_ponte")).toBe(false); }); it("peut acheter un nœud si le prérequis est 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, "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); }); }); describe("buyEvolutionNode", () => { it("débloque le nœud et déduit l'ADN", () => { const state = { ...DEFAULT_STATE, ancestralDna: 5 }; const result = buyEvolutionNode(state, "ponte_amelioree"); expect(result).not.toBeNull(); expect(result!.ancestralDna).toBe(4); expect(result!.evolutionTree.find((n) => n.id === "ponte_amelioree")!.unlocked).toBe(true); }); it("retourne null si impossible", () => { 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 change rien si aucun nœud débloqué", () => { const result = resetEvolutionTree({ ...DEFAULT_STATE, ancestralDna: 10 }); expect(result.ancestralDna).toBe(10); }); }); describe("getClickMultiplierFromTree", () => { it("retourne 1 si aucun nœud click débloqué", () => { expect(getClickMultiplierFromTree(DEFAULT_EVOLUTION_TREE)).toBe(1); }); it("retourne 2 si Ponte Améliorée débloquée", () => { const tree = DEFAULT_EVOLUTION_TREE.map((n) => n.id === "ponte_amelioree" ? { ...n, unlocked: true } : n ); 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", () => { it("retourne 1 si aucun nœud production débloqué", () => { expect(getProductionMultiplierFromTree(DEFAULT_EVOLUTION_TREE)).toBe(1); }); it("retourne 1.5 si Instinct Grégaire débloqué", () => { const tree = DEFAULT_EVOLUTION_TREE.map((n) => n.id === "instinct_gregaire" ? { ...n, unlocked: true } : n ); expect(getProductionMultiplierFromTree(tree)).toBe(1.5); }); }); describe("getStartBonusFromTree", () => { it("retourne 0 si aucun nœud start débloqué", () => { expect(getStartBonusFromTree(DEFAULT_EVOLUTION_TREE)).toBe(0); }); it("retourne 100 si Mémoire Génétique débloquée", () => { const tree = DEFAULT_EVOLUTION_TREE.map((n) => n.id === "memoire_genetique" ? { ...n, unlocked: true } : n ); 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("unlock_generator (Résilience)", () => { it("prestige avec Résilience donne 1 Lac Mystique", () => { const state = { ...DEFAULT_STATE, resources: 2_000_000, generators: DEFAULT_STATE.generators.map((g) => ({ ...g, owned: 5 })), evolutionTree: DEFAULT_STATE.evolutionTree.map((n) => n.id === "resilience" ? { ...n, unlocked: true } : n ), }; const result = applyPrestige(state); const lac = result.generators.find((g) => g.id === "lac"); expect(lac!.owned).toBe(1); }); it("prestige sans Résilience donne 0 Lac Mystique", () => { const state = { ...DEFAULT_STATE, resources: 2_000_000, generators: DEFAULT_STATE.generators.map((g) => ({ ...g, owned: 5 })), }; const result = applyPrestige(state); const lac = result.generators.find((g) => g.id === "lac"); expect(lac!.owned).toBe(0); }); }); describe("auto_click (getAutoClicksPerSecond)", () => { it("retourne 0 si auto_ponte non débloqué", () => { expect(getAutoClicksPerSecond(DEFAULT_EVOLUTION_TREE)).toBe(0); }); it("retourne 1 si auto_ponte débloqué", () => { const tree = DEFAULT_EVOLUTION_TREE.map((n) => n.id === "auto_ponte" ? { ...n, unlocked: true } : n ); expect(getAutoClicksPerSecond(tree)).toBe(1); }); }); 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) --- describe("offlineEfficiency", () => { it("retourne 1.0 pour absence < 60s (pas offline)", () => { expect(offlineEfficiency(30_000)).toBe(1); }); it("retourne 1.0 pour absence de 10min (phase 100%)", () => { expect(offlineEfficiency(10 * 60_000)).toBe(1); }); it("retourne 1.0 à exactement 15min", () => { expect(offlineEfficiency(15 * 60_000)).toBe(1); }); it("retourne ~0.625 à 30min (milieu decay 1.0→0.25)", () => { const eff = offlineEfficiency(30 * 60_000); // 30min = 15min dans la phase decay (15min-1h = 45min total) // t = 15/45 = 0.333 → eff = 1 - 0.333 * 0.75 = 0.75 expect(eff).toBeCloseTo(0.75, 1); }); it("retourne 0.25 à exactement 1h (fin du decay)", () => { expect(offlineEfficiency(60 * 60_000)).toBeCloseTo(0.25); }); it("retourne ~0.125 à 1h30 (milieu 0.25→0)", () => { const eff = offlineEfficiency(90 * 60_000); expect(eff).toBeCloseTo(0.125, 1); }); it("retourne 0 à exactement 2h", () => { expect(offlineEfficiency(2 * 60 * 60_000)).toBe(0); }); it("retourne 0 après 2h (cap)", () => { expect(offlineEfficiency(5 * 60 * 60_000)).toBe(0); }); it("courbe monotone décroissante", () => { const points = [0, 10, 15, 30, 45, 60, 90, 120, 180].map( (min) => offlineEfficiency(min * 60_000) ); for (let i = 1; i < points.length; i++) { expect(points[i]).toBeLessThanOrEqual(points[i - 1]); } }); }); describe("computeOfflineGains", () => { const stateWithProd = { ...DEFAULT_STATE, generators: DEFAULT_STATE.generators.map((g, i) => i === 0 ? { ...g, owned: 10 } : g ), lastTick: 0, lastOnline: 0, }; const pps = DEFAULT_GENERATORS[0].baseProduction * 10; // 1/s it("gains normaux si absence < 60s", () => { const gains = computeOfflineGains(stateWithProd, 30_000); // < threshold → computeIdleGains classique expect(gains).toBeCloseTo(pps * 30); }); it("gains < idle pur pour absence de 1h", () => { const gains = computeOfflineGains(stateWithProd, 60 * 60_000); const fullIdleGains = pps * 3600; expect(gains).toBeLessThan(fullIdleGains); expect(gains).toBeGreaterThan(0); }); it("gains = 0 pour absence > 2h si prod constante", () => { // > 2h : tout tombe à 0%, mais les premières 2h produisent encore const gains = computeOfflineGains(stateWithProd, 3 * 60 * 60_000); const gainsAt2h = computeOfflineGains(stateWithProd, 2 * 60 * 60_000); // gains at 3h should equal gains at 2h (nothing added after 2h) expect(gains).toBeCloseTo(gainsAt2h, 0); }); it("retourne 0 si aucune production", () => { const gains = computeOfflineGains({ ...DEFAULT_STATE, lastTick: 0 }, 60 * 60_000); expect(gains).toBe(0); }); });