Files
Mana-Loop/src/lib/game/stores/craftingStore.ts
T
n8n-gitea e45c206321
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m19s
fix: resolve enchanting spec vs code discrepancies (issue #324)
- D2: Move spell_iceShard to basic-spells.ts at cost 75 (canonical), remove duplicate from frost-spells.ts
- D7: disenchantEquipment now adds 'Ready for Enchantment' tag and resets rarity to 'common'
- D8: disenchantEquipment now credits recovered mana to raw mana pool
- D14: Wire enchantPower stat from discipline effects into efficiencyBonus via new getEnchantingEfficiencyBonus() helper
- D3: Fix spec file reference from crafting-attunements.ts to data/attunements.ts
- D1/D6: Add missing metalSpellFocus to spec capacity table
2026-06-09 09:33:30 +02:00

350 lines
13 KiB
TypeScript

// ─── Crafting Store ─────────────────────────────────────────────────────
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import type { EquipmentSlot } from '../types/equipmentSlot';
import type { CraftingStore, CraftingState } from './craftingStore.types';
import * as CraftingUtils from '../crafting-utils';
import * as CraftingDesign from '../crafting-design';
import { useManaStore } from './manaStore';
import { useCombatStore } from './combatStore';
import { useUIStore } from './uiStore';
import { getEnchantingEfficiencyBonus } from '../effects/discipline-effects';
import * as ApplicationActions from '../crafting-actions/application-actions';
import * as PreparationActions from '../crafting-actions/preparation-actions';
import { equipItem as equipItemAction, unequipItem as unequipItemAction } from '../crafting-actions/equipment-actions';
import { ErrorCode } from '../utils/result';
import { createSafeStorage } from '../utils/safe-persist';
import { createDefaultCraftingState } from './crafting-initial-state';
import { craftMaterial as craftMaterialAction } from '../crafting-actions/crafting-material-actions';
import { processEquipmentCraftingTick } from './crafting-equipment-tick';
import { startCraftingEquipment, cancelEquipmentCrafting, startFabricatorCrafting } from './pipelines/equipment-crafting';
export const useCraftingStore = create<CraftingStore>()(
persist(
(set, get) => {
const defaultState = createDefaultCraftingState();
return {
...defaultState,
// Actions
setDesignProgress: (progress) => set({ designProgress: progress }),
setDesignProgress2: (progress) => set({ designProgress2: progress }),
setPreparationProgress: (progress) => set({ preparationProgress: progress }),
setApplicationProgress: (progress) => set({ applicationProgress: progress }),
setEquipmentCraftingProgress: (progress) => set({ equipmentCraftingProgress: progress }),
// Enchantment design actions
startDesigningEnchantment: (name, equipmentTypeId, effects) => {
const state = get(); // crafting state
const efficiencyBonus = getEnchantingEfficiencyBonus();
const validation = CraftingDesign.validateDesignEffects(effects, equipmentTypeId, 0, state.unlockedEffects);
if (!validation.valid) return false;
const equipType = CraftingUtils.getEquipmentType(equipmentTypeId);
if (!equipType) return false;
const totalCapacityCost = CraftingDesign.calculateDesignCapacityCost(effects, efficiencyBonus);
if (totalCapacityCost > equipType.baseCapacity) return false;
let updates: Partial<CraftingState> = {};
if (!state.designProgress) {
updates = {
designProgress: {
designId: CraftingUtils.generateDesignId(),
progress: 0,
required: CraftingDesign.calculateDesignTime(effects),
name,
equipmentType: equipmentTypeId,
effects,
},
};
// Update currentAction in combatStore
useCombatStore.setState({ currentAction: 'design' });
} else if (!state.designProgress2) {
updates = {
designProgress2: {
designId: CraftingUtils.generateDesignId(),
progress: 0,
required: CraftingDesign.calculateDesignTime(effects),
name,
equipmentType: equipmentTypeId,
effects,
},
};
useCombatStore.setState({ currentAction: 'design' });
} else {
return false;
}
set(updates);
return true;
},
cancelDesign: (slot?: 1 | 2) => {
const state = get();
if (slot === 2) {
if (state.designProgress2) {
set({ designProgress2: null });
}
} else if (slot === 1) {
if (state.designProgress) {
set({ designProgress: null });
useCombatStore.setState({ currentAction: 'meditate' });
}
} else if (state.designProgress) {
set({ designProgress: null });
useCombatStore.setState({ currentAction: 'meditate' });
} else if (state.designProgress2) {
set({ designProgress2: null });
}
},
deleteDesign: (designId) => {
set((state) => ({
enchantmentDesigns: state.enchantmentDesigns.filter(d => d.id !== designId),
}));
},
// Enchantment design save
saveDesign: (design) => {
const state = get();
if (state.designProgress2 && state.designProgress2.designId === design.id) {
set((s) => ({
enchantmentDesigns: [...s.enchantmentDesigns, design],
designProgress2: null,
}));
} else {
set((s) => ({
enchantmentDesigns: [...s.enchantmentDesigns, design],
designProgress: null,
}));
useCombatStore.setState({ currentAction: 'meditate' });
}
},
// Enchantment application actions
startApplying: (equipmentInstanceId, designId) => {
const currentAction = useCombatStore.getState().currentAction;
const result = ApplicationActions.startApplying(
equipmentInstanceId,
designId,
get,
set as unknown as (partial: Partial<CraftingState>) => void,
currentAction
);
if (result) {
useCombatStore.setState({ currentAction: 'enchant' });
}
return result;
},
pauseApplication: () => {
ApplicationActions.pauseApplication(get, set as unknown as (partial: Partial<CraftingState>) => void);
},
resumeApplication: () => {
ApplicationActions.resumeApplication(get, set as unknown as (partial: Partial<CraftingState>) => void);
},
cancelApplication: () => {
ApplicationActions.cancelApplication(get, set as unknown as (partial: Partial<CraftingState>) => void);
useCombatStore.setState({ currentAction: 'meditate' });
},
// Preparation actions
startPreparing: (equipmentInstanceId) => {
const rawMana = useManaStore.getState().rawMana;
const result = PreparationActions.startPreparing(
equipmentInstanceId,
rawMana,
get,
set
);
if (result) {
useCombatStore.setState({ currentAction: 'prepare' });
set({ lastError: null });
} else {
const state = get();
const instance = state.equipmentInstances[equipmentInstanceId];
let message = 'Cannot start preparation';
if (!instance) {
message = `Equipment instance not found: ${equipmentInstanceId}`;
} else if (instance.tags?.includes('Ready for Enchantment')) {
message = 'Equipment is already prepared';
} else {
message = 'Insufficient mana for preparation';
}
set({ lastError: { code: ErrorCode.INVALID_INPUT, message, timestamp: Date.now() } });
}
return result;
},
cancelPreparation: () => {
PreparationActions.cancelPreparation(get, set);
useCombatStore.setState({ currentAction: 'meditate' });
},
startCraftingEquipment: (blueprintId: string) => startCraftingEquipment(blueprintId, get, set),
cancelEquipmentCrafting: () => cancelEquipmentCrafting(get, set),
// Fabricator crafting — uses elemental mana instead of raw mana
startFabricatorCrafting: (recipeId: string) => startFabricatorCrafting(recipeId, get, set),
// Material crafting — instant crafting of materials
craftMaterial: (recipeId: string) => {
const state = get();
const result = craftMaterialAction(recipeId, state.lootInventory.materials);
if (!result.success) return false;
if (result.newMaterials) {
set((s) => ({ lootInventory: { ...s.lootInventory, materials: result.newMaterials! } }));
}
return true;
},
// Enchantment selection actions
setSelectedEquipmentType: (type) => {
set((s) => ({ enchantmentSelection: { ...s.enchantmentSelection, selectedEquipmentType: type }}));
},
setSelectedEffects: (effects) => {
set((s) => ({ enchantmentSelection: { ...s.enchantmentSelection, selectedEffects: effects } }));
},
setDesignName: (name) => {
set((s) => ({ enchantmentSelection: { ...s.enchantmentSelection, designName: name } }));
},
setSelectedDesign: (id) => {
set((s) => ({ enchantmentSelection: { ...s.enchantmentSelection, selectedDesign: id } }));
},
setSelectedEquipmentInstance: (id) => {
set((s) => ({ enchantmentSelection: { ...s.enchantmentSelection, selectedEquipmentInstance: id } }));
},
resetEnchantmentSelection: () => {
set((s) => ({
enchantmentSelection: {
selectedEquipmentType: null,
selectedEffects: [],
designName: '',
selectedDesign: null,
selectedEquipmentInstance: null,
},
}));
},
// Loot inventory actions
deleteMaterial: (materialId: string, amount: number) => {
set((state) => {
const newMaterials = { ...state.lootInventory.materials };
const currentAmount = newMaterials[materialId] || 0;
const newAmount = Math.max(0, currentAmount - amount);
if (newAmount <= 0) {
delete newMaterials[materialId];
} else {
newMaterials[materialId] = newAmount;
}
return {
lootInventory: {
...state.lootInventory,
materials: newMaterials,
},
};
});
},
deleteEquipmentInstance: (instanceId: string) => {
set((state) => {
let newEquipped = { ...state.equippedInstances };
for (const [slot, id] of Object.entries(newEquipped)) {
if (id === instanceId) {
newEquipped[slot as EquipmentSlot] = null;
}
}
const newInstances = { ...state.equipmentInstances };
delete newInstances[instanceId];
return {
equippedInstances: newEquipped,
equipmentInstances: newInstances,
};
});
},
equipItem: (instanceId: string, slot: EquipmentSlot) => {
return equipItemAction(instanceId, slot, get, set);
},
unequipItem: (slot: EquipmentSlot) => {
unequipItemAction(slot, set);
},
clearLastError: () => set({ lastError: null }),
unlockEffects: (effectIds: string[]) => {
set((state) => {
const existing = new Set(state.unlockedEffects);
let changed = false;
for (const id of effectIds) {
if (!existing.has(id)) {
existing.add(id);
changed = true;
}
}
if (!changed) return state;
return { unlockedEffects: Array.from(existing) };
});
},
unlockRecipes: (recipeIds: string[]) => {
set((state) => {
const existing = new Set(state.unlockedRecipes);
let changed = false;
for (const id of recipeIds) {
if (!existing.has(id)) {
existing.add(id);
changed = true;
}
}
if (!changed) return state;
return { unlockedRecipes: Array.from(existing) };
});
},
processEquipmentCraftingTick: (): { completed: boolean; logMessage?: string } => {
const state = get();
return processEquipmentCraftingTick(state, set as unknown as (partial: Partial<CraftingState>) => void);
},
resetCrafting: () => {
set(createDefaultCraftingState());
},
};
},
{
storage: createSafeStorage(),
name: 'mana-loop-crafting',
version: 1,
partialize: (state) => ({
designProgress: state.designProgress,
designProgress2: state.designProgress2,
preparationProgress: state.preparationProgress,
applicationProgress: state.applicationProgress,
equipmentCraftingProgress: state.equipmentCraftingProgress,
enchantmentDesigns: state.enchantmentDesigns,
unlockedEffects: state.unlockedEffects,
unlockedRecipes: state.unlockedRecipes,
equipmentInstances: state.equipmentInstances,
equippedInstances: state.equippedInstances,
lootInventory: state.lootInventory,
enchantmentSelection: state.enchantmentSelection,
lastError: state.lastError,
}),
}
)
);