From ce38975c10aa6b9afc1422499ead52bef23d2261 Mon Sep 17 00:00:00 2001 From: Tetardtek Date: Sat, 28 Mar 2026 20:10:21 +0100 Subject: [PATCH] =?UTF-8?q?fix:=20store=20reactivity=20=E2=80=94=20version?= =?UTF-8?q?=20counter=20pattern=20for=20deep=20state=20changes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit $state caused Svelte 5 to lose track of deep property changes (evolutionTree[i].branch, generators[i].owned, etc.). Fix: _stateVersion counter + getState() creates explicit reactive deps. canPrestige/productionPerSecond are now live getters, no manual updateDerived(). --- Frontend/src/lib/stores/game.svelte.ts | 106 ++++++++++++------------- 1 file changed, 52 insertions(+), 54 deletions(-) diff --git a/Frontend/src/lib/stores/game.svelte.ts b/Frontend/src/lib/stores/game.svelte.ts index dcbf028..0d8b094 100644 --- a/Frontend/src/lib/stores/game.svelte.ts +++ b/Frontend/src/lib/stores/game.svelte.ts @@ -43,8 +43,11 @@ export interface OfflineReport { } // --- Reactive state (Svelte 5 runes) --- +// $state.raw for GameState — replaced entirely on every mutation, never deep-patched. +// This ensures Svelte detects every change by reference equality. -let state = $state({ ...DEFAULT_STATE, lastTick: Date.now(), lastOnline: Date.now() }); +let _stateVersion = $state(0); +let _state: GameState = { ...DEFAULT_STATE, lastTick: Date.now(), lastOnline: Date.now() }; let playSeconds = $state(0); let ready = $state(false); let offlineReport = $state(null); @@ -52,8 +55,18 @@ let showPrestigeScreen = $state(false); let lastClickGain = $state(0); let lastClickDouble = $state(false); let lastClickCrit = $state(false); -let canPrestige = $state(false); -let productionPerSecond = $state(0); + +// Bump version to trigger reactivity on state reads +function setState(newState: GameState) { + _state = newState; + _stateVersion++; +} + +function getState(): GameState { + // Read _stateVersion to create a reactive dependency + void _stateVersion; + return _state; +} // --- Local storage --- @@ -100,19 +113,12 @@ function hydrateWithOffline(saved: GameState, now: number): { state: GameState; }; } -// --- Derived --- - -function updateDerived() { - canPrestige = canPrestigeCheck(state); - productionPerSecond = totalProductionPerSecond(state); -} - // --- Actions --- function tick() { if (!ready) return; const now = Date.now(); - const updated = applyIdleGains(state, now); + const updated = applyIdleGains(_state, now); updated.lastOnline = now; // Auto-click from evolution tree @@ -135,130 +141,119 @@ function tick() { } saveLocal(updated); - state = updated; + setState(updated); playSeconds += 1; - updateDerived(); } function click() { if (!ready) return; - const result = applyClick(applyIdleGains(state, Date.now())); + const result = applyClick(applyIdleGains(_state, Date.now())); saveLocal(result.state); - state = result.state; + setState(result.state); lastClickGain = result.gain; lastClickDouble = result.isDouble; lastClickCrit = result.isCrit; - updateDerived(); } function buy(genId: string) { if (!ready) return; - const withIdle = applyIdleGains(state, Date.now()); + const withIdle = applyIdleGains(_state, Date.now()); const updated = buyGenerator(withIdle, genId); if (!updated) return; saveLocal(updated); - state = updated; - updateDerived(); + setState(updated); } function buyNode(nodeId: string) { if (!ready) return; - const updated = buyEvolutionNode(state, nodeId); + const updated = buyEvolutionNode(_state, nodeId); if (!updated) return; const node = updated.evolutionTree.find((n) => n.id === nodeId); saveLocal(updated); if (node?.capstone) { toast(`Capstone debloque : ${node.name} !`, 'reward', 5000); } - state = updated; - updateDerived(); + setState(updated); } function prestige() { if (!ready) return; - if (!canPrestigeCheck(state)) return; - const updated = applyPrestige(state); + if (!canPrestigeCheck(_state)) return; + const updated = applyPrestige(_state); saveLocal(updated); toast(`Generation #${updated.prestigeCount} — Nouvelle vie !`, 'success', 4000); - state = updated; + setState(updated); showPrestigeScreen = false; - updateDerived(); } function equipCosmetic(cosmeticId: string) { if (!ready) return; - const cosState = { inventory: state.cosmeticInventory, equipped: state.cosmeticEquipped }; + const cosState = { inventory: _state.cosmeticInventory, equipped: _state.cosmeticEquipped }; const updated = equipCosmeticFn(cosState, cosmeticId); - const newState = { ...state, cosmeticEquipped: updated.equipped }; + const newState = { ..._state, cosmeticEquipped: updated.equipped }; saveLocal(newState); - state = newState; + setState(newState); } function unequipCosmetic(slot: CosmeticSlot) { if (!ready) return; - const cosState = { inventory: state.cosmeticInventory, equipped: state.cosmeticEquipped }; + const cosState = { inventory: _state.cosmeticInventory, equipped: _state.cosmeticEquipped }; const updated = unequipSlotFn(cosState, slot); - const newState = { ...state, cosmeticEquipped: updated.equipped }; + const newState = { ..._state, cosmeticEquipped: updated.equipped }; saveLocal(newState); - state = newState; + setState(newState); } function doResetTree() { if (!ready) return; - if (!canResetTree(state)) return; - const updated = resetEvolutionTree(state); + if (!canResetTree(_state)) return; + const updated = resetEvolutionTree(_state); saveLocal(updated); - state = updated; - updateDerived(); + setState(updated); } function doUpgradeConvergence() { if (!ready) return; - const updated = upgradeConvergence(state); + const updated = upgradeConvergence(_state); if (!updated) return; saveLocal(updated); - state = updated; - updateDerived(); + setState(updated); } function doClaimMilestone(milestoneId: string) { if (!ready) return; - const updated = claimMilestoneFn(state, milestoneId); + const updated = claimMilestoneFn(_state, milestoneId); if (!updated) return; saveLocal(updated); toast('Milestone debloque !', 'reward', 4000); - state = updated; + setState(updated); } function reset() { const fresh = { ...DEFAULT_STATE, lastTick: Date.now(), lastOnline: Date.now() }; saveLocal(fresh); - state = fresh; + setState(fresh); playSeconds = 0; ready = true; offlineReport = null; - canPrestige = false; - productionPerSecond = 0; } function loadFromServer(serverState: GameState) { const migrated = migrateSave(serverState as unknown as Record); const result = hydrateWithOffline(migrated, Date.now()); saveLocal(result.state); - state = result.state; + setState(result.state); ready = true; offlineReport = result.report; - updateDerived(); } function initGuest() { const local = loadLocalState(); const result = hydrateWithOffline(local, Date.now()); saveLocal(result.state); - state = result.state; + setState(result.state); ready = true; offlineReport = result.report; - updateDerived(); } function dismissOfflineReport() { @@ -273,10 +268,13 @@ function closePrestige() { showPrestigeScreen = false; } -// --- Public API (single object export for ergonomic access) --- +// --- Public API --- +// All state reads go through getState() which depends on _stateVersion. +// This guarantees that any component reading gameStore.state re-renders +// whenever setState() is called — even for deep properties like evolutionTree[i].branch. export const gameStore = { - get state() { return state; }, + get state() { return getState(); }, get playSeconds() { return playSeconds; }, get ready() { return ready; }, get offlineReport() { return offlineReport; }, @@ -284,8 +282,8 @@ export const gameStore = { get lastClickGain() { return lastClickGain; }, get lastClickDouble() { return lastClickDouble; }, get lastClickCrit() { return lastClickCrit; }, - get canPrestige() { return canPrestige; }, - get productionPerSecond() { return productionPerSecond; }, + get canPrestige() { return canPrestigeCheck(getState()); }, + get productionPerSecond() { return totalProductionPerSecond(getState()); }, tick, click, @@ -304,6 +302,6 @@ export const gameStore = { openPrestige, closePrestige, generatorCost: genCost, - generatorCostWithTree: (gen: Parameters[0]) => genCost(gen, state.evolutionTree), - getClickGain: () => getClickGain(state), + generatorCostWithTree: (gen: Parameters[0]) => genCost(gen, getState().evolutionTree), + getClickGain: () => getClickGain(getState()), };