refactor: split bloated state types into State + Actions interfaces (issue #102)
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m19s

- CombatState: split into CombatState (data) + CombatActions + CombatStore
- PrestigeState: split into PrestigeState (data) + PrestigeActions + PrestigeStore
- ManaState: split into ManaState (data) + ManaActions + ManaStore
- GameState: deprecated, removed from barrel exports
- crafting-actions: updated to use CraftingState instead of GameState
- combat-utils/mana-utils: replaced Pick<GameState,...> with focused interfaces
- DisciplineCardProps: split into Definition + Runtime + Callbacks
- stores/index.ts: now exports both State and Actions types
This commit is contained in:
2026-05-20 21:05:22 +02:00
parent ee893e8973
commit 8a7ddaae27
24 changed files with 411 additions and 321 deletions
+2 -2
View File
@@ -3,7 +3,7 @@
// All external data (signedPacts, etc.) is passed in as parameters.
import { SPELLS_DEF, GUARDIANS, HOURS_PER_TICK } from '../constants';
import type { CombatState } from './combat-state.types';
import type { CombatStore, CombatState } from './combat-state.types';
import type { SpellState } from '../types';
import { getFloorMaxHP, getFloorElement, calcDamage, canAffordSpellCost, deductSpellCost } from '../utils';
import { computeDisciplineEffects } from '../effects/discipline-effects';
@@ -22,7 +22,7 @@ export interface CombatTickResult {
}
export function processCombatTick(
get: () => CombatState,
get: () => CombatStore,
set: (state: Partial<CombatState>) => void,
rawMana: number,
elements: Record<string, { current: number; max: number; unlocked: boolean }>,
+10 -2
View File
@@ -3,6 +3,8 @@
import type { GameAction, SpellState, FloorState, GolemancyState, ActivityLogEntry, AchievementState, EquipmentSpellState, ActivityEventType } from '../types';
// ─── Combat State (data only) ─────────────────────────────────────────────────
export interface CombatState {
// Floor state
currentFloor: number;
@@ -49,8 +51,11 @@ export interface CombatState {
totalSpellsCast: number;
totalDamageDealt: number;
totalCraftsCompleted: number;
}
// Actions
// ─── Combat Actions ───────────────────────────────────────────────────────────
export interface CombatActions {
setCurrentFloor: (floor: number) => void;
advanceFloor: () => void;
setFloorHP: (hp: number) => void;
@@ -119,5 +124,8 @@ export interface CombatState {
// Debug helpers
debugSetFloor: (floor: number) => void;
resetFloorHP: () => void;
debugSetTime: (day: number, hour: number) => void;
}
// ─── Combined Combat Store Type ───────────────────────────────────────────────
export type CombatStore = CombatState & CombatActions;
+2 -2
View File
@@ -9,9 +9,9 @@ import { usePrestigeStore } from './prestigeStore';
import { generateFloorState } from '../utils/room-utils';
import { addActivityLogEntry } from '../utils/activity-log';
import { processCombatTick, makeInitialSpells } from './combat-actions';
import type { CombatState } from './combat-state.types';
import type { CombatStore } from './combat-state.types';
export const useCombatStore = create<CombatState>()(
export const useCombatStore = create<CombatStore>()(
persist(
(set, get) => ({
currentFloor: 1,
+6 -4
View File
@@ -183,24 +183,26 @@ export const useCraftingStore = create<CraftingStore>()(
// Enchantment application actions
startApplying: (equipmentInstanceId, designId) => {
const currentAction = useCombatStore.getState().currentAction;
return ApplicationActions.startApplying(
equipmentInstanceId,
designId,
get,
set
set as unknown as (partial: Partial<CraftingState>) => void,
currentAction
);
},
pauseApplication: () => {
ApplicationActions.pauseApplication(get, set);
ApplicationActions.pauseApplication(get, set as unknown as (partial: Partial<CraftingState>) => void);
},
resumeApplication: () => {
ApplicationActions.resumeApplication(get, set);
ApplicationActions.resumeApplication(get, set as unknown as (partial: Partial<CraftingState>) => void);
},
cancelApplication: () => {
ApplicationActions.cancelApplication(set);
ApplicationActions.cancelApplication(set as unknown as (partial: Partial<CraftingState>) => void);
useCombatStore.setState({ currentAction: 'meditate' });
},
+1 -1
View File
@@ -39,7 +39,7 @@ export const createGatherMana = () => () => {
// Compute click mana with discipline bonuses (mana-channeling → clickManaMultiplier)
const cm = computeClickMana(
{ skills: {} },
{},
disciplineEffects,
);
+1 -3
View File
@@ -76,9 +76,7 @@ export function useManaStats() {
disciplineEffects,
);
const clickMana = computeClickMana({
skills: {},
}, disciplineEffects);
const clickMana = computeClickMana({}, disciplineEffects);
const meditationMultiplier = getMeditationBonus(meditateTicks, {}, upgradeEffects.meditationEfficiency);
const incursionStrength = getIncursionStrength(day, hour);
+4 -3
View File
@@ -6,13 +6,13 @@ export { useUIStore } from './uiStore';
export type { UIState } from './uiStore';
export { usePrestigeStore } from './prestigeStore';
export type { PrestigeState } from './prestigeStore';
export type { PrestigeState, PrestigeActions, PrestigeStore } from './prestigeStore';
export { useManaStore, makeInitialElements } from './manaStore';
export type { ManaState } from './manaStore';
export type { ManaState, ManaActions, ManaStore } from './manaStore';
export { useCombatStore, makeInitialSpells } from './combatStore';
export type { CombatState } from './combat-state.types';
export type { CombatState, CombatActions, CombatStore } from './combat-state.types';
export { useCraftingStore } from './craftingStore';
export type { CraftingState, CraftingActions } from './craftingStore.types';
@@ -21,6 +21,7 @@ export { useAttunementStore } from './attunementStore';
export type { AttunementStoreState } from './attunementStore';
export { useDisciplineStore } from './discipline-slice';
export type { DisciplineStoreState, DisciplineStoreActions, DisciplineStore } from './discipline-slice';
export { useGameStore } from './gameStore';
export { useGameLoop } from './gameHooks';
+55 -46
View File
@@ -6,23 +6,28 @@ import { persist } from 'zustand/middleware';
import { ELEMENTS, MANA_PER_ELEMENT, BASE_UNLOCKED_ELEMENTS } from '../constants';
import type { ElementState } from '../types';
// ─── Mana State (data only) ─────────────────────────────────────────────────
export interface ManaState {
rawMana: number;
meditateTicks: number;
totalManaGathered: number;
elements: Record<string, ElementState>;
// Actions
}
// ─── Mana Actions ────────────────────────────────────────────────────────────
export interface ManaActions {
setRawMana: (amount: number) => void;
addRawMana: (amount: number, maxMana: number) => void;
spendRawMana: (amount: number) => boolean;
gatherMana: (amount: number, maxMana: number) => void;
// Meditation
setMeditateTicks: (ticks: number) => void;
incrementMeditateTicks: () => void;
resetMeditateTicks: () => void;
// Elements
convertMana: (element: string, amount: number) => boolean;
unlockElement: (element: string, cost: number) => boolean;
@@ -30,10 +35,10 @@ export interface ManaState {
spendElementMana: (element: string, amount: number) => boolean;
setElementMax: (max: number) => void;
craftComposite: (target: string, recipe: string[]) => boolean;
// Helper for gameStore coordination
processConvertAction: (rawMana: number) => { rawMana: number; elements: Record<string, ElementState> } | null;
// Reset
resetMana: (
prestigeUpgrades: Record<string, number>,
@@ -43,7 +48,11 @@ export interface ManaState {
) => void;
}
export const useManaStore = create<ManaState>()(
// ─── Combined Mana Store Type ────────────────────────────────────────────────
export type ManaStore = ManaState & ManaActions;
export const useManaStore = create<ManaStore>()(
persist(
(set, get) => ({
rawMana: 10,
@@ -59,62 +68,62 @@ export const useManaStore = create<ManaState>()(
}
])
) as Record<string, ElementState>,
setRawMana: (amount: number) => {
set({ rawMana: Math.max(0, amount) });
},
addRawMana: (amount: number, maxMana: number) => {
set((state) => ({
rawMana: Math.min(state.rawMana + amount, maxMana),
totalManaGathered: state.totalManaGathered + amount,
}));
},
spendRawMana: (amount: number) => {
const state = get();
if (state.rawMana < amount) return false;
set({ rawMana: state.rawMana - amount });
return true;
},
gatherMana: (amount: number, maxMana: number) => {
set((state) => ({
rawMana: Math.min(state.rawMana + amount, maxMana),
totalManaGathered: state.totalManaGathered + amount,
}));
},
setMeditateTicks: (ticks: number) => {
set({ meditateTicks: ticks });
},
incrementMeditateTicks: () => {
set((state) => ({ meditateTicks: state.meditateTicks + 1 }));
},
resetMeditateTicks: () => {
set({ meditateTicks: 0 });
},
convertMana: (element: string, amount: number) => {
const state = get();
const elem = state.elements[element];
if (!elem?.unlocked) return false;
const cost = MANA_PER_ELEMENT * amount;
if (state.rawMana < cost) return false;
if (elem.current >= elem.max) return false;
const canConvert = Math.min(
amount,
Math.floor(state.rawMana / MANA_PER_ELEMENT),
elem.max - elem.current
);
if (canConvert <= 0) return false;
set({
rawMana: state.rawMana - canConvert * MANA_PER_ELEMENT,
elements: {
@@ -122,15 +131,15 @@ export const useManaStore = create<ManaState>()(
[element]: { ...elem, current: elem.current + canConvert },
},
});
return true;
},
unlockElement: (element: string, cost: number) => {
const state = get();
if (state.elements[element]?.unlocked) return false;
if (state.rawMana < cost) return false;
set({
rawMana: state.rawMana - cost,
elements: {
@@ -138,15 +147,15 @@ export const useManaStore = create<ManaState>()(
[element]: { ...state.elements[element], unlocked: true },
},
});
return true;
},
addElementMana: (element: string, amount: number, max: number) => {
set((state) => {
const elem = state.elements[element];
if (!elem) return state;
return {
elements: {
...state.elements,
@@ -158,22 +167,22 @@ export const useManaStore = create<ManaState>()(
};
});
},
spendElementMana: (element: string, amount: number) => {
const state = get();
const elem = state.elements[element];
if (!elem || elem.current < amount) return false;
set({
elements: {
...state.elements,
[element]: { ...elem, current: elem.current - amount },
},
});
return true;
},
setElementMax: (max: number) => {
set((state) => ({
elements: Object.fromEntries(
@@ -181,21 +190,21 @@ export const useManaStore = create<ManaState>()(
) as Record<string, ElementState>,
}));
},
craftComposite: (target: string, recipe: string[]) => {
const state = get();
// Count required ingredients
const costs: Record<string, number> = {};
recipe.forEach(r => {
costs[r] = (costs[r] || 0) + 1;
});
// Check if we have all ingredients
for (const [r, amt] of Object.entries(costs)) {
if ((state.elements[r]?.current || 0) < amt) return false;
}
// Deduct ingredients
const newElems = { ...state.elements };
for (const [r, amt] of Object.entries(costs)) {
@@ -204,7 +213,7 @@ export const useManaStore = create<ManaState>()(
current: newElems[r].current - amt,
};
}
// Add crafted element
const targetElem = newElems[target];
newElems[target] = {
@@ -212,38 +221,38 @@ export const useManaStore = create<ManaState>()(
current: (targetElem?.current || 0) + 1,
unlocked: true,
};
set({ elements: newElems });
return true;
},
processConvertAction: (rawMana: number) => {
const state = get();
const elements = { ...state.elements };
const unlockedElements = Object.entries(elements)
.filter(([, e]) => e.unlocked && e.current < e.max);
if (unlockedElements.length === 0 || rawMana < 100) return null;
unlockedElements.sort((a, b) => (b[1].max - b[1].current) - (a[1].max - a[1].current));
const [targetId, targetState] = unlockedElements[0];
const canConvert = Math.min(
Math.floor(rawMana / 100),
targetState.max - targetState.current
);
if (canConvert <= 0) return null;
rawMana -= canConvert * 100;
const updatedElements = {
...elements,
[targetId]: { ...targetState, current: targetState.current + canConvert }
};
return { rawMana, elements: updatedElements };
},
resetMana: (
prestigeUpgrades: Record<string, number>,
skills: Record<string, number> = {},
@@ -253,7 +262,7 @@ export const useManaStore = create<ManaState>()(
const elementMax = 10 + (prestigeUpgrades.elemMax || 0) * 5;
const startingMana = 10 + (prestigeUpgrades.manaStart || 0) * 10;
const elements = makeInitialElements(elementMax, prestigeUpgrades);
set({
rawMana: startingMana,
meditateTicks: 0,
@@ -279,7 +288,7 @@ export function makeInitialElements(
prestigeUpgrades: Record<string, number> = {}
): Record<string, ElementState> {
const elemStart = (prestigeUpgrades.elemStart || 0) * 5;
const elements: Record<string, ElementState> = {};
Object.keys(ELEMENTS).forEach(k => {
const isUnlocked = BASE_UNLOCKED_ELEMENTS.includes(k);
@@ -289,6 +298,6 @@ export function makeInitialElements(
unlocked: isUnlocked,
};
});
return elements;
}
+59 -53
View File
@@ -6,23 +6,25 @@ import { persist } from 'zustand/middleware';
import type { Memory } from '../types';
import { GUARDIANS, PRESTIGE_DEF } from '../constants';
// ─── Prestige State (data only) ──────────────────────────────────────────────
export interface PrestigeState {
// Loop counter
loopCount: number;
// Insight
insight: number;
totalInsight: number;
loopInsight: number; // Insight earned at end of current loop
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[];
@@ -34,8 +36,11 @@ export interface PrestigeState {
}>;
pactRitualFloor: number | null;
pactRitualProgress: number;
// Actions
}
// ─── Prestige Actions ────────────────────────────────────────────────────────
export interface PrestigeActions {
doPrestige: (id: string) => boolean;
addMemory: (memory: Memory) => void;
removeMemory: (skillId: string) => void;
@@ -46,7 +51,7 @@ export interface PrestigeState {
updatePactRitualProgress: (hours: number) => void;
removePact: (floor: number) => void;
defeatGuardian: (floor: number) => void;
// Methods called by gameStore
addSignedPact: (floor: number) => void;
removeDefeatedGuardian: (floor: number) => void;
@@ -59,14 +64,14 @@ export interface PrestigeState {
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, {
@@ -77,40 +82,41 @@ export interface PrestigeState {
}>) => void;
}
const initialState = {
// ─── Combined Prestige Store Type ────────────────────────────────────────────
export type PrestigeStore = PrestigeState & PrestigeActions;
// ─── Initial State ───────────────────────────────────────────────────────────
const initialState: PrestigeState = {
loopCount: 0,
insight: 0,
totalInsight: 0,
loopInsight: 0,
prestigeUpgrades: {} as Record<string, number>,
prestigeUpgrades: {},
memorySlots: 3,
pactSlots: 1,
memories: [] as Memory[],
defeatedGuardians: [] as number[],
signedPacts: [] as number[],
signedPactDetails: {} as Record<number, {
floor: number;
guardianId: string;
signedAt: { day: number; hour: number };
skillLevels: Record<string, number>;
}>,
pactRitualFloor: null as number | null,
memories: [],
defeatedGuardians: [],
signedPacts: [],
signedPactDetails: {},
pactRitualFloor: null,
pactRitualProgress: 0,
};
export const usePrestigeStore = create<PrestigeState>()(
export const usePrestigeStore = create<PrestigeStore>()(
persist(
(set, get) => ({
...initialState,
doPrestige: (id: string) => {
const state = get();
const pd = PRESTIGE_DEF[id];
if (!pd) return false;
const lvl = state.prestigeUpgrades[id] || 0;
if (lvl >= pd.max || state.insight < pd.cost) return false;
const newPU = { ...state.prestigeUpgrades, [id]: lvl + 1 };
set({
insight: state.insight - pd.cost,
@@ -120,114 +126,114 @@ export const usePrestigeStore = create<PrestigeState>()(
});
return true;
},
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 false;
if (!state.defeatedGuardians.includes(floor)) return false;
if (state.signedPacts.includes(floor)) return false;
if (state.signedPacts.length >= state.pactSlots) return false;
if (rawMana < guardian.pactCost) return false;
if (state.pactRitualFloor !== null) return false;
set({
pactRitualFloor: floor,
pactRitualProgress: 0,
});
return true;
},
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>,
@@ -247,7 +253,7 @@ export const usePrestigeStore = create<PrestigeState>()(
loopInsight: 0,
});
},
startNewLoop: (insightGained: number) => {
const state = get();
set({
@@ -262,15 +268,15 @@ export const usePrestigeStore = create<PrestigeState>()(
pactRitualProgress: 0,
});
},
setLoopInsight: (insight: number) => {
set({ loopInsight: insight });
},
resetPrestige: () => {
set(initialState);
},
// Debug helpers
debugSetSignedPacts: (pacts: number[]) => {
set({ signedPacts: pacts });