Files
Mana-Loop/src/components/game/tabs/CraftingTab.tsx
T

966 lines
42 KiB
TypeScript
Executable File
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useState } 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 { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import {
Wand2, Scroll, Hammer, Sparkles, Trash2, Plus, Minus,
Package, Zap, Clock, ChevronRight, Circle, Anvil
} from 'lucide-react';
import { EQUIPMENT_TYPES } from '@/lib/game/data/equipment';
import { ENCHANTMENT_EFFECTS, calculateEffectCapacityCost } from '@/lib/game/data/enchantment-effects';
import { CRAFTING_RECIPES, canCraftRecipe } from '@/lib/game/data/crafting-recipes';
import { LOOT_DROPS, RARITY_COLORS } from '@/lib/game/data/loot-drops';
import type { EquipmentInstance, EnchantmentDesign, DesignEffect, AppliedEnchantment, LootInventory, EquipmentCraftingProgress } from '@/lib/game/types';
import { fmt, type GameStore } from '@/lib/game/store';
// Slot display names
const SLOT_NAMES: Record<EquipmentSlot, string> = {
mainHand: 'Main Hand',
offHand: 'Off Hand',
head: 'Head',
body: 'Body',
hands: 'Hands',
feet: 'Feet',
accessory1: 'Accessory 1',
accessory2: 'Accessory 2',
};
export interface CraftingTabProps {
store: GameStore;
}
export function CraftingTab({ store }: CraftingTabProps) {
const equippedInstances = store.equippedInstances;
const equipmentInstances = store.equipmentInstances;
const enchantmentDesigns = store.enchantmentDesigns;
const designProgress = store.designProgress;
const preparationProgress = store.preparationProgress;
const applicationProgress = store.applicationProgress;
const equipmentCraftingProgress = store.equipmentCraftingProgress;
const rawMana = store.rawMana;
const skills = store.skills;
const currentAction = store.currentAction;
const unlockedEffects = store.unlockedEffects;
const lootInventory = store.lootInventory;
const startDesigningEnchantment = store.startDesigningEnchantment;
const cancelDesign = store.cancelDesign;
const saveDesign = store.saveDesign;
const deleteDesign = store.deleteDesign;
const startPreparing = store.startPreparing;
const cancelPreparation = store.cancelPreparation;
const startApplying = store.startApplying;
const pauseApplication = store.pauseApplication;
const resumeApplication = store.resumeApplication;
const cancelApplication = store.cancelApplication;
const disenchantEquipment = store.disenchantEquipment;
const getAvailableCapacity = store.getAvailableCapacity;
const startCraftingEquipment = store.startCraftingEquipment;
const cancelEquipmentCrafting = store.cancelEquipmentCrafting;
const deleteMaterial = store.deleteMaterial;
const [craftingStage, setCraftingStage] = useState<'design' | 'prepare' | 'apply' | 'craft'>('craft');
const [selectedEquipmentType, setSelectedEquipmentType] = useState<string | null>(null);
const [selectedEquipmentInstance, setSelectedEquipmentInstance] = useState<string | null>(null);
const [selectedDesign, setSelectedDesign] = useState<string | null>(null);
// Design creation state
const [designName, setDesignName] = useState('');
const [selectedEffects, setSelectedEffects] = useState<DesignEffect[]>([]);
const enchantingLevel = skills.enchanting || 0;
const efficiencyBonus = (skills.efficientEnchant || 0) * 0.05;
// Get equipped items as array
const equippedItems = Object.entries(equippedInstances)
.filter(([, instanceId]) => instanceId && equipmentInstances[instanceId])
.map(([slot, instanceId]) => ({
slot: slot as EquipmentSlot,
instance: equipmentInstances[instanceId!],
}));
// Calculate total capacity cost for current design
const designCapacityCost = selectedEffects.reduce(
(total, eff) => total + calculateEffectCapacityCost(eff.effectId, eff.stacks, efficiencyBonus),
0
);
// Get capacity limit for selected equipment type
const selectedEquipmentCapacity = selectedEquipmentType ? EQUIPMENT_TYPES[selectedEquipmentType]?.baseCapacity || 0 : 0;
const isOverCapacity = selectedEquipmentType ? designCapacityCost > selectedEquipmentCapacity : false;
// Calculate design time
const designTime = selectedEffects.reduce((total, eff) => total + 0.5 * eff.stacks, 1);
// Add effect to design
const addEffect = (effectId: string) => {
const existing = selectedEffects.find(e => e.effectId === effectId);
const effectDef = ENCHANTMENT_EFFECTS[effectId];
if (!effectDef) return;
if (existing) {
if (existing.stacks < effectDef.maxStacks) {
setSelectedEffects(selectedEffects.map(e =>
e.effectId === effectId
? { ...e, stacks: e.stacks + 1 }
: e
));
}
} else {
setSelectedEffects([...selectedEffects, {
effectId,
stacks: 1,
capacityCost: calculateEffectCapacityCost(effectId, 1, efficiencyBonus),
}]);
}
};
// Remove effect from design
const removeEffect = (effectId: string) => {
const existing = selectedEffects.find(e => e.effectId === effectId);
if (!existing) return;
if (existing.stacks > 1) {
setSelectedEffects(selectedEffects.map(e =>
e.effectId === effectId
? { ...e, stacks: e.stacks - 1 }
: e
));
} else {
setSelectedEffects(selectedEffects.filter(e => e.effectId !== effectId));
}
};
// Create design
const handleCreateDesign = () => {
if (!designName || !selectedEquipmentType || selectedEffects.length === 0) return;
const success = startDesigningEnchantment(designName, selectedEquipmentType, selectedEffects);
if (success) {
// Reset form
setDesignName('');
setSelectedEquipmentType(null);
setSelectedEffects([]);
}
};
// Get available effects for selected equipment type (only unlocked ones)
const getAvailableEffects = () => {
if (!selectedEquipmentType) return [];
const type = EQUIPMENT_TYPES[selectedEquipmentType];
if (!type) return [];
return Object.values(ENCHANTMENT_EFFECTS).filter(
effect =>
effect.allowedEquipmentCategories.includes(type.category) &&
unlockedEffects.includes(effect.id)
);
};
// Render design stage
const renderDesignStage = () => (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Equipment Type Selection */}
<Card className="bg-gray-900/80 border-gray-700">
<CardHeader className="pb-2">
<CardTitle className="text-amber-400 text-sm">1. Select Equipment Type</CardTitle>
</CardHeader>
<CardContent>
{designProgress ? (
<div className="space-y-3">
<div className="text-sm text-gray-400">
Designing for: {EQUIPMENT_TYPES[designProgress.equipmentType]?.name}
</div>
<div className="text-sm font-semibold text-amber-300">{designProgress.name}</div>
<Progress value={(designProgress.progress / designProgress.required) * 100} className="h-3" />
<div className="flex justify-between text-xs text-gray-400">
<span>{designProgress.progress.toFixed(1)}h / {designProgress.required.toFixed(1)}h</span>
<Button size="sm" variant="outline" onClick={cancelDesign}>Cancel</Button>
</div>
</div>
) : (
<ScrollArea className="h-64">
<div className="grid grid-cols-2 gap-2">
{Object.values(EQUIPMENT_TYPES).map(type => (
<div
key={type.id}
className={`p-2 rounded border cursor-pointer transition-all ${
selectedEquipmentType === type.id
? 'border-amber-500 bg-amber-900/20'
: 'border-gray-700 bg-gray-800/50 hover:border-gray-600'
}`}
onClick={() => setSelectedEquipmentType(type.id)}
>
<div className="text-sm font-semibold">{type.name}</div>
<div className="text-xs text-gray-400">Cap: {type.baseCapacity}</div>
</div>
))}
</div>
</ScrollArea>
)}
</CardContent>
</Card>
{/* Effect Selection */}
<Card className="bg-gray-900/80 border-gray-700">
<CardHeader className="pb-2">
<CardTitle className="text-amber-400 text-sm">2. Select Effects</CardTitle>
</CardHeader>
<CardContent>
{enchantingLevel < 1 ? (
<div className="text-center text-gray-400 py-8">
<Wand2 className="w-12 h-12 mx-auto mb-2 opacity-50" />
<p>Learn Enchanting skill to design enchantments</p>
</div>
) : designProgress ? (
<div className="space-y-2">
<div className="text-sm text-gray-400">Design in progress...</div>
{designProgress.effects.map(eff => {
const def = ENCHANTMENT_EFFECTS[eff.effectId];
return (
<div key={eff.effectId} className="flex justify-between text-sm">
<span>{def?.name} x{eff.stacks}</span>
<span className="text-gray-400">{eff.capacityCost} cap</span>
</div>
);
})}
</div>
) : !selectedEquipmentType ? (
<div className="text-center text-gray-400 py-8">
Select an equipment type first
</div>
) : (
<>
<ScrollArea className="h-48 mb-4">
<div className="space-y-2">
{getAvailableEffects().map(effect => {
const selected = selectedEffects.find(e => e.effectId === effect.id);
const cost = calculateEffectCapacityCost(effect.id, (selected?.stacks || 0) + 1, efficiencyBonus);
return (
<div
key={effect.id}
className={`p-2 rounded border transition-all ${
selected
? 'border-purple-500 bg-purple-900/20'
: 'border-gray-700 bg-gray-800/50'
}`}
>
<div className="flex justify-between items-start">
<div className="flex-1">
<div className="text-sm font-semibold">{effect.name}</div>
<div className="text-xs text-gray-400">{effect.description}</div>
<div className="text-xs text-gray-500 mt-1">
Cost: {effect.baseCapacityCost} cap | Max: {effect.maxStacks}
</div>
</div>
<div className="flex gap-1">
{selected && (
<Button
size="sm"
variant="outline"
className="h-6 w-6 p-0"
onClick={() => removeEffect(effect.id)}
>
<Minus className="w-3 h-3" />
</Button>
)}
<Button
size="sm"
variant="outline"
className="h-6 w-6 p-0"
onClick={() => addEffect(effect.id)}
disabled={!selected && selectedEffects.length >= 5}
>
<Plus className="w-3 h-3" />
</Button>
</div>
</div>
{selected && (
<Badge variant="outline" className="mt-1 text-xs">
{selected.stacks}/{effect.maxStacks}
</Badge>
)}
</div>
);
})}
</div>
</ScrollArea>
{/* Selected effects summary */}
<Separator className="bg-gray-700 my-2" />
<div className="space-y-2">
<input
type="text"
placeholder="Design name..."
value={designName}
onChange={(e) => setDesignName(e.target.value)}
className="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm"
/>
<div className="flex justify-between text-sm">
<span>Total Capacity:</span>
<span className={isOverCapacity ? 'text-red-400' : 'text-green-400'}>
{designCapacityCost.toFixed(0)} / {selectedEquipmentCapacity}
</span>
</div>
<div className="flex justify-between text-sm text-gray-400">
<span>Design Time:</span>
<span>{designTime.toFixed(1)}h</span>
</div>
<Button
className="w-full"
disabled={!designName || selectedEffects.length === 0 || isOverCapacity}
onClick={handleCreateDesign}
>
{isOverCapacity ? 'Over Capacity!' : `Start Design (${designTime.toFixed(1)}h)`}
</Button>
</div>
</>
)}
</CardContent>
</Card>
{/* Saved Designs */}
<Card className="bg-gray-900/80 border-gray-700 lg:col-span-2">
<CardHeader className="pb-2">
<CardTitle className="text-amber-400 text-sm">Saved Designs ({enchantmentDesigns.length})</CardTitle>
</CardHeader>
<CardContent>
{enchantmentDesigns.length === 0 ? (
<div className="text-center text-gray-400 py-4">
No saved designs yet
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
{enchantmentDesigns.map(design => (
<div
key={design.id}
className={`p-3 rounded border ${
selectedDesign === design.id
? 'border-amber-500 bg-amber-900/20'
: 'border-gray-700 bg-gray-800/50'
}`}
onClick={() => setSelectedDesign(design.id)}
>
<div className="flex justify-between items-start">
<div>
<div className="font-semibold">{design.name}</div>
<div className="text-xs text-gray-400">
{EQUIPMENT_TYPES[design.equipmentType]?.name}
</div>
</div>
<Button
size="sm"
variant="ghost"
className="h-6 w-6 p-0 text-gray-400 hover:text-red-400"
onClick={(e) => {
e.stopPropagation();
deleteDesign(design.id);
}}
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
<div className="mt-2 text-xs text-gray-400">
{design.effects.length} effects | {design.totalCapacityUsed} cap
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
</div>
);
// Render prepare stage
const renderPrepareStage = () => (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Equipment Selection */}
<Card className="bg-gray-900/80 border-gray-700">
<CardHeader className="pb-2">
<CardTitle className="text-amber-400 text-sm">Select Equipment to Prepare or Disenchant</CardTitle>
</CardHeader>
<CardContent>
{preparationProgress ? (
<div className="space-y-3">
<div className="text-sm text-gray-400">
Preparing: {equipmentInstances[preparationProgress.equipmentInstanceId]?.name}
</div>
<Progress value={(preparationProgress.progress / preparationProgress.required) * 100} className="h-3" />
<div className="flex justify-between text-xs text-gray-400">
<span>{preparationProgress.progress.toFixed(1)}h / {preparationProgress.required.toFixed(1)}h</span>
<span>Mana paid: {fmt(preparationProgress.manaCostPaid)}</span>
</div>
<Button size="sm" variant="outline" onClick={cancelPreparation}>Cancel</Button>
</div>
) : (
<ScrollArea className="h-64">
<div className="space-y-2">
{equippedItems.map(({ slot, instance }) => {
const hasEnchantments = instance.enchantments.length > 0;
return (
<div
key={instance.instanceId}
className={`p-3 rounded border cursor-pointer transition-all ${
selectedEquipmentInstance === instance.instanceId
? 'border-amber-500 bg-amber-900/20'
: 'border-gray-700 bg-gray-800/50 hover:border-gray-600'
} ${hasEnchantments ? 'border-l-4 border-l-red-600' : ''}`}
onClick={() => setSelectedEquipmentInstance(instance.instanceId)}
>
<div className="flex justify-between">
<div>
<div className="font-semibold">{instance.name}</div>
<div className="text-xs text-gray-400">{SLOT_NAMES[slot]}</div>
{hasEnchantments && (
<div className="text-xs text-red-400 mt-1">
{instance.enchantments.length} enchantments - Disenchant to apply new
</div>
)}
</div>
<div className="text-right text-sm">
<div className="text-green-400">{instance.usedCapacity}/{instance.totalCapacity} cap</div>
<div className="text-xs text-gray-400">{instance.enchantments.length} enchants</div>
</div>
</div>
</div>
);
})}
{equippedItems.length === 0 && (
<div className="text-center text-gray-400 py-4">No equipped items</div>
)}
</div>
</ScrollArea>
)}
</CardContent>
</Card>
{/* Preparation Details */}
<Card className="bg-gray-900/80 border-gray-700">
<CardHeader className="pb-2">
<CardTitle className="text-amber-400 text-sm">Preparation Details</CardTitle>
</CardHeader>
<CardContent>
{!selectedEquipmentInstance ? (
<div className="text-center text-gray-400 py-8">
Select equipment to prepare or disenchant
</div>
) : preparationProgress ? (
<div className="text-gray-400">Preparation in progress...</div>
) : (
(() => {
const instance = equipmentInstances[selectedEquipmentInstance];
const hasEnchantments = instance.enchantments.length > 0;
const prepTime = 2 + Math.floor(instance.totalCapacity / 50);
const manaCost = instance.totalCapacity * 10;
// Calculate disenchant recovery
const disenchantLevel = skills.disenchanting || 0;
const recoveryRate = 0.1 + disenchantLevel * 0.2;
const totalRecoverable = instance.enchantments.reduce(
(sum, e) => sum + Math.floor(e.actualCost * recoveryRate),
0
);
return (
<div className="space-y-4">
<div className="text-lg font-semibold">{instance.name}</div>
<Separator className="bg-gray-700" />
{/* Disenchant option for enchanted gear */}
{hasEnchantments && (
<div className="p-3 rounded border border-red-600/50 bg-red-900/20 space-y-3">
<div className="text-sm font-semibold text-red-400"> Equipment has enchantments</div>
<div className="text-xs text-gray-400">
You must disenchant before applying new enchantments.
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-400">Recoverable Mana:</span>
<span className="text-green-400">{fmt(totalRecoverable)}</span>
</div>
<Button
className="w-full bg-red-600 hover:bg-red-700"
onClick={() => disenchantEquipment(instance.instanceId)}
>
<Trash2 className="w-4 h-4 mr-2" />
Disenchant & Recover {fmt(totalRecoverable)} Mana
</Button>
</div>
)}
{/* Prepare option for non-enchanted gear */}
{!hasEnchantments && (
<>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-gray-400">Capacity:</span>
<span>{instance.usedCapacity}/{instance.totalCapacity}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-400">Prep Time:</span>
<span>{prepTime}h</span>
</div>
<div className="flex justify-between">
<span className="text-gray-400">Mana Cost:</span>
<span className={rawMana < manaCost ? 'text-red-400' : 'text-green-400'}>
{fmt(manaCost)}
</span>
</div>
</div>
<Button
className="w-full"
disabled={rawMana < manaCost}
onClick={() => startPreparing(selectedEquipmentInstance)}
>
Start Preparation ({prepTime}h, {fmt(manaCost)} mana)
</Button>
</>
)}
</div>
);
})()
)}
</CardContent>
</Card>
</div>
);
// Render apply stage
const renderApplyStage = () => (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Equipment & Design Selection */}
<Card className="bg-gray-900/80 border-gray-700">
<CardHeader className="pb-2">
<CardTitle className="text-amber-400 text-sm">Select Equipment & Design</CardTitle>
</CardHeader>
<CardContent>
{applicationProgress ? (
<div className="space-y-3">
<div className="text-sm text-gray-400">
Applying to: {equipmentInstances[applicationProgress.equipmentInstanceId]?.name}
</div>
<Progress value={(applicationProgress.progress / applicationProgress.required) * 100} className="h-3" />
<div className="flex justify-between text-xs text-gray-400">
<span>{applicationProgress.progress.toFixed(1)}h / {applicationProgress.required.toFixed(1)}h</span>
<span>Mana spent: {fmt(applicationProgress.manaSpent)}</span>
</div>
<div className="flex gap-2">
{applicationProgress.paused ? (
<Button size="sm" onClick={resumeApplication}>Resume</Button>
) : (
<Button size="sm" variant="outline" onClick={pauseApplication}>Pause</Button>
)}
<Button size="sm" variant="outline" onClick={cancelApplication}>Cancel</Button>
</div>
</div>
) : (
<div className="space-y-4">
<div>
<div className="text-sm text-gray-400 mb-2">Equipment (without enchantments):</div>
<ScrollArea className="h-32">
<div className="space-y-1">
{equippedItems
.filter(({ instance }) => instance.enchantments.length === 0)
.map(({ slot, instance }) => (
<div
key={instance.instanceId}
className={`p-2 rounded border cursor-pointer text-sm ${
selectedEquipmentInstance === instance.instanceId
? 'border-amber-500 bg-amber-900/20'
: 'border-gray-700 bg-gray-800/50'
}`}
onClick={() => setSelectedEquipmentInstance(instance.instanceId)}
>
{instance.name} ({instance.usedCapacity}/{instance.totalCapacity} cap)
</div>
))}
{equippedItems.filter(({ instance }) => instance.enchantments.length === 0).length === 0 && (
<div className="text-center text-gray-500 text-xs py-2">
No unenchanted equipment available. Disenchant in Prepare stage first.
</div>
)}
</div>
</ScrollArea>
</div>
<div>
<div className="text-sm text-gray-400 mb-2">Design:</div>
<ScrollArea className="h-32">
<div className="space-y-1">
{enchantmentDesigns.map(design => (
<div
key={design.id}
className={`p-2 rounded border cursor-pointer text-sm ${
selectedDesign === design.id
? 'border-purple-500 bg-purple-900/20'
: 'border-gray-700 bg-gray-800/50'
}`}
onClick={() => setSelectedDesign(design.id)}
>
{design.name} ({design.totalCapacityUsed} cap)
</div>
))}
</div>
</ScrollArea>
</div>
</div>
)}
</CardContent>
</Card>
{/* Application Details */}
<Card className="bg-gray-900/80 border-gray-700">
<CardHeader className="pb-2">
<CardTitle className="text-amber-400 text-sm">Apply Enchantment</CardTitle>
</CardHeader>
<CardContent>
{!selectedEquipmentInstance || !selectedDesign ? (
<div className="text-center text-gray-400 py-8">
Select equipment and a design
</div>
) : applicationProgress ? (
<div className="text-gray-400">Application in progress...</div>
) : (
(() => {
const instance = equipmentInstances[selectedEquipmentInstance];
const design = enchantmentDesigns.find(d => d.id === selectedDesign);
if (!design) return null;
const availableCap = instance.totalCapacity - instance.usedCapacity;
const canFit = availableCap >= design.totalCapacityUsed;
const applicationTime = 2 + design.effects.reduce((t, e) => t + e.stacks, 0);
const manaPerHour = 20 + design.effects.reduce((t, e) => t + e.stacks * 5, 0);
return (
<div className="space-y-4">
<div className="text-lg font-semibold">{design.name}</div>
<div className="text-sm text-gray-400"> {instance.name}</div>
<Separator className="bg-gray-700" />
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-gray-400">Required Capacity:</span>
<span className={canFit ? 'text-green-400' : 'text-red-400'}>
{design.totalCapacityUsed} / {availableCap} available
</span>
</div>
<div className="flex justify-between">
<span className="text-gray-400">Application Time:</span>
<span>{applicationTime}h</span>
</div>
<div className="flex justify-between">
<span className="text-gray-400">Mana per Hour:</span>
<span>{manaPerHour}</span>
</div>
</div>
<div className="text-sm text-gray-400">
Effects:
<ul className="list-disc list-inside">
{design.effects.map(eff => (
<li key={eff.effectId}>
{ENCHANTMENT_EFFECTS[eff.effectId]?.name} x{eff.stacks}
</li>
))}
</ul>
</div>
<Button
className="w-full"
disabled={!canFit}
onClick={() => startApplying(selectedEquipmentInstance, selectedDesign)}
>
Apply Enchantment
</Button>
</div>
);
})()
)}
</CardContent>
</Card>
</div>
);
// Render equipment crafting stage
const renderCraftStage = () => (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Blueprint Selection */}
<Card className="bg-gray-900/80 border-gray-700">
<CardHeader className="pb-2">
<CardTitle className="text-amber-400 text-sm flex items-center gap-2">
<Anvil className="w-4 h-4" />
Available Blueprints
</CardTitle>
</CardHeader>
<CardContent>
{equipmentCraftingProgress ? (
<div className="space-y-3">
<div className="text-sm text-gray-400">
Crafting: {CRAFTING_RECIPES[equipmentCraftingProgress.blueprintId]?.name}
</div>
<Progress value={(equipmentCraftingProgress.progress / equipmentCraftingProgress.required) * 100} className="h-3" />
<div className="flex justify-between text-xs text-gray-400">
<span>{equipmentCraftingProgress.progress.toFixed(1)}h / {equipmentCraftingProgress.required.toFixed(1)}h</span>
<span>Mana spent: {fmt(equipmentCraftingProgress.manaSpent)}</span>
</div>
<Button size="sm" variant="outline" onClick={cancelEquipmentCrafting}>Cancel</Button>
</div>
) : (
<ScrollArea className="h-64">
<div className="space-y-2">
{lootInventory.blueprints.length === 0 ? (
<div className="text-center text-gray-400 py-4">
<Package className="w-12 h-12 mx-auto mb-2 opacity-50" />
<p>No blueprints discovered yet.</p>
<p className="text-xs mt-1">Defeat guardians to find blueprints!</p>
</div>
) : (
lootInventory.blueprints.map(bpId => {
const recipe = CRAFTING_RECIPES[bpId];
if (!recipe) return null;
const { canCraft, missingMaterials, missingMana } = canCraftRecipe(
recipe,
lootInventory.materials,
rawMana
);
const rarityStyle = RARITY_COLORS[recipe.rarity];
return (
<div
key={bpId}
className="p-3 rounded border bg-gray-800/50"
style={{ borderColor: rarityStyle?.color }}
>
<div className="flex justify-between items-start mb-2">
<div>
<div className="font-semibold" style={{ color: rarityStyle?.color }}>
{recipe.name}
</div>
<div className="text-xs text-gray-400 capitalize">{recipe.rarity}</div>
</div>
<Badge variant="outline" className="text-xs">
{EQUIPMENT_TYPES[recipe.equipmentTypeId]?.category}
</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">
<div className="text-gray-500">Materials:</div>
{Object.entries(recipe.materials).map(([matId, amount]) => {
const available = lootInventory.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>
);
})}
<div className="flex justify-between mt-2">
<span>Mana Cost:</span>
<span className={rawMana >= recipe.manaCost ? 'text-green-400' : 'text-red-400'}>
{fmt(recipe.manaCost)}
</span>
</div>
<div className="flex justify-between">
<span>Craft Time:</span>
<span>{recipe.craftTime}h</span>
</div>
</div>
<Button
className="w-full mt-3"
size="sm"
disabled={!canCraft || currentAction === 'craft'}
onClick={() => startCraftingEquipment(bpId)}
>
{canCraft ? 'Craft Equipment' : 'Missing Resources'}
</Button>
</div>
);
})
)}
</div>
</ScrollArea>
)}
</CardContent>
</Card>
{/* Materials Inventory */}
<Card className="bg-gray-900/80 border-gray-700">
<CardHeader className="pb-2">
<CardTitle className="text-amber-400 text-sm flex items-center gap-2">
<Package className="w-4 h-4" />
Materials ({Object.values(lootInventory.materials).reduce((a, b) => a + b, 0)})
</CardTitle>
</CardHeader>
<CardContent>
<ScrollArea className="h-64">
{Object.keys(lootInventory.materials).length === 0 ? (
<div className="text-center text-gray-400 py-4">
<Sparkles className="w-12 h-12 mx-auto mb-2 opacity-50" />
<p>No materials collected yet.</p>
<p className="text-xs mt-1">Defeat floors to gather materials!</p>
</div>
) : (
<div className="grid grid-cols-2 gap-2">
{Object.entries(lootInventory.materials).map(([matId, count]) => {
if (count <= 0) return null;
const drop = LOOT_DROPS[matId];
if (!drop) return null;
const rarityStyle = RARITY_COLORS[drop.rarity];
return (
<div
key={matId}
className="p-2 rounded border bg-gray-800/50 group relative"
style={{ borderColor: rarityStyle?.color }}
>
<div className="flex items-start justify-between">
<div>
<div className="text-sm font-semibold" style={{ color: rarityStyle?.color }}>
{drop.name}
</div>
<div className="text-xs text-gray-400">x{count}</div>
</div>
<Button
variant="ghost"
size="sm"
className="h-5 w-5 p-0 opacity-0 group-hover:opacity-100 text-red-400 hover:text-red-300 hover:bg-red-900/20"
onClick={() => deleteMaterial(matId, count)}
>
<Trash2 className="w-3 h-3" />
</Button>
</div>
</div>
);
})}
</div>
)}
</ScrollArea>
</CardContent>
</Card>
</div>
);
return (
<div className="space-y-4">
{/* Stage Tabs */}
<Tabs value={craftingStage} onValueChange={(v) => setCraftingStage(v as typeof craftingStage)}>
<TabsList className="bg-gray-800/50">
<TabsTrigger value="craft" className="data-[state=active]:bg-cyan-600">
<Anvil className="w-4 h-4 mr-1" />
Craft
</TabsTrigger>
<TabsTrigger value="design" className="data-[state=active]:bg-amber-600">
<Scroll className="w-4 h-4 mr-1" />
Design
</TabsTrigger>
<TabsTrigger value="prepare" className="data-[state=active]:bg-amber-600">
<Hammer className="w-4 h-4 mr-1" />
Prepare
</TabsTrigger>
<TabsTrigger value="apply" className="data-[state=active]:bg-amber-600">
<Sparkles className="w-4 h-4 mr-1" />
Apply
</TabsTrigger>
</TabsList>
<TabsContent value="craft" className="mt-4">
{renderCraftStage()}
</TabsContent>
<TabsContent value="design" className="mt-4">
{renderDesignStage()}
</TabsContent>
<TabsContent value="prepare" className="mt-4">
{renderPrepareStage()}
</TabsContent>
<TabsContent value="apply" className="mt-4">
{renderApplyStage()}
</TabsContent>
</Tabs>
{/* Current Activity Indicator */}
{currentAction === 'craft' && equipmentCraftingProgress && (
<Card className="bg-cyan-900/30 border-cyan-600">
<CardContent className="py-3 flex items-center justify-between">
<div className="flex items-center gap-2">
<Anvil className="w-5 h-5 text-cyan-400" />
<span>Crafting equipment...</span>
</div>
<div className="text-sm text-gray-400">
{((equipmentCraftingProgress.progress / equipmentCraftingProgress.required) * 100).toFixed(0)}%
</div>
</CardContent>
</Card>
)}
{currentAction === 'design' && designProgress && (
<Card className="bg-purple-900/30 border-purple-600">
<CardContent className="py-3 flex items-center justify-between">
<div className="flex items-center gap-2">
<Scroll className="w-5 h-5 text-purple-400" />
<span>Designing enchantment...</span>
</div>
<div className="text-sm text-gray-400">
{((designProgress.progress / designProgress.required) * 100).toFixed(0)}%
</div>
</CardContent>
</Card>
)}
{currentAction === 'prepare' && preparationProgress && (
<Card className="bg-blue-900/30 border-blue-600">
<CardContent className="py-3 flex items-center justify-between">
<div className="flex items-center gap-2">
<Hammer className="w-5 h-5 text-blue-400" />
<span>Preparing equipment...</span>
</div>
<div className="text-sm text-gray-400">
{((preparationProgress.progress / preparationProgress.required) * 100).toFixed(0)}%
</div>
</CardContent>
</Card>
)}
{currentAction === 'enchant' && applicationProgress && (
<Card className="bg-amber-900/30 border-amber-600">
<CardContent className="py-3 flex items-center justify-between">
<div className="flex items-center gap-2">
<Sparkles className="w-5 h-5 text-amber-400" />
<span>{applicationProgress.paused ? 'Enchantment paused' : 'Applying enchantment...'}</span>
</div>
<div className="flex items-center gap-2">
<div className="text-sm text-gray-400">
{((applicationProgress.progress / applicationProgress.required) * 100).toFixed(0)}%
</div>
{applicationProgress.paused ? (
<Button size="sm" onClick={resumeApplication}>Resume</Button>
) : (
<Button size="sm" variant="outline" onClick={pauseApplication}>Pause</Button>
)}
</div>
</CardContent>
</Card>
)}
</div>
);
}