461 lines
18 KiB
TypeScript
Executable File
461 lines
18 KiB
TypeScript
Executable File
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Button } from '@/components/ui/button';
|
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
import { Separator } from '@/components/ui/separator';
|
|
import { Input } from '@/components/ui/input';
|
|
import {
|
|
Gem, Sparkles, Scroll, Droplet, Trash2, Search,
|
|
Package, Sword, Shield, Shirt, Crown, ArrowUpDown,
|
|
Wrench, AlertTriangle
|
|
} from 'lucide-react';
|
|
import type { LootInventory as LootInventoryType, EquipmentInstance, ElementState } from '@/lib/game/types';
|
|
import { LOOT_DROPS, RARITY_COLORS } from '@/lib/game/data/loot-drops';
|
|
import { EQUIPMENT_TYPES } from '@/lib/game/data/equipment';
|
|
import { ELEMENTS } from '@/lib/game/constants';
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
} from '@/components/ui/alert-dialog';
|
|
|
|
interface LootInventoryProps {
|
|
inventory: LootInventoryType;
|
|
elements?: Record<string, ElementState>;
|
|
equipmentInstances?: Record<string, EquipmentInstance>;
|
|
onDeleteMaterial?: (materialId: string, amount: number) => void;
|
|
onDeleteEquipment?: (instanceId: string) => void;
|
|
}
|
|
|
|
type SortMode = 'name' | 'rarity' | 'count';
|
|
type FilterMode = 'all' | 'materials' | 'essence' | 'blueprints' | 'equipment';
|
|
|
|
const RARITY_ORDER = {
|
|
common: 0,
|
|
uncommon: 1,
|
|
rare: 2,
|
|
epic: 3,
|
|
legendary: 4,
|
|
mythic: 5,
|
|
};
|
|
|
|
const CATEGORY_ICONS: Record<string, typeof Sword> = {
|
|
caster: Sword,
|
|
shield: Shield,
|
|
catalyst: Sparkles,
|
|
head: Crown,
|
|
body: Shirt,
|
|
hands: Wrench,
|
|
feet: Package,
|
|
accessory: Gem,
|
|
};
|
|
|
|
export function LootInventoryDisplay({
|
|
inventory,
|
|
elements,
|
|
equipmentInstances = {},
|
|
onDeleteMaterial,
|
|
onDeleteEquipment,
|
|
}: LootInventoryProps) {
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [sortMode, setSortMode] = useState<SortMode>('rarity');
|
|
const [filterMode, setFilterMode] = useState<FilterMode>('all');
|
|
const [deleteConfirm, setDeleteConfirm] = useState<{ type: 'material' | 'equipment'; id: string; name: string } | null>(null);
|
|
|
|
// Count items
|
|
const materialCount = Object.values(inventory.materials).reduce((a, b) => a + b, 0);
|
|
const essenceCount = elements ? Object.values(elements).reduce((a, e) => a + e.current, 0) : 0;
|
|
const blueprintCount = inventory.blueprints.length;
|
|
const equipmentCount = Object.keys(equipmentInstances).length;
|
|
const totalItems = materialCount + blueprintCount + equipmentCount;
|
|
|
|
// Filter and sort materials
|
|
const filteredMaterials = Object.entries(inventory.materials)
|
|
.filter(([id, count]) => {
|
|
if (count <= 0) return false;
|
|
const drop = LOOT_DROPS[id];
|
|
if (!drop) return false;
|
|
if (searchTerm && !drop.name.toLowerCase().includes(searchTerm.toLowerCase())) return false;
|
|
return true;
|
|
})
|
|
.sort(([aId, aCount], [bId, bCount]) => {
|
|
const aDrop = LOOT_DROPS[aId];
|
|
const bDrop = LOOT_DROPS[bId];
|
|
if (!aDrop || !bDrop) return 0;
|
|
|
|
switch (sortMode) {
|
|
case 'name':
|
|
return aDrop.name.localeCompare(bDrop.name);
|
|
case 'rarity':
|
|
return RARITY_ORDER[bDrop.rarity] - RARITY_ORDER[aDrop.rarity];
|
|
case 'count':
|
|
return bCount - aCount;
|
|
default:
|
|
return 0;
|
|
}
|
|
});
|
|
|
|
// Filter and sort essence
|
|
const filteredEssence = elements
|
|
? Object.entries(elements)
|
|
.filter(([id, state]) => {
|
|
if (!state.unlocked || state.current <= 0) return false;
|
|
if (searchTerm && !ELEMENTS[id]?.name.toLowerCase().includes(searchTerm.toLowerCase())) return false;
|
|
return true;
|
|
})
|
|
.sort(([aId, aState], [bId, bState]) => {
|
|
switch (sortMode) {
|
|
case 'name':
|
|
return (ELEMENTS[aId]?.name || aId).localeCompare(ELEMENTS[bId]?.name || bId);
|
|
case 'count':
|
|
return bState.current - aState.current;
|
|
default:
|
|
return 0;
|
|
}
|
|
})
|
|
: [];
|
|
|
|
// Filter and sort equipment
|
|
const filteredEquipment = Object.entries(equipmentInstances)
|
|
.filter(([id, instance]) => {
|
|
if (searchTerm && !instance.name.toLowerCase().includes(searchTerm.toLowerCase())) return false;
|
|
return true;
|
|
})
|
|
.sort(([aId, aInst], [bId, bInst]) => {
|
|
switch (sortMode) {
|
|
case 'name':
|
|
return aInst.name.localeCompare(bInst.name);
|
|
case 'rarity':
|
|
return RARITY_ORDER[bInst.rarity] - RARITY_ORDER[aInst.rarity];
|
|
default:
|
|
return 0;
|
|
}
|
|
});
|
|
|
|
// Check if we have anything to show
|
|
const hasItems = totalItems > 0 || essenceCount > 0;
|
|
|
|
if (!hasItems) {
|
|
return (
|
|
<Card className="bg-gray-900/80 border-gray-700">
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-amber-400 game-panel-title text-xs flex items-center gap-2">
|
|
<Gem className="w-4 h-4" />
|
|
Inventory
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-gray-500 text-sm text-center py-4">
|
|
No items collected yet. Defeat floors and guardians to find loot!
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
const handleDeleteMaterial = (materialId: string) => {
|
|
const drop = LOOT_DROPS[materialId];
|
|
if (drop) {
|
|
setDeleteConfirm({ type: 'material', id: materialId, name: drop.name });
|
|
}
|
|
};
|
|
|
|
const handleDeleteEquipment = (instanceId: string) => {
|
|
const instance = equipmentInstances[instanceId];
|
|
if (instance) {
|
|
setDeleteConfirm({ type: 'equipment', id: instanceId, name: instance.name });
|
|
}
|
|
};
|
|
|
|
const confirmDelete = () => {
|
|
if (!deleteConfirm) return;
|
|
|
|
if (deleteConfirm.type === 'material' && onDeleteMaterial) {
|
|
onDeleteMaterial(deleteConfirm.id, inventory.materials[deleteConfirm.id] || 0);
|
|
} else if (deleteConfirm.type === 'equipment' && onDeleteEquipment) {
|
|
onDeleteEquipment(deleteConfirm.id);
|
|
}
|
|
|
|
setDeleteConfirm(null);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<Card className="bg-gray-900/80 border-gray-700">
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-amber-400 game-panel-title text-xs flex items-center gap-2">
|
|
<Gem className="w-4 h-4" />
|
|
Inventory
|
|
<Badge className="ml-auto bg-gray-800 text-gray-300 text-xs">
|
|
{totalItems} items
|
|
</Badge>
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-3">
|
|
{/* Search and Filter Controls */}
|
|
<div className="flex gap-2">
|
|
<div className="relative flex-1">
|
|
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3 h-3 text-gray-500" />
|
|
<Input
|
|
placeholder="Search..."
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
className="h-7 pl-7 bg-gray-800/50 border-gray-700 text-xs"
|
|
/>
|
|
</div>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="h-7 px-2 bg-gray-800/50"
|
|
onClick={() => setSortMode(sortMode === 'rarity' ? 'name' : sortMode === 'name' ? 'count' : 'rarity')}
|
|
>
|
|
<ArrowUpDown className="w-3 h-3" />
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Filter Tabs */}
|
|
<div className="flex gap-1 flex-wrap">
|
|
{[
|
|
{ mode: 'all' as FilterMode, label: 'All' },
|
|
{ mode: 'materials' as FilterMode, label: `Materials (${materialCount})` },
|
|
{ mode: 'essence' as FilterMode, label: `Essence (${essenceCount})` },
|
|
{ mode: 'blueprints' as FilterMode, label: `Blueprints (${blueprintCount})` },
|
|
{ mode: 'equipment' as FilterMode, label: `Equipment (${equipmentCount})` },
|
|
].map(({ mode, label }) => (
|
|
<Button
|
|
key={mode}
|
|
variant={filterMode === mode ? 'default' : 'outline'}
|
|
size="sm"
|
|
className={`h-6 px-2 text-xs ${filterMode === mode ? 'bg-amber-600 hover:bg-amber-700' : 'bg-gray-800/50'}`}
|
|
onClick={() => setFilterMode(mode)}
|
|
>
|
|
{label}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
|
|
<Separator className="bg-gray-700" />
|
|
|
|
<ScrollArea className="h-64">
|
|
<div className="space-y-3">
|
|
{/* Materials */}
|
|
{(filterMode === 'all' || filterMode === 'materials') && filteredMaterials.length > 0 && (
|
|
<div>
|
|
<div className="text-xs text-gray-500 mb-2 flex items-center gap-1">
|
|
<Sparkles className="w-3 h-3" />
|
|
Materials
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-2">
|
|
{filteredMaterials.map(([id, count]) => {
|
|
const drop = LOOT_DROPS[id];
|
|
if (!drop) return null;
|
|
const rarityStyle = RARITY_COLORS[drop.rarity];
|
|
return (
|
|
<div
|
|
key={id}
|
|
className="p-2 rounded border bg-gray-800/50 group relative"
|
|
style={{
|
|
borderColor: rarityStyle?.color || '#9CA3AF',
|
|
}}
|
|
>
|
|
<div className="flex items-start justify-between">
|
|
<div>
|
|
<div className="text-xs font-semibold" style={{ color: rarityStyle?.color }}>
|
|
{drop.name}
|
|
</div>
|
|
<div className="text-xs text-gray-400">
|
|
x{count}
|
|
</div>
|
|
<div className="text-xs text-gray-500 capitalize">
|
|
{drop.rarity}
|
|
</div>
|
|
</div>
|
|
{onDeleteMaterial && (
|
|
<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={() => handleDeleteMaterial(id)}
|
|
>
|
|
<Trash2 className="w-3 h-3" />
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Essence */}
|
|
{(filterMode === 'all' || filterMode === 'essence') && filteredEssence.length > 0 && (
|
|
<div>
|
|
<div className="text-xs text-gray-500 mb-2 flex items-center gap-1">
|
|
<Droplet className="w-3 h-3" />
|
|
Elemental Essence
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-2">
|
|
{filteredEssence.map(([id, state]) => {
|
|
const elem = ELEMENTS[id];
|
|
if (!elem) return null;
|
|
return (
|
|
<div
|
|
key={id}
|
|
className="p-2 rounded border bg-gray-800/50"
|
|
style={{
|
|
borderColor: elem.color,
|
|
}}
|
|
>
|
|
<div className="flex items-center gap-1">
|
|
<span style={{ color: elem.color }}>{elem.sym}</span>
|
|
<span className="text-xs font-semibold" style={{ color: elem.color }}>
|
|
{elem.name}
|
|
</span>
|
|
</div>
|
|
<div className="text-xs text-gray-400">
|
|
{state.current} / {state.max}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Blueprints */}
|
|
{(filterMode === 'all' || filterMode === 'blueprints') && inventory.blueprints.length > 0 && (
|
|
<div>
|
|
<div className="text-xs text-gray-500 mb-2 flex items-center gap-1">
|
|
<Scroll className="w-3 h-3" />
|
|
Blueprints (permanent)
|
|
</div>
|
|
<div className="flex flex-wrap gap-1">
|
|
{inventory.blueprints.map((id) => {
|
|
const drop = LOOT_DROPS[id];
|
|
if (!drop) return null;
|
|
const rarityStyle = RARITY_COLORS[drop.rarity];
|
|
return (
|
|
<Badge
|
|
key={id}
|
|
className="text-xs"
|
|
style={{
|
|
backgroundColor: `${rarityStyle?.color}20`,
|
|
color: rarityStyle?.color,
|
|
borderColor: rarityStyle?.color,
|
|
}}
|
|
>
|
|
{drop.name}
|
|
</Badge>
|
|
);
|
|
})}
|
|
</div>
|
|
<div className="text-xs text-gray-500 mt-1 italic">
|
|
Blueprints are permanent unlocks - use them to craft equipment
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Equipment */}
|
|
{(filterMode === 'all' || filterMode === 'equipment') && filteredEquipment.length > 0 && (
|
|
<div>
|
|
<div className="text-xs text-gray-500 mb-2 flex items-center gap-1">
|
|
<Package className="w-3 h-3" />
|
|
Equipment
|
|
</div>
|
|
<div className="space-y-2">
|
|
{filteredEquipment.map(([id, instance]) => {
|
|
const type = EQUIPMENT_TYPES[instance.typeId];
|
|
const Icon = type ? CATEGORY_ICONS[type.category] || Package : Package;
|
|
const rarityStyle = RARITY_COLORS[instance.rarity];
|
|
|
|
return (
|
|
<div
|
|
key={id}
|
|
className="p-2 rounded border bg-gray-800/50 group"
|
|
style={{
|
|
borderColor: rarityStyle?.color || '#9CA3AF',
|
|
}}
|
|
>
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex items-start gap-2">
|
|
<Icon className="w-4 h-4 mt-0.5" style={{ color: rarityStyle?.color }} />
|
|
<div>
|
|
<div className="text-xs font-semibold" style={{ color: rarityStyle?.color }}>
|
|
{instance.name}
|
|
</div>
|
|
<div className="text-xs text-gray-400">
|
|
{type?.name} • {instance.usedCapacity}/{instance.totalCapacity} cap
|
|
</div>
|
|
<div className="text-xs text-gray-500 capitalize">
|
|
{instance.rarity} • {instance.enchantments.length} enchants
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{onDeleteEquipment && (
|
|
<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={() => handleDeleteEquipment(id)}
|
|
>
|
|
<Trash2 className="w-3 h-3" />
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</ScrollArea>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Delete Confirmation Dialog */}
|
|
<AlertDialog open={!!deleteConfirm} onOpenChange={() => setDeleteConfirm(null)}>
|
|
<AlertDialogContent className="bg-gray-900 border-gray-700">
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle className="text-amber-400 flex items-center gap-2">
|
|
<AlertTriangle className="w-5 h-5" />
|
|
Delete Item
|
|
</AlertDialogTitle>
|
|
<AlertDialogDescription className="text-gray-300">
|
|
Are you sure you want to delete <strong>{deleteConfirm?.name}</strong>?
|
|
{deleteConfirm?.type === 'material' && (
|
|
<span className="block mt-2 text-red-400">
|
|
This will delete ALL {inventory.materials[deleteConfirm?.id || ''] || 0} of this material!
|
|
</span>
|
|
)}
|
|
{deleteConfirm?.type === 'equipment' && (
|
|
<span className="block mt-2 text-red-400">
|
|
This equipment and all its enchantments will be permanently lost!
|
|
</span>
|
|
)}
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel className="bg-gray-800 border-gray-700">Cancel</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
className="bg-red-600 hover:bg-red-700"
|
|
onClick={confirmDelete}
|
|
>
|
|
Delete
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</>
|
|
);
|
|
}
|