Phase 3: Split CraftingTab.tsx into crafting stage components

This commit is contained in:
Unknown
2026-04-24 13:05:12 +02:00
parent c46981d81b
commit a528feb8e2
7 changed files with 1067 additions and 843 deletions
@@ -0,0 +1,356 @@
'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 { Wand2, Scroll, Trash2, Plus, Minus } from 'lucide-react';
import { EQUIPMENT_TYPES } from '@/lib/game/data/equipment';
import { ENCHANTMENT_EFFECTS, calculateEffectCapacityCost } from '@/lib/game/data/enchantment-effects';
import { LOOT_DROPS, RARITY_COLORS } from '@/lib/game/data/loot-drops';
import type { EquipmentInstance, EnchantmentDesign, DesignEffect, 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 EnchantmentDesignerProps {
store: GameStore;
selectedEquipmentType: string | null;
setSelectedEquipmentType: (type: string | null) => void;
selectedEffects: DesignEffect[];
setSelectedEffects: (effects: DesignEffect[]) => void;
designName: string;
setDesignName: (name: string) => void;
selectedDesign: string | null;
setSelectedDesign: (id: string | null) => void;
}
export function EnchantmentDesigner({
store,
selectedEquipmentType,
setSelectedEquipmentType,
selectedEffects,
setSelectedEffects,
designName,
setDesignName,
selectedDesign,
setSelectedDesign,
}: EnchantmentDesignerProps) {
const enchantmentDesigns = store.enchantmentDesigns;
const designProgress = store.designProgress;
const startDesigningEnchantment = store.startDesigningEnchantment;
const cancelDesign = store.cancelDesign;
const deleteDesign = store.deleteDesign;
const unlockedEffects = store.unlockedEffects;
const skills = store.skills;
const enchantingLevel = skills.enchanting || 0;
const efficiencyBonus = (skills.efficientEnchant || 0) * 0.05;
// 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
return (
<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>
);
}