diff --git a/docs/circular-deps.txt b/docs/circular-deps.txt index 225e5db..d83cfaf 100644 --- a/docs/circular-deps.txt +++ b/docs/circular-deps.txt @@ -1,4 +1,4 @@ # Circular Dependencies -Generated: 2026-05-27T12:39:49.991Z +Generated: 2026-05-27T13:22:21.442Z No circular dependencies found. ✅ diff --git a/docs/dependency-graph.json b/docs/dependency-graph.json index ca7311b..c919130 100644 --- a/docs/dependency-graph.json +++ b/docs/dependency-graph.json @@ -1,6 +1,6 @@ { "_meta": { - "generated": "2026-05-27T12:39:48.093Z", + "generated": "2026-05-27T13:22:19.613Z", "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." }, @@ -365,10 +365,22 @@ "data/equipment/equipment-types-data.ts", "data/equipment/types.ts" ], + "data/fabricator-material-recipes.ts": [ + "data/fabricator-recipe-types.ts" + ], + "data/fabricator-physical-recipes.ts": [ + "data/fabricator-recipe-types.ts" + ], "data/fabricator-recipe-types.ts": [ "data/equipment/types.ts" ], "data/fabricator-recipes.ts": [ + "data/fabricator-material-recipes.ts", + "data/fabricator-physical-recipes.ts", + "data/fabricator-recipe-types.ts", + "data/fabricator-wizard-recipes.ts" + ], + "data/fabricator-wizard-recipes.ts": [ "data/fabricator-recipe-types.ts" ], "data/golems/base-golems.ts": [ diff --git a/docs/project-structure.txt b/docs/project-structure.txt index 2ef42f6..6a9b5d3 100644 --- a/docs/project-structure.txt +++ b/docs/project-structure.txt @@ -209,6 +209,7 @@ Mana-Loop/ │ │ │ ├── cross-module-prestige-discipline.test.ts │ │ │ ├── discipline-math.test.ts │ │ │ ├── discipline-prerequisites.test.ts +│ │ │ ├── discipline-reactivate-bug.test.ts │ │ │ ├── enemy-barrier-utils.test.ts │ │ │ ├── enemy-generator.test.ts │ │ │ ├── enemy-utils.test.ts diff --git a/src/components/game/tabs/DisciplinesTab.tsx b/src/components/game/tabs/DisciplinesTab.tsx index d14ba8f..fd69238 100644 --- a/src/components/game/tabs/DisciplinesTab.tsx +++ b/src/components/game/tabs/DisciplinesTab.tsx @@ -218,16 +218,17 @@ export const DisciplinesTab: React.FC = () => { const [activeAttunement, setActiveAttunement] = useState('base'); + const rawMana = useManaStore((s) => s.rawMana); const elements = useManaStore((s) => s.elements); const signedPacts = usePrestigeStore((s) => s.signedPacts); const handleToggle = useCallback((id: string, paused: boolean) => { if (paused) { - activate(id, { elements, signedPacts }); + activate(id, { rawMana, elements, signedPacts }); } else { deactivate(id); } - }, [activate, deactivate, elements]); + }, [activate, deactivate, rawMana, elements, signedPacts]); const activeTab = ATTUNEMENT_TABS.find((t) => t.key === activeAttunement); diff --git a/src/lib/game/__tests__/discipline-reactivate-bug.test.ts b/src/lib/game/__tests__/discipline-reactivate-bug.test.ts new file mode 100644 index 0000000..7fb06da --- /dev/null +++ b/src/lib/game/__tests__/discipline-reactivate-bug.test.ts @@ -0,0 +1,87 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { useDisciplineStore } from '../stores/discipline-slice'; + +function resetDisciplineStore() { + useDisciplineStore.setState({ + disciplines: {}, + activeIds: [], + concurrentLimit: 1, + totalXP: 0, + processedPerks: [], + practicingCallbacks: null, + }); +} + +describe('DisciplineStore — reactivate after deactivate (bug #163)', () => { + beforeEach(resetDisciplineStore); + + it('BUG: should reactivate a raw discipline after deactivate (requires rawMana in gameState)', () => { + // First activation succeeds because disciplineState is undefined (optimistic) + useDisciplineStore.getState().activate('raw-mastery'); + expect(useDisciplineStore.getState().activeIds).toContain('raw-mastery'); + + // Deactivate + useDisciplineStore.getState().deactivate('raw-mastery'); + expect(useDisciplineStore.getState().activeIds).not.toContain('raw-mastery'); + expect(useDisciplineStore.getState().disciplines['raw-mastery'].paused).toBe(true); + + // Reactivate WITHOUT rawMana — reproduces the bug (silently fails) + useDisciplineStore.getState().activate('raw-mastery', { elements: {}, signedPacts: [] }); + expect(useDisciplineStore.getState().activeIds).not.toContain('raw-mastery'); // BUG: still inactive + + // Reactivate WITH rawMana — the fix + useDisciplineStore.getState().activate('raw-mastery', { rawMana: 1000, elements: {}, signedPacts: [] }); + expect(useDisciplineStore.getState().activeIds).toContain('raw-mastery'); + expect(useDisciplineStore.getState().disciplines['raw-mastery'].paused).toBe(false); + }); + + it('should reactivate a discipline with elements after deactivating it', () => { + useDisciplineStore.getState().activate('attune-fire', { + elements: { fire: { unlocked: true, current: 100, max: 100 } }, + }); + expect(useDisciplineStore.getState().activeIds).toContain('attune-fire'); + + useDisciplineStore.getState().deactivate('attune-fire'); + expect(useDisciplineStore.getState().activeIds).not.toContain('attune-fire'); + + useDisciplineStore.getState().activate('attune-fire', { + elements: { fire: { unlocked: true, current: 100, max: 100 } }, + }); + expect(useDisciplineStore.getState().activeIds).toContain('attune-fire'); + expect(useDisciplineStore.getState().disciplines['attune-fire'].paused).toBe(false); + }); + + it('should reactivate after processTick auto-pauses due to no mana', () => { + useDisciplineStore.getState().activate('raw-mastery', { rawMana: 100, elements: {} }); + expect(useDisciplineStore.getState().activeIds).toContain('raw-mastery'); + + // Tick with no mana — discipline auto-pauses + useDisciplineStore.getState().processTick({ rawMana: 0, elements: {} }); + expect(useDisciplineStore.getState().activeIds).not.toContain('raw-mastery'); + expect(useDisciplineStore.getState().disciplines['raw-mastery'].paused).toBe(true); + + // Reactivate with sufficient mana + useDisciplineStore.getState().activate('raw-mastery', { rawMana: 1000, elements: {} }); + expect(useDisciplineStore.getState().activeIds).toContain('raw-mastery'); + expect(useDisciplineStore.getState().disciplines['raw-mastery'].paused).toBe(false); + }); + + it('should preserve XP when deactivating and reactivating', () => { + useDisciplineStore.getState().activate('raw-mastery', { rawMana: 1000, elements: {} }); + useDisciplineStore.getState().processTick({ rawMana: 1000, elements: {} }); + useDisciplineStore.getState().processTick({ rawMana: 1000, elements: {} }); + useDisciplineStore.getState().processTick({ rawMana: 1000, elements: {} }); + expect(useDisciplineStore.getState().disciplines['raw-mastery'].xp).toBe(3); + + useDisciplineStore.getState().deactivate('raw-mastery'); + expect(useDisciplineStore.getState().disciplines['raw-mastery'].xp).toBe(3); + expect(useDisciplineStore.getState().disciplines['raw-mastery'].paused).toBe(true); + + useDisciplineStore.getState().activate('raw-mastery', { rawMana: 1000, elements: {} }); + expect(useDisciplineStore.getState().activeIds).toContain('raw-mastery'); + expect(useDisciplineStore.getState().disciplines['raw-mastery'].xp).toBe(3); + + useDisciplineStore.getState().processTick({ rawMana: 1000, elements: {} }); + expect(useDisciplineStore.getState().disciplines['raw-mastery'].xp).toBe(4); + }); +});