feat: offline gains — courbe inversée 2h, cap 25%, écran résumé
offlineEfficiency() : 100% (0-15min) → 25% (1h) → 0% (2h). computeOfflineGains() intègre la courbe par tranches de 1min. GameState.lastOnline ajouté, store hydrate avec offline report. OfflineReport.tsx affiché au retour si absence > 60s. 13 nouveaux tests (66 total, tous passent).
This commit is contained in:
@@ -13,6 +13,8 @@ import {
|
||||
getClickMultiplierFromTree,
|
||||
getProductionMultiplierFromTree,
|
||||
getStartBonusFromTree,
|
||||
offlineEfficiency,
|
||||
computeOfflineGains,
|
||||
DEFAULT_STATE,
|
||||
DEFAULT_GENERATORS,
|
||||
DEFAULT_EVOLUTION_TREE,
|
||||
@@ -364,3 +366,90 @@ describe("Evolution Tree", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// --- 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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user