393 lines
16 KiB
TypeScript
393 lines
16 KiB
TypeScript
'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 { CRAFTING_RECIPES } from '@/lib/game/data/crafting-recipes';
|
|
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)
|
|
);
|
|
};
|
|
|
|
// Get equipment types that the player has blueprints for
|
|
const getOwnedEquipmentTypes = () => {
|
|
const ownedBlueprints = store.lootInventory.blueprints || [];
|
|
|
|
// Map blueprint IDs to equipment type IDs
|
|
const ownedEquipmentTypeIds = new Set<string>();
|
|
for (const blueprintId of ownedBlueprints) {
|
|
const recipe = CRAFTING_RECIPES[blueprintId];
|
|
if (recipe) {
|
|
ownedEquipmentTypeIds.add(recipe.equipmentTypeId);
|
|
}
|
|
}
|
|
|
|
// Also include the starting equipment types (basicStaff, civilianShirt, civilianShoes)
|
|
// These are the types the player starts with, so they should be able to design for them
|
|
ownedEquipmentTypeIds.add('basicStaff');
|
|
ownedEquipmentTypeIds.add('civilianShirt');
|
|
ownedEquipmentTypeIds.add('civilianShoes');
|
|
ownedEquipmentTypeIds.add('apprenticeWand');
|
|
ownedEquipmentTypeIds.add('clothHood');
|
|
ownedEquipmentTypeIds.add('civilianGloves');
|
|
ownedEquipmentTypeIds.add('copperRing');
|
|
|
|
return Object.values(EQUIPMENT_TYPES).filter(type => ownedEquipmentTypeIds.has(type.id));
|
|
};
|
|
|
|
const ownedEquipmentTypes = getOwnedEquipmentTypes();
|
|
|
|
// 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">
|
|
{ownedEquipmentTypes.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>
|
|
{ownedEquipmentTypes.length === 0 && (
|
|
<div className="text-center text-gray-400 py-4 text-sm">
|
|
No equipment blueprints owned. Craft or find equipment blueprints first.
|
|
</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>
|
|
);
|
|
}
|
|
|
|
EnchantmentDesigner.displayName = "EnchantmentDesigner";
|