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:
@@ -5,6 +5,7 @@ import Navbar from "./components/navbar";
|
|||||||
import Footer from "./components/footer";
|
import Footer from "./components/footer";
|
||||||
import { GameTick } from "./components/GameTick";
|
import { GameTick } from "./components/GameTick";
|
||||||
import { GameSync } from "./components/GameSync";
|
import { GameSync } from "./components/GameSync";
|
||||||
|
import { OfflineReport } from "./components/OfflineReport";
|
||||||
|
|
||||||
import navData from "./data/NavBarData.json";
|
import navData from "./data/NavBarData.json";
|
||||||
|
|
||||||
@@ -15,6 +16,7 @@ function App() {
|
|||||||
<>
|
<>
|
||||||
<GameTick />
|
<GameTick />
|
||||||
<GameSync />
|
<GameSync />
|
||||||
|
<OfflineReport />
|
||||||
<Navbar
|
<Navbar
|
||||||
navData={navData}
|
navData={navData}
|
||||||
toggleRain={toggleRain}
|
toggleRain={toggleRain}
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import {
|
|||||||
getClickMultiplierFromTree,
|
getClickMultiplierFromTree,
|
||||||
getProductionMultiplierFromTree,
|
getProductionMultiplierFromTree,
|
||||||
getStartBonusFromTree,
|
getStartBonusFromTree,
|
||||||
|
offlineEfficiency,
|
||||||
|
computeOfflineGains,
|
||||||
DEFAULT_STATE,
|
DEFAULT_STATE,
|
||||||
DEFAULT_GENERATORS,
|
DEFAULT_GENERATORS,
|
||||||
DEFAULT_EVOLUTION_TREE,
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
65
Frontend/src/components/OfflineReport.tsx
Normal file
65
Frontend/src/components/OfflineReport.tsx
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
// OfflineReport.tsx — Écran "Pendant ton absence..." affiché au retour offline
|
||||||
|
|
||||||
|
import { useGameStore } from "../store/useGameStore";
|
||||||
|
import { formatNumber } from "../utils/formatNumber";
|
||||||
|
|
||||||
|
function formatDuration(ms: number): string {
|
||||||
|
const minutes = Math.floor(ms / 60_000);
|
||||||
|
if (minutes < 60) return `${minutes}min`;
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
const remainMinutes = minutes % 60;
|
||||||
|
return remainMinutes > 0 ? `${hours}h${remainMinutes}min` : `${hours}h`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function OfflineReport() {
|
||||||
|
const report = useGameStore((s) => s.offlineReport);
|
||||||
|
const dismiss = useGameStore((s) => s.dismissOfflineReport);
|
||||||
|
|
||||||
|
if (!report || !report.wasOffline) return null;
|
||||||
|
|
||||||
|
const effPercent = Math.round(report.efficiency * 100);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm">
|
||||||
|
<div className="gp max-w-sm w-full mx-4 text-center">
|
||||||
|
<span className="gp-title text-lg!">Pendant ton absence...</span>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-3 mt-2">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="gp-label">Durée</span>
|
||||||
|
<span className="gp-value">{formatDuration(report.duration)}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="gp-label">Efficacité marais</span>
|
||||||
|
<span className={`gp-value ${effPercent > 50 ? "gp-accent-green" : "gp-accent-amber"}`}>
|
||||||
|
{effPercent}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="gp-sep" />
|
||||||
|
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="gp-label">Têtards récoltés</span>
|
||||||
|
<span className="gp-value gp-accent-green text-lg!">
|
||||||
|
+{formatNumber(report.gains)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{report.efficiency < 0.5 && (
|
||||||
|
<p className="gp-label text-center">
|
||||||
|
Le marais s'endort sans toi... Joue activement pour maximiser ta production !
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={dismiss}
|
||||||
|
className="gp-btn gp-btn--buy w-full mt-3 py-2!"
|
||||||
|
>
|
||||||
|
Retour au marais
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -26,6 +26,7 @@ export interface GameState {
|
|||||||
clickMultiplier: number;
|
clickMultiplier: number;
|
||||||
generators: Generator[];
|
generators: Generator[];
|
||||||
lastTick: number; // timestamp ms — lazy calc reference
|
lastTick: number; // timestamp ms — lazy calc reference
|
||||||
|
lastOnline: number; // timestamp ms — dernière activité réelle (tick actif)
|
||||||
prestigeCount: number;
|
prestigeCount: number;
|
||||||
prestigeMultiplier: number; // legacy — conservé pour compat, remplacé par arbre
|
prestigeMultiplier: number; // legacy — conservé pour compat, remplacé par arbre
|
||||||
ancestralDna: number;
|
ancestralDna: number;
|
||||||
@@ -95,6 +96,52 @@ export function getStartBonusFromTree(tree: EvolutionNode[]): number {
|
|||||||
.reduce((sum, n) => sum + n.value, 0);
|
.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) ---
|
// --- Core economy (mis à jour pour intégrer l'arbre) ---
|
||||||
|
|
||||||
// Coût d'achat du N-ième générateur : baseCost × 1.15^owned
|
// 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,
|
ancestralDna: state.ancestralDna + dnaGained,
|
||||||
lifetimeTadpoles: 0,
|
lifetimeTadpoles: 0,
|
||||||
lastTick: Date.now(),
|
lastTick: Date.now(),
|
||||||
|
lastOnline: Date.now(),
|
||||||
// evolutionTree persiste — jamais reset
|
// evolutionTree persiste — jamais reset
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -201,6 +249,7 @@ export const DEFAULT_STATE: GameState = {
|
|||||||
clickMultiplier: 1,
|
clickMultiplier: 1,
|
||||||
generators: DEFAULT_GENERATORS,
|
generators: DEFAULT_GENERATORS,
|
||||||
lastTick: Date.now(),
|
lastTick: Date.now(),
|
||||||
|
lastOnline: Date.now(),
|
||||||
prestigeCount: 0,
|
prestigeCount: 0,
|
||||||
prestigeMultiplier: 1,
|
prestigeMultiplier: 1,
|
||||||
ancestralDna: 0,
|
ancestralDna: 0,
|
||||||
|
|||||||
@@ -14,18 +14,23 @@ import {
|
|||||||
canPrestige as canPrestigeCheck,
|
canPrestige as canPrestigeCheck,
|
||||||
totalProductionPerSecond,
|
totalProductionPerSecond,
|
||||||
generatorCost as genCost,
|
generatorCost as genCost,
|
||||||
|
computeOfflineGains,
|
||||||
|
offlineEfficiency,
|
||||||
} from "../core/economy";
|
} from "../core/economy";
|
||||||
|
|
||||||
const SAVE_KEY = "clickerz_state";
|
const SAVE_KEY = "clickerz_state";
|
||||||
|
const OFFLINE_THRESHOLD = 60_000; // 60s — same as economy.ts
|
||||||
|
|
||||||
function loadLocalState(): GameState {
|
function loadLocalState(): GameState {
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(SAVE_KEY);
|
const raw = localStorage.getItem(SAVE_KEY);
|
||||||
if (!raw) return { ...DEFAULT_STATE, lastTick: Date.now() };
|
if (!raw) return { ...DEFAULT_STATE, lastTick: Date.now(), lastOnline: Date.now() };
|
||||||
const saved = JSON.parse(raw) as GameState;
|
const saved = JSON.parse(raw) as GameState;
|
||||||
|
// Backfill lastOnline for old saves
|
||||||
|
if (!saved.lastOnline) saved.lastOnline = saved.lastTick;
|
||||||
return applyIdleGains(saved, Date.now());
|
return applyIdleGains(saved, Date.now());
|
||||||
} catch {
|
} catch {
|
||||||
return { ...DEFAULT_STATE, lastTick: Date.now() };
|
return { ...DEFAULT_STATE, lastTick: Date.now(), lastOnline: Date.now() };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,11 +38,22 @@ function saveLocal(state: GameState): void {
|
|||||||
localStorage.setItem(SAVE_KEY, JSON.stringify(state));
|
localStorage.setItem(SAVE_KEY, JSON.stringify(state));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OfflineReport {
|
||||||
|
wasOffline: boolean;
|
||||||
|
duration: number; // ms
|
||||||
|
gains: number;
|
||||||
|
efficiency: number; // 0-1 average
|
||||||
|
}
|
||||||
|
|
||||||
interface GameStore {
|
interface GameStore {
|
||||||
// State
|
// State
|
||||||
state: GameState;
|
state: GameState;
|
||||||
playSeconds: number;
|
playSeconds: number;
|
||||||
ready: boolean; // true once the authoritative state is loaded
|
ready: boolean;
|
||||||
|
|
||||||
|
// Offline report (shown once after load)
|
||||||
|
offlineReport: OfflineReport | null;
|
||||||
|
dismissOfflineReport: () => void;
|
||||||
|
|
||||||
// Derived (recalculated on tick)
|
// Derived (recalculated on tick)
|
||||||
canPrestige: boolean;
|
canPrestige: boolean;
|
||||||
@@ -51,24 +67,69 @@ interface GameStore {
|
|||||||
prestige: () => void;
|
prestige: () => void;
|
||||||
reset: () => void;
|
reset: () => void;
|
||||||
loadFromServer: (serverState: GameState) => void;
|
loadFromServer: (serverState: GameState) => void;
|
||||||
initGuest: () => void; // fallback when no server save (guest mode)
|
initGuest: () => void;
|
||||||
generatorCost: typeof genCost;
|
generatorCost: typeof genCost;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start with DEFAULT_STATE — game is NOT ready until loadFromServer or initGuest
|
function hydrateWithOffline(saved: GameState, now: number): { state: GameState; report: OfflineReport | null } {
|
||||||
const initialState = { ...DEFAULT_STATE, lastTick: Date.now() };
|
// Backfill lastOnline for old saves
|
||||||
|
if (!saved.lastOnline) saved.lastOnline = saved.lastTick;
|
||||||
|
|
||||||
|
const elapsed = now - saved.lastTick;
|
||||||
|
|
||||||
|
if (elapsed <= OFFLINE_THRESHOLD) {
|
||||||
|
// Normal idle — no offline report
|
||||||
|
const hydrated = applyIdleGains(saved, now);
|
||||||
|
return {
|
||||||
|
state: { ...hydrated, lastOnline: now },
|
||||||
|
report: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Offline — use degraded curve
|
||||||
|
const gains = computeOfflineGains(saved, now);
|
||||||
|
const pps = totalProductionPerSecond(saved);
|
||||||
|
const fullGains = pps * (elapsed / 1000);
|
||||||
|
const avgEfficiency = fullGains > 0 ? gains / fullGains : 0;
|
||||||
|
|
||||||
|
const hydrated: GameState = {
|
||||||
|
...saved,
|
||||||
|
resources: saved.resources + gains,
|
||||||
|
lifetimeTadpoles: saved.lifetimeTadpoles + gains,
|
||||||
|
lastTick: now,
|
||||||
|
lastOnline: now,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
state: hydrated,
|
||||||
|
report: {
|
||||||
|
wasOffline: true,
|
||||||
|
duration: elapsed,
|
||||||
|
gains,
|
||||||
|
efficiency: avgEfficiency,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState = { ...DEFAULT_STATE, lastTick: Date.now(), lastOnline: Date.now() };
|
||||||
|
|
||||||
export const useGameStore = create<GameStore>((set, get) => ({
|
export const useGameStore = create<GameStore>((set, get) => ({
|
||||||
state: initialState,
|
state: initialState,
|
||||||
playSeconds: 0,
|
playSeconds: 0,
|
||||||
ready: false,
|
ready: false,
|
||||||
|
offlineReport: null,
|
||||||
canPrestige: false,
|
canPrestige: false,
|
||||||
productionPerSecond: 0,
|
productionPerSecond: 0,
|
||||||
|
|
||||||
|
dismissOfflineReport: () => set({ offlineReport: null }),
|
||||||
|
|
||||||
tick: () => {
|
tick: () => {
|
||||||
if (!get().ready) return;
|
if (!get().ready) return;
|
||||||
|
const now = Date.now();
|
||||||
set((s) => {
|
set((s) => {
|
||||||
const updated = applyIdleGains(s.state, Date.now());
|
const updated = applyIdleGains(s.state, now);
|
||||||
|
// Mark as actively online
|
||||||
|
updated.lastOnline = now;
|
||||||
saveLocal(updated);
|
saveLocal(updated);
|
||||||
return {
|
return {
|
||||||
state: updated,
|
state: updated,
|
||||||
@@ -134,37 +195,40 @@ export const useGameStore = create<GameStore>((set, get) => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
reset: () => {
|
reset: () => {
|
||||||
const fresh = { ...DEFAULT_STATE, lastTick: Date.now() };
|
const fresh = { ...DEFAULT_STATE, lastTick: Date.now(), lastOnline: Date.now() };
|
||||||
saveLocal(fresh);
|
saveLocal(fresh);
|
||||||
set({
|
set({
|
||||||
state: fresh,
|
state: fresh,
|
||||||
playSeconds: 0,
|
playSeconds: 0,
|
||||||
ready: true,
|
ready: true,
|
||||||
|
offlineReport: null,
|
||||||
canPrestige: false,
|
canPrestige: false,
|
||||||
productionPerSecond: 0,
|
productionPerSecond: 0,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// Server save loaded — this IS the authority
|
|
||||||
loadFromServer: (serverState: GameState) => {
|
loadFromServer: (serverState: GameState) => {
|
||||||
const hydrated = applyIdleGains(serverState, Date.now());
|
const { state: hydrated, report } = hydrateWithOffline(serverState, Date.now());
|
||||||
saveLocal(hydrated); // mirror to localStorage as cache
|
saveLocal(hydrated);
|
||||||
set({
|
set({
|
||||||
state: hydrated,
|
state: hydrated,
|
||||||
ready: true,
|
ready: true,
|
||||||
|
offlineReport: report,
|
||||||
canPrestige: canPrestigeCheck(hydrated),
|
canPrestige: canPrestigeCheck(hydrated),
|
||||||
productionPerSecond: totalProductionPerSecond(hydrated),
|
productionPerSecond: totalProductionPerSecond(hydrated),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// Guest mode — no server save, use localStorage or fresh state
|
|
||||||
initGuest: () => {
|
initGuest: () => {
|
||||||
const local = loadLocalState();
|
const local = loadLocalState();
|
||||||
|
const { state: hydrated, report } = hydrateWithOffline(local, Date.now());
|
||||||
|
saveLocal(hydrated);
|
||||||
set({
|
set({
|
||||||
state: local,
|
state: hydrated,
|
||||||
ready: true,
|
ready: true,
|
||||||
canPrestige: canPrestigeCheck(local),
|
offlineReport: report,
|
||||||
productionPerSecond: totalProductionPerSecond(local),
|
canPrestige: canPrestigeCheck(hydrated),
|
||||||
|
productionPerSecond: totalProductionPerSecond(hydrated),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user