fix: resolve TS compilation errors and all 7 circular dependencies
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m25s

TypeScript fixes:
- gameStore.ts: replace result.ok with result.success (Result<void> uses success not ok)
- gameStore.ts: fix undefined newProgress variable → ctx.prestige.pactRitualProgress + HOURS_PER_TICK
- prestigeStore.ts: replace result.ok with result.success

Circular dependency fixes:
- Extract GameCoordinatorState to stores/gameStore.types.ts to break gameStore↔tick-pipeline/gameActions/gameLoopActions cycle
- Remove getDodgeChance re-export from floor-utils.ts to break floor-utils↔room-utils↔enemy-utils cycle
- Replace direct combatStore import in discipline-slice.ts with callback pattern to break discipline-slice↔combatStore↔combat-actions↔discipline-effects cycle

Verification: tsc --noEmit clean, madge --circular clean (0 circular deps)
This commit is contained in:
2026-05-26 21:55:55 +02:00
parent 1aea72c013
commit 06c3fe4380
12 changed files with 53 additions and 35 deletions
+16 -15
View File
@@ -1,17 +1,18 @@
# Circular Dependencies # Circular Dependencies
Generated: 2026-05-26T18:59:01.066Z Generated: $(date -u +"%Y-%m-%dT%H:%M:%S.%3NZ")
Found: 7 circular chain(s) — these MUST be fixed before modifying involved files. Found: 0 circular chain(s) — clean!
1. Processed 135 files (1.6s) (2 warnings) ## Status
2. 1) effects/discipline-effects.ts > stores/discipline-slice.ts > stores/combatStore.ts > stores/combat-actions.ts All 7 previously-known circular dependency chains have been resolved:
3. 2) utils/floor-utils.ts > utils/room-utils.ts > utils/enemy-utils.ts 1. ✅ effects/discipline-effects.ts > stores/discipline-slice.ts > stores/combatStore.ts > stores/combat-actions.ts
4. 3) utils/floor-utils.ts > utils/room-utils.ts - Fixed by: discipline-slice.ts uses callbacks instead of directly importing combatStore
5. 4) stores/gameStore.ts > stores/gameActions.ts 2. ✅ utils/floor-utils.ts > utils/room-utils.ts > utils/enemy-utils.ts
6. 5) stores/gameStore.ts > stores/gameLoopActions.ts - Fixed by: removed re-export of getDodgeChance from floor-utils.ts
7. 6) stores/gameStore.ts > stores/tick-pipeline.ts 3. ✅ utils/floor-utils.ts > utils/room-utils.ts
- Fixed by: same as above
## How to fix 4. ✅ stores/gameStore.ts > stores/gameActions.ts
1. Identify which import in the chain can be extracted to a shared types/utils file. - Fixed by: extracted GameCoordinatorState to gameStore.types.ts
2. Move the shared type or function there. 5. ✅ stores/gameStore.ts > stores/gameLoopActions.ts
3. Both files import from the new shared module instead of each other. - Fixed by: extracted GameCoordinatorState to gameStore.types.ts
4. Run: bunx madge --circular src/lib/game (should return clean) 6. ✅ stores/gameStore.ts > stores/tick-pipeline.ts
- Fixed by: extracted GameCoordinatorState to gameStore.types.ts
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"_meta": { "_meta": {
"generated": "2026-05-26T18:58:59.230Z", "generated": "2026-05-26T19:43:50.116Z",
"description": "Import dependency graph for src/lib/game. Keys are files, values are arrays of files they import.", "description": "Import dependency graph for src/lib/game. Keys are files, values are arrays of files they import.",
"usage": "To find what a file affects, search for its path in the VALUES. To find what a file depends on, look at its KEY entry." "usage": "To find what a file affects, search for its path in the VALUES. To find what a file depends on, look at its KEY entry."
}, },
+1
View File
@@ -334,6 +334,7 @@ Mana-Loop/
│ │ │ ├── gameHooks.ts │ │ │ ├── gameHooks.ts
│ │ │ ├── gameLoopActions.ts │ │ │ ├── gameLoopActions.ts
│ │ │ ├── gameStore.ts │ │ │ ├── gameStore.ts
│ │ │ ├── gameStore.types.ts
│ │ │ ├── index.ts │ │ │ ├── index.ts
│ │ │ ├── manaStore.ts │ │ │ ├── manaStore.ts
│ │ │ ├── prestigeStore.ts │ │ │ ├── prestigeStore.ts
+10 -3
View File
@@ -21,7 +21,7 @@ import { enchanterSpecialDisciplines } from '../data/disciplines/enchanter-speci
import { fabricatorDisciplines } from '../data/disciplines/fabricator'; import { fabricatorDisciplines } from '../data/disciplines/fabricator';
import { invokerDisciplines } from '../data/disciplines/invoker'; import { invokerDisciplines } from '../data/disciplines/invoker';
import { MAX_CONCURRENT_DISCIPLINES } from '../types/disciplines'; import { MAX_CONCURRENT_DISCIPLINES } from '../types/disciplines';
import { useCombatStore } from './combatStore';
const ALL_DISCIPLINES = [ const ALL_DISCIPLINES = [
...baseDisciplines, ...baseDisciplines,
@@ -43,6 +43,7 @@ export interface DisciplineStoreState {
concurrentLimit: number; concurrentLimit: number;
totalXP: number; totalXP: number;
processedPerks: string[]; processedPerks: string[];
practicingCallbacks: { onStartPracticing: () => void; onStopPracticing: () => void } | null;
} }
export interface DisciplineStoreActions { export interface DisciplineStoreActions {
@@ -53,6 +54,7 @@ export interface DisciplineStoreActions {
elements: Record<string, ElementState>; elements: Record<string, ElementState>;
unlockedEffects: string[]; unlockedEffects: string[];
}; };
setPracticingCallbacks(callbacks: { onStartPracticing: () => void; onStopPracticing: () => void }): void;
} }
export type DisciplineStore = DisciplineStoreState & DisciplineStoreActions; export type DisciplineStore = DisciplineStoreState & DisciplineStoreActions;
@@ -65,6 +67,7 @@ export const useDisciplineStore = create<DisciplineStore>()(
concurrentLimit: MAX_CONCURRENT_DISCIPLINES, concurrentLimit: MAX_CONCURRENT_DISCIPLINES,
totalXP: 0, totalXP: 0,
processedPerks: [], processedPerks: [],
practicingCallbacks: null,
activate(id, gameState) { activate(id, gameState) {
set((s) => { set((s) => {
@@ -114,7 +117,7 @@ export const useDisciplineStore = create<DisciplineStore>()(
const discState = existing || { id, xp: 0, paused: false }; const discState = existing || { id, xp: 0, paused: false };
// Set currentAction to 'practicing' (only overrides 'meditate') // Set currentAction to 'practicing' (only overrides 'meditate')
useCombatStore.getState().startPracticing(); get().practicingCallbacks?.onStartPracticing?.();
return { return {
disciplines: { ...s.disciplines, [id]: { ...discState, paused: false } }, disciplines: { ...s.disciplines, [id]: { ...discState, paused: false } },
activeIds: [...s.activeIds, id], activeIds: [...s.activeIds, id],
@@ -127,7 +130,7 @@ export const useDisciplineStore = create<DisciplineStore>()(
const newActiveIds = s.activeIds.filter((aid) => aid !== id); const newActiveIds = s.activeIds.filter((aid) => aid !== id);
// If no more active disciplines, restore currentAction to 'meditate' // If no more active disciplines, restore currentAction to 'meditate'
if (newActiveIds.length === 0) { if (newActiveIds.length === 0) {
useCombatStore.getState().stopPracticing(); get().practicingCallbacks?.onStopPracticing?.();
} }
return { return {
activeIds: newActiveIds, activeIds: newActiveIds,
@@ -138,6 +141,10 @@ export const useDisciplineStore = create<DisciplineStore>()(
}); });
}, },
setPracticingCallbacks(callbacks) {
set({ practicingCallbacks: callbacks });
},
processTick(mana) { processTick(mana) {
const s = get(); const s = get();
let rawMana = mana.rawMana; let rawMana = mana.rawMana;
+1 -1
View File
@@ -1,5 +1,5 @@
import { computeMaxMana, computeClickMana } from '../utils'; import { computeMaxMana, computeClickMana } from '../utils';
import type { GameCoordinatorState } from './gameStore'; import type { GameCoordinatorState } from './gameStore.types';
import { useUIStore } from './uiStore'; import { useUIStore } from './uiStore';
import { usePrestigeStore } from './prestigeStore'; import { usePrestigeStore } from './prestigeStore';
import { useManaStore } from './manaStore'; import { useManaStore } from './manaStore';
+1 -1
View File
@@ -1,5 +1,5 @@
import { calcInsight, getFloorMaxHP } from '../utils'; import { calcInsight, getFloorMaxHP } from '../utils';
import type { GameCoordinatorState } from './gameStore'; import type { GameCoordinatorState } from './gameStore.types';
import { makeInitialSpells } from './combatStore'; import { makeInitialSpells } from './combatStore';
import { SPELLS_DEF } from '../constants'; import { SPELLS_DEF } from '../constants';
import { useUIStore } from './uiStore'; import { useUIStore } from './uiStore';
+8 -10
View File
@@ -31,14 +31,7 @@ import { createSafeStorage } from '../utils/safe-persist';
import { createStartNewLoop } from './gameLoopActions'; import { createStartNewLoop } from './gameLoopActions';
import { buildTickContext, applyTickWrites } from './tick-pipeline'; import { buildTickContext, applyTickWrites } from './tick-pipeline';
import type { TickContext, TickWrites } from './tick-pipeline'; import type { TickContext, TickWrites } from './tick-pipeline';
import type { GameCoordinatorState } from './gameStore.types';
export interface GameCoordinatorState {
day: number;
hour: number;
incursionStrength: number;
containmentWards: number;
initialized: boolean;
}
export interface GameCoordinatorStore extends GameCoordinatorState { export interface GameCoordinatorStore extends GameCoordinatorState {
tick: () => void; tick: () => void;
@@ -63,6 +56,11 @@ export const useGameStore = create<GameCoordinatorStore>()(
...initialState, ...initialState,
initGame: () => { initGame: () => {
// Wire discipline store ↔ combat store callbacks (breaks circular dependency)
useDisciplineStore.getState().setPracticingCallbacks({
onStartPracticing: () => useCombatStore.getState().startPracticing(),
onStopPracticing: () => useCombatStore.getState().stopPracticing(),
});
set({ initialized: true }); set({ initialized: true });
}, },
@@ -234,7 +232,7 @@ export const useGameStore = create<GameCoordinatorStore>()(
const manaStore = useManaStore.getState(); const manaStore = useManaStore.getState();
for (const manaType of guardian.unlocksMana || []) { for (const manaType of guardian.unlocksMana || []) {
const result = manaStore.unlockElement(manaType, 0); const result = manaStore.unlockElement(manaType, 0);
if (result.ok) { if (result.success) {
addLog(`${manaType.charAt(0).toUpperCase() + manaType.slice(1)} mana unlocked!`); addLog(`${manaType.charAt(0).toUpperCase() + manaType.slice(1)} mana unlocked!`);
} }
} }
@@ -249,7 +247,7 @@ export const useGameStore = create<GameCoordinatorStore>()(
} else { } else {
writes.prestige = { writes.prestige = {
...(writes.prestige || {}), ...(writes.prestige || {}),
pactRitualProgress: newProgress, pactRitualProgress: ctx.prestige.pactRitualProgress + HOURS_PER_TICK,
}; };
} }
} }
+11
View File
@@ -0,0 +1,11 @@
// ─── Game Coordinator Types ────────────────────────────────────────────────────
// Shared data-only types extracted from gameStore.ts to break circular dependencies.
// This file must NOT import from any other store file.
export interface GameCoordinatorState {
day: number;
hour: number;
incursionStrength: number;
containmentWards: number;
initialized: boolean;
}
+2 -1
View File
@@ -25,7 +25,8 @@ export type { DisciplineStoreState, DisciplineStoreActions, DisciplineStore } fr
export { useGameStore } from './gameStore'; export { useGameStore } from './gameStore';
export { useGameLoop } from './gameHooks'; export { useGameLoop } from './gameHooks';
export type { GameCoordinatorState, GameCoordinatorStore } from './gameStore'; export type { GameCoordinatorState } from './gameStore.types';
export type { GameCoordinatorStore } from './gameStore';
// Re-export utilities from utils.ts and computed-stats // Re-export utilities from utils.ts and computed-stats
export { export {
+1 -1
View File
@@ -156,7 +156,7 @@ export const usePrestigeStore = create<PrestigeStore>()(
const manaStore = useManaStore.getState(); const manaStore = useManaStore.getState();
for (const manaType of guardian.unlocksMana || []) { for (const manaType of guardian.unlocksMana || []) {
const result = manaStore.unlockElement(manaType, 0); const result = manaStore.unlockElement(manaType, 0);
if (result.ok) { if (result.success) {
addLog(`${manaType.charAt(0).toUpperCase() + manaType.slice(1)} mana unlocked!`); addLog(`${manaType.charAt(0).toUpperCase() + manaType.slice(1)} mana unlocked!`);
} }
} }
+1 -1
View File
@@ -10,7 +10,7 @@ import type { CombatState } from './combat-state.types';
import type { CraftingState } from './craftingStore.types'; import type { CraftingState } from './craftingStore.types';
import type { AttunementStoreState } from './attunementStore'; import type { AttunementStoreState } from './attunementStore';
import type { DisciplineStoreState } from './discipline-slice'; import type { DisciplineStoreState } from './discipline-slice';
import type { GameCoordinatorState } from './gameStore'; import type { GameCoordinatorState } from './gameStore.types';
// ─── Read-only snapshot of all store states at tick start ────────────────────── // ─── Read-only snapshot of all store states at tick start ──────────────────────
export interface TickContext { export interface TickContext {
-1
View File
@@ -21,4 +21,3 @@ export function getFloorElement(floor: number): string {
return FLOOR_ELEM_CYCLE[idx]; return FLOOR_ELEM_CYCLE[idx];
} }
export { getDodgeChance } from './room-utils';