feat: add material crafting recipes to Fabricator
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 4m25s

This commit is contained in:
2026-05-27 14:13:46 +02:00
parent cbeb0b50ad
commit 3f20991d2d
12 changed files with 579 additions and 64 deletions
@@ -0,0 +1,101 @@
'use client';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Separator } from '@/components/ui/separator';
import { LOOT_DROPS, LOOT_RARITY_COLORS } from '@/lib/game/data/loot-drops';
import { canCraftRecipe, MANA_TYPE_LABELS } from '@/lib/game/data/fabricator-recipes';
import type { FabricatorRecipe } from '@/lib/game/data/fabricator-recipes';
interface MaterialRecipeCardProps {
recipe: FabricatorRecipe;
materials: Record<string, number>;
rawMana: number;
elementalMana: Record<string, { current: number; max: number; unlocked: boolean }>;
onCraft: (recipe: FabricatorRecipe) => void;
}
export function MaterialRecipeCard({
recipe,
materials,
rawMana,
elementalMana,
onCraft,
}: MaterialRecipeCardProps) {
const pool = recipe.manaType === 'raw'
? rawMana
: (elementalMana[recipe.manaType]?.current ?? 0);
const { canCraft } = canCraftRecipe(recipe, materials, pool, recipe.manaType);
const resultDrop = recipe.resultMaterial ? LOOT_DROPS[recipe.resultMaterial] : null;
const resultRarity = resultDrop ? LOOT_RARITY_COLORS[resultDrop.rarity] : null;
return (
<div
className="p-3 rounded border bg-gray-800/50"
style={{ borderColor: resultRarity?.color ?? '#6B7280' }}
>
<div className="flex justify-between items-start mb-2">
<div>
<div className="font-semibold" style={{ color: resultRarity?.color ?? '#D1D5DB' }}>
{recipe.name}
</div>
<div className="text-xs text-gray-400 capitalize">{recipe.rarity}</div>
</div>
<Badge variant="outline" className="text-xs">
{recipe.resultAmount}x Output
</Badge>
</div>
<div className="text-xs text-gray-400 mb-2">{recipe.description}</div>
<Separator className="bg-gray-700 my-2" />
<div className="text-xs space-y-1">
{Object.keys(recipe.materials).length > 0 && (
<>
<div className="text-gray-500">Input Materials:</div>
{Object.entries(recipe.materials).map(([matId, amount]) => {
const available = materials[matId] || 0;
const matDrop = LOOT_DROPS[matId];
const hasEnough = available >= amount;
return (
<div key={matId} className="flex justify-between">
<span>{matDrop?.name ?? matId}</span>
<span className={hasEnough ? 'text-green-400' : 'text-red-400'}>
{available} / {amount}
</span>
</div>
);
})}
</>
)}
{recipe.manaCost > 0 && (
<div className="flex justify-between mt-2">
<span>{MANA_TYPE_LABELS[recipe.manaType]?.split(' ')[1] ?? recipe.manaType} Mana:</span>
<span className={pool >= recipe.manaCost ? 'text-green-400' : 'text-red-400'}>
{pool} / {recipe.manaCost}
</span>
</div>
)}
<div className="flex justify-between">
<span>Produces:</span>
<span className="text-amber-400">
{recipe.resultAmount}x {resultDrop?.name ?? recipe.resultMaterial}
</span>
</div>
</div>
<Button
className="w-full mt-3"
size="sm"
disabled={!canCraft}
onClick={() => onCraft(recipe)}
>
{canCraft ? 'Craft' : 'Missing Resources'}
</Button>
</div>
);
}