fix: fabricator recipes now use correct elemental mana type
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m37s
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:
@@ -1,4 +1,4 @@
|
|||||||
# Circular Dependencies
|
# Circular Dependencies
|
||||||
Generated: 2026-05-26T19:57:56.106Z
|
Generated: 2026-05-27T08:45:44.894Z
|
||||||
|
|
||||||
No circular dependencies found. ✅
|
No circular dependencies found. ✅
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_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.",
|
"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."
|
"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."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -369,6 +369,7 @@ Mana-Loop/
|
|||||||
│ │ ├── crafting-attunements.ts
|
│ │ ├── crafting-attunements.ts
|
||||||
│ │ ├── crafting-design.ts
|
│ │ ├── crafting-design.ts
|
||||||
│ │ ├── crafting-equipment.ts
|
│ │ ├── crafting-equipment.ts
|
||||||
|
│ │ ├── crafting-fabricator.ts
|
||||||
│ │ ├── crafting-loot.ts
|
│ │ ├── crafting-loot.ts
|
||||||
│ │ ├── crafting-prep.ts
|
│ │ ├── crafting-prep.ts
|
||||||
│ │ ├── crafting-utils.ts
|
│ │ ├── crafting-utils.ts
|
||||||
|
|||||||
@@ -27,20 +27,27 @@ const MANA_TYPE_LABELS: Record<string, string> = {
|
|||||||
function RecipeCard({
|
function RecipeCard({
|
||||||
recipe,
|
recipe,
|
||||||
materials,
|
materials,
|
||||||
manaAmount,
|
rawMana,
|
||||||
|
elementalMana,
|
||||||
onCraft,
|
onCraft,
|
||||||
isCrafting,
|
isCrafting,
|
||||||
}: {
|
}: {
|
||||||
recipe: FabricatorRecipe;
|
recipe: FabricatorRecipe;
|
||||||
materials: Record<string, number>;
|
materials: Record<string, number>;
|
||||||
manaAmount: number;
|
rawMana: number;
|
||||||
|
elementalMana: Record<string, { current: number; max: number; unlocked: boolean }>;
|
||||||
onCraft: (recipe: FabricatorRecipe) => void;
|
onCraft: (recipe: FabricatorRecipe) => void;
|
||||||
isCrafting: boolean;
|
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(
|
const { canCraft } = canCraftRecipe(
|
||||||
recipe,
|
recipe,
|
||||||
materials,
|
materials,
|
||||||
manaAmount,
|
pool,
|
||||||
|
recipe.manaType,
|
||||||
);
|
);
|
||||||
const rarityStyle = LOOT_RARITY_COLORS[recipe.rarity];
|
const rarityStyle = LOOT_RARITY_COLORS[recipe.rarity];
|
||||||
|
|
||||||
@@ -85,8 +92,8 @@ function RecipeCard({
|
|||||||
|
|
||||||
<div className="flex justify-between mt-2">
|
<div className="flex justify-between mt-2">
|
||||||
<span>{MANA_TYPE_LABELS[recipe.manaType]?.split(' ')[1] ?? recipe.manaType} Mana:</span>
|
<span>{MANA_TYPE_LABELS[recipe.manaType]?.split(' ')[1] ?? recipe.manaType} Mana:</span>
|
||||||
<span className={manaAmount >= recipe.manaCost ? 'text-green-400' : 'text-red-400'}>
|
<span className={pool >= recipe.manaCost ? 'text-green-400' : 'text-red-400'}>
|
||||||
{manaAmount} / {recipe.manaCost}
|
{pool} / {recipe.manaCost}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -114,7 +121,8 @@ export function FabricatorSubTab() {
|
|||||||
const lootInventory = useCraftingStore((s) => s.lootInventory);
|
const lootInventory = useCraftingStore((s) => s.lootInventory);
|
||||||
const equipmentCraftingProgress = useCraftingStore((s) => s.equipmentCraftingProgress);
|
const equipmentCraftingProgress = useCraftingStore((s) => s.equipmentCraftingProgress);
|
||||||
const rawMana = useManaStore((s) => s.rawMana);
|
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 cancelEquipmentCrafting = useCraftingStore((s) => s.cancelEquipmentCrafting);
|
||||||
|
|
||||||
const availableManaTypes = useMemo(() => {
|
const availableManaTypes = useMemo(() => {
|
||||||
@@ -129,8 +137,7 @@ export function FabricatorSubTab() {
|
|||||||
const isCrafting = equipmentCraftingProgress !== null;
|
const isCrafting = equipmentCraftingProgress !== null;
|
||||||
|
|
||||||
const handleCraft = (recipe: FabricatorRecipe) => {
|
const handleCraft = (recipe: FabricatorRecipe) => {
|
||||||
// Use the existing equipment crafting system with a fabricator-specific blueprint ID
|
startFabricatorCrafting(recipe.id);
|
||||||
startCraftingEquipment(`fabricator-${recipe.id}`);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -197,7 +204,8 @@ export function FabricatorSubTab() {
|
|||||||
key={recipe.id}
|
key={recipe.id}
|
||||||
recipe={recipe}
|
recipe={recipe}
|
||||||
materials={lootInventory.materials}
|
materials={lootInventory.materials}
|
||||||
manaAmount={rawMana}
|
rawMana={rawMana}
|
||||||
|
elementalMana={elements}
|
||||||
onCraft={handleCraft}
|
onCraft={handleCraft}
|
||||||
isCrafting={isCrafting}
|
isCrafting={isCrafting}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -206,6 +206,7 @@ export function canCraftRecipe(
|
|||||||
recipe: FabricatorRecipe,
|
recipe: FabricatorRecipe,
|
||||||
materials: Record<string, number>,
|
materials: Record<string, number>,
|
||||||
manaAmount: number,
|
manaAmount: number,
|
||||||
|
manaType?: string,
|
||||||
): { canCraft: boolean; missingMaterials: Record<string, number>; missingMana: number } {
|
): { canCraft: boolean; missingMaterials: Record<string, number>; missingMana: number } {
|
||||||
const missingMaterials: Record<string, number> = {};
|
const missingMaterials: Record<string, number> = {};
|
||||||
let canCraft = true;
|
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);
|
const missingMana = Math.max(0, recipe.manaCost - manaAmount);
|
||||||
if (missingMana > 0) {
|
if (missingMana > 0) {
|
||||||
canCraft = false;
|
canCraft = false;
|
||||||
|
|||||||
@@ -16,6 +16,12 @@ import { equipItem as equipItemAction, unequipItem as unequipItemAction } from '
|
|||||||
import { ErrorCode } from '../utils/result';
|
import { ErrorCode } from '../utils/result';
|
||||||
import { createSafeStorage } from '../utils/safe-persist';
|
import { createSafeStorage } from '../utils/safe-persist';
|
||||||
import { createInitialEquipmentInstances } from './crafting-initial-state';
|
import { createInitialEquipmentInstances } from './crafting-initial-state';
|
||||||
|
import {
|
||||||
|
getFabricatorRecipe,
|
||||||
|
deductFabricatorMana,
|
||||||
|
deductMaterials,
|
||||||
|
makeFabricatorProgress,
|
||||||
|
} from '../crafting-fabricator';
|
||||||
|
|
||||||
export const useCraftingStore = create<CraftingStore>()(
|
export const useCraftingStore = create<CraftingStore>()(
|
||||||
persist(
|
persist(
|
||||||
@@ -235,29 +241,39 @@ export const useCraftingStore = create<CraftingStore>()(
|
|||||||
},
|
},
|
||||||
|
|
||||||
cancelEquipmentCrafting: () => {
|
cancelEquipmentCrafting: () => {
|
||||||
const state = get();
|
const progress = get().equipmentCraftingProgress;
|
||||||
const progress = state.equipmentCraftingProgress;
|
|
||||||
if (!progress) return;
|
if (!progress) return;
|
||||||
|
const cancelResult = CraftingEquipment.cancelEquipmentCrafting(progress.blueprintId, progress.manaSpent);
|
||||||
// Get cancel result (mana refund)
|
|
||||||
const cancelResult = CraftingEquipment.cancelEquipmentCrafting(
|
|
||||||
progress.blueprintId,
|
|
||||||
progress.manaSpent
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update crafting store state
|
|
||||||
set({ equipmentCraftingProgress: null });
|
set({ equipmentCraftingProgress: null });
|
||||||
|
|
||||||
// Refund mana to mana store
|
|
||||||
useManaStore.setState((s) => ({ rawMana: s.rawMana + cancelResult.manaRefund }));
|
useManaStore.setState((s) => ({ rawMana: s.rawMana + cancelResult.manaRefund }));
|
||||||
|
|
||||||
// Update combat store (reset action)
|
|
||||||
useCombatStore.setState({ currentAction: 'meditate' });
|
useCombatStore.setState({ currentAction: 'meditate' });
|
||||||
|
|
||||||
// Add log message to UI store
|
|
||||||
useUIStore.getState().addLog(cancelResult.logMessage);
|
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
|
// Enchantment selection actions
|
||||||
setSelectedEquipmentType: (type) => {
|
setSelectedEquipmentType: (type) => {
|
||||||
set((s) => ({ enchantmentSelection: { ...s.enchantmentSelection, selectedEquipmentType: type }}));
|
set((s) => ({ enchantmentSelection: { ...s.enchantmentSelection, selectedEquipmentType: type }}));
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ export interface CraftingActions {
|
|||||||
equipItem: (instanceId: string, slot: EquipmentSlot) => boolean;
|
equipItem: (instanceId: string, slot: EquipmentSlot) => boolean;
|
||||||
unequipItem: (slot: EquipmentSlot) => void;
|
unequipItem: (slot: EquipmentSlot) => void;
|
||||||
startCraftingEquipment: (blueprintId: string) => boolean;
|
startCraftingEquipment: (blueprintId: string) => boolean;
|
||||||
|
startFabricatorCrafting: (recipeId: string) => boolean;
|
||||||
cancelEquipmentCrafting: () => void;
|
cancelEquipmentCrafting: () => void;
|
||||||
setSelectedEquipmentType: (type: string | null) => void;
|
setSelectedEquipmentType: (type: string | null) => void;
|
||||||
setSelectedEffects: (effects: DesignEffect[]) => void;
|
setSelectedEffects: (effects: DesignEffect[]) => void;
|
||||||
|
|||||||
Reference in New Issue
Block a user