diff --git a/docs/circular-deps.txt b/docs/circular-deps.txt index b82164e..5517506 100644 --- a/docs/circular-deps.txt +++ b/docs/circular-deps.txt @@ -1,11 +1,14 @@ # Circular Dependencies -Generated: 2026-06-14T19:56:38.228Z -Found: 4 circular chain(s) — these MUST be fixed before modifying involved files. +Generated: 2026-06-14T21:49:16.203Z +Found: 7 circular chain(s) — these MUST be fixed before modifying involved files. 1. 1) data/guardian-encounters.ts > data/guardian-procedural.ts -2. 2) stores/golem-combat-actions.ts > stores/golem-combat-helpers.ts -3. 3) stores/attunementStore.ts > stores/combatStore.ts > stores/combat-descent-actions.ts -4. 4) stores/attunementStore.ts > stores/combatStore.ts > stores/combat-descent-actions.ts > stores/non-combat-room-actions.ts +2. 2) stores/combatStore.ts > stores/combat-actions.ts > stores/combat-room-enchantments.ts > stores/craftingStore.ts +3. 3) stores/combatStore.ts > stores/combat-actions.ts > stores/combat-room-enchantments.ts > stores/craftingStore.ts > stores/crafting-equipment-tick.ts +4. 4) stores/combatStore.ts > stores/combat-actions.ts > stores/combat-room-enchantments.ts > stores/craftingStore.ts > stores/pipelines/equipment-crafting.ts +5. 5) stores/golem-combat-actions.ts > stores/golem-combat-helpers.ts +6. 6) stores/attunementStore.ts > stores/combatStore.ts > stores/combat-descent-actions.ts +7. 7) stores/attunementStore.ts > stores/combatStore.ts > stores/combat-descent-actions.ts > stores/non-combat-room-actions.ts ## How to fix 1. Identify which import in the chain can be extracted to a shared types/utils file. diff --git a/docs/dependency-graph.json b/docs/dependency-graph.json index 4731cab..6706809 100644 --- a/docs/dependency-graph.json +++ b/docs/dependency-graph.json @@ -1,6 +1,6 @@ { "_meta": { - "generated": "2026-06-14T19:56:35.928Z", + "generated": "2026-06-14T21:49:13.932Z", "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." }, @@ -245,6 +245,9 @@ "data/disciplines/elemental.ts": [ "types/disciplines.ts" ], + "data/disciplines/enchanter-combat.ts": [ + "types/disciplines.ts" + ], "data/disciplines/enchanter-special.ts": [ "types/disciplines.ts" ], @@ -265,6 +268,7 @@ "data/disciplines/elemental-regen-advanced.ts", "data/disciplines/elemental-regen.ts", "data/disciplines/elemental.ts", + "data/disciplines/enchanter-combat.ts", "data/disciplines/enchanter-special.ts", "data/disciplines/enchanter-spells.ts", "data/disciplines/enchanter-utility.ts", @@ -558,6 +562,7 @@ "stores/combat-damage.ts", "stores/combat-invocation.ts", "stores/combat-melee.ts", + "stores/combat-room-enchantments.ts", "stores/combat-state.types.ts", "stores/dot-runtime.ts", "stores/golem-combat-actions.ts", @@ -618,6 +623,14 @@ "utils/index.ts", "utils/spire-utils.ts" ], + "stores/combat-room-enchantments.ts": [ + "data/guardian-encounters.ts", + "effects/discipline-effects.ts", + "stores/combat-state.types.ts", + "stores/craftingStore.ts", + "types.ts", + "utils/room-enchantments-utils.ts" + ], "stores/combat-state.types.ts": [ "types.ts", "utils/invocation-utils.ts" @@ -983,6 +996,10 @@ "data/guardian-encounters.ts" ], "utils/result.ts": [], + "utils/room-enchantments-utils.ts": [ + "data/enchantments/special-effects.ts", + "types.ts" + ], "utils/room-utils.ts": [ "constants.ts", "data/guardian-encounters.ts", diff --git a/docs/project-structure.txt b/docs/project-structure.txt index 82e8c8d..05a1818 100644 --- a/docs/project-structure.txt +++ b/docs/project-structure.txt @@ -224,6 +224,7 @@ Mana-Loop/ │ │ │ │ ├── day30-blank-page.test.ts │ │ │ │ ├── design-validation-perk-gating.test.ts │ │ │ │ ├── discipline-deactivate-on-spire-entry.test.ts +│ │ │ │ ├── discipline-effects-reactivity.test.ts │ │ │ │ ├── discipline-math.test.ts │ │ │ │ ├── discipline-prerequisites.test.ts │ │ │ │ ├── discipline-reactivate-bug.test.ts diff --git a/src/lib/game/__tests__/discipline-effects-reactivity.test.ts b/src/lib/game/__tests__/discipline-effects-reactivity.test.ts new file mode 100644 index 0000000..b3a426f --- /dev/null +++ b/src/lib/game/__tests__/discipline-effects-reactivity.test.ts @@ -0,0 +1,150 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { computeDisciplineEffects } from '../effects/discipline-effects'; +import { useDisciplineStore } from '../stores/discipline-slice'; +import { baseDisciplines } from '../data/disciplines/base'; + +// ─── Test Fixtures ──────────────────────────────────────────────────────────── + +const rawMastery = baseDisciplines.find((d) => d.id === 'raw-mastery')!; + +/** + * Helper: set the discipline store to a known state. + * We use the store's setState directly so we can control exactly what + * computeDisciplineEffects sees when it calls getState(). + */ +function setDisciplineState(overrides: Partial>) { + useDisciplineStore.setState(overrides as any); +} + +// ─── Bug B: autoPaused disciplines should still contribute stat bonuses ─────── + +describe('computeDisciplineEffects — autoPaused discipline bonuses (Bug B)', () => { + beforeEach(() => { + // Reset to clean state + setDisciplineState({ + disciplines: {}, + activeIds: [], + }); + }); + + it('should include stat bonuses from an auto-paused discipline with XP', () => { + setDisciplineState({ + disciplines: { + 'raw-mastery': { id: 'raw-mastery', xp: 1000, paused: false, autoPaused: true }, + }, + activeIds: ['raw-mastery'], + }); + + const effects = computeDisciplineEffects(); + expect(effects.bonuses.maxManaBonus).toBeGreaterThan(0); + }); + + it('should NOT include stat bonuses from a manually paused discipline', () => { + setDisciplineState({ + disciplines: { + 'raw-mastery': { id: 'raw-mastery', xp: 1000, paused: true, autoPaused: false }, + }, + activeIds: [], + }); + + const effects = computeDisciplineEffects(); + expect(effects.bonuses.maxManaBonus || 0).toBe(0); + }); + + it('should include stat bonuses from an active (non-paused) discipline', () => { + setDisciplineState({ + disciplines: { + 'raw-mastery': { id: 'raw-mastery', xp: 500, paused: false, autoPaused: false }, + }, + activeIds: ['raw-mastery'], + }); + + const effects = computeDisciplineEffects(); + expect(effects.bonuses.maxManaBonus).toBeGreaterThan(0); + }); + + it('auto-paused discipline with 0 XP should not contribute bonuses', () => { + setDisciplineState({ + disciplines: { + 'raw-mastery': { id: 'raw-mastery', xp: 0, paused: false, autoPaused: true }, + }, + activeIds: ['raw-mastery'], + }); + + const effects = computeDisciplineEffects(); + expect(effects.bonuses.maxManaBonus || 0).toBe(0); + }); + + it('should distinguish autoPaused from paused: mixed scenario', () => { + setDisciplineState({ + disciplines: { + 'raw-mastery': { id: 'raw-mastery', xp: 1000, paused: false, autoPaused: true }, + }, + activeIds: ['raw-mastery'], + }); + + const effects = computeDisciplineEffects(); + // autoPaused=true, paused=false → should be included + expect(effects.bonuses.maxManaBonus).toBeGreaterThan(0); + + // Now manually pause it + setDisciplineState({ + disciplines: { + 'raw-mastery': { id: 'raw-mastery', xp: 1000, paused: true, autoPaused: false }, + }, + activeIds: [], + }); + + const effects2 = computeDisciplineEffects(); + expect(effects2.bonuses.maxManaBonus || 0).toBe(0); + }); +}); + +// ─── Bug A concept: computeDisciplineEffects reads current state ────────────── + +describe('computeDisciplineEffects — reactive state reads (Bug A)', () => { + beforeEach(() => { + setDisciplineState({ + disciplines: {}, + activeIds: [], + }); + }); + + it('should return updated bonuses when discipline XP changes in the store', () => { + // Start with some XP + setDisciplineState({ + disciplines: { + 'raw-mastery': { id: 'raw-mastery', xp: 100, paused: false, autoPaused: false }, + }, + activeIds: ['raw-mastery'], + }); + + const effects1 = computeDisciplineEffects(); + const bonus1 = effects1.bonuses.maxManaBonus; + expect(bonus1).toBeGreaterThan(0); + + // Simulate tick: XP increases + setDisciplineState({ + disciplines: { + 'raw-mastery': { id: 'raw-mastery', xp: 5000, paused: false, autoPaused: false }, + }, + activeIds: ['raw-mastery'], + }); + + const effects2 = computeDisciplineEffects(); + const bonus2 = effects2.bonuses.maxManaBonus; + + // Higher XP → higher bonus (stat bonus is monotonically increasing) + expect(bonus2).toBeGreaterThan(bonus1); + }); + + it('should return zero bonuses when store is empty (fresh game)', () => { + setDisciplineState({ + disciplines: {}, + activeIds: [], + }); + + const effects = computeDisciplineEffects(); + expect(effects.bonuses.maxManaBonus || 0).toBe(0); + }); +}); diff --git a/src/lib/game/effects/discipline-effects.ts b/src/lib/game/effects/discipline-effects.ts index 407131d..f4a47bb 100644 --- a/src/lib/game/effects/discipline-effects.ts +++ b/src/lib/game/effects/discipline-effects.ts @@ -84,7 +84,7 @@ export interface DisciplineEffectsResult { export function computeDisciplineEffects(_state?: DisciplineStoreState): DisciplineEffectsResult { const { disciplines } = useDisciplineStore.getState(); const activeDiscs = Object.entries(disciplines) - .filter(([, disc]) => disc && disc.xp > 0 && !disc.autoPaused) + .filter(([, disc]) => disc && disc.xp > 0 && !disc.paused) .map(([id, disc]) => ({ id, disc, def: ALL_DISCIPLINES.find(d => d.id === id) })) .filter((entry): entry is { id: string; disc: DisciplineState; def: NonNullable } => !!entry.def); diff --git a/src/lib/game/hooks/useGameDerived.ts b/src/lib/game/hooks/useGameDerived.ts index 8e49bdf..5d7fa62 100644 --- a/src/lib/game/hooks/useGameDerived.ts +++ b/src/lib/game/hooks/useGameDerived.ts @@ -8,6 +8,7 @@ import { useManaStore } from '../stores/manaStore'; import { useCombatStore } from '../stores/combatStore'; import { usePrestigeStore } from '../stores/prestigeStore'; import { useAttunementStore } from '../stores/attunementStore'; +import { useDisciplineStore } from '../stores/discipline-slice'; import { computeEffects } from '../effects/upgrade-effects'; import { computeMaxMana, @@ -39,9 +40,11 @@ export function useManaStats() { const hour = useGameStore((s) => s.hour); const attunements = useAttunementStore((s) => s.attunements); + const disciplines = useDisciplineStore((s) => s.disciplines); + const disciplineEffects = useMemo( () => computeDisciplineEffects(), - [] + [disciplines] ); const upgradeEffects = useMemo( @@ -60,8 +63,8 @@ export function useManaStats() { ); const clickMana = useMemo( - () => computeClickMana(), - [] + () => computeClickMana(disciplineEffects), + [disciplineEffects] ); const meditationCap = 5.0 + disciplineEffects.meditationCapBonus;