From fef7de8d0949282b68ca13b489ac92ae0f3dc5b0 Mon Sep 17 00:00:00 2001 From: n8n-gitea Date: Wed, 10 Jun 2026 09:56:14 +0200 Subject: [PATCH] chore: commit investigation state --- docs/circular-deps.txt | 2 +- docs/dependency-graph.json | 2 +- docs/project-structure.txt | 1 + src/lib/game/__tests__/earth-desync.test.ts | 145 ++++++++++++++++++++ src/lib/game/stores/manaStore.ts | 20 ++- 5 files changed, 167 insertions(+), 3 deletions(-) create mode 100644 src/lib/game/__tests__/earth-desync.test.ts diff --git a/docs/circular-deps.txt b/docs/circular-deps.txt index 519effd..26dade1 100644 --- a/docs/circular-deps.txt +++ b/docs/circular-deps.txt @@ -1,5 +1,5 @@ # Circular Dependencies -Generated: 2026-06-09T16:48:20.172Z +Generated: 2026-06-09T17:09:05.689Z Found: 2 circular chain(s) — these MUST be fixed before modifying involved files. 1. 1) stores/golem-combat-actions.ts > stores/golem-combat-helpers.ts diff --git a/docs/dependency-graph.json b/docs/dependency-graph.json index a5a408a..9e4d2d6 100644 --- a/docs/dependency-graph.json +++ b/docs/dependency-graph.json @@ -1,6 +1,6 @@ { "_meta": { - "generated": "2026-06-09T16:48:18.218Z", + "generated": "2026-06-09T17:09:03.568Z", "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 591425f..38c84e7 100644 --- a/docs/project-structure.txt +++ b/docs/project-structure.txt @@ -215,6 +215,7 @@ Mana-Loop/ │ │ │ │ ├── discipline-math.test.ts │ │ │ │ ├── discipline-prerequisites.test.ts │ │ │ │ ├── discipline-reactivate-bug.test.ts +│ │ │ │ ├── earth-desync.test.ts │ │ │ │ ├── enemy-barrier-utils.test.ts │ │ │ │ ├── enemy-defenses.test.ts │ │ │ │ ├── enemy-generator.test.ts diff --git a/src/lib/game/__tests__/earth-desync.test.ts b/src/lib/game/__tests__/earth-desync.test.ts new file mode 100644 index 0000000..6481b7e --- /dev/null +++ b/src/lib/game/__tests__/earth-desync.test.ts @@ -0,0 +1,145 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { useManaStore } from '../stores/manaStore'; +import { ELEMENTS, BASE_UNLOCKED_ELEMENTS } from '../constants/elements'; + +// ─── Regression test: Earth element desync after Unlock All ───────────────── +// https://gitea.tailf367e3.ts.net/Anexim/Mana-Loop/issues/338 +// https://gitea.tailf367e3.ts.net/Anexim/Mana-Loop/issues/339 + +function resetManaStore() { + useManaStore.setState({ + rawMana: 10, + meditateTicks: 0, + totalManaGathered: 0, + elements: Object.fromEntries( + Object.keys(ELEMENTS).map(k => [ + k, + { + current: BASE_UNLOCKED_ELEMENTS.includes(k) ? 0 : 0, + max: 10, + baseMax: 10, + unlocked: BASE_UNLOCKED_ELEMENTS.includes(k), + }, + ]) + ) as Record, + elementRegen: {}, + }); +} + +describe('Earth element desync after Unlock All', () => { + beforeEach(() => { + resetManaStore(); + }); + + it('earth element exists in initial state', () => { + const elements = useManaStore.getState().elements; + expect(elements.earth).toBeDefined(); + expect(elements.earth.unlocked).toBe(false); + }); + + it('unlockElement unlocks earth correctly', () => { + const result = useManaStore.getState().unlockElement('earth', 0); + expect(result.success).toBe(true); + const earth = useManaStore.getState().elements.earth; + expect(earth).toBeDefined(); + expect(earth.unlocked).toBe(true); + expect(earth.current).toBe(0); + expect(earth.max).toBe(10); + expect(earth.baseMax).toBe(10); + }); + + it('unlockElement preserves earth state fields', () => { + // First give earth some mana + useManaStore.setState((s) => ({ + elements: { + ...s.elements, + earth: { ...s.elements.earth, current: 5, max: 20, baseMax: 20 }, + }, + })); + + const result = useManaStore.getState().unlockElement('earth', 0); + expect(result.success).toBe(true); + + const earth = useManaStore.getState().elements.earth; + expect(earth.unlocked).toBe(true); + expect(earth.current).toBe(5); + expect(earth.max).toBe(20); + expect(earth.baseMax).toBe(20); + }); + + it('unlock all elements via unlockElement preserves all fields', () => { + const elements = useManaStore.getState().elements; + for (const id of Object.keys(elements)) { + if (!elements[id].unlocked) { + useManaStore.getState().unlockElement(id, 0); + } + } + + const updatedElements = useManaStore.getState().elements; + const earth = updatedElements.earth; + + expect(earth).toBeDefined(); + expect(earth.unlocked).toBe(true); + expect(earth.current).toBe(0); + expect(earth.max).toBe(10); + expect(earth.baseMax).toBe(10); + }); + + it('all 22 elements are unlocked after unlock all', () => { + const elements = useManaStore.getState().elements; + for (const id of Object.keys(elements)) { + if (!elements[id].unlocked) { + useManaStore.getState().unlockElement(id, 0); + } + } + + const updatedElements = useManaStore.getState().elements; + const unlockedCount = Object.values(updatedElements).filter(e => e.unlocked).length; + expect(unlockedCount).toBe(Object.keys(ELEMENTS).length); + }); + + it('earth shows in unlocked elements list after unlock all', () => { + const elements = useManaStore.getState().elements; + for (const id of Object.keys(elements)) { + if (!elements[id].unlocked) { + useManaStore.getState().unlockElement(id, 0); + } + } + + const updatedElements = useManaStore.getState().elements; + const unlockedIds = Object.entries(updatedElements) + .filter(([, state]) => state.unlocked) + .map(([id]) => id); + + expect(unlockedIds).toContain('earth'); + }); + + it('earth element has valid state after unlock (not missing fields)', () => { + useManaStore.getState().unlockElement('earth', 0); + const earth = useManaStore.getState().elements.earth; + + // These would be undefined if the element state was corrupted + expect(earth.current).toBeDefined(); + expect(earth.max).toBeDefined(); + expect(earth.baseMax).toBeDefined(); + expect(earth.unlocked).toBe(true); + }); + + it('unlockElement with cost 0 does not deduct raw mana', () => { + const before = useManaStore.getState().rawMana; + useManaStore.getState().unlockElement('earth', 0); + const after = useManaStore.getState().rawMana; + expect(after).toBe(before); + }); + + it('unlockElement twice for earth is a no-op', () => { + const result1 = useManaStore.getState().unlockElement('earth', 0); + expect(result1.success).toBe(true); + + const result2 = useManaStore.getState().unlockElement('earth', 0); + expect(result2.success).toBe(false); // Already unlocked + + const earth = useManaStore.getState().elements.earth; + expect(earth.unlocked).toBe(true); + }); +}); diff --git a/src/lib/game/stores/manaStore.ts b/src/lib/game/stores/manaStore.ts index 883026d..8e71332 100755 --- a/src/lib/game/stores/manaStore.ts +++ b/src/lib/game/stores/manaStore.ts @@ -105,7 +105,13 @@ export const useManaStore = create()( if (state.elements[element]?.unlocked) return fail(ErrorCode.INVALID_INPUT, `Element ${element} is already unlocked`); if (state.rawMana < cost) return fail(ErrorCode.INSUFFICIENT_MANA, `Need ${cost} raw mana, have ${state.rawMana}`); - set({ rawMana: state.rawMana - cost, elements: { ...state.elements, [element]: { ...state.elements[element], unlocked: true } } }); + // If the element doesn't exist in the store (e.g. from an old save), create it + const existing = state.elements[element]; + const newElement = existing + ? { ...existing, unlocked: true } + : { current: 0, max: 10, baseMax: 10, unlocked: true }; + + set({ rawMana: state.rawMana - cost, elements: { ...state.elements, [element]: newElement } }); return okVoid(); }, @@ -174,6 +180,18 @@ export const useManaStore = create()( persistedState.elements[k].baseMax = persistedState.elements[k].max ?? 10; } } + // Add any missing elements that exist in ELEMENTS but not in the save + for (const k of Object.keys(ELEMENTS)) { + if (!persistedState.elements[k]) { + const isUnlocked = BASE_UNLOCKED_ELEMENTS.includes(k); + persistedState.elements[k] = { + current: isUnlocked ? 0 : 0, + max: 10, + baseMax: 10, + unlocked: isUnlocked, + }; + } + } } return persistedState; },