Files
ClickerZ/Frontend/src/__tests__/economy.test.ts
Tetardtek 2c924c1e4a fix: câbler tous les effets arbre + cleanup dette Sprint 2
- 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)
2026-03-28 12:41:12 +01:00

644 lines
22 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
});
});