Phase 3: Split CraftingTab.tsx into crafting stage components
This commit is contained in:
@@ -0,0 +1,50 @@
|
|||||||
|
# Phase 3: Refactor Large Files - Progress
|
||||||
|
|
||||||
|
## Completed Refactorings (All Committed & Pushed)
|
||||||
|
|
||||||
|
### 1. `types.ts` (516 lines) ✅
|
||||||
|
- **Commit**: `eb81ccb Phase 3: Split types.ts into domain-specific files`
|
||||||
|
- **Result**: Split into `types/elements.ts`, `types/attunements.ts`, `types/spells.ts`, `types/skills.ts`, `types/equipment.ts`, `types/game.ts`, `types/index.ts`
|
||||||
|
- **Build**: ✅ Passes
|
||||||
|
|
||||||
|
### 2. `constants.ts` (1436 lines) ✅
|
||||||
|
- **Commit**: `f8520e1 Phase 3: Split constants.ts into domain-specific files`
|
||||||
|
- **Result**: Split into `constants/elements.ts`, `constants/guardians.ts`, `constants/spells.ts`, `constants/skills.ts`, `constants/prestige.ts`, `constants/rooms.ts`, `constants/core.ts`, `constants/index.ts`
|
||||||
|
- **Build**: ✅ Passes
|
||||||
|
|
||||||
|
### 3. `enchantment-effects.ts` (846 lines) ✅
|
||||||
|
- **Commit**: `c46981d Phase 3: Split enchantment-effects.ts into category files`
|
||||||
|
- **Result**: Split into `data/enchantments/spell-effects.ts`, `mana-effects.ts`, `combat-effects.ts`, `elemental-effects.ts`, `defense-effects.ts`, `utility-effects.ts`, `special-effects.ts`, `enchantment-types.ts`, `index.ts`
|
||||||
|
- **Build**: ✅ Passes
|
||||||
|
|
||||||
|
## Failed Refactorings
|
||||||
|
|
||||||
|
### 1. `store.ts` (2464 lines) ❌
|
||||||
|
- **Issue**: Sub-agent made changes that broke build (`Cannot read properties of undefined (reading 'mainHand')`)
|
||||||
|
- **Action**: Reverted changes with `git restore .`
|
||||||
|
- **Status**: Flagged as "too large for current sub-agent setup"
|
||||||
|
|
||||||
|
### 2. `skill-evolution.ts` (2312 lines) ❌
|
||||||
|
- **Issue**: Larger than `store.ts` which failed
|
||||||
|
- **Status**: Flagged as "too large for current sub-agent setup"
|
||||||
|
|
||||||
|
## Next Files to Refactor
|
||||||
|
|
||||||
|
### High Priority (Smaller, Likely to Work)
|
||||||
|
1. `src/components/game/tabs/CraftingTab.tsx` (965 lines) - Split by crafting stage
|
||||||
|
2. `src/lib/game/computed-stats.ts` (492 lines) - Split by responsibility
|
||||||
|
3. `src/lib/game/utils.ts` (372 lines) - Split by responsibility
|
||||||
|
|
||||||
|
### Medium Priority
|
||||||
|
4. `src/components/game/tabs/DebugTab.tsx` (700 lines) - Split by functional area
|
||||||
|
5. `src/lib/game/stores/gameStore.ts` (509 lines) - Clean up coordinator
|
||||||
|
6. `src/app/page.tsx` (465 lines) - Lazy load tabs
|
||||||
|
|
||||||
|
## Build Status
|
||||||
|
✅ Build passes after each successful refactoring
|
||||||
|
✅ All commits pushed to remote
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Sub-agents work best with files under ~1500 lines
|
||||||
|
- Focused prompts yield better results
|
||||||
|
- Larger files (2000+ lines) tend to break builds or fail silently
|
||||||
@@ -0,0 +1,211 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Progress } from '@/components/ui/progress';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
|
import { Separator } from '@/components/ui/separator';
|
||||||
|
import { ENCHANTMENT_EFFECTS } from '@/lib/game/data/enchantment-effects';
|
||||||
|
import type { EquipmentInstance, EnchantmentDesign, 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 EnchantmentApplierProps {
|
||||||
|
store: GameStore;
|
||||||
|
selectedEquipmentInstance: string | null;
|
||||||
|
setSelectedEquipmentInstance: (id: string | null) => void;
|
||||||
|
selectedDesign: string | null;
|
||||||
|
setSelectedDesign: (id: string | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EnchantmentApplier({
|
||||||
|
store,
|
||||||
|
selectedEquipmentInstance,
|
||||||
|
setSelectedEquipmentInstance,
|
||||||
|
selectedDesign,
|
||||||
|
setSelectedDesign,
|
||||||
|
}: EnchantmentApplierProps) {
|
||||||
|
const equippedInstances = store.equippedInstances;
|
||||||
|
const equipmentInstances = store.equipmentInstances;
|
||||||
|
const enchantmentDesigns = store.enchantmentDesigns;
|
||||||
|
const applicationProgress = store.applicationProgress;
|
||||||
|
const rawMana = store.rawMana;
|
||||||
|
const startApplying = store.startApplying;
|
||||||
|
const pauseApplication = store.pauseApplication;
|
||||||
|
const resumeApplication = store.resumeApplication;
|
||||||
|
const cancelApplication = store.cancelApplication;
|
||||||
|
|
||||||
|
// 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!],
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Progress } from '@/components/ui/progress';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
|
import { Separator } from '@/components/ui/separator';
|
||||||
|
import { Trash2 } from 'lucide-react';
|
||||||
|
import { EQUIPMENT_TYPES } from '@/lib/game/data/equipment';
|
||||||
|
import type { EquipmentInstance, 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 EnchantmentPreparerProps {
|
||||||
|
store: GameStore;
|
||||||
|
selectedEquipmentInstance: string | null;
|
||||||
|
setSelectedEquipmentInstance: (id: string | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EnchantmentPreparer({
|
||||||
|
store,
|
||||||
|
selectedEquipmentInstance,
|
||||||
|
setSelectedEquipmentInstance,
|
||||||
|
}: EnchantmentPreparerProps) {
|
||||||
|
const equippedInstances = store.equippedInstances;
|
||||||
|
const equipmentInstances = store.equipmentInstances;
|
||||||
|
const preparationProgress = store.preparationProgress;
|
||||||
|
const rawMana = store.rawMana;
|
||||||
|
const skills = store.skills;
|
||||||
|
const startPreparing = store.startPreparing;
|
||||||
|
const cancelPreparation = store.cancelPreparation;
|
||||||
|
const disenchantEquipment = store.disenchantEquipment;
|
||||||
|
|
||||||
|
// 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!],
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
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 { Package, Sparkles, Trash2, Anvil } from 'lucide-react';
|
||||||
|
import { CRAFTING_RECIPES, canCraftRecipe } from '@/lib/game/data/crafting-recipes';
|
||||||
|
import { LOOT_DROPS, RARITY_COLORS } from '@/lib/game/data/loot-drops';
|
||||||
|
import type { EquipmentInstance, AppliedEnchantment, LootInventory, EquipmentCraftingProgress } from '@/lib/game/types';
|
||||||
|
import { fmt, type GameStore } from '@/lib/game/store';
|
||||||
|
|
||||||
|
export interface EquipmentCrafterProps {
|
||||||
|
store: GameStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EquipmentCrafter({ store }: EquipmentCrafterProps) {
|
||||||
|
const lootInventory = store.lootInventory;
|
||||||
|
const equipmentCraftingProgress = store.equipmentCraftingProgress;
|
||||||
|
const rawMana = store.rawMana;
|
||||||
|
const currentAction = store.currentAction;
|
||||||
|
const startCraftingEquipment = store.startCraftingEquipment;
|
||||||
|
const cancelEquipmentCrafting = store.cancelEquipmentCrafting;
|
||||||
|
const deleteMaterial = store.deleteMaterial;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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">
|
||||||
|
{recipe.equipmentTypeId ? 'Equipment' : 'Other'}
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
// Barrel file for crafting components
|
||||||
|
|
||||||
|
export { EnchantmentDesigner, type EnchantmentDesignerProps } from './EnchantmentDesigner';
|
||||||
|
export { EnchantmentPreparer, type EnchantmentPreparerProps } from './EnchantmentPreparer';
|
||||||
|
export { EnchantmentApplier, type EnchantmentApplierProps } from './EnchantmentApplier';
|
||||||
|
export { EquipmentCrafter, type EquipmentCrafterProps } from './EquipmentCrafter';
|
||||||
@@ -4,862 +4,39 @@ import { useState } from 'react';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Progress } from '@/components/ui/progress';
|
import { Progress } from '@/components/ui/progress';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
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 { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
import {
|
import { Scroll, Hammer, Sparkles, Anvil } from 'lucide-react';
|
||||||
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 type { EquipmentInstance, EnchantmentDesign, DesignEffect, AppliedEnchantment, LootInventory, EquipmentCraftingProgress } from '@/lib/game/types';
|
||||||
import { fmt, type GameStore } from '@/lib/game/store';
|
import { fmt, type GameStore } from '@/lib/game/store';
|
||||||
|
import {
|
||||||
// Slot display names
|
EnchantmentDesigner,
|
||||||
const SLOT_NAMES: Record<EquipmentSlot, string> = {
|
EnchantmentPreparer,
|
||||||
mainHand: 'Main Hand',
|
EnchantmentApplier,
|
||||||
offHand: 'Off Hand',
|
EquipmentCrafter,
|
||||||
head: 'Head',
|
} from '@/components/game/crafting';
|
||||||
body: 'Body',
|
|
||||||
hands: 'Hands',
|
|
||||||
feet: 'Feet',
|
|
||||||
accessory1: 'Accessory 1',
|
|
||||||
accessory2: 'Accessory 2',
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface CraftingTabProps {
|
export interface CraftingTabProps {
|
||||||
store: GameStore;
|
store: GameStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CraftingTab({ store }: CraftingTabProps) {
|
export function CraftingTab({ store }: CraftingTabProps) {
|
||||||
const equippedInstances = store.equippedInstances;
|
const currentAction = store.currentAction;
|
||||||
const equipmentInstances = store.equipmentInstances;
|
|
||||||
const enchantmentDesigns = store.enchantmentDesigns;
|
|
||||||
const designProgress = store.designProgress;
|
const designProgress = store.designProgress;
|
||||||
const preparationProgress = store.preparationProgress;
|
const preparationProgress = store.preparationProgress;
|
||||||
const applicationProgress = store.applicationProgress;
|
const applicationProgress = store.applicationProgress;
|
||||||
const equipmentCraftingProgress = store.equipmentCraftingProgress;
|
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 pauseApplication = store.pauseApplication;
|
||||||
const resumeApplication = store.resumeApplication;
|
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 [craftingStage, setCraftingStage] = useState<'design' | 'prepare' | 'apply' | 'craft'>('craft');
|
||||||
const [selectedEquipmentType, setSelectedEquipmentType] = useState<string | null>(null);
|
const [selectedEquipmentType, setSelectedEquipmentType] = useState<string | null>(null);
|
||||||
const [selectedEquipmentInstance, setSelectedEquipmentInstance] = useState<string | null>(null);
|
const [selectedEquipmentInstance, setSelectedEquipmentInstance] = useState<string | null>(null);
|
||||||
const [selectedDesign, setSelectedDesign] = useState<string | null>(null);
|
const [selectedDesign, setSelectedDesign] = useState<string | null>(null);
|
||||||
|
|
||||||
// Design creation state
|
// Design creation state
|
||||||
const [designName, setDesignName] = useState('');
|
const [designName, setDesignName] = useState('');
|
||||||
const [selectedEffects, setSelectedEffects] = useState<DesignEffect[]>([]);
|
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 (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Stage Tabs */}
|
{/* Stage Tabs */}
|
||||||
@@ -882,21 +59,41 @@ export function CraftingTab({ store }: CraftingTabProps) {
|
|||||||
Apply
|
Apply
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="craft" className="mt-4">
|
<TabsContent value="craft" className="mt-4">
|
||||||
{renderCraftStage()}
|
<EquipmentCrafter store={store} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="design" className="mt-4">
|
<TabsContent value="design" className="mt-4">
|
||||||
{renderDesignStage()}
|
<EnchantmentDesigner
|
||||||
|
store={store}
|
||||||
|
selectedEquipmentType={selectedEquipmentType}
|
||||||
|
setSelectedEquipmentType={setSelectedEquipmentType}
|
||||||
|
selectedEffects={selectedEffects}
|
||||||
|
setSelectedEffects={setSelectedEffects}
|
||||||
|
designName={designName}
|
||||||
|
setDesignName={setDesignName}
|
||||||
|
selectedDesign={selectedDesign}
|
||||||
|
setSelectedDesign={setSelectedDesign}
|
||||||
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="prepare" className="mt-4">
|
<TabsContent value="prepare" className="mt-4">
|
||||||
{renderPrepareStage()}
|
<EnchantmentPreparer
|
||||||
|
store={store}
|
||||||
|
selectedEquipmentInstance={selectedEquipmentInstance}
|
||||||
|
setSelectedEquipmentInstance={setSelectedEquipmentInstance}
|
||||||
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="apply" className="mt-4">
|
<TabsContent value="apply" className="mt-4">
|
||||||
{renderApplyStage()}
|
<EnchantmentApplier
|
||||||
|
store={store}
|
||||||
|
selectedEquipmentInstance={selectedEquipmentInstance}
|
||||||
|
setSelectedEquipmentInstance={setSelectedEquipmentInstance}
|
||||||
|
selectedDesign={selectedDesign}
|
||||||
|
setSelectedDesign={setSelectedDesign}
|
||||||
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
{/* Current Activity Indicator */}
|
{/* Current Activity Indicator */}
|
||||||
{currentAction === 'craft' && equipmentCraftingProgress && (
|
{currentAction === 'craft' && equipmentCraftingProgress && (
|
||||||
<Card className="bg-cyan-900/30 border-cyan-600">
|
<Card className="bg-cyan-900/30 border-cyan-600">
|
||||||
@@ -911,7 +108,7 @@ export function CraftingTab({ store }: CraftingTabProps) {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{currentAction === 'design' && designProgress && (
|
{currentAction === 'design' && designProgress && (
|
||||||
<Card className="bg-purple-900/30 border-purple-600">
|
<Card className="bg-purple-900/30 border-purple-600">
|
||||||
<CardContent className="py-3 flex items-center justify-between">
|
<CardContent className="py-3 flex items-center justify-between">
|
||||||
@@ -925,7 +122,7 @@ export function CraftingTab({ store }: CraftingTabProps) {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{currentAction === 'prepare' && preparationProgress && (
|
{currentAction === 'prepare' && preparationProgress && (
|
||||||
<Card className="bg-blue-900/30 border-blue-600">
|
<Card className="bg-blue-900/30 border-blue-600">
|
||||||
<CardContent className="py-3 flex items-center justify-between">
|
<CardContent className="py-3 flex items-center justify-between">
|
||||||
@@ -939,7 +136,7 @@ export function CraftingTab({ store }: CraftingTabProps) {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{currentAction === 'enchant' && applicationProgress && (
|
{currentAction === 'enchant' && applicationProgress && (
|
||||||
<Card className="bg-amber-900/30 border-amber-600">
|
<Card className="bg-amber-900/30 border-amber-600">
|
||||||
<CardContent className="py-3 flex items-center justify-between">
|
<CardContent className="py-3 flex items-center justify-between">
|
||||||
|
|||||||
Reference in New Issue
Block a user