chore: commit investigation state
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m25s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m25s
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
# Circular Dependencies
|
# 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.
|
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
|
1. 1) stores/golem-combat-actions.ts > stores/golem-combat-helpers.ts
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_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.",
|
"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."
|
"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."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -215,6 +215,7 @@ Mana-Loop/
|
|||||||
│ │ │ │ ├── discipline-math.test.ts
|
│ │ │ │ ├── discipline-math.test.ts
|
||||||
│ │ │ │ ├── discipline-prerequisites.test.ts
|
│ │ │ │ ├── discipline-prerequisites.test.ts
|
||||||
│ │ │ │ ├── discipline-reactivate-bug.test.ts
|
│ │ │ │ ├── discipline-reactivate-bug.test.ts
|
||||||
|
│ │ │ │ ├── earth-desync.test.ts
|
||||||
│ │ │ │ ├── enemy-barrier-utils.test.ts
|
│ │ │ │ ├── enemy-barrier-utils.test.ts
|
||||||
│ │ │ │ ├── enemy-defenses.test.ts
|
│ │ │ │ ├── enemy-defenses.test.ts
|
||||||
│ │ │ │ ├── enemy-generator.test.ts
|
│ │ │ │ ├── enemy-generator.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<string, { current: number; max: number; baseMax: number; unlocked: boolean }>,
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -105,7 +105,13 @@ export const useManaStore = create<ManaStore>()(
|
|||||||
if (state.elements[element]?.unlocked) return fail(ErrorCode.INVALID_INPUT, `Element ${element} is already unlocked`);
|
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}`);
|
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();
|
return okVoid();
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -174,6 +180,18 @@ export const useManaStore = create<ManaStore>()(
|
|||||||
persistedState.elements[k].baseMax = persistedState.elements[k].max ?? 10;
|
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;
|
return persistedState;
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user