diff --git a/docs/circular-deps.txt b/docs/circular-deps.txt
index ba2b365..d7dde38 100644
--- a/docs/circular-deps.txt
+++ b/docs/circular-deps.txt
@@ -1,5 +1,5 @@
# Circular Dependencies
-Generated: 2026-05-19T20:37:58.097Z
+Generated: 2026-05-19T20:59:58.496Z
Found: 3 circular chain(s) — these MUST be fixed before modifying involved files.
1. Processed 121 files (1.2s) (4 warnings)
diff --git a/docs/dependency-graph.json b/docs/dependency-graph.json
index dc26a7f..a7d1ba5 100644
--- a/docs/dependency-graph.json
+++ b/docs/dependency-graph.json
@@ -1,6 +1,6 @@
{
"_meta": {
- "generated": "2026-05-19T20:37:56.730Z",
+ "generated": "2026-05-19T20:59:57.136Z",
"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."
},
diff --git a/docs/project-structure.txt b/docs/project-structure.txt
index 08bfea2..878bdc7 100644
--- a/docs/project-structure.txt
+++ b/docs/project-structure.txt
@@ -88,6 +88,9 @@ Mana-Loop/
│ │ │ │ └── index.tsx
│ │ │ ├── shared/
│ │ │ ├── tabs/
+│ │ │ │ ├── CraftingTab/
+│ │ │ │ │ ├── EnchanterSubTab.tsx
+│ │ │ │ │ └── FabricatorSubTab.tsx
│ │ │ │ ├── DebugTab/
│ │ │ │ │ ├── AchievementDebugSection.tsx
│ │ │ │ │ ├── AttunementDebugSection.tsx
@@ -112,6 +115,8 @@ Mana-Loop/
│ │ │ │ ├── ActivityLog.tsx
│ │ │ │ ├── AttunementsTab.test.ts
│ │ │ │ ├── AttunementsTab.tsx
+│ │ │ │ ├── CraftingTab.test.ts
+│ │ │ │ ├── CraftingTab.tsx
│ │ │ │ ├── DebugTab.test.ts
│ │ │ │ ├── DebugTab.tsx
│ │ │ │ ├── DisciplinesTab.tsx
@@ -257,6 +262,7 @@ Mana-Loop/
│ │ │ ├── crafting-recipes.ts
│ │ │ ├── enchantment-effects.ts
│ │ │ ├── enchantment-types.ts
+│ │ │ ├── fabricator-recipes.ts
│ │ │ └── loot-drops.ts
│ │ ├── effects/
│ │ │ ├── discipline-effects.ts
diff --git a/src/app/page.tsx b/src/app/page.tsx
index e5418f1..eec169e 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -52,6 +52,7 @@ const EquipmentTab = lazy(() => import('@/components/game/tabs').then(module =>
const GolemancyTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.GolemancyTab })));
const GuardianPactsTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.GuardianPactsTab })));
const SpireSummaryTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.SpireSummaryTab })));
+const CraftingTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.CraftingTab })));
const TabLoadingFallback = () =>
Loading...
;
@@ -249,6 +250,7 @@ export default function ManaLoopGame() {
🗿 Golemancy
📜 Pacts
🏔️ Spire
+ ⚒️ Crafting
@@ -342,6 +344,14 @@ export default function ManaLoopGame() {
+
+
+ crafting tab failed to load.}>
+ }>
+
+
+
+
diff --git a/src/components/game/tabs/CraftingTab.test.ts b/src/components/game/tabs/CraftingTab.test.ts
new file mode 100644
index 0000000..e4a2f2d
--- /dev/null
+++ b/src/components/game/tabs/CraftingTab.test.ts
@@ -0,0 +1,180 @@
+import { describe, it, expect } from 'vitest';
+
+// ─── Test: CraftingTab barrel export ──────────────────────────────────────────
+
+describe('CraftingTab module structure', () => {
+ it('exports CraftingTab from its module', async () => {
+ const mod = await import('./CraftingTab');
+ expect(mod.CraftingTab).toBeDefined();
+ expect(typeof mod.CraftingTab).toBe('function');
+ });
+
+ it('CraftingTab has correct displayName', async () => {
+ const { CraftingTab } = await import('./CraftingTab');
+ expect(CraftingTab.displayName).toBe('CraftingTab');
+ });
+});
+
+// ─── Test: CraftingTab in tabs barrel index ────────────────────────────────────
+
+describe('Tab barrel export', () => {
+ it('includes CraftingTab in the tabs index', async () => {
+ const mod = await import('@/components/game/tabs');
+ expect(mod.CraftingTab).toBeDefined();
+ expect(typeof mod.CraftingTab).toBe('function');
+ });
+});
+
+// ─── Test: FabricatorSubTab ────────────────────────────────────────────────────
+
+describe('FabricatorSubTab module', () => {
+ it('exports FabricatorSubTab', async () => {
+ const mod = await import('./CraftingTab/FabricatorSubTab');
+ expect(mod.FabricatorSubTab).toBeDefined();
+ expect(typeof mod.FabricatorSubTab).toBe('function');
+ });
+
+ it('FabricatorSubTab has correct displayName', async () => {
+ const { FabricatorSubTab } = await import('./CraftingTab/FabricatorSubTab');
+ expect(FabricatorSubTab.displayName).toBe('FabricatorSubTab');
+ });
+});
+
+// ─── Test: EnchanterSubTab ─────────────────────────────────────────────────────
+
+describe('EnchanterSubTab module', () => {
+ it('exports EnchanterSubTab', async () => {
+ const mod = await import('./CraftingTab/EnchanterSubTab');
+ expect(mod.EnchanterSubTab).toBeDefined();
+ expect(typeof mod.EnchanterSubTab).toBe('function');
+ });
+
+ it('EnchanterSubTab has correct displayName', async () => {
+ const { EnchanterSubTab } = await import('./CraftingTab/EnchanterSubTab');
+ expect(EnchanterSubTab.displayName).toBe('EnchanterSubTab');
+ });
+});
+
+// ─── Test: Fabricator recipes data ─────────────────────────────────────────────
+
+describe('Fabricator recipes data', () => {
+ it('exports FABRICATOR_RECIPES', async () => {
+ const mod = await import('@/lib/game/data/fabricator-recipes');
+ expect(mod.FABRICATOR_RECIPES).toBeDefined();
+ expect(Object.keys(mod.FABRICATOR_RECIPES).length).toBeGreaterThan(0);
+ });
+
+ it('all recipes have required fields', async () => {
+ const { FABRICATOR_RECIPES } = await import('@/lib/game/data/fabricator-recipes');
+ for (const recipe of Object.values(FABRICATOR_RECIPES)) {
+ expect(recipe.id).toBeTruthy();
+ expect(recipe.name).toBeTruthy();
+ expect(recipe.description).toBeTruthy();
+ expect(recipe.manaType).toBeTruthy();
+ expect(recipe.equipmentTypeId).toBeTruthy();
+ expect(recipe.slot).toBeTruthy();
+ expect(recipe.materials).toBeDefined();
+ expect(recipe.manaCost).toBeGreaterThan(0);
+ expect(recipe.craftTime).toBeGreaterThan(0);
+ expect(recipe.rarity).toBeTruthy();
+ expect(recipe.gearTrait).toBeTruthy();
+ }
+ });
+
+ it('only uses valid solid/structural mana types', async () => {
+ const { FABRICATOR_RECIPES } = await import('@/lib/game/data/fabricator-recipes');
+ const validManaTypes = new Set(['earth', 'metal', 'crystal', 'sand']);
+ for (const recipe of Object.values(FABRICATOR_RECIPES)) {
+ expect(validManaTypes.has(recipe.manaType)).toBe(true);
+ }
+ });
+
+ it('no fire, water, air, light, dark, death, stellar, or void gear recipes', async () => {
+ const { FABRICATOR_RECIPES } = await import('@/lib/game/data/fabricator-recipes');
+ const invalidTypes = new Set(['fire', 'water', 'air', 'light', 'dark', 'death', 'stellar', 'void']);
+ for (const recipe of Object.values(FABRICATOR_RECIPES)) {
+ expect(invalidTypes.has(recipe.manaType)).toBe(false);
+ }
+ });
+
+ it('getRecipesByManaType filters correctly', async () => {
+ const { getRecipesByManaType, FABRICATOR_RECIPES } = await import('@/lib/game/data/fabricator-recipes');
+ const earthRecipes = getRecipesByManaType('earth');
+ expect(earthRecipes.length).toBeGreaterThan(0);
+ for (const r of earthRecipes) {
+ expect(r.manaType).toBe('earth');
+ }
+
+ const empty = getRecipesByManaType('nonexistent');
+ expect(empty).toEqual([]);
+ });
+
+ it('getRecipeById returns correct recipe', async () => {
+ const { getRecipeById, FABRICATOR_RECIPES } = await import('@/lib/game/data/fabricator-recipes');
+ const firstId = Object.values(FABRICATOR_RECIPES)[0].id;
+ const recipe = getRecipeById(firstId);
+ expect(recipe).toBeDefined();
+ expect(recipe!.id).toBe(firstId);
+
+ const missing = getRecipeById('nonexistent');
+ expect(missing).toBeUndefined();
+ });
+
+ it('canCraftRecipe returns correct availability', async () => {
+ const { canCraftRecipe, FABRICATOR_RECIPES } = await import('@/lib/game/data/fabricator-recipes');
+ const recipe = Object.values(FABRICATOR_RECIPES)[0];
+
+ // Empty materials => cannot craft
+ const result1 = canCraftRecipe(recipe, {}, 0);
+ expect(result1.canCraft).toBe(false);
+ expect(Object.keys(result1.missingMaterials).length).toBeGreaterThan(0);
+ expect(result1.missingMana).toBeGreaterThan(0);
+
+ // Sufficient materials and mana => can craft
+ const sufficientMats: Record = {};
+ for (const [matId, amount] of Object.entries(recipe.materials)) {
+ sufficientMats[matId] = amount;
+ }
+ const result2 = canCraftRecipe(recipe, sufficientMats, recipe.manaCost);
+ expect(result2.canCraft).toBe(true);
+ expect(Object.keys(result2.missingMaterials)).toHaveLength(0);
+ expect(result2.missingMana).toBe(0);
+ });
+});
+
+// ─── Test: File size limits ────────────────────────────────────────────────────
+
+describe('File size limits (400 lines max)', () => {
+ it('CraftingTab.tsx is under 400 lines', async () => {
+ const fs = await import('fs');
+ const path = await import('path');
+ const content = fs.readFileSync(
+ path.join(__dirname, 'CraftingTab.tsx'),
+ 'utf-8',
+ );
+ const lines = content.split('\n').length;
+ expect(lines).toBeLessThan(400);
+ });
+
+ it('FabricatorSubTab.tsx is under 400 lines', async () => {
+ const fs = await import('fs');
+ const path = await import('path');
+ const content = fs.readFileSync(
+ path.join(__dirname, 'CraftingTab', 'FabricatorSubTab.tsx'),
+ 'utf-8',
+ );
+ const lines = content.split('\n').length;
+ expect(lines).toBeLessThan(400);
+ });
+
+ it('EnchanterSubTab.tsx is under 400 lines', async () => {
+ const fs = await import('fs');
+ const path = await import('path');
+ const content = fs.readFileSync(
+ path.join(__dirname, 'CraftingTab', 'EnchanterSubTab.tsx'),
+ 'utf-8',
+ );
+ const lines = content.split('\n').length;
+ expect(lines).toBeLessThan(400);
+ });
+});
diff --git a/src/components/game/tabs/CraftingTab.tsx b/src/components/game/tabs/CraftingTab.tsx
new file mode 100644
index 0000000..83e30d9
--- /dev/null
+++ b/src/components/game/tabs/CraftingTab.tsx
@@ -0,0 +1,54 @@
+'use client';
+
+import { useState } from 'react';
+import clsx from 'clsx';
+import { Hammer, Sparkles } from 'lucide-react';
+import { DebugName } from '@/components/game/debug/debug-context';
+import { FabricatorSubTab } from './CraftingTab/FabricatorSubTab';
+import { EnchanterSubTab } from './CraftingTab/EnchanterSubTab';
+
+type CraftingAttunement = 'fabricator' | 'enchanter';
+
+interface CraftingSubTab {
+ key: CraftingAttunement;
+ label: string;
+ icon: typeof Hammer;
+}
+
+const CRAFTING_SUB_TABS: CraftingSubTab[] = [
+ { key: 'fabricator', label: 'Fabricator', icon: Hammer },
+ { key: 'enchanter', label: 'Enchanter', icon: Sparkles },
+];
+
+export function CraftingTab() {
+ const [activeSubTab, setActiveSubTab] = useState('fabricator');
+
+ return (
+
+
+ {/* Sub-tab bar — same clsx pattern as DisciplinesTab */}
+
+ {CRAFTING_SUB_TABS.map(({ key, label, icon: Icon }) => (
+
+ ))}
+
+
+ {/* Sub-tab content */}
+ {activeSubTab === 'fabricator' &&
}
+ {activeSubTab === 'enchanter' &&
}
+
+
+ );
+}
+
+CraftingTab.displayName = 'CraftingTab';
diff --git a/src/components/game/tabs/CraftingTab/EnchanterSubTab.tsx b/src/components/game/tabs/CraftingTab/EnchanterSubTab.tsx
new file mode 100644
index 0000000..c4db3ab
--- /dev/null
+++ b/src/components/game/tabs/CraftingTab/EnchanterSubTab.tsx
@@ -0,0 +1,100 @@
+'use client';
+
+import { useState } from 'react';
+import clsx from 'clsx';
+import { PenLine, FlaskConical, Sparkles } from 'lucide-react';
+import {
+ EnchantmentDesigner,
+ EnchantmentPreparer,
+ EnchantmentApplier,
+} from '@/components/game/crafting';
+import { useCraftingStore } from '@/lib/game/stores';
+import type { DesignEffect } from '@/lib/game/types';
+
+type EnchanterPhase = 'design' | 'prepare' | 'apply';
+
+const PHASES: { key: EnchanterPhase; label: string; icon: typeof PenLine }[] = [
+ { key: 'design', label: 'Design', icon: PenLine },
+ { key: 'prepare', label: 'Prepare', icon: FlaskConical },
+ { key: 'apply', label: 'Apply', icon: Sparkles },
+];
+
+export function EnchanterSubTab() {
+ const [activePhase, setActivePhase] = useState('design');
+
+ // Shared state for the enchantment flow
+ const [selectedEquipmentType, setSelectedEquipmentType] = useState(null);
+ const [selectedEffects, setSelectedEffects] = useState([]);
+ const [designName, setDesignName] = useState('');
+ const [selectedDesign, setSelectedDesign] = useState(null);
+ const [selectedEquipmentInstance, setSelectedEquipmentInstance] = useState(null);
+
+ const resetEnchantmentSelection = useCraftingStore((s) => s.resetEnchantmentSelection);
+
+ const handlePhaseChange = (phase: EnchanterPhase) => {
+ setActivePhase(phase);
+ };
+
+ const handleEnchantmentApplied = () => {
+ // Reset selection after successful application
+ resetEnchantmentSelection();
+ setSelectedEquipmentInstance(null);
+ setSelectedDesign(null);
+ // Go back to design phase
+ setActivePhase('design');
+ };
+
+ return (
+
+ {/* Phase selector */}
+
+ {PHASES.map(({ key, label, icon: Icon }) => (
+
+ ))}
+
+
+ {/* Phase content */}
+ {activePhase === 'design' && (
+
+ )}
+
+ {activePhase === 'prepare' && (
+
+ )}
+
+ {activePhase === 'apply' && (
+
+ )}
+
+ );
+}
+
+EnchanterSubTab.displayName = 'EnchanterSubTab';
diff --git a/src/components/game/tabs/CraftingTab/FabricatorSubTab.tsx b/src/components/game/tabs/CraftingTab/FabricatorSubTab.tsx
new file mode 100644
index 0000000..04e2458
--- /dev/null
+++ b/src/components/game/tabs/CraftingTab/FabricatorSubTab.tsx
@@ -0,0 +1,260 @@
+'use client';
+
+import { useState, useMemo } from 'react';
+import { Button } from '@/components/ui/button';
+import { Progress } from '@/components/ui/progress';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+import { ScrollArea } from '@/components/ui/scroll-area';
+import { Separator } from '@/components/ui/separator';
+import { Anvil, Hammer, Package } from 'lucide-react';
+import {
+ FABRICATOR_RECIPES,
+ getRecipesByManaType,
+ canCraftRecipe,
+} from '@/lib/game/data/fabricator-recipes';
+import { LOOT_DROPS, LOOT_RARITY_COLORS } from '@/lib/game/data/loot-drops';
+import { useCraftingStore, useManaStore } from '@/lib/game/stores';
+import type { FabricatorRecipe } from '@/lib/game/data/fabricator-recipes';
+
+const MANA_TYPE_LABELS: Record = {
+ earth: '⛰️ Earth',
+ metal: '🔩 Metal',
+ crystal: '💎 Crystal',
+ sand: '🏜️ Sand',
+};
+
+function RecipeCard({
+ recipe,
+ materials,
+ manaAmount,
+ onCraft,
+ isCrafting,
+}: {
+ recipe: FabricatorRecipe;
+ materials: Record;
+ manaAmount: number;
+ onCraft: (recipe: FabricatorRecipe) => void;
+ isCrafting: boolean;
+}) {
+ const { canCraft, missingMaterials, missingMana } = canCraftRecipe(
+ recipe,
+ materials,
+ manaAmount,
+ );
+ const rarityStyle = LOOT_RARITY_COLORS[recipe.rarity];
+
+ return (
+
+
+
+
+ {recipe.name}
+
+
{recipe.rarity}
+
+
+ {MANA_TYPE_LABELS[recipe.manaType] ?? recipe.manaType}
+
+
+
+
{recipe.description}
+
{recipe.gearTrait}
+
+
+
+
+
Materials:
+ {Object.entries(recipe.materials).map(([matId, amount]) => {
+ const available = materials[matId] || 0;
+ const matDrop = LOOT_DROPS[matId];
+ const hasEnough = available >= amount;
+
+ return (
+
+ {matDrop?.name ?? matId}
+
+ {available} / {amount}
+
+
+ );
+ })}
+
+
+ {MANA_TYPE_LABELS[recipe.manaType]?.split(' ')[1] ?? recipe.manaType} Mana:
+ = recipe.manaCost ? 'text-green-400' : 'text-red-400'}>
+ {manaAmount} / {recipe.manaCost}
+
+
+
+
+ Craft Time:
+ {recipe.craftTime}h
+
+
+
+
+
+ );
+}
+
+export function FabricatorSubTab() {
+ const [selectedManaType, setSelectedManaType] = useState('earth');
+
+ 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 cancelEquipmentCrafting = useCraftingStore((s) => s.cancelEquipmentCrafting);
+
+ const availableManaTypes = useMemo(() => {
+ return [...new Set(FABRICATOR_RECIPES.map((r) => r.manaType))];
+ }, []);
+
+ const filteredRecipes = useMemo(
+ () => getRecipesByManaType(selectedManaType),
+ [selectedManaType],
+ );
+
+ const isCrafting = equipmentCraftingProgress !== null;
+
+ const handleCraft = (recipe: FabricatorRecipe) => {
+ // Use the existing equipment crafting system with a fabricator-specific blueprint ID
+ startCraftingEquipment(`fabricator-${recipe.id}`);
+ };
+
+ return (
+
+ {/* Mana type filter */}
+
+ {availableManaTypes.map((mt) => (
+
+ ))}
+
+
+
+ {/* Recipe list */}
+
+
+
+
+ {MANA_TYPE_LABELS[selectedManaType] ?? selectedManaType} Recipes
+
+
+
+ {isCrafting ? (
+
+
+ Crafting: {equipmentCraftingProgress.blueprintId}
+
+
+
+
+ {equipmentCraftingProgress.progress.toFixed(1)}h /{' '}
+ {equipmentCraftingProgress.required.toFixed(1)}h
+
+ Mana spent: {equipmentCraftingProgress.manaSpent}
+
+
+
+ ) : (
+
+
+ {filteredRecipes.length === 0 ? (
+
+
+
No recipes for this mana type yet.
+
+ ) : (
+ filteredRecipes.map((recipe) => (
+
+ ))
+ )}
+
+
+ )}
+
+
+
+ {/* Materials inventory */}
+
+
+
+
+ Materials ({Object.values(lootInventory.materials).reduce((a, b) => a + b, 0)})
+
+
+
+
+ {Object.keys(lootInventory.materials).length === 0 ? (
+
+
+
No materials collected yet.
+
Defeat floors to gather materials!
+
+ ) : (
+
+ {Object.entries(lootInventory.materials).map(([matId, count]) => {
+ if (count <= 0) return null;
+ const drop = LOOT_DROPS[matId];
+ if (!drop) return null;
+
+ const rarityStyle = LOOT_RARITY_COLORS[drop.rarity];
+
+ return (
+
+
+ {drop.name}
+
+
x{count}
+
+ );
+ })}
+
+ )}
+
+
+
+
+
+ );
+}
+
+FabricatorSubTab.displayName = 'FabricatorSubTab';
diff --git a/src/components/game/tabs/index.ts b/src/components/game/tabs/index.ts
index 7d745ba..de791d3 100644
--- a/src/components/game/tabs/index.ts
+++ b/src/components/game/tabs/index.ts
@@ -12,3 +12,4 @@ export { EquipmentTab } from './EquipmentTab';
export { GolemancyTab } from './GolemancyTab';
export { GuardianPactsTab } from './GuardianPactsTab';
export { SpireSummaryTab } from './SpireSummaryTab';
+export { CraftingTab } from './CraftingTab';
diff --git a/src/lib/game/data/fabricator-recipes.ts b/src/lib/game/data/fabricator-recipes.ts
new file mode 100644
index 0000000..04918c2
--- /dev/null
+++ b/src/lib/game/data/fabricator-recipes.ts
@@ -0,0 +1,227 @@
+// ─── Fabricator Recipes ──────────────────────────────────────────────────────
+// Crafting recipes for the Fabricator attunement.
+// Each recipe is tied to a mana type the player has unlocked.
+
+import type { EquipmentSlot } from './equipment/types';
+
+export interface FabricatorRecipe {
+ id: string;
+ name: string;
+ description: string;
+ /** Mana type required to craft this recipe (must be unlocked) */
+ manaType: string;
+ /** Equipment type ID produced */
+ equipmentTypeId: string;
+ /** Which slot the resulting equipment occupies */
+ slot: EquipmentSlot;
+ /** Materials required: materialId -> count */
+ materials: Record;
+ /** Mana cost in the recipe's mana type */
+ manaCost: number;
+ /** Craft time in hours */
+ craftTime: number;
+ /** Rarity tier */
+ rarity: 'common' | 'uncommon' | 'rare' | 'epic' | 'legendary';
+ /** Flavor text describing the gear's properties */
+ gearTrait: string;
+}
+
+export const FABRICATOR_RECIPES: FabricatorRecipe[] = [
+ // ─── Earth Gear (Compacted Earth — high defense) ──────────────────────
+ {
+ id: 'earthHelm',
+ name: 'Earthen Helm',
+ description: 'A sturdy helm carved from compacted stone.',
+ manaType: 'earth',
+ equipmentTypeId: 'wizardHat',
+ slot: 'head',
+ materials: { manaCrystalDust: 4, arcaneShard: 2 },
+ manaCost: 200,
+ craftTime: 3,
+ rarity: 'uncommon',
+ gearTrait: '+15% physical resistance',
+ },
+ {
+ id: 'earthChest',
+ name: 'Stoneguard Armor',
+ description: 'Heavy stone plates layered over leather. Slow but nearly impenetrable.',
+ manaType: 'earth',
+ equipmentTypeId: 'scholarRobe',
+ slot: 'body',
+ materials: { manaCrystalDust: 8, arcaneShard: 4, elementalCore: 1 },
+ manaCost: 500,
+ craftTime: 6,
+ rarity: 'rare',
+ gearTrait: '+25% physical resistance, -10% cast speed',
+ },
+ {
+ id: 'earthBoots',
+ name: 'Stonegreaves',
+ description: 'Boots reinforced with compacted earth. Firm footing in any battle.',
+ manaType: 'earth',
+ equipmentTypeId: 'travelerBoots',
+ slot: 'feet',
+ materials: { manaCrystalDust: 3, arcaneShard: 1 },
+ manaCost: 150,
+ craftTime: 2,
+ rarity: 'uncommon',
+ gearTrait: '+10% physical resistance',
+ },
+
+ // ─── Metal Gear (Fire+Earth — balanced offense/defense) ──────────────
+ {
+ id: 'metalBlade',
+ name: 'Metal Blade',
+ description: 'A blade forged from condensed metal mana. Sharp and durable.',
+ manaType: 'metal',
+ equipmentTypeId: 'steelBlade',
+ slot: 'mainHand',
+ materials: { manaCrystalDust: 6, arcaneShard: 3, elementalCore: 2 },
+ manaCost: 400,
+ craftTime: 5,
+ rarity: 'rare',
+ gearTrait: '+20% damage, +10% durability',
+ },
+ {
+ id: 'metalShield',
+ name: 'Metal Kite Shield',
+ description: 'A polished metal shield. Reliable protection without excessive weight.',
+ manaType: 'metal',
+ equipmentTypeId: 'runicShield',
+ slot: 'offHand',
+ materials: { manaCrystalDust: 7, arcaneShard: 4, elementalCore: 1 },
+ manaCost: 450,
+ craftTime: 5,
+ rarity: 'rare',
+ gearTrait: '+20% block chance',
+ },
+ {
+ id: 'metalGloves',
+ name: 'Metalweave Gauntlets',
+ description: 'Gauntlets woven with metal mana threads. Protective yet dexterous.',
+ manaType: 'metal',
+ equipmentTypeId: 'spellweaveGloves',
+ slot: 'hands',
+ materials: { manaCrystalDust: 4, arcaneShard: 2 },
+ manaCost: 250,
+ craftTime: 3,
+ rarity: 'uncommon',
+ gearTrait: '+10% damage, +10% physical resistance',
+ },
+
+ // ─── Crystal Gear (Sand+Sand+Light — high enchantment capacity) ──────
+ {
+ id: 'crystalWand',
+ name: 'Crystal Focus Wand',
+ description: 'A wand with a pure crystal core. Exceptional mana conductivity.',
+ manaType: 'crystal',
+ equipmentTypeId: 'crystalWand',
+ slot: 'mainHand',
+ materials: { manaCrystalDust: 10, arcaneShard: 5, elementalCore: 3 },
+ manaCost: 600,
+ craftTime: 6,
+ rarity: 'epic',
+ gearTrait: '+40% enchantment capacity',
+ },
+ {
+ id: 'crystalRing',
+ name: 'Crystal Ring',
+ description: 'A ring set with a mana crystal. Amplifies enchantment effects.',
+ manaType: 'crystal',
+ equipmentTypeId: 'silverRing',
+ slot: 'accessory1',
+ materials: { manaCrystalDust: 5, arcaneShard: 3, elementalCore: 1 },
+ manaCost: 350,
+ craftTime: 3,
+ rarity: 'rare',
+ gearTrait: '+15% enchantment capacity',
+ },
+ {
+ id: 'crystalAmulet',
+ name: 'Crystal Pendant',
+ description: 'An amulet housing a crystal shard. Enhances all enchantments worn.',
+ manaType: 'crystal',
+ equipmentTypeId: 'silverAmulet',
+ slot: 'accessory2',
+ materials: { manaCrystalDust: 6, arcaneShard: 3, elementalCore: 2 },
+ manaCost: 400,
+ craftTime: 4,
+ rarity: 'rare',
+ gearTrait: '+10% all enchantment effects',
+ },
+
+ // ─── Sand Gear (Earth+Water — lightweight, agile) ────────────────────
+ {
+ id: 'sandBoots',
+ name: 'Sandstrider Boots',
+ description: 'Boots infused with sand mana. Light as air, silent as dust.',
+ manaType: 'sand',
+ equipmentTypeId: 'travelerBoots',
+ slot: 'feet',
+ materials: { manaCrystalDust: 3, arcaneShard: 1 },
+ manaCost: 120,
+ craftTime: 2,
+ rarity: 'uncommon',
+ gearTrait: '+15% cast speed, +10% evasion',
+ },
+ {
+ id: 'sandGloves',
+ name: 'Sandweave Gloves',
+ description: 'Gloves woven from sand mana. Nimble fingers for delicate enchanting.',
+ manaType: 'sand',
+ equipmentTypeId: 'spellweaveGloves',
+ slot: 'hands',
+ materials: { manaCrystalDust: 3, arcaneShard: 2 },
+ manaCost: 140,
+ craftTime: 2,
+ rarity: 'uncommon',
+ gearTrait: '+10% cast speed',
+ },
+ {
+ id: 'sandVest',
+ name: 'Sandcloth Vest',
+ description: 'A light vest woven from sand mana. Offers minimal protection but maximum mobility.',
+ manaType: 'sand',
+ equipmentTypeId: 'scholarRobe',
+ slot: 'body',
+ materials: { manaCrystalDust: 5, arcaneShard: 2, elementalCore: 1 },
+ manaCost: 300,
+ craftTime: 4,
+ rarity: 'rare',
+ gearTrait: '+20% cast speed, +5% evasion',
+ },
+];
+
+// ─── Helpers ──────────────────────────────────────────────────────────────────
+
+export function getRecipesByManaType(manaType: string): FabricatorRecipe[] {
+ return FABRICATOR_RECIPES.filter(r => r.manaType === manaType);
+}
+
+export function getRecipeById(id: string): FabricatorRecipe | undefined {
+ return FABRICATOR_RECIPES.find(r => r.id === id);
+}
+
+export function canCraftRecipe(
+ recipe: FabricatorRecipe,
+ materials: Record,
+ manaAmount: number,
+): { canCraft: boolean; missingMaterials: Record; missingMana: number } {
+ const missingMaterials: Record = {};
+ 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 missingMana = Math.max(0, recipe.manaCost - manaAmount);
+ if (missingMana > 0) {
+ canCraft = false;
+ }
+
+ return { canCraft, missingMaterials, missingMana };
+}