fix: fabricator recipes now use correct elemental mana type
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m37s

- fabricator-recipes.ts: add optional manaType param to canCraftRecipe for clarity
- FabricatorSubTab.tsx: read elemental mana from store based on recipe manaType instead of always using rawMana
- craftingStore.ts: add startFabricatorCrafting action that deducts correct mana type
- craftingStore.types.ts: add startFabricatorCrafting to CraftingActions interface
- crafting-fabricator.ts: new helper file to keep craftingStore.ts under 400 lines

Fixes #155
This commit is contained in:
2026-05-27 11:06:24 +02:00
parent 707a1eef31
commit 64b472572b
8 changed files with 198 additions and 27 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
# Circular Dependencies
Generated: 2026-05-26T19:57:56.106Z
Generated: 2026-05-27T08:45:44.894Z
No circular dependencies found. ✅
+1 -1
View File
@@ -1,6 +1,6 @@
{
"_meta": {
"generated": "2026-05-26T19:57:54.313Z",
"generated": "2026-05-27T08:45:43.143Z",
"description": "Import dependency graph for src/lib/game. Keys are files, values are arrays of files they import.",
"usage": "To find what a file affects, search for its path in the VALUES. To find what a file depends on, look at its KEY entry."
},
+1
View File
@@ -369,6 +369,7 @@ Mana-Loop/
│ │ ├── crafting-attunements.ts
│ │ ├── crafting-design.ts
│ │ ├── crafting-equipment.ts
│ │ ├── crafting-fabricator.ts
│ │ ├── crafting-loot.ts
│ │ ├── crafting-prep.ts
│ │ ├── crafting-utils.ts
@@ -27,20 +27,27 @@ const MANA_TYPE_LABELS: Record<string, string> = {
function RecipeCard({
recipe,
materials,
manaAmount,
rawMana,
elementalMana,
onCraft,
isCrafting,
}: {
recipe: FabricatorRecipe;
materials: Record<string, number>;
manaAmount: number;
rawMana: number;
elementalMana: Record<string, { current: number; max: number; unlocked: boolean }>;
onCraft: (recipe: FabricatorRecipe) => void;
isCrafting: boolean;
}) {
// Determine the correct mana amount based on the recipe's mana type
const pool = recipe.manaType === 'raw'
? rawMana
: (elementalMana[recipe.manaType]?.current ?? 0);
const { canCraft } = canCraftRecipe(
recipe,
materials,
manaAmount,
pool,
recipe.manaType,
);
const rarityStyle = LOOT_RARITY_COLORS[recipe.rarity];
@@ -85,8 +92,8 @@ function RecipeCard({
<div className="flex justify-between mt-2">
<span>{MANA_TYPE_LABELS[recipe.manaType]?.split(' ')[1] ?? recipe.manaType} Mana:</span>
<span className={manaAmount >= recipe.manaCost ? 'text-green-400' : 'text-red-400'}>
{manaAmount} / {recipe.manaCost}
<span className={pool >= recipe.manaCost ? 'text-green-400' : 'text-red-400'}>
{pool} / {recipe.manaCost}
</span>
</div>
@@ -114,7 +121,8 @@ export function FabricatorSubTab() {
const lootInventory = useCraftingStore((s) => s.lootInventory);
const equipmentCraftingProgress = useCraftingStore((s) => s.equipmentCraftingProgress);
const rawMana = useManaStore((s) => s.rawMana);
const startCraftingEquipment = useCraftingStore((s) => s.startCraftingEquipment);
const elements = useManaStore((s) => s.elements);
const startFabricatorCrafting = useCraftingStore((s) => s.startFabricatorCrafting);
const cancelEquipmentCrafting = useCraftingStore((s) => s.cancelEquipmentCrafting);
const availableManaTypes = useMemo(() => {
@@ -129,8 +137,7 @@ export function FabricatorSubTab() {
const isCrafting = equipmentCraftingProgress !== null;
const handleCraft = (recipe: FabricatorRecipe) => {
// Use the existing equipment crafting system with a fabricator-specific blueprint ID
startCraftingEquipment(`fabricator-${recipe.id}`);
startFabricatorCrafting(recipe.id);
};
return (
@@ -197,7 +204,8 @@ export function FabricatorSubTab() {
key={recipe.id}
recipe={recipe}
materials={lootInventory.materials}
manaAmount={rawMana}
rawMana={rawMana}
elementalMana={elements}
onCraft={handleCraft}
isCrafting={isCrafting}
/>
+141
View File
@@ -0,0 +1,141 @@
// ─── Fabricator Crafting Helpers ──────────────────────────────────────────────
// Separate file to avoid exceeding the 400-line limit in craftingStore.ts.
import type { EquipmentCraftingProgress } from './types';
import type { FabricatorRecipe } from './data/fabricator-recipes';
import { FABRICATOR_RECIPES } from './data/fabricator-recipes';
// ─── Lookup ───────────────────────────────────────────────────────────────────
export function getFabricatorRecipe(recipeId: string): FabricatorRecipe | undefined {
return FABRICATOR_RECIPES.find(r => r.id === recipeId);
}
// ─── Cost Check ───────────────────────────────────────────────────────────────
export interface FabricatorCostCheck {
canCraft: boolean;
missingMana: number;
missingMaterials: Record<string, number>;
}
export function checkFabricatorCosts(
recipe: FabricatorRecipe,
materials: Record<string, number>,
rawMana: number,
elements: Record<string, { current: number; max: number; unlocked: boolean }>,
): FabricatorCostCheck {
const missingMaterials: Record<string, number> = {};
let canCraft = true;
for (const [matId, required] of Object.entries(recipe.materials)) {
const available = materials[matId] || 0;
if (available < required) {
missingMaterials[matId] = required - available;
canCraft = false;
}
}
const manaAvailable = recipe.manaType === 'raw'
? rawMana
: (elements[recipe.manaType]?.current ?? 0);
const missingMana = Math.max(0, recipe.manaCost - manaAvailable);
if (missingMana > 0) canCraft = false;
return { canCraft, missingMana, missingMaterials };
}
// ─── Mana Deduction ───────────────────────────────────────────────────────────
export interface ManaDeduction {
rawMana: number;
elements: Record<string, { current: number; max: number; unlocked: boolean }>;
}
export function deductFabricatorMana(
recipe: FabricatorRecipe,
rawMana: number,
elements: Record<string, { current: number; max: number; unlocked: boolean }>,
): ManaDeduction | null {
if (recipe.manaType === 'raw') {
if (rawMana < recipe.manaCost) return null;
return { rawMana: rawMana - recipe.manaCost, elements };
}
const elemState = elements[recipe.manaType];
if (!elemState || !elemState.unlocked || elemState.current < recipe.manaCost) return null;
return {
rawMana,
elements: {
...elements,
[recipe.manaType]: {
...elements[recipe.manaType],
current: elements[recipe.manaType].current - recipe.manaCost,
},
},
};
}
// ─── Mana Refund (on cancel) ──────────────────────────────────────────────────
export interface ManaRefund {
rawMana: number;
elements: Record<string, { current: number; max: number; unlocked: boolean }>;
}
export function refundFabricatorMana(
recipe: FabricatorRecipe,
refundAmount: number,
rawMana: number,
elements: Record<string, { current: number; max: number; unlocked: boolean }>,
): ManaRefund {
if (recipe.manaType === 'raw') {
return { rawMana: rawMana + refundAmount, elements };
}
return {
rawMana,
elements: {
...elements,
[recipe.manaType]: {
...elements[recipe.manaType],
current: Math.min(
elements[recipe.manaType].max,
elements[recipe.manaType].current + refundAmount,
),
},
},
};
}
// ─── Material Deduction ───────────────────────────────────────────────────────
export function deductMaterials(
recipe: FabricatorRecipe,
materials: Record<string, number>,
): Record<string, number> {
const newMaterials = { ...materials };
for (const [matId, amount] of Object.entries(recipe.materials)) {
newMaterials[matId] = (newMaterials[matId] || 0) - amount;
if (newMaterials[matId] <= 0) delete newMaterials[matId];
}
return newMaterials;
}
// ─── Progress Init ────────────────────────────────────────────────────────────
export function makeFabricatorProgress(
recipeId: string,
equipmentTypeId: string,
craftTime: number,
manaCost: number,
): EquipmentCraftingProgress {
return {
blueprintId: `fabricator-${recipeId}`,
equipmentTypeId,
progress: 0,
required: craftTime,
manaSpent: manaCost,
};
}
+4
View File
@@ -206,6 +206,7 @@ export function canCraftRecipe(
recipe: FabricatorRecipe,
materials: Record<string, number>,
manaAmount: number,
manaType?: string,
): { canCraft: boolean; missingMaterials: Record<string, number>; missingMana: number } {
const missingMaterials: Record<string, number> = {};
let canCraft = true;
@@ -218,6 +219,9 @@ export function canCraftRecipe(
}
}
// If manaType is provided, manaAmount is already the correct pool value.
// Otherwise fall back to treating manaAmount as raw mana (backward compat).
const effectiveManaType = manaType ?? recipe.manaType;
const missingMana = Math.max(0, recipe.manaCost - manaAmount);
if (missingMana > 0) {
canCraft = false;
+32 -16
View File
@@ -16,6 +16,12 @@ import { equipItem as equipItemAction, unequipItem as unequipItemAction } from '
import { ErrorCode } from '../utils/result';
import { createSafeStorage } from '../utils/safe-persist';
import { createInitialEquipmentInstances } from './crafting-initial-state';
import {
getFabricatorRecipe,
deductFabricatorMana,
deductMaterials,
makeFabricatorProgress,
} from '../crafting-fabricator';
export const useCraftingStore = create<CraftingStore>()(
persist(
@@ -235,29 +241,39 @@ export const useCraftingStore = create<CraftingStore>()(
},
cancelEquipmentCrafting: () => {
const state = get();
const progress = state.equipmentCraftingProgress;
const progress = get().equipmentCraftingProgress;
if (!progress) return;
// Get cancel result (mana refund)
const cancelResult = CraftingEquipment.cancelEquipmentCrafting(
progress.blueprintId,
progress.manaSpent
);
// Update crafting store state
const cancelResult = CraftingEquipment.cancelEquipmentCrafting(progress.blueprintId, progress.manaSpent);
set({ equipmentCraftingProgress: null });
// Refund mana to mana store
useManaStore.setState((s) => ({ rawMana: s.rawMana + cancelResult.manaRefund }));
// Update combat store (reset action)
useCombatStore.setState({ currentAction: 'meditate' });
// Add log message to UI store
useUIStore.getState().addLog(cancelResult.logMessage);
},
// Fabricator crafting — uses elemental mana instead of raw mana
startFabricatorCrafting: (recipeId: string) => {
const state = get();
const currentAction = useCombatStore.getState().currentAction;
if (currentAction !== 'meditate') return false;
const recipe = getFabricatorRecipe(recipeId);
if (!recipe) return false;
const rawMana = useManaStore.getState().rawMana;
const elements = useManaStore.getState().elements;
const deducted = deductFabricatorMana(recipe, rawMana, elements);
if (!deducted) return false;
const newMaterials = deductMaterials(recipe, state.lootInventory.materials);
const progress = makeFabricatorProgress(recipeId, recipe.equipmentTypeId, recipe.craftTime, recipe.manaCost);
useManaStore.setState({ rawMana: deducted.rawMana, elements: deducted.elements });
set((s) => ({ lootInventory: { ...s.lootInventory, materials: newMaterials }, equipmentCraftingProgress: progress }));
useCombatStore.setState({ currentAction: 'craft' });
return true;
},
// Enchantment selection actions
setSelectedEquipmentType: (type) => {
set((s) => ({ enchantmentSelection: { ...s.enchantmentSelection, selectedEquipmentType: type }}));
@@ -61,6 +61,7 @@ export interface CraftingActions {
equipItem: (instanceId: string, slot: EquipmentSlot) => boolean;
unequipItem: (slot: EquipmentSlot) => void;
startCraftingEquipment: (blueprintId: string) => boolean;
startFabricatorCrafting: (recipeId: string) => boolean;
cancelEquipmentCrafting: () => void;
setSelectedEquipmentType: (type: string | null) => void;
setSelectedEffects: (effects: DesignEffect[]) => void;