feat: add wizard and physical gear branches to Fabricator
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m21s

- Split fabricator-recipes.ts into 4 files (all under 400 lines):
  - fabricator-recipes.ts: core/elemental equipment recipes + helpers
  - fabricator-wizard-recipes.ts: 7 wizard branch recipes (staffs, circlet, robe, catalyst, pendant)
  - fabricator-physical-recipes.ts: 9 physical branch recipes (blades, helm, robe, boots, gauntlets, shields)
  - fabricator-material-recipes.ts: 12 material crafting recipes
- Added branch filter UI (All/Elemental/Wizard/Physical) to FabricatorSubTab
- All 902 tests pass
This commit is contained in:
2026-05-27 15:22:16 +02:00
parent 9a2da67006
commit 5e76fe7145
9 changed files with 510 additions and 314 deletions
@@ -7,7 +7,7 @@ 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, FlaskConical, Hammer, Package } from 'lucide-react';
import { Anvil, FlaskConical, Hammer, Package, Sparkles, Sword } from 'lucide-react';
import { MaterialRecipeCard } from './MaterialRecipeCard';
import {
FABRICATOR_RECIPES,
@@ -20,6 +20,24 @@ 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 BRANCH_RECIPE_IDS = new Set([
'oakStaff', 'arcanistStaff', 'battlestaff', 'arcanistCirclet', 'arcanistRobe',
'voidCatalyst', 'arcanistPendant',
'crystalBlade', 'arcanistBlade', 'voidBlade', 'battleHelm', 'battleRobe',
'battleBoots', 'combatGauntlets', 'runicShield', 'manaShield',
]);
function isWizardBranch(recipe: FabricatorRecipe): boolean {
return ['oakStaff', 'arcanistStaff', 'battlestaff', 'arcanistCirclet',
'arcanistRobe', 'voidCatalyst', 'arcanistPendant'].includes(recipe.id);
}
function isPhysicalBranch(recipe: FabricatorRecipe): boolean {
return ['crystalBlade', 'arcanistBlade', 'voidBlade', 'battleHelm',
'battleRobe', 'battleBoots', 'combatGauntlets', 'runicShield',
'manaShield'].includes(recipe.id);
}
function RecipeCard({
recipe,
materials,
@@ -35,7 +53,6 @@ function RecipeCard({
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);
@@ -111,9 +128,12 @@ function RecipeCard({
);
}
type BranchFilter = 'all' | 'elemental' | 'wizard' | 'physical';
export function FabricatorSubTab() {
const [selectedManaType, setSelectedManaType] = useState<string>('earth');
const [activeSection, setActiveSection] = useState<'equipment' | 'materials'>('equipment');
const [branchFilter, setBranchFilter] = useState<BranchFilter>('all');
const lootInventory = useCraftingStore((s) => s.lootInventory);
const equipmentCraftingProgress = useCraftingStore((s) => s.equipmentCraftingProgress);
@@ -127,10 +147,17 @@ export function FabricatorSubTab() {
return [...new Set(FABRICATOR_RECIPES.map((r) => r.manaType))];
}, []);
const filteredRecipes = useMemo(
() => getRecipesByManaType(selectedManaType),
[selectedManaType],
);
const filteredRecipes = useMemo(() => {
let recipes = FABRICATOR_RECIPES;
if (branchFilter === 'elemental') {
recipes = recipes.filter(r => !BRANCH_RECIPE_IDS.has(r.id));
} else if (branchFilter === 'wizard') {
recipes = recipes.filter(r => isWizardBranch(r));
} else if (branchFilter === 'physical') {
recipes = recipes.filter(r => isPhysicalBranch(r));
}
return recipes.filter(r => r.manaType === selectedManaType);
}, [selectedManaType, branchFilter]);
const isCrafting = equipmentCraftingProgress !== null;
@@ -166,20 +193,58 @@ export function FabricatorSubTab() {
</Button>
</div>
{/* Mana type filter — only for equipment */}
{/* Branch filter — only for equipment */}
{activeSection === 'equipment' && (
<div className="flex gap-2 flex-wrap">
{availableManaTypes.map((mt) => (
<>
<div className="flex gap-2 flex-wrap">
<Button
key={mt}
variant={selectedManaType === mt ? 'default' : 'outline'}
variant={branchFilter === 'all' ? 'default' : 'outline'}
size="sm"
onClick={() => setSelectedManaType(mt)}
onClick={() => setBranchFilter('all')}
>
{MANA_TYPE_LABELS[mt] ?? mt}
<Anvil className="w-3 h-3 mr-1" />
All
</Button>
))}
</div>
<Button
variant={branchFilter === 'elemental' ? 'default' : 'outline'}
size="sm"
onClick={() => setBranchFilter('elemental')}
>
<Anvil className="w-3 h-3 mr-1" />
Elemental
</Button>
<Button
variant={branchFilter === 'wizard' ? 'default' : 'outline'}
size="sm"
onClick={() => setBranchFilter('wizard')}
>
<Sparkles className="w-3 h-3 mr-1" />
Wizard
</Button>
<Button
variant={branchFilter === 'physical' ? 'default' : 'outline'}
size="sm"
onClick={() => setBranchFilter('physical')}
>
<Sword className="w-3 h-3 mr-1" />
Physical
</Button>
</div>
{/* Mana type filter */}
<div className="flex gap-2 flex-wrap">
{availableManaTypes.map((mt) => (
<Button
key={mt}
variant={selectedManaType === mt ? 'default' : 'outline'}
size="sm"
onClick={() => setSelectedManaType(mt)}
>
{MANA_TYPE_LABELS[mt] ?? mt}
</Button>
))}
</div>
</>
)}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">