feat: recreate Crafting Tab with Fabricator and Enchanter sub-tabs
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 2m18s

- Add fabricator-recipes.ts with 12 recipes across earth/metal/crystal/sand mana types
- Add FabricatorSubTab with mana-type filtering, recipe cards, materials inventory
- Add EnchanterSubTab integrating existing 3-phase flow (Design → Prepare → Apply)
- Add CraftingTab main component with clsx-based sub-tab system (matches DisciplinesTab pattern)
- Wire into tabs barrel export and page.tsx with lazy loading + DebugName wrapper
- Add 17 tests covering exports, displayNames, recipe data integrity, helpers, file sizes
- All files under 400 lines
This commit is contained in:
2026-05-20 02:32:37 +02:00
parent 9882578627
commit 1c7fc8c551
10 changed files with 840 additions and 2 deletions
@@ -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<EnchanterPhase>('design');
// Shared state for the enchantment flow
const [selectedEquipmentType, setSelectedEquipmentType] = useState<string | null>(null);
const [selectedEffects, setSelectedEffects] = useState<DesignEffect[]>([]);
const [designName, setDesignName] = useState('');
const [selectedDesign, setSelectedDesign] = useState<string | null>(null);
const [selectedEquipmentInstance, setSelectedEquipmentInstance] = useState<string | null>(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 (
<div className="space-y-4">
{/* Phase selector */}
<div className="flex gap-2">
{PHASES.map(({ key, label, icon: Icon }) => (
<button
key={key}
onClick={() => handlePhaseChange(key)}
className={clsx('rounded px-3 py-1 text-sm font-medium flex items-center gap-1.5', {
'bg-purple-600 text-white': activePhase === key,
'text-gray-400 hover:text-gray-200': activePhase !== key,
})}
>
<Icon className="w-3.5 h-3.5" />
{label}
</button>
))}
</div>
{/* Phase content */}
{activePhase === 'design' && (
<EnchantmentDesigner
selectedEquipmentType={selectedEquipmentType}
setSelectedEquipmentType={setSelectedEquipmentType}
selectedEffects={selectedEffects}
setSelectedEffects={setSelectedEffects}
designName={designName}
setDesignName={setDesignName}
selectedDesign={selectedDesign}
setSelectedDesign={setSelectedDesign}
/>
)}
{activePhase === 'prepare' && (
<EnchantmentPreparer
selectedEquipmentInstance={selectedEquipmentInstance}
setSelectedEquipmentInstance={setSelectedEquipmentInstance}
/>
)}
{activePhase === 'apply' && (
<EnchantmentApplier
selectedEquipmentInstance={selectedEquipmentInstance}
setSelectedEquipmentInstance={setSelectedEquipmentInstance}
selectedDesign={selectedDesign}
setSelectedDesign={setSelectedDesign}
onEnchantmentApplied={handleEnchantmentApplied}
/>
)}
</div>
);
}
EnchanterSubTab.displayName = 'EnchanterSubTab';