e45c206321
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m19s
- 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
350 lines
13 KiB
TypeScript
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,
|
|
}),
|
|
}
|
|
)
|
|
);
|