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:
2026-03-28 11:44:59 +01:00
parent 90761b3e13
commit 3ba10dad5f
5 changed files with 284 additions and 15 deletions

View File

@@ -26,6 +26,7 @@ export interface GameState {
clickMultiplier: number;
generators: Generator[];
lastTick: number; // timestamp ms — lazy calc reference
lastOnline: number; // timestamp ms — dernière activité réelle (tick actif)
prestigeCount: number;
prestigeMultiplier: number; // legacy — conservé pour compat, remplacé par arbre
ancestralDna: number;
@@ -95,6 +96,52 @@ export function getStartBonusFromTree(tree: EvolutionNode[]): number {
.reduce((sum, n) => sum + n.value, 0);
}
// --- Offline gains (courbe inversée) ---
const OFFLINE_THRESHOLD = 60_000; // 60s — en-dessous = idle normal, au-dessus = offline
const OFFLINE_FULL_MS = 15 * 60_000; // 0-15min : 100%
const OFFLINE_DECAY_END_MS = 60 * 60_000; // 15min-1h : 100% → 25%
const OFFLINE_ZERO_MS = 2 * 60 * 60_000; // 1h-2h : 25% → 0%
const OFFLINE_FLOOR = 0.25; // plancher de la phase de decay
// Retourne le multiplicateur d'efficacité offline (1.0 → 0.0)
// basé sur le temps d'absence en ms
export function offlineEfficiency(elapsedMs: number): number {
if (elapsedMs <= OFFLINE_THRESHOLD) return 1; // pas offline
if (elapsedMs <= OFFLINE_FULL_MS) return 1; // 0-15min : 100%
if (elapsedMs <= OFFLINE_DECAY_END_MS) {
// 15min-1h : linéaire 1.0 → 0.25
const t = (elapsedMs - OFFLINE_FULL_MS) / (OFFLINE_DECAY_END_MS - OFFLINE_FULL_MS);
return 1 - t * (1 - OFFLINE_FLOOR);
}
if (elapsedMs <= OFFLINE_ZERO_MS) {
// 1h-2h : linéaire 0.25 → 0.0
const t = (elapsedMs - OFFLINE_DECAY_END_MS) / (OFFLINE_ZERO_MS - OFFLINE_DECAY_END_MS);
return OFFLINE_FLOOR * (1 - t);
}
return 0; // >2h : rien
}
// Calcule les gains offline avec la courbe dégressive
// Intègre la courbe par tranches de 1 minute pour plus de précision
export function computeOfflineGains(state: GameState, now: number): number {
const elapsed = now - state.lastTick;
if (elapsed <= OFFLINE_THRESHOLD) return computeIdleGains(state, now);
const pps = totalProductionPerSecond(state);
if (pps <= 0) return 0;
// Intégration par tranches de 60s
const STEP = 60_000;
let total = 0;
for (let t = 0; t < elapsed; t += STEP) {
const chunk = Math.min(STEP, elapsed - t);
const eff = offlineEfficiency(t + chunk / 2); // milieu de la tranche
total += pps * (chunk / 1000) * eff;
}
return total;
}
// --- Core economy (mis à jour pour intégrer l'arbre) ---
// Coût d'achat du N-ième générateur : baseCost × 1.15^owned
@@ -183,6 +230,7 @@ export function applyPrestige(state: GameState): GameState {
ancestralDna: state.ancestralDna + dnaGained,
lifetimeTadpoles: 0,
lastTick: Date.now(),
lastOnline: Date.now(),
// evolutionTree persiste — jamais reset
};
}
@@ -201,6 +249,7 @@ export const DEFAULT_STATE: GameState = {
clickMultiplier: 1,
generators: DEFAULT_GENERATORS,
lastTick: Date.now(),
lastOnline: Date.now(),
prestigeCount: 0,
prestigeMultiplier: 1,
ancestralDna: 0,