refactor: complete error handling standardization (issue #101)
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m26s
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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user