[priority: high] gameStore.ts tick() orchestrates 7 stores with tangled cross-store coupling #103

Closed
opened 2026-05-20 10:59:27 +02:00 by Anexim · 3 comments
Owner

Severity: High — the core game loop has tight coupling to 7 sibling stores, making it fragile and hard to modify.

Findings:

  • tick() calls getState() on 7 different stores: useUIStore, usePrestigeStore, useManaStore, useCombatStore, useCraftingStore, useAttunementStore, useDisciplineStore
  • Back-and-forth state reads and writes across store boundaries in a single function
  • Data translation at store boundaries happens in multiple places (ad-hoc partial state objects)
  • craftingStore.ts creates tempState = { ...get(), rawMana } as any — wide parameter bags
  • No guaranteed rehydration order for 7 Zustand persist stores

Affected files:

  • src/lib/game/stores/gameStore.ts
  • src/lib/game/stores/craftingStore.ts
  • src/lib/game/stores/combat-actions.ts
  • src/lib/game/stores/manaStore.ts, src/lib/game/stores/prestigeStore.ts

Suggested fix: Refactor to a pipeline pattern: read all states → compute all updates → write all states. Each store should compute its own next state from inputs. Add rehydration checks using Zustand's _hasRehydrated flag.

Confidence: High
Dimension: mid_level_elegance (score: 60.0%) / initialization_coupling

**Severity:** High — the core game loop has tight coupling to 7 sibling stores, making it fragile and hard to modify. **Findings:** - `tick()` calls `getState()` on 7 different stores: useUIStore, usePrestigeStore, useManaStore, useCombatStore, useCraftingStore, useAttunementStore, useDisciplineStore - Back-and-forth state reads and writes across store boundaries in a single function - Data translation at store boundaries happens in multiple places (ad-hoc partial state objects) - `craftingStore.ts` creates `tempState = { ...get(), rawMana } as any` — wide parameter bags - No guaranteed rehydration order for 7 Zustand persist stores **Affected files:** - `src/lib/game/stores/gameStore.ts` - `src/lib/game/stores/craftingStore.ts` - `src/lib/game/stores/combat-actions.ts` - `src/lib/game/stores/manaStore.ts`, `src/lib/game/stores/prestigeStore.ts` **Suggested fix:** Refactor to a pipeline pattern: read all states → compute all updates → write all states. Each store should compute its own next state from inputs. Add rehydration checks using Zustand's `_hasRehydrated` flag. **Confidence:** High **Dimension:** mid_level_elegance (score: 60.0%) / initialization_coupling
Anexim added the ai:todo label 2026-05-20 10:59:27 +02:00
n8n-gitea was assigned by Anexim 2026-05-20 10:59:27 +02:00
Author
Owner

Starting work on refactoring gameStore.ts tick() to use a pipeline pattern. Current analysis complete — the tick() function has 7 getState() calls, ad-hoc partial state objects, and back-and-forth writes across store boundaries. Plan: create a TickPipeline that reads all states → computes all updates → writes all states, with rehydration guards.

Starting work on refactoring gameStore.ts tick() to use a pipeline pattern. Current analysis complete — the tick() function has 7 getState() calls, ad-hoc partial state objects, and back-and-forth writes across store boundaries. Plan: create a TickPipeline that reads all states → computes all updates → writes all states, with rehydration guards.
Author
Owner

Analysis Summary

Current problems in tick():

  1. 7 getState() calls at the top (ui, prestige, mana, combat, crafting, attunement, discipline)
  2. Mid-function getState() calls for cross-store writes (useUIStore, usePrestigeStore, useManaStore, useCombatStore, useAttunementStore)
  3. craftingStore.ts creates tempState = { ...get(), rawMana } as any — wide parameter bag anti-pattern
  4. combat-actions.ts calls usePrestigeStore.getState().signedPacts inside the combat loop
  5. No rehydration guards — if persist hasn't rehydrated, tick() reads stale/empty state
  6. Data translation at store boundaries happens in multiple places

Proposed fix — TickPipeline pattern:

  1. Create TickContext — a single read-only snapshot of all store states at tick start
  2. Create TickResult — a single write-batch with all state updates
  3. Each store exposes a computeTick(ctx: TickContext): PartialState method
  4. tick() orchestrates: read all → compute all → write all
  5. Add rehydration guard using Zustand's persist _hasRehydrated flag
  6. Remove cross-store getState() calls from combat-actions.ts and craftingStore.ts

