diff --git a/docs/circular-deps.txt b/docs/circular-deps.txt index 07ecb3b..af4d847 100644 --- a/docs/circular-deps.txt +++ b/docs/circular-deps.txt @@ -1,5 +1,5 @@ # Circular Dependencies -Generated: 2026-06-12T07:45:32.728Z +Generated: 2026-06-12T08:05:45.261Z 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 93a6139..4f7188b 100644 --- a/docs/dependency-graph.json +++ b/docs/dependency-graph.json @@ -1,6 +1,6 @@ { "_meta": { - "generated": "2026-06-12T07:45:30.561Z", + "generated": "2026-06-12T08:05:43.098Z", "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." }, diff --git a/docs/project-structure.txt b/docs/project-structure.txt index f993252..f9bafc1 100644 --- a/docs/project-structure.txt +++ b/docs/project-structure.txt @@ -203,6 +203,7 @@ Mana-Loop/ │ │ │ │ ├── bug-352-golem-mana-wipe.test.ts │ │ │ │ ├── bug-353-preparation-mana.test.ts │ │ │ │ ├── bug-354-unlock-attunement.test.ts +│ │ │ │ ├── bug-377-mana-auto-unlock.test.ts │ │ │ │ ├── bug-fixes.test.ts │ │ │ │ ├── combat-actions.test.ts │ │ │ │ ├── combat-utils.test.ts diff --git a/src/lib/game/__tests__/bug-377-mana-auto-unlock.test.ts b/src/lib/game/__tests__/bug-377-mana-auto-unlock.test.ts new file mode 100644 index 0000000..7265db2 --- /dev/null +++ b/src/lib/game/__tests__/bug-377-mana-auto-unlock.test.ts @@ -0,0 +1,66 @@ +/** + * Regression test for Bug #377: + * "All 22 mana types auto-unlocked on first tick via conversion system" + * + * Expected: Only 'transference' should be unlocked at game start. + * The conversion system must NOT auto-unlock elements that have non-zero rates. + */ +import { describe, it, expect, beforeEach } from 'vitest'; +import { useManaStore } from '../stores/manaStore'; +import { resetAllStores, tickN } from './cross-module-helpers'; +import { BASE_UNLOCKED_ELEMENTS } from '../constants/elements'; + +function getUnlockedElements(): string[] { + const elements = useManaStore.getState().elements; + return Object.entries(elements) + .filter(([, v]) => v.unlocked) + .map(([k]) => k); +} + +describe('Bug #377 — mana types must not auto-unlock on first tick', () => { + beforeEach(() => { + resetAllStores(); + }); + + it('only transference is unlocked at game start', () => { + const unlocked = getUnlockedElements(); + expect(unlocked).toEqual(BASE_UNLOCKED_ELEMENTS); + expect(unlocked).toHaveLength(1); + expect(unlocked).toContain('transference'); + }); + + it('running one tick does not unlock any new elements', () => { + const before = getUnlockedElements(); + expect(before).toEqual(['transference']); + + // Run a single game tick + tickN(1); + + const after = getUnlockedElements(); + expect(after).toEqual(['transference']); + expect(after).toHaveLength(1); + }); + + it('running multiple ticks does not unlock exotic elements', () => { + // Run 20 ticks (simulating ~4 seconds of game time) + tickN(20); + + const unlocked = getUnlockedElements(); + expect(unlocked).toEqual(['transference']); + + // Explicitly verify exotic elements remain locked + const elements = useManaStore.getState().elements; + const exoticElements = ['crystal', 'stellar', 'void', 'soul', 'time', 'plasma']; + for (const elem of exoticElements) { + expect(elements[elem]?.unlocked).toBe(false); + } + }); + + it('conversion rates for locked elements do not cause unlock after many ticks', () => { + // Run 50 ticks + tickN(50); + + const unlocked = getUnlockedElements(); + expect(unlocked).toEqual(['transference']); + }); +}); diff --git a/src/lib/game/stores/gameStore.ts b/src/lib/game/stores/gameStore.ts index 61e1dac..97f5365 100644 --- a/src/lib/game/stores/gameStore.ts +++ b/src/lib/game/stores/gameStore.ts @@ -207,8 +207,7 @@ export const useGameStore = create()( } // Apply net element regen to pools: produced - drained (component consumption) for (const [elem, entry] of Object.entries(conversionResult.rates)) { - if (entry.paused || !elements[elem]) continue; - if (!elements[elem].unlocked) elements[elem] = { ...elements[elem], unlocked: true }; + if (entry.paused || !elements[elem] || !elements[elem].unlocked) continue; const netRate = elementRegen[elem]; if (netRate === 0) continue; const delta = netRate * HOURS_PER_TICK; @@ -320,8 +319,7 @@ export const useGameStore = create()( const regenDelta = Math.max(0, netBoostedRegen - netRawRegen); rawMana = Math.min(rawMana + regenDelta * HOURS_PER_TICK, maxMana); for (const [elem, entry] of Object.entries(conversionResult.rates)) { - if (entry.paused || entry.finalRate <= 0 || !elements[elem]) continue; - if (!elements[elem].unlocked) elements[elem] = { ...elements[elem], unlocked: true }; + if (entry.paused || entry.finalRate <= 0 || !elements[elem] || !elements[elem].unlocked) continue; // Normal conversion already applied above; add only the 9× delta elements[elem] = { ...elements[elem], current: Math.min(elements[elem].max, elements[elem].current + entry.finalRate * 9 * HOURS_PER_TICK) }; }