- double_click_chance + crit_click_chance câblés dans applyClick (RNG) - auto_click câblé dans le tick (auto-pontes/s) - unlock_generator (Résilience) → 1 Lac Mystique gratuit au prestige - ponte_critique requires double_ponte (fix branche morte) - achievement_scaling retiré (nœud absent), full_tree + symbiose fixés - Particule feedback coloré (crit=ambre, double=violet) - 99 tests (tous passent)
644 lines
22 KiB
TypeScript
644 lines
22 KiB
TypeScript
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);
|
||
});
|
||
});
|