diff --git a/Frontend/src/components/GameSync.tsx b/Frontend/src/components/GameSync.tsx index 84edfc7..8342e88 100644 --- a/Frontend/src/components/GameSync.tsx +++ b/Frontend/src/components/GameSync.tsx @@ -1,22 +1,47 @@ // GameSync.tsx — Bridge useSaveSync ↔ Zustand store -// Monter une seule fois dans App. Silencieux en mode invité (pas de token). +// Serveur = autorité. Attend la save serveur avant de rendre le jeu jouable. +// Guest mode (pas connecté) : init depuis localStorage immédiatement. -import { useCallback } from "react"; +import { useCallback, useEffect, useRef } from "react"; import { useGameStore } from "../store/useGameStore"; +import { useAuth } from "../context/AuthContext"; import { useSaveSync } from "../hooks/useSaveSync"; export function GameSync() { const state = useGameStore((s) => s.state); + const ready = useGameStore((s) => s.ready); const loadFromServer = useGameStore((s) => s.loadFromServer); + const initGuest = useGameStore((s) => s.initGuest); const playSeconds = useGameStore((s) => s.playSeconds); + const { user, loading: authLoading } = useAuth(); + const initDone = useRef(false); const getGameState = useCallback(() => state, [state]); - useSaveSync({ + const { serverLoaded } = useSaveSync({ getGameState, onLoad: loadFromServer, playTimeSeconds: playSeconds, }); + // Once auth resolves: if no user or no server save → init guest + useEffect(() => { + if (authLoading || initDone.current || ready) return; + + // Not logged in → guest mode immediately + if (!user) { + initDone.current = true; + initGuest(); + return; + } + + // Logged in but server save loaded (or confirmed empty) → useSaveSync handles it + // If serverLoaded is true and store isn't ready yet, it means server had no save + if (serverLoaded && !ready) { + initDone.current = true; + initGuest(); // use localStorage as starting point, server will save it on next sync + } + }, [authLoading, user, serverLoaded, ready, initGuest]); + return null; } diff --git a/Frontend/src/hooks/useSaveSync.ts b/Frontend/src/hooks/useSaveSync.ts index 52e70a2..0114d41 100644 --- a/Frontend/src/hooks/useSaveSync.ts +++ b/Frontend/src/hooks/useSaveSync.ts @@ -1,7 +1,7 @@ // useSaveSync.ts — Auto-save game state to backend every 30s -// Cookie-based auth — credentials sent automatically +// Serveur = autorité. Cookie-based auth — credentials sent automatically. -import { useEffect, useRef, useCallback } from "react"; +import { useEffect, useRef, useCallback, useState } from "react"; import { useAuth } from "../context/AuthContext"; import type { GameState } from "../core/economy"; @@ -37,18 +37,29 @@ export function useSaveSync({ getGameState, onLoad, playTimeSeconds }: SaveSyncO const { user } = useAuth(); const lastSaveRef = useRef(null); const loadedRef = useRef(false); + const [serverLoaded, setServerLoaded] = useState(false); - // Load save on mount (once) + // Load save from server on mount (once, only if logged in) useEffect(() => { - if (loadedRef.current || !user) return; + if (loadedRef.current || !user) { + // Not logged in → signal immediately + if (!user) setServerLoaded(true); + return; + } loadedRef.current = true; apiRequest("/save").then((data) => { if (data?.gameState) { onLoad(data.gameState); lastSaveRef.current = data.lastSave; - console.info("[SaveSync] Loaded save from server"); + console.info("[SaveSync] Loaded save from server — server is authority"); + } else { + console.info("[SaveSync] No server save found — starting fresh"); } + setServerLoaded(true); + }).catch(() => { + console.warn("[SaveSync] Server unreachable — falling back to local"); + setServerLoaded(true); }); }, [onLoad, user]); @@ -99,5 +110,5 @@ export function useSaveSync({ getGameState, onLoad, playTimeSeconds }: SaveSyncO return () => window.removeEventListener("beforeunload", handleUnload); }, [getGameState, playTimeSeconds, user]); - return { saveToServer, lastSave: lastSaveRef.current }; + return { saveToServer, lastSave: lastSaveRef.current, serverLoaded }; } diff --git a/Frontend/src/pages/Home.jsx b/Frontend/src/pages/Home.jsx index 97f714c..6d7980a 100755 --- a/Frontend/src/pages/Home.jsx +++ b/Frontend/src/pages/Home.jsx @@ -16,11 +16,22 @@ import "../scss/components/game-panels.scss"; export default function Home() { const [toggleRain] = useOutletContext(); + const ready = useGameStore((s) => s.ready); const click = useGameStore((s) => s.click); const resources = useGameStore((s) => s.state.resources); const state = useGameStore((s) => s.state); const clickGain = getClickGain(state); + if (!ready) { + return ( +
+

+ Chargement de ta progression... +

+
+ ); + } + const createParticle = useCallback((clientX, clientY) => { const particle = document.createElement("span"); particle.className = "click-particle"; diff --git a/Frontend/src/store/useGameStore.ts b/Frontend/src/store/useGameStore.ts index 08c1b94..90e7f4f 100644 --- a/Frontend/src/store/useGameStore.ts +++ b/Frontend/src/store/useGameStore.ts @@ -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((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((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((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((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((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((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, }));