diff --git a/docs/phase3-progress.md b/docs/phase3-progress.md new file mode 100644 index 0000000..e022ce3 --- /dev/null +++ b/docs/phase3-progress.md @@ -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 diff --git a/src/components/game/crafting/EnchantmentApplier.tsx b/src/components/game/crafting/EnchantmentApplier.tsx new file mode 100644 index 0000000..aab6605 --- /dev/null +++ b/src/components/game/crafting/EnchantmentApplier.tsx @@ -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 = { + 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 ( +
+ {/* Equipment & Design Selection */} + + + Select Equipment & Design + + + {applicationProgress ? ( +
+
+ Applying to: {equipmentInstances[applicationProgress.equipmentInstanceId]?.name} +
+ +
+ {applicationProgress.progress.toFixed(1)}h / {applicationProgress.required.toFixed(1)}h + Mana spent: {fmt(applicationProgress.manaSpent)} +
+
+ {applicationProgress.paused ? ( + + ) : ( + + )} + +
+
+ ) : ( +
+
+
Equipment (without enchantments):
+ +
+ {equippedItems + .filter(({ instance }) => instance.enchantments.length === 0) + .map(({ slot, instance }) => ( +
setSelectedEquipmentInstance(instance.instanceId)} + > + {instance.name} ({instance.usedCapacity}/{instance.totalCapacity} cap) +
+ ))} + {equippedItems.filter(({ instance }) => instance.enchantments.length === 0).length === 0 && ( +
+ No unenchanted equipment available. Disenchant in Prepare stage first. +
+ )} +
+
+
+ +
+
Design:
+ +
+ {enchantmentDesigns.map(design => ( +
setSelectedDesign(design.id)} + > + {design.name} ({design.totalCapacityUsed} cap) +
+ ))} +
+
+
+
+ )} +
+
+ + {/* Application Details */} + + + Apply Enchantment + + + {!selectedEquipmentInstance || !selectedDesign ? ( +
+ Select equipment and a design +
+ ) : applicationProgress ? ( +
Application in progress...
+ ) : ( + (() => { + 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 ( +
+
{design.name}
+
→ {instance.name}
+ + +
+
+ Required Capacity: + + {design.totalCapacityUsed} / {availableCap} available + +
+
+ Application Time: + {applicationTime}h +
+
+ Mana per Hour: + {manaPerHour} +
+
+ +
+ Effects: +
    + {design.effects.map(eff => ( +
  • + {ENCHANTMENT_EFFECTS[eff.effectId]?.name} x{eff.stacks} +
  • + ))} +
+
+ + +
+ ); + })() + )} +
+
+
+ ); +} diff --git a/src/components/game/crafting/EnchantmentDesigner.tsx b/src/components/game/crafting/EnchantmentDesigner.tsx new file mode 100644 index 0000000..f30bcec --- /dev/null +++ b/src/components/game/crafting/EnchantmentDesigner.tsx @@ -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 = { + 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 ( +
+ {/* Equipment Type Selection */} + + + 1. Select Equipment Type + + + {designProgress ? ( +
+
+ Designing for: {EQUIPMENT_TYPES[designProgress.equipmentType]?.name} +
+
{designProgress.name}
+ +
+ {designProgress.progress.toFixed(1)}h / {designProgress.required.toFixed(1)}h + +
+
+ ) : ( + +
+ {Object.values(EQUIPMENT_TYPES).map(type => ( +
setSelectedEquipmentType(type.id)} + > +
{type.name}
+
Cap: {type.baseCapacity}
+
+ ))} +
+
+ )} +
+
+ + {/* Effect Selection */} + + + 2. Select Effects + + + {enchantingLevel < 1 ? ( +
+ +

Learn Enchanting skill to design enchantments

+
+ ) : designProgress ? ( +
+
Design in progress...
+ {designProgress.effects.map(eff => { + const def = ENCHANTMENT_EFFECTS[eff.effectId]; + return ( +
+ {def?.name} x{eff.stacks} + {eff.capacityCost} cap +
+ ); + })} +
+ ) : !selectedEquipmentType ? ( +
+ Select an equipment type first +
+ ) : ( + <> + +
+ {getAvailableEffects().map(effect => { + const selected = selectedEffects.find(e => e.effectId === effect.id); + const cost = calculateEffectCapacityCost(effect.id, (selected?.stacks || 0) + 1, efficiencyBonus); + + return ( +
+
+
+
{effect.name}
+
{effect.description}
+
+ Cost: {effect.baseCapacityCost} cap | Max: {effect.maxStacks} +
+
+
+ {selected && ( + + )} + +
+
+ {selected && ( + + {selected.stacks}/{effect.maxStacks} + + )} +
+ ); + })} +
+
+ + {/* Selected effects summary */} + +
+ setDesignName(e.target.value)} + className="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm" + /> +
+ Total Capacity: + + {designCapacityCost.toFixed(0)} / {selectedEquipmentCapacity} + +
+
+ Design Time: + {designTime.toFixed(1)}h +
+ +
+ + )} +
+
+ + {/* Saved Designs */} + + + Saved Designs ({enchantmentDesigns.length}) + + + {enchantmentDesigns.length === 0 ? ( +
+ No saved designs yet +
+ ) : ( +
+ {enchantmentDesigns.map(design => ( +
setSelectedDesign(design.id)} + > +
+
+
{design.name}
+
+ {EQUIPMENT_TYPES[design.equipmentType]?.name} +
+
+ +
+
+ {design.effects.length} effects | {design.totalCapacityUsed} cap +
+
+ ))} +
+ )} +
+
+
+ ); +} diff --git a/src/components/game/crafting/EnchantmentPreparer.tsx b/src/components/game/crafting/EnchantmentPreparer.tsx new file mode 100644 index 0000000..ba79f09 --- /dev/null +++ b/src/components/game/crafting/EnchantmentPreparer.tsx @@ -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 = { + 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 ( +
+ {/* Equipment Selection */} + + + Select Equipment to Prepare or Disenchant + + + {preparationProgress ? ( +
+
+ Preparing: {equipmentInstances[preparationProgress.equipmentInstanceId]?.name} +
+ +
+ {preparationProgress.progress.toFixed(1)}h / {preparationProgress.required.toFixed(1)}h + Mana paid: {fmt(preparationProgress.manaCostPaid)} +
+ +
+ ) : ( + +
+ {equippedItems.map(({ slot, instance }) => { + const hasEnchantments = instance.enchantments.length > 0; + return ( +
setSelectedEquipmentInstance(instance.instanceId)} + > +
+
+
{instance.name}
+
{SLOT_NAMES[slot]}
+ {hasEnchantments && ( +
+ ⚠️ {instance.enchantments.length} enchantments - Disenchant to apply new +
+ )} +
+
+
{instance.usedCapacity}/{instance.totalCapacity} cap
+
{instance.enchantments.length} enchants
+
+
+
+ ); + })} + {equippedItems.length === 0 && ( +
No equipped items
+ )} +
+
+ )} +
+
+ + {/* Preparation Details */} + + + Preparation Details + + + {!selectedEquipmentInstance ? ( +
+ Select equipment to prepare or disenchant +
+ ) : preparationProgress ? ( +
Preparation in progress...
+ ) : ( + (() => { + 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 ( +
+
{instance.name}
+ + + {/* Disenchant option for enchanted gear */} + {hasEnchantments && ( +
+
⚠️ Equipment has enchantments
+
+ You must disenchant before applying new enchantments. +
+
+ Recoverable Mana: + {fmt(totalRecoverable)} +
+ +
+ )} + + {/* Prepare option for non-enchanted gear */} + {!hasEnchantments && ( + <> +
+
+ Capacity: + {instance.usedCapacity}/{instance.totalCapacity} +
+
+ Prep Time: + {prepTime}h +
+
+ Mana Cost: + + {fmt(manaCost)} + +
+
+ + + )} +
+ ); + })() + )} +
+
+
+ ); +} diff --git a/src/components/game/crafting/EquipmentCrafter.tsx b/src/components/game/crafting/EquipmentCrafter.tsx new file mode 100644 index 0000000..9f34cbd --- /dev/null +++ b/src/components/game/crafting/EquipmentCrafter.tsx @@ -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 ( +
+ {/* Blueprint Selection */} + + + + + Available Blueprints + + + + {equipmentCraftingProgress ? ( +
+
+ Crafting: {CRAFTING_RECIPES[equipmentCraftingProgress.blueprintId]?.name} +
+ +
+ {equipmentCraftingProgress.progress.toFixed(1)}h / {equipmentCraftingProgress.required.toFixed(1)}h + Mana spent: {fmt(equipmentCraftingProgress.manaSpent)} +
+ +
+ ) : ( + +
+ {lootInventory.blueprints.length === 0 ? ( +
+ +

No blueprints discovered yet.

+

Defeat guardians to find blueprints!

+
+ ) : ( + 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 ( +
+
+
+
+ {recipe.name} +
+
{recipe.rarity}
+
+ + {recipe.equipmentTypeId ? 'Equipment' : 'Other'} + +
+ +
{recipe.description}
+ + + +
+
Materials:
+ {Object.entries(recipe.materials).map(([matId, amount]) => { + const available = lootInventory.materials[matId] || 0; + const matDrop = LOOT_DROPS[matId]; + const hasEnough = available >= amount; + + return ( +
+ {matDrop?.name || matId} + + {available} / {amount} + +
+ ); + })} + +
+ Mana Cost: + = recipe.manaCost ? 'text-green-400' : 'text-red-400'}> + {fmt(recipe.manaCost)} + +
+ +
+ Craft Time: + {recipe.craftTime}h +
+
+ + +
+ ); + }) + )} +
+
+ )} +
+
+ + {/* Materials Inventory */} + + + + + Materials ({Object.values(lootInventory.materials).reduce((a, b) => a + b, 0)}) + + + + + {Object.keys(lootInventory.materials).length === 0 ? ( +
+ +

No materials collected yet.

+

Defeat floors to gather materials!

+
+ ) : ( +
+ {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 ( +
+
+
+
+ {drop.name} +
+
x{count}
+
+ +
+
+ ); + })} +
+ )} +
+
+
+
+ ); +} diff --git a/src/components/game/crafting/index.tsx b/src/components/game/crafting/index.tsx new file mode 100644 index 0000000..7e79b4f --- /dev/null +++ b/src/components/game/crafting/index.tsx @@ -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'; diff --git a/src/components/game/tabs/CraftingTab.tsx b/src/components/game/tabs/CraftingTab.tsx index 5b2e608..48e10cf 100755 --- a/src/components/game/tabs/CraftingTab.tsx +++ b/src/components/game/tabs/CraftingTab.tsx @@ -4,862 +4,39 @@ import { useState } from 'react'; import { Button } from '@/components/ui/button'; import { Progress } from '@/components/ui/progress'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { Badge } from '@/components/ui/badge'; -import { ScrollArea } from '@/components/ui/scroll-area'; -import { Separator } from '@/components/ui/separator'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import { - Wand2, Scroll, Hammer, Sparkles, Trash2, Plus, Minus, - Package, Zap, Clock, ChevronRight, Circle, Anvil -} from 'lucide-react'; -import { EQUIPMENT_TYPES } from '@/lib/game/data/equipment'; -import { ENCHANTMENT_EFFECTS, calculateEffectCapacityCost } from '@/lib/game/data/enchantment-effects'; -import { CRAFTING_RECIPES, canCraftRecipe } from '@/lib/game/data/crafting-recipes'; -import { LOOT_DROPS, RARITY_COLORS } from '@/lib/game/data/loot-drops'; +import { Scroll, Hammer, Sparkles, Anvil } from 'lucide-react'; import type { EquipmentInstance, EnchantmentDesign, DesignEffect, AppliedEnchantment, LootInventory, EquipmentCraftingProgress } from '@/lib/game/types'; import { fmt, type GameStore } from '@/lib/game/store'; - -// Slot display names -const SLOT_NAMES: Record = { - mainHand: 'Main Hand', - offHand: 'Off Hand', - head: 'Head', - body: 'Body', - hands: 'Hands', - feet: 'Feet', - accessory1: 'Accessory 1', - accessory2: 'Accessory 2', -}; +import { + EnchantmentDesigner, + EnchantmentPreparer, + EnchantmentApplier, + EquipmentCrafter, +} from '@/components/game/crafting'; export interface CraftingTabProps { store: GameStore; } export function CraftingTab({ store }: CraftingTabProps) { - const equippedInstances = store.equippedInstances; - const equipmentInstances = store.equipmentInstances; - const enchantmentDesigns = store.enchantmentDesigns; + const currentAction = store.currentAction; const designProgress = store.designProgress; const preparationProgress = store.preparationProgress; const applicationProgress = store.applicationProgress; const equipmentCraftingProgress = store.equipmentCraftingProgress; - const rawMana = store.rawMana; - const skills = store.skills; - const currentAction = store.currentAction; - const unlockedEffects = store.unlockedEffects; - const lootInventory = store.lootInventory; - const startDesigningEnchantment = store.startDesigningEnchantment; - const cancelDesign = store.cancelDesign; - const saveDesign = store.saveDesign; - const deleteDesign = store.deleteDesign; - const startPreparing = store.startPreparing; - const cancelPreparation = store.cancelPreparation; - const startApplying = store.startApplying; const pauseApplication = store.pauseApplication; const resumeApplication = store.resumeApplication; - const cancelApplication = store.cancelApplication; - const disenchantEquipment = store.disenchantEquipment; - const getAvailableCapacity = store.getAvailableCapacity; - const startCraftingEquipment = store.startCraftingEquipment; - const cancelEquipmentCrafting = store.cancelEquipmentCrafting; - const deleteMaterial = store.deleteMaterial; + const [craftingStage, setCraftingStage] = useState<'design' | 'prepare' | 'apply' | 'craft'>('craft'); const [selectedEquipmentType, setSelectedEquipmentType] = useState(null); const [selectedEquipmentInstance, setSelectedEquipmentInstance] = useState(null); const [selectedDesign, setSelectedDesign] = useState(null); - + // Design creation state const [designName, setDesignName] = useState(''); const [selectedEffects, setSelectedEffects] = useState([]); - - 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 = () => ( -
- {/* Equipment Type Selection */} - - - 1. Select Equipment Type - - - {designProgress ? ( -
-
- Designing for: {EQUIPMENT_TYPES[designProgress.equipmentType]?.name} -
-
{designProgress.name}
- -
- {designProgress.progress.toFixed(1)}h / {designProgress.required.toFixed(1)}h - -
-
- ) : ( - -
- {Object.values(EQUIPMENT_TYPES).map(type => ( -
setSelectedEquipmentType(type.id)} - > -
{type.name}
-
Cap: {type.baseCapacity}
-
- ))} -
-
- )} -
-
- - {/* Effect Selection */} - - - 2. Select Effects - - - {enchantingLevel < 1 ? ( -
- -

Learn Enchanting skill to design enchantments

-
- ) : designProgress ? ( -
-
Design in progress...
- {designProgress.effects.map(eff => { - const def = ENCHANTMENT_EFFECTS[eff.effectId]; - return ( -
- {def?.name} x{eff.stacks} - {eff.capacityCost} cap -
- ); - })} -
- ) : !selectedEquipmentType ? ( -
- Select an equipment type first -
- ) : ( - <> - -
- {getAvailableEffects().map(effect => { - const selected = selectedEffects.find(e => e.effectId === effect.id); - const cost = calculateEffectCapacityCost(effect.id, (selected?.stacks || 0) + 1, efficiencyBonus); - - return ( -
-
-
-
{effect.name}
-
{effect.description}
-
- Cost: {effect.baseCapacityCost} cap | Max: {effect.maxStacks} -
-
-
- {selected && ( - - )} - -
-
- {selected && ( - - {selected.stacks}/{effect.maxStacks} - - )} -
- ); - })} -
-
- - {/* Selected effects summary */} - -
- setDesignName(e.target.value)} - className="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm" - /> -
- Total Capacity: - - {designCapacityCost.toFixed(0)} / {selectedEquipmentCapacity} - -
-
- Design Time: - {designTime.toFixed(1)}h -
- -
- - )} -
-
- - {/* Saved Designs */} - - - Saved Designs ({enchantmentDesigns.length}) - - - {enchantmentDesigns.length === 0 ? ( -
- No saved designs yet -
- ) : ( -
- {enchantmentDesigns.map(design => ( -
setSelectedDesign(design.id)} - > -
-
-
{design.name}
-
- {EQUIPMENT_TYPES[design.equipmentType]?.name} -
-
- -
-
- {design.effects.length} effects | {design.totalCapacityUsed} cap -
-
- ))} -
- )} -
-
-
- ); - - // Render prepare stage - const renderPrepareStage = () => ( -
- {/* Equipment Selection */} - - - Select Equipment to Prepare or Disenchant - - - {preparationProgress ? ( -
-
- Preparing: {equipmentInstances[preparationProgress.equipmentInstanceId]?.name} -
- -
- {preparationProgress.progress.toFixed(1)}h / {preparationProgress.required.toFixed(1)}h - Mana paid: {fmt(preparationProgress.manaCostPaid)} -
- -
- ) : ( - -
- {equippedItems.map(({ slot, instance }) => { - const hasEnchantments = instance.enchantments.length > 0; - return ( -
setSelectedEquipmentInstance(instance.instanceId)} - > -
-
-
{instance.name}
-
{SLOT_NAMES[slot]}
- {hasEnchantments && ( -
- ⚠️ {instance.enchantments.length} enchantments - Disenchant to apply new -
- )} -
-
-
{instance.usedCapacity}/{instance.totalCapacity} cap
-
{instance.enchantments.length} enchants
-
-
-
- ); - })} - {equippedItems.length === 0 && ( -
No equipped items
- )} -
-
- )} -
-
- - {/* Preparation Details */} - - - Preparation Details - - - {!selectedEquipmentInstance ? ( -
- Select equipment to prepare or disenchant -
- ) : preparationProgress ? ( -
Preparation in progress...
- ) : ( - (() => { - 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 ( -
-
{instance.name}
- - - {/* Disenchant option for enchanted gear */} - {hasEnchantments && ( -
-
⚠️ Equipment has enchantments
-
- You must disenchant before applying new enchantments. -
-
- Recoverable Mana: - {fmt(totalRecoverable)} -
- -
- )} - - {/* Prepare option for non-enchanted gear */} - {!hasEnchantments && ( - <> -
-
- Capacity: - {instance.usedCapacity}/{instance.totalCapacity} -
-
- Prep Time: - {prepTime}h -
-
- Mana Cost: - - {fmt(manaCost)} - -
-
- - - )} -
- ); - })() - )} -
-
-
- ); - - // Render apply stage - const renderApplyStage = () => ( -
- {/* Equipment & Design Selection */} - - - Select Equipment & Design - - - {applicationProgress ? ( -
-
- Applying to: {equipmentInstances[applicationProgress.equipmentInstanceId]?.name} -
- -
- {applicationProgress.progress.toFixed(1)}h / {applicationProgress.required.toFixed(1)}h - Mana spent: {fmt(applicationProgress.manaSpent)} -
-
- {applicationProgress.paused ? ( - - ) : ( - - )} - -
-
- ) : ( -
-
-
Equipment (without enchantments):
- -
- {equippedItems - .filter(({ instance }) => instance.enchantments.length === 0) - .map(({ slot, instance }) => ( -
setSelectedEquipmentInstance(instance.instanceId)} - > - {instance.name} ({instance.usedCapacity}/{instance.totalCapacity} cap) -
- ))} - {equippedItems.filter(({ instance }) => instance.enchantments.length === 0).length === 0 && ( -
- No unenchanted equipment available. Disenchant in Prepare stage first. -
- )} -
-
-
- -
-
Design:
- -
- {enchantmentDesigns.map(design => ( -
setSelectedDesign(design.id)} - > - {design.name} ({design.totalCapacityUsed} cap) -
- ))} -
-
-
-
- )} -
-
- - {/* Application Details */} - - - Apply Enchantment - - - {!selectedEquipmentInstance || !selectedDesign ? ( -
- Select equipment and a design -
- ) : applicationProgress ? ( -
Application in progress...
- ) : ( - (() => { - 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 ( -
-
{design.name}
-
→ {instance.name}
- - -
-
- Required Capacity: - - {design.totalCapacityUsed} / {availableCap} available - -
-
- Application Time: - {applicationTime}h -
-
- Mana per Hour: - {manaPerHour} -
-
- -
- Effects: -
    - {design.effects.map(eff => ( -
  • - {ENCHANTMENT_EFFECTS[eff.effectId]?.name} x{eff.stacks} -
  • - ))} -
-
- - -
- ); - })() - )} -
-
-
- ); - - // Render equipment crafting stage - const renderCraftStage = () => ( -
- {/* Blueprint Selection */} - - - - - Available Blueprints - - - - {equipmentCraftingProgress ? ( -
-
- Crafting: {CRAFTING_RECIPES[equipmentCraftingProgress.blueprintId]?.name} -
- -
- {equipmentCraftingProgress.progress.toFixed(1)}h / {equipmentCraftingProgress.required.toFixed(1)}h - Mana spent: {fmt(equipmentCraftingProgress.manaSpent)} -
- -
- ) : ( - -
- {lootInventory.blueprints.length === 0 ? ( -
- -

No blueprints discovered yet.

-

Defeat guardians to find blueprints!

-
- ) : ( - 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 ( -
-
-
-
- {recipe.name} -
-
{recipe.rarity}
-
- - {EQUIPMENT_TYPES[recipe.equipmentTypeId]?.category} - -
- -
{recipe.description}
- - - -
-
Materials:
- {Object.entries(recipe.materials).map(([matId, amount]) => { - const available = lootInventory.materials[matId] || 0; - const matDrop = LOOT_DROPS[matId]; - const hasEnough = available >= amount; - - return ( -
- {matDrop?.name || matId} - - {available} / {amount} - -
- ); - })} - -
- Mana Cost: - = recipe.manaCost ? 'text-green-400' : 'text-red-400'}> - {fmt(recipe.manaCost)} - -
- -
- Craft Time: - {recipe.craftTime}h -
-
- - -
- ); - }) - )} -
-
- )} -
-
- - {/* Materials Inventory */} - - - - - Materials ({Object.values(lootInventory.materials).reduce((a, b) => a + b, 0)}) - - - - - {Object.keys(lootInventory.materials).length === 0 ? ( -
- -

No materials collected yet.

-

Defeat floors to gather materials!

-
- ) : ( -
- {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 ( -
-
-
-
- {drop.name} -
-
x{count}
-
- -
-
- ); - })} -
- )} -
-
-
-
- ); - + return (
{/* Stage Tabs */} @@ -882,21 +59,41 @@ export function CraftingTab({ store }: CraftingTabProps) { Apply - + - {renderCraftStage()} + - {renderDesignStage()} + - {renderPrepareStage()} + - {renderApplyStage()} + - + {/* Current Activity Indicator */} {currentAction === 'craft' && equipmentCraftingProgress && ( @@ -911,7 +108,7 @@ export function CraftingTab({ store }: CraftingTabProps) { )} - + {currentAction === 'design' && designProgress && ( @@ -925,7 +122,7 @@ export function CraftingTab({ store }: CraftingTabProps) { )} - + {currentAction === 'prepare' && preparationProgress && ( @@ -939,7 +136,7 @@ export function CraftingTab({ store }: CraftingTabProps) { )} - + {currentAction === 'enchant' && applicationProgress && (