49f8de01ca
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)
318 lines
10 KiB
TypeScript
Executable File
318 lines
10 KiB
TypeScript
Executable File
// ─── Prestige Store ───────────────────────────────────────────────────────────
|
|
// Handles insight, prestige upgrades, memories, loops, pacts
|
|
|
|
import { create } from 'zustand';
|
|
import { persist } from 'zustand/middleware';
|
|
import { createSafeStorage } from '../utils/safe-persist';
|
|
import type { Memory } from '../types';
|
|
import { GUARDIANS, PRESTIGE_DEF } from '../constants';
|
|
import { ok, okVoid, fail, ErrorCode } from '../utils/result';
|
|
import type { Result } from '../utils/result';
|
|
|
|
// ─── Prestige State (data only) ──────────────────────────────────────────────
|
|
|
|
export interface PrestigeState {
|
|
// Loop counter
|
|
loopCount: number;
|
|
|
|
// Insight
|
|
insight: number;
|
|
totalInsight: number;
|
|
loopInsight: number;
|
|
|
|
// Prestige upgrades
|
|
prestigeUpgrades: Record<string, number>;
|
|
memorySlots: number;
|
|
pactSlots: number;
|
|
|
|
// Memories (skills preserved across loops)
|
|
memories: Memory[];
|
|
|
|
// Guardian pacts
|
|
defeatedGuardians: number[];
|
|
signedPacts: number[];
|
|
signedPactDetails: Record<number, {
|
|
floor: number;
|
|
guardianId: string;
|
|
signedAt: { day: number; hour: number };
|
|
skillLevels: Record<string, number>;
|
|
}>;
|
|
pactRitualFloor: number | null;
|
|
pactRitualProgress: number;
|
|
}
|
|
|
|
// ─── Prestige Actions ────────────────────────────────────────────────────────
|
|
|
|
export interface PrestigeActions {
|
|
doPrestige: (id: string) => Result<void>;
|
|
addMemory: (memory: Memory) => void;
|
|
removeMemory: (skillId: string) => void;
|
|
clearMemories: () => void;
|
|
startPactRitual: (floor: number, rawMana: number) => Result<void>;
|
|
cancelPactRitual: () => void;
|
|
completePactRitual: (addLog: (msg: string) => void) => void;
|
|
updatePactRitualProgress: (hours: number) => void;
|
|
removePact: (floor: number) => void;
|
|
defeatGuardian: (floor: number) => void;
|
|
|
|
// Methods called by gameStore
|
|
addSignedPact: (floor: number) => void;
|
|
removeDefeatedGuardian: (floor: number) => void;
|
|
setPactRitualFloor: (floor: number | null) => void;
|
|
addDefeatedGuardian: (floor: number) => void;
|
|
incrementLoopCount: () => void;
|
|
resetPrestigeForNewLoop: (
|
|
totalInsight: number,
|
|
prestigeUpgrades: Record<string, number>,
|
|
memories: Memory[],
|
|
memorySlots: number
|
|
) => void;
|
|
|
|
// Loop management
|
|
startNewLoop: (insightGained: number) => void;
|
|
setLoopInsight: (insight: number) => void;
|
|
|
|
// Reset
|
|
resetPrestige: () => void;
|
|
|
|
// Debug helpers
|
|
debugSetSignedPacts: (pacts: number[]) => void;
|
|
debugSetPactDetails: (details: Record<number, {
|
|
floor: number;
|
|
guardianId: string;
|
|
signedAt: { day: number; hour: number };
|
|
skillLevels: Record<string, number>;
|
|
}>) => void;
|
|
}
|
|
|
|
// ─── Combined Prestige Store Type ────────────────────────────────────────────
|
|
|
|
export type PrestigeStore = PrestigeState & PrestigeActions;
|
|
|
|
// ─── Initial State ───────────────────────────────────────────────────────────
|
|
|
|
const initialState: PrestigeState = {
|
|
loopCount: 0,
|
|
insight: 0,
|
|
totalInsight: 0,
|
|
loopInsight: 0,
|
|
prestigeUpgrades: {},
|
|
memorySlots: 3,
|
|
pactSlots: 1,
|
|
memories: [],
|
|
defeatedGuardians: [],
|
|
signedPacts: [],
|
|
signedPactDetails: {},
|
|
pactRitualFloor: null,
|
|
pactRitualProgress: 0,
|
|
};
|
|
|
|
export const usePrestigeStore = create<PrestigeStore>()(
|
|
persist(
|
|
(set, get) => ({
|
|
...initialState,
|
|
|
|
doPrestige: (id: string) => {
|
|
const state = get();
|
|
const pd = PRESTIGE_DEF[id];
|
|
if (!pd) return fail(ErrorCode.INVALID_PRESTIGE_ID, `Unknown prestige upgrade: ${id}`);
|
|
|
|
const lvl = state.prestigeUpgrades[id] || 0;
|
|
if (lvl >= pd.max) return fail(ErrorCode.PRESTIGE_MAX_LEVEL, `Upgrade ${id} is already at max level (${pd.max})`);
|
|
if (state.insight < pd.cost) return fail(ErrorCode.INSUFFICIENT_INSIGHT, `Need ${pd.cost} insight, have ${state.insight}`);
|
|
|
|
const newPU = { ...state.prestigeUpgrades, [id]: lvl + 1 };
|
|
set({
|
|
insight: state.insight - pd.cost,
|
|
prestigeUpgrades: newPU,
|
|
memorySlots: id === 'deepMemory' ? state.memorySlots + 1 : state.memorySlots,
|
|
pactSlots: id === 'pactBinding' ? state.pactSlots + 1 : state.pactSlots,
|
|
});
|
|
return okVoid();
|
|
},
|
|
|
|
addMemory: (memory: Memory) => {
|
|
const state = get();
|
|
if (state.memories.length >= state.memorySlots) return;
|
|
if (state.memories.some(m => m.skillId === memory.skillId)) return;
|
|
|
|
set({ memories: [...state.memories, memory] });
|
|
},
|
|
|
|
removeMemory: (skillId: string) => {
|
|
set((state) => ({
|
|
memories: state.memories.filter(m => m.skillId !== skillId),
|
|
}));
|
|
},
|
|
|
|
clearMemories: () => {
|
|
set({ memories: [] });
|
|
},
|
|
|
|
startPactRitual: (floor: number, rawMana: number) => {
|
|
const state = get();
|
|
const guardian = GUARDIANS[floor];
|
|
if (!guardian) return fail(ErrorCode.INVALID_INPUT, `No guardian at floor ${floor}`);
|
|
|
|
if (!state.defeatedGuardians.includes(floor)) return fail(ErrorCode.GUARDIAN_NOT_DEFEATED, `Guardian at floor ${floor} has not been defeated`);
|
|
if (state.signedPacts.includes(floor)) return fail(ErrorCode.PACT_ALREADY_SIGNED, `Pact with ${guardian.name} is already signed`);
|
|
if (state.signedPacts.length >= state.pactSlots) return fail(ErrorCode.PACT_SLOTS_FULL, `All pact slots are full (${state.pactSlots})`);
|
|
if (rawMana < guardian.pactCost) return fail(ErrorCode.INSUFFICIENT_MANA, `Need ${guardian.pactCost} raw mana, have ${rawMana}`);
|
|
if (state.pactRitualFloor !== null) return fail(ErrorCode.RITUAL_IN_PROGRESS, `A pact ritual is already in progress for floor ${state.pactRitualFloor}`);
|
|
|
|
set({
|
|
pactRitualFloor: floor,
|
|
pactRitualProgress: 0,
|
|
});
|
|
return okVoid();
|
|
},
|
|
|
|
cancelPactRitual: () => {
|
|
set({
|
|
pactRitualFloor: null,
|
|
pactRitualProgress: 0,
|
|
});
|
|
},
|
|
|
|
completePactRitual: (addLog: (msg: string) => void) => {
|
|
const state = get();
|
|
if (state.pactRitualFloor === null) return;
|
|
|
|
const guardian = GUARDIANS[state.pactRitualFloor];
|
|
if (!guardian) return;
|
|
|
|
set({
|
|
signedPacts: [...state.signedPacts, state.pactRitualFloor],
|
|
defeatedGuardians: state.defeatedGuardians.filter(f => f !== state.pactRitualFloor),
|
|
pactRitualFloor: null,
|
|
pactRitualProgress: 0,
|
|
});
|
|
|
|
addLog(`📜 Pact signed with ${guardian.name}! You have gained their boons.`);
|
|
},
|
|
|
|
updatePactRitualProgress: (hours: number) => {
|
|
set((state) => ({
|
|
pactRitualProgress: state.pactRitualProgress + hours,
|
|
}));
|
|
},
|
|
|
|
removePact: (floor: number) => {
|
|
set((state) => ({
|
|
signedPacts: state.signedPacts.filter(f => f !== floor),
|
|
}));
|
|
},
|
|
|
|
defeatGuardian: (floor: number) => {
|
|
const state = get();
|
|
if (state.defeatedGuardians.includes(floor) || state.signedPacts.includes(floor)) return;
|
|
|
|
set({
|
|
defeatedGuardians: [...state.defeatedGuardians, floor],
|
|
});
|
|
},
|
|
|
|
addSignedPact: (floor: number) => {
|
|
const state = get();
|
|
if (state.signedPacts.includes(floor)) return;
|
|
set({ signedPacts: [...state.signedPacts, floor] });
|
|
},
|
|
|
|
removeDefeatedGuardian: (floor: number) => {
|
|
set((state) => ({
|
|
defeatedGuardians: state.defeatedGuardians.filter(f => f !== floor),
|
|
}));
|
|
},
|
|
|
|
setPactRitualFloor: (floor: number | null) => {
|
|
set({ pactRitualFloor: floor, pactRitualProgress: 0 });
|
|
},
|
|
|
|
addDefeatedGuardian: (floor: number) => {
|
|
const state = get();
|
|
if (state.defeatedGuardians.includes(floor) || state.signedPacts.includes(floor)) return;
|
|
set({ defeatedGuardians: [...state.defeatedGuardians, floor] });
|
|
},
|
|
|
|
incrementLoopCount: () => {
|
|
set((state) => ({ loopCount: state.loopCount + 1 }));
|
|
},
|
|
|
|
resetPrestigeForNewLoop: (
|
|
totalInsight: number,
|
|
prestigeUpgrades: Record<string, number>,
|
|
memories: Memory[],
|
|
memorySlots: number
|
|
) => {
|
|
set({
|
|
insight: totalInsight,
|
|
prestigeUpgrades,
|
|
memories,
|
|
memorySlots,
|
|
// Reset loop-specific state
|
|
defeatedGuardians: [],
|
|
signedPacts: [],
|
|
pactRitualFloor: null,
|
|
pactRitualProgress: 0,
|
|
loopInsight: 0,
|
|
});
|
|
},
|
|
|
|
startNewLoop: (insightGained: number) => {
|
|
const state = get();
|
|
set({
|
|
loopCount: state.loopCount + 1,
|
|
insight: state.insight + insightGained,
|
|
totalInsight: state.totalInsight + insightGained,
|
|
loopInsight: 0,
|
|
// Reset loop-specific state
|
|
defeatedGuardians: [],
|
|
signedPacts: [],
|
|
pactRitualFloor: null,
|
|
pactRitualProgress: 0,
|
|
});
|
|
},
|
|
|
|
setLoopInsight: (insight: number) => {
|
|
set({ loopInsight: insight });
|
|
},
|
|
|
|
resetPrestige: () => {
|
|
set(initialState);
|
|
},
|
|
|
|
// Debug helpers
|
|
debugSetSignedPacts: (pacts: number[]) => {
|
|
set({ signedPacts: pacts });
|
|
},
|
|
debugSetPactDetails: (details: Record<number, {
|
|
floor: number;
|
|
guardianId: string;
|
|
signedAt: { day: number; hour: number };
|
|
skillLevels: Record<string, number>;
|
|
}>) => {
|
|
set({ signedPactDetails: details });
|
|
},
|
|
}),
|
|
{
|
|
storage: createSafeStorage(),
|
|
name: 'mana-loop-prestige',
|
|
partialize: (state) => ({
|
|
loopCount: state.loopCount,
|
|
insight: state.insight,
|
|
totalInsight: state.totalInsight,
|
|
loopInsight: state.loopInsight,
|
|
prestigeUpgrades: state.prestigeUpgrades,
|
|
memorySlots: state.memorySlots,
|
|
pactSlots: state.pactSlots,
|
|
memories: state.memories,
|
|
defeatedGuardians: state.defeatedGuardians,
|
|
signedPacts: state.signedPacts,
|
|
signedPactDetails: state.signedPactDetails,
|
|
pactRitualFloor: state.pactRitualFloor,
|
|
pactRitualProgress: state.pactRitualProgress,
|
|
}),
|
|
}
|
|
)
|
|
);
|