Files to modify:

  • src/lib/game/stores/gameStore.ts — main refactoring
  • src/lib/game/stores/combat-actions.ts — pass signedPacts as parameter
  • src/lib/game/stores/craftingStore.ts — remove tempState anti-pattern
  • New: src/lib/game/stores/tick-pipeline.ts — pipeline types and orchestration

Estimated scope: Medium — refactoring only, no new features.

## Analysis Summary **Current problems in `tick()`:** 1. 7 `getState()` calls at the top (ui, prestige, mana, combat, crafting, attunement, discipline) 2. Mid-function `getState()` calls for cross-store writes (useUIStore, usePrestigeStore, useManaStore, useCombatStore, useAttunementStore) 3. `craftingStore.ts` creates `tempState = { ...get(), rawMana } as any` — wide parameter bag anti-pattern 4. `combat-actions.ts` calls `usePrestigeStore.getState().signedPacts` inside the combat loop 5. No rehydration guards — if persist hasn't rehydrated, tick() reads stale/empty state 6. Data translation at store boundaries happens in multiple places **Proposed fix — TickPipeline pattern:** 1. Create `TickContext` — a single read-only snapshot of all store states at tick start 2. Create `TickResult` — a single write-batch with all state updates 3. Each store exposes a `computeTick(ctx: TickContext): PartialState` method 4. `tick()` orchestrates: read all → compute all → write all 5. Add rehydration guard using Zustand's persist `_hasRehydrated` flag 6. Remove cross-store `getState()` calls from combat-actions.ts and craftingStore.ts **Files to modify:** - `src/lib/game/stores/gameStore.ts` — main refactoring - `src/lib/game/stores/combat-actions.ts` — pass signedPacts as parameter - `src/lib/game/stores/craftingStore.ts` — remove tempState anti-pattern - New: `src/lib/game/stores/tick-pipeline.ts` — pipeline types and orchestration **Estimated scope:** Medium — refactoring only, no new features.
Author
Owner

Resolution Summary

Refactored gameStore.ts tick() from a tangled cross-store orchestrator to a clean read → compute → write pipeline.

Changes

New file:

  • src/lib/game/stores/tick-pipeline.tsTickContext, TickWrites types + buildTickContext() / applyTickWrites() orchestrator

Modified files:

  • gameStore.tstick() now: (1) snapshots all 7 store states into a TickContext, (2) computes all updates into a TickWrites batch, (3) applies all writes via applyTickWrites(). Eliminated all mid-function getState() calls.
  • combat-actions.tsprocessCombatTick() now accepts signedPacts: number[] as an explicit parameter instead of calling usePrestigeStore.getState().signedPacts inside the combat loop. Returns CombatTickResult with full combat state for the write batch.
  • combatStore.ts / combat-state.types.ts — Updated processCombatTick signature to pass through signedPacts.
  • craftingStore.ts — Removed tempState = { ...get(), rawMana } as any anti-pattern. startPreparing now passes rawMana as an explicit parameter.
  • preparation-actions.ts — Refactored to accept rawMana: number directly instead of reading from a GameState bag.

Verification

  • All 639 tests pass (28 test files)
  • Next.js build succeeds
  • All files under 400 lines
  • Pre-commit hooks pass
## Resolution Summary Refactored `gameStore.ts tick()` from a tangled cross-store orchestrator to a clean **read → compute → write** pipeline. ### Changes **New file:** - `src/lib/game/stores/tick-pipeline.ts` — `TickContext`, `TickWrites` types + `buildTickContext()` / `applyTickWrites()` orchestrator **Modified files:** - `gameStore.ts` — `tick()` now: (1) snapshots all 7 store states into a `TickContext`, (2) computes all updates into a `TickWrites` batch, (3) applies all writes via `applyTickWrites()`. Eliminated all mid-function `getState()` calls. - `combat-actions.ts` — `processCombatTick()` now accepts `signedPacts: number[]` as an explicit parameter instead of calling `usePrestigeStore.getState().signedPacts` inside the combat loop. Returns `CombatTickResult` with full combat state for the write batch. - `combatStore.ts` / `combat-state.types.ts` — Updated `processCombatTick` signature to pass through `signedPacts`. - `craftingStore.ts` — Removed `tempState = { ...get(), rawMana } as any` anti-pattern. `startPreparing` now passes `rawMana` as an explicit parameter. - `preparation-actions.ts` — Refactored to accept `rawMana: number` directly instead of reading from a `GameState` bag. ### Verification - ✅ All 639 tests pass (28 test files) - ✅ Next.js build succeeds - ✅ All files under 400 lines - ✅ Pre-commit hooks pass
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Anexim/Mana-Loop#103