refactor: complete error handling standardization (issue #101)
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m26s

Prestige Store:
- Convert doPrestige() to return Result<void> with specific error codes
  (INVALID_PRESTIGE_ID, PRESTIGE_MAX_LEVEL, INSUFFICIENT_INSIGHT)
- Convert startPactRitual() to return Result<void> with specific error codes
  (GUARDIAN_NOT_DEFEATED, PACT_ALREADY_SIGNED, PACT_SLOTS_FULL,
   INSUFFICIENT_MANA, RITUAL_IN_PROGRESS)

Combat Actions:
- Add try/catch wrapper inside processCombatTick with safe fallback defaults
- Add makeDefaultCombatTickResult helper for error recovery

LocalStorage Error Handling:
- Create safe-persist.ts utility wrapping localStorage with error handling
  (corrupted JSON, quota exceeded, unexpected failures)
- Update all 8 Zustand stores to use createSafeStorage() in persist middleware

UI Updates:
- Update GuardianPactsTab to use Result pattern for ritual error messages

Tests:
- Update store-actions-combat-prestige.test.ts for Result return types
- Update store-actions.test.ts ManaStore tests for Result pattern
- Remove duplicate Prestige/Discipline sections from store-actions.test.ts
- All files under 400 line limit

601 tests pass (3 pre-existing failures in spire-utils.test.ts)
This commit is contained in:
2026-05-22 09:19:20 +02:00
parent 8a7ddaae27
commit 49f8de01ca
21 changed files with 542 additions and 547 deletions
+39 -16
View File
@@ -5,6 +5,9 @@ import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { ELEMENTS, MANA_PER_ELEMENT, BASE_UNLOCKED_ELEMENTS } from '../constants';
import type { ElementState } from '../types';
import { ok, okVoid, fail, ErrorCode } from '../utils/result';
import { createSafeStorage } from '../utils/safe-persist';
import type { Result } from '../utils/result';
// ─── Mana State (data only) ─────────────────────────────────────────────────
@@ -29,12 +32,12 @@ export interface ManaActions {
resetMeditateTicks: () => void;
// Elements
convertMana: (element: string, amount: number) => boolean;
unlockElement: (element: string, cost: number) => boolean;
convertMana: (element: string, amount: number) => Result<{ converted: number }>;
unlockElement: (element: string, cost: number) => Result<void>;
addElementMana: (element: string, amount: number, max: number) => void;
spendElementMana: (element: string, amount: number) => boolean;
spendElementMana: (element: string, amount: number) => Result<void>;
setElementMax: (max: number) => void;
craftComposite: (target: string, recipe: string[]) => boolean;
craftComposite: (target: string, recipe: string[]) => Result<void>;
// Helper for gameStore coordination
processConvertAction: (rawMana: number) => { rawMana: number; elements: Record<string, ElementState> } | null;
@@ -110,11 +113,17 @@ export const useManaStore = create<ManaStore>()(
convertMana: (element: string, amount: number) => {
const state = get();
const elem = state.elements[element];
if (!elem?.unlocked) return false;
if (!elem?.unlocked) {
return fail(ErrorCode.ELEMENT_NOT_UNLOCKED, `Element ${element} is not unlocked`);
}
const cost = MANA_PER_ELEMENT * amount;
if (state.rawMana < cost) return false;
if (elem.current >= elem.max) return false;
if (state.rawMana < cost) {
return fail(ErrorCode.INSUFFICIENT_MANA, `Need ${cost} raw mana, have ${state.rawMana}`);
}
if (elem.current >= elem.max) {
return fail(ErrorCode.ELEMENT_MAX_CAPACITY, `Element ${element} is at max capacity`);
}
const canConvert = Math.min(
amount,
@@ -122,7 +131,9 @@ export const useManaStore = create<ManaStore>()(
elem.max - elem.current
);
if (canConvert <= 0) return false;
if (canConvert <= 0) {
return fail(ErrorCode.INVALID_INPUT, 'Cannot convert 0 or negative amount');
}
set({
rawMana: state.rawMana - canConvert * MANA_PER_ELEMENT,
@@ -132,13 +143,17 @@ export const useManaStore = create<ManaStore>()(
},
});
return true;
return ok({ converted: canConvert });
},
unlockElement: (element: string, cost: number) => {
const state = get();
if (state.elements[element]?.unlocked) return false;
if (state.rawMana < cost) return false;
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,
@@ -148,7 +163,7 @@ export const useManaStore = create<ManaStore>()(
},
});
return true;
return okVoid();
},
addElementMana: (element: string, amount: number, max: number) => {
@@ -171,7 +186,12 @@ export const useManaStore = create<ManaStore>()(
spendElementMana: (element: string, amount: number) => {
const state = get();
const elem = state.elements[element];
if (!elem || elem.current < amount) return false;
if (!elem) {
return fail(ErrorCode.INVALID_ELEMENT, `Element ${element} does not exist`);
}
if (elem.current < amount) {
return fail(ErrorCode.INSUFFICIENT_MANA, `Need ${amount} ${element} mana, have ${elem.current}`);
}
set({
elements: {
@@ -180,7 +200,7 @@ export const useManaStore = create<ManaStore>()(
},
});
return true;
return okVoid();
},
setElementMax: (max: number) => {
@@ -202,7 +222,9 @@ export const useManaStore = create<ManaStore>()(
// Check if we have all ingredients
for (const [r, amt] of Object.entries(costs)) {
if ((state.elements[r]?.current || 0) < amt) return false;
if ((state.elements[r]?.current || 0) < amt) {
return fail(ErrorCode.INSUFFICIENT_MANA, `Need ${amt} ${r} mana, have ${state.elements[r]?.current || 0}`);
}
}
// Deduct ingredients
@@ -223,7 +245,7 @@ export const useManaStore = create<ManaStore>()(
};
set({ elements: newElems });
return true;
return okVoid();
},
processConvertAction: (rawMana: number) => {
@@ -272,6 +294,7 @@ export const useManaStore = create<ManaStore>()(
},
}),
{
storage: createSafeStorage(),
name: 'mana-loop-mana',
partialize: (state) => ({
rawMana: state.rawMana,