feat: server-authoritative save — wait for server before game starts
Some checks failed
CI/CD — Build & Deploy / Build & Deploy (push) Failing after 18s

- Store starts in ready:false — no game actions until authority loaded
- useSaveSync signals serverLoaded (success, empty, or unreachable)
- GameSync orchestrates: logged in → wait server, guest → localStorage
- Home shows loading screen until ready
- localStorage = cache only, server = authority for logged-in users
This commit is contained in:
2026-03-24 14:30:53 +01:00
parent 3fc5e98069
commit 8ce54bfb03
4 changed files with 94 additions and 21 deletions

View File

@@ -1,4 +1,5 @@
// useGameStore.ts — Zustand store, source unique de l'état game
// Serveur = autorité. localStorage = fallback invité uniquement.
// Lazy calculation pattern : gains passifs calculés au read depuis lastTick
import { create } from "zustand";
@@ -17,7 +18,7 @@ import {
const SAVE_KEY = "clickerz_state";
function loadState(): GameState {
function loadLocalState(): GameState {
try {
const raw = localStorage.getItem(SAVE_KEY);
if (!raw) return { ...DEFAULT_STATE, lastTick: Date.now() };
@@ -28,7 +29,7 @@ function loadState(): GameState {
}
}
function saveState(state: GameState): void {
function saveLocal(state: GameState): void {
localStorage.setItem(SAVE_KEY, JSON.stringify(state));
}
@@ -36,6 +37,7 @@ interface GameStore {
// State
state: GameState;
playSeconds: number;
ready: boolean; // true once the authoritative state is loaded
// Derived (recalculated on tick)
canPrestige: boolean;
@@ -49,19 +51,25 @@ interface GameStore {
prestige: () => void;
reset: () => void;
loadFromServer: (serverState: GameState) => void;
initGuest: () => void; // fallback when no server save (guest mode)
generatorCost: typeof genCost;
}
// Start with DEFAULT_STATE — game is NOT ready until loadFromServer or initGuest
const initialState = { ...DEFAULT_STATE, lastTick: Date.now() };
export const useGameStore = create<GameStore>((set, get) => ({
state: loadState(),
state: initialState,
playSeconds: 0,
canPrestige: canPrestigeCheck(loadState()),
productionPerSecond: totalProductionPerSecond(loadState()),
ready: false,
canPrestige: false,
productionPerSecond: 0,
tick: () => {
if (!get().ready) return;
set((s) => {
const updated = applyIdleGains(s.state, Date.now());
saveState(updated);
saveLocal(updated);
return {
state: updated,
playSeconds: s.playSeconds + 1,
@@ -72,9 +80,10 @@ export const useGameStore = create<GameStore>((set, get) => ({
},
click: () => {
if (!get().ready) return;
set((s) => {
const updated = applyClick(applyIdleGains(s.state, Date.now()));
saveState(updated);
saveLocal(updated);
return {
state: updated,
canPrestige: canPrestigeCheck(updated),
@@ -84,11 +93,12 @@ export const useGameStore = create<GameStore>((set, get) => ({
},
buy: (genId: string) => {
if (!get().ready) return;
set((s) => {
const withIdle = applyIdleGains(s.state, Date.now());
const updated = buyGenerator(withIdle, genId);
if (!updated) return s;
saveState(updated);
saveLocal(updated);
return {
state: updated,
productionPerSecond: totalProductionPerSecond(updated),
@@ -97,10 +107,11 @@ export const useGameStore = create<GameStore>((set, get) => ({
},
buyNode: (nodeId: string) => {
if (!get().ready) return;
set((s) => {
const updated = buyEvolutionNode(s.state, nodeId);
if (!updated) return s;
saveState(updated);
saveLocal(updated);
return {
state: updated,
productionPerSecond: totalProductionPerSecond(updated),
@@ -109,10 +120,11 @@ export const useGameStore = create<GameStore>((set, get) => ({
},
prestige: () => {
if (!get().ready) return;
set((s) => {
if (!canPrestigeCheck(s.state)) return s;
const updated = applyPrestige(s.state);
saveState(updated);
saveLocal(updated);
return {
state: updated,
canPrestige: canPrestigeCheck(updated),
@@ -123,24 +135,38 @@ export const useGameStore = create<GameStore>((set, get) => ({
reset: () => {
const fresh = { ...DEFAULT_STATE, lastTick: Date.now() };
saveState(fresh);
saveLocal(fresh);
set({
state: fresh,
playSeconds: 0,
ready: true,
canPrestige: false,
productionPerSecond: 0,
});
},
// Server save loaded — this IS the authority
loadFromServer: (serverState: GameState) => {
const hydrated = applyIdleGains(serverState, Date.now());
saveState(hydrated);
saveLocal(hydrated); // mirror to localStorage as cache
set({
state: hydrated,
ready: true,
canPrestige: canPrestigeCheck(hydrated),
productionPerSecond: totalProductionPerSecond(hydrated),
});
},
// Guest mode — no server save, use localStorage or fresh state
initGuest: () => {
const local = loadLocalState();
set({
state: local,
ready: true,
canPrestige: canPrestigeCheck(local),
productionPerSecond: totalProductionPerSecond(local),
});
},
generatorCost: genCost,
}));