diff --git a/docs/circular-deps.txt b/docs/circular-deps.txt index 67d3be4..e952961 100644 --- a/docs/circular-deps.txt +++ b/docs/circular-deps.txt @@ -1,5 +1,5 @@ # Circular Dependencies -Generated: 2026-06-12T05:02:18.108Z +Generated: 2026-06-12T07:05:52.901Z Found: 4 circular chain(s) — these MUST be fixed before modifying involved files. 1. 1) data/guardian-encounters.ts > data/guardian-procedural.ts diff --git a/docs/dependency-graph.json b/docs/dependency-graph.json index 1f21e7f..88ba3c9 100644 --- a/docs/dependency-graph.json +++ b/docs/dependency-graph.json @@ -1,6 +1,6 @@ { "_meta": { - "generated": "2026-06-12T05:02:13.512Z", + "generated": "2026-06-12T07:05:50.633Z", "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." }, @@ -581,6 +581,13 @@ "stores/prestigeStore.ts", "utils/spire-utils.ts" ], + "stores/combat-reset.ts": [ + "stores/combat-actions.ts", + "stores/combat-state.types.ts", + "types.ts", + "utils/index.ts", + "utils/spire-utils.ts" + ], "stores/combat-state.types.ts": [ "types.ts" ], @@ -588,6 +595,7 @@ "data/guardian-encounters.ts", "stores/combat-actions.ts", "stores/combat-descent-actions.ts", + "stores/combat-reset.ts", "stores/combat-state.types.ts", "stores/discipline-slice.ts", "stores/golemancy-actions.ts", diff --git a/docs/project-structure.txt b/docs/project-structure.txt index 247ce92..61d471b 100644 --- a/docs/project-structure.txt +++ b/docs/project-structure.txt @@ -250,7 +250,8 @@ Mana-Loop/ │ │ │ │ ├── store-actions-discipline.test.ts │ │ │ │ ├── store-actions-mana.test.ts │ │ │ │ ├── store-actions.test.ts -│ │ │ │ └── tick-integration.test.ts +│ │ │ │ ├── tick-integration.test.ts +│ │ │ │ └── unlock-base-elements.test.ts │ │ │ ├── constants/ │ │ │ │ ├── spells-modules/ │ │ │ │ │ ├── advanced-spells.ts diff --git a/src/components/game/debug/GameStateDebug.tsx b/src/components/game/debug/GameStateDebug.tsx index dbf135c..072cd4d 100644 --- a/src/components/game/debug/GameStateDebug.tsx +++ b/src/components/game/debug/GameStateDebug.tsx @@ -11,6 +11,7 @@ import { } from 'lucide-react'; import { DebugName, useDebug } from '@/components/game/debug/debug-context'; import { useGameStore, useManaStore, useUIStore, usePrestigeStore, useCraftingStore } from '@/lib/game/stores'; +import { BASE_ELEMENTS } from '@/lib/game/constants/elements'; import { computeTotalMaxMana } from '@/lib/game/effects'; import { getUnifiedEffects } from '@/lib/game/effects'; @@ -263,8 +264,9 @@ export function GameStateDebug() { }; const handleUnlockBase = () => { - ['fire', 'water', 'air', 'earth', 'light', 'dark', 'death'].forEach(e => { - if (!elements[e]?.unlocked) { + const currentElements = useManaStore.getState().elements; + BASE_ELEMENTS.forEach(e => { + if (!currentElements[e]?.unlocked) { unlockElement(e, 500); } }); diff --git a/src/components/game/tabs/DebugTab/GameStateDebugSection.tsx b/src/components/game/tabs/DebugTab/GameStateDebugSection.tsx index 7bbf1db..80ce878 100644 --- a/src/components/game/tabs/DebugTab/GameStateDebugSection.tsx +++ b/src/components/game/tabs/DebugTab/GameStateDebugSection.tsx @@ -11,6 +11,7 @@ import { } from 'lucide-react'; import { DebugName, useDebug } from '@/components/game/debug/debug-context'; import { useGameStore, useManaStore, useUIStore, usePrestigeStore, useCraftingStore } from '@/lib/game/stores'; +import { BASE_ELEMENTS } from '@/lib/game/constants/elements'; import { computeTotalMaxMana } from '@/lib/game/effects'; import { getUnifiedEffects } from '@/lib/game/effects'; @@ -245,8 +246,9 @@ export function GameStateDebugSection() { }; const handleUnlockBase = () => { - ['fire', 'water', 'air', 'earth', 'light', 'dark', 'death'].forEach(e => { - if (!elements[e]?.unlocked) { + const currentElements = useManaStore.getState().elements; + BASE_ELEMENTS.forEach(e => { + if (!currentElements[e]?.unlocked) { unlockElement(e, 0); } }); diff --git a/src/lib/game/__tests__/unlock-base-elements.test.ts b/src/lib/game/__tests__/unlock-base-elements.test.ts new file mode 100644 index 0000000..080e4ba --- /dev/null +++ b/src/lib/game/__tests__/unlock-base-elements.test.ts @@ -0,0 +1,125 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { useManaStore } from '../stores/manaStore'; +import { ELEMENTS, BASE_UNLOCKED_ELEMENTS, BASE_ELEMENTS } from '../constants/elements'; + +// ─── Regression test: "Unlock All Base Elements" only unlocks 7 base ─────── +// Issue #374: Button was unlocking all 22 elements instead of just 7 base. + +function resetManaStore() { + useManaStore.setState({ + rawMana: 10000, + meditateTicks: 0, + totalManaGathered: 0, + elements: Object.fromEntries( + Object.keys(ELEMENTS).map(k => [ + k, + { + current: 0, + max: 10, + baseMax: 10, + unlocked: BASE_UNLOCKED_ELEMENTS.includes(k), + }, + ]) + ) as Record, + elementRegen: {}, + }); +} + +describe('Unlock All Base Elements debug button', () => { + beforeEach(() => { + resetManaStore(); + }); + + it('BASE_ELEMENTS contains exactly 7 base elements', () => { + expect(BASE_ELEMENTS).toHaveLength(7); + expect(BASE_ELEMENTS).toContain('fire'); + expect(BASE_ELEMENTS).toContain('water'); + expect(BASE_ELEMENTS).toContain('air'); + expect(BASE_ELEMENTS).toContain('earth'); + expect(BASE_ELEMENTS).toContain('light'); + expect(BASE_ELEMENTS).toContain('dark'); + expect(BASE_ELEMENTS).toContain('death'); + }); + + it('BASE_ELEMENTS does NOT contain utility, composite, or exotic elements', () => { + expect(BASE_ELEMENTS).not.toContain('transference'); + expect(BASE_ELEMENTS).not.toContain('metal'); + expect(BASE_ELEMENTS).not.toContain('sand'); + expect(BASE_ELEMENTS).not.toContain('lightning'); + expect(BASE_ELEMENTS).not.toContain('frost'); + expect(BASE_ELEMENTS).not.toContain('blackflame'); + expect(BASE_ELEMENTS).not.toContain('radiantflames'); + expect(BASE_ELEMENTS).not.toContain('miasma'); + expect(BASE_ELEMENTS).not.toContain('shadowglass'); + expect(BASE_ELEMENTS).not.toContain('crystal'); + expect(BASE_ELEMENTS).not.toContain('stellar'); + expect(BASE_ELEMENTS).not.toContain('void'); + expect(BASE_ELEMENTS).not.toContain('soul'); + expect(BASE_ELEMENTS).not.toContain('time'); + expect(BASE_ELEMENTS).not.toContain('plasma'); + }); + + it('unlockElement for all BASE_ELEMENTS only unlocks those 7', () => { + const store = useManaStore.getState(); + + // All base elements should start locked (only transference is base-unlocked) + BASE_ELEMENTS.forEach(e => { + expect(store.elements[e]?.unlocked).toBe(false); + }); + + // Unlock all base elements (cost 0, simulating debug button) + BASE_ELEMENTS.forEach(e => { + store.unlockElement(e, 0); + }); + + const afterState = useManaStore.getState(); + + // All 7 base elements should now be unlocked + BASE_ELEMENTS.forEach(e => { + expect(afterState.elements[e]?.unlocked).toBe(true); + }); + + // Non-base elements should NOT be unlocked + const nonBaseElements = Object.keys(ELEMENTS).filter( + e => !BASE_ELEMENTS.includes(e as never) && !BASE_UNLOCKED_ELEMENTS.includes(e) + ); + nonBaseElements.forEach(e => { + expect(afterState.elements[e]?.unlocked).toBe(false); + }); + }); + + it('debug handleUnlockBase does not unlock transference', () => { + const store = useManaStore.getState(); + + // Verify transference starts unlocked + expect(store.elements['transference']?.unlocked).toBe(true); + + // Run the unlock logic + BASE_ELEMENTS.forEach(e => { + if (!store.elements[e]?.unlocked) { + store.unlockElement(e, 0); + } + }); + + const afterState = useManaStore.getState(); + // transference was already unlocked, should still be unlocked + expect(afterState.elements['transference']?.unlocked).toBe(true); + // But no new utility elements were unlocked + expect(afterState.elements['metal']?.unlocked).toBe(false); + }); + + it('total unlocked count after unlock base is exactly 8 (7 base + transference)', () => { + const store = useManaStore.getState(); + const unlockedBefore = Object.values(store.elements).filter(e => e.unlocked).length; + // Only transference should be unlocked initially + expect(unlockedBefore).toBe(1); + + BASE_ELEMENTS.forEach(e => { + store.unlockElement(e, 0); + }); + + const afterState = useManaStore.getState(); + const unlockedAfter = Object.values(afterState.elements).filter(e => e.unlocked).length; + expect(unlockedAfter).toBe(8); // 7 base + transference + }); +}); diff --git a/src/lib/game/constants/elements.ts b/src/lib/game/constants/elements.ts index 3b1e6d3..66ee269 100644 --- a/src/lib/game/constants/elements.ts +++ b/src/lib/game/constants/elements.ts @@ -110,3 +110,6 @@ export const ELEMENT_ICON_NAMES: Record = { // ─── Base Unlocked Elements ─────────────────────────────────────────────────── export const BASE_UNLOCKED_ELEMENTS = ['transference']; + +// ─── Base Element IDs (7 base elements only — NOT composite/exotic/utility) ── +export const BASE_ELEMENTS = ['fire', 'water', 'air', 'earth', 'light', 'dark', 'death'] as const; diff --git a/src/lib/game/constants/index.ts b/src/lib/game/constants/index.ts index cdab4d4..9ee166a 100644 --- a/src/lib/game/constants/index.ts +++ b/src/lib/game/constants/index.ts @@ -8,7 +8,7 @@ export { getStudySpeedMultiplier, getStudyCostMultiplier } from './core'; // Element-related constants export { MANA_PER_ELEMENT, rawCost, elemCost, ELEMENTS, FLOOR_ELEM_CYCLE } from './elements'; -export { ELEMENT_OPPOSITES, ELEMENT_ICON_NAMES, BASE_UNLOCKED_ELEMENTS } from './elements'; +export { ELEMENT_OPPOSITES, ELEMENT_ICON_NAMES, BASE_UNLOCKED_ELEMENTS, BASE_ELEMENTS } from './elements'; // Spell constants export { SPELLS_DEF } from './spells';