0e1e506213
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m18s
- Add getCraftingCostReduction() and applyCostReduction() helpers in crafting-fabricator.ts - Apply cost reduction in deductMaterials() and checkFabricatorCosts() - Apply cost reduction in startFabricatorCrafting() and cancelEquipmentCrafting() pipeline - Update canCraftRecipe() in fabricator-recipes.ts to accept costReduction param - Update FabricatorSubTab and MaterialRecipeCard UIs to display discounted costs - Spec formula: actualCost = ceil(baseCost × (1 - craftingCostReduction / 100)) Fixes #316
113 lines
4.0 KiB
TypeScript
113 lines
4.0 KiB
TypeScript
'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 } from '@/lib/game/data/fabricator-recipes';
|
|
import { MANA_TYPE_LABELS } from '@/lib/game/data/fabricator-recipe-types';
|
|
import type { FabricatorRecipe } from '@/lib/game/data/fabricator-recipes';
|
|
import { DebugName } from '@/components/game/debug/debug-context';
|
|
import { getCraftingCostReduction, applyCostReduction } from '@/lib/game/crafting-fabricator';
|
|
|
|
interface MaterialRecipeCardProps {
|
|
recipe: FabricatorRecipe;
|
|
materials: Record<string, number>;
|
|
rawMana: number;
|
|
elementalMana: Record<string, { current: number; max: number; unlocked: boolean }>;
|
|
onCraft: (recipe: FabricatorRecipe) => void;
|
|
costReduction?: number;
|
|
}
|
|
|
|
export function MaterialRecipeCard({
|
|
recipe,
|
|
materials,
|
|
rawMana,
|
|
elementalMana,
|
|
onCraft,
|
|
costReduction = 0,
|
|
}: MaterialRecipeCardProps) {
|
|
const pool = recipe.manaType === 'raw'
|
|
? rawMana
|
|
: (elementalMana[recipe.manaType]?.current ?? 0);
|
|
const { canCraft } = canCraftRecipe(recipe, materials, pool, recipe.manaType, costReduction);
|
|
const resultDrop = recipe.resultMaterial ? LOOT_DROPS[recipe.resultMaterial] : null;
|
|
const resultRarity = resultDrop ? LOOT_RARITY_COLORS[resultDrop.rarity] : null;
|
|
|
|
return (
|
|
<DebugName name="MaterialRecipeCard">
|
|
<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, rawAmount]) => {
|
|
const reducedAmount = applyCostReduction(rawAmount, costReduction);
|
|
const available = materials[matId] || 0;
|
|
const matDrop = LOOT_DROPS[matId];
|
|
const hasEnough = available >= reducedAmount;
|
|
|
|
return (
|
|
<div key={matId} className="flex justify-between">
|
|
<span>{matDrop?.name ?? matId}</span>
|
|
<span className={hasEnough ? 'text-green-400' : 'text-red-400'}>
|
|
{available} / {reducedAmount}
|
|
{costReduction > 0 && rawAmount !== reducedAmount && (
|
|
<span className="text-gray-500 ml-1">(was {rawAmount})</span>
|
|
)}
|
|
</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>
|
|
</DebugName>
|
|
);
|
|
}
|