/** * LootTableBuilder Component * Visual editor for configuring lootbox drop pools with items and currencies. */ import { useState, useCallback, useEffect } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Card, CardContent } from "@/components/ui/card"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Slider } from "@/components/ui/slider"; import { Plus, Trash2, Package, Coins, Sparkles, GripVertical, Percent } from "lucide-react"; import { cn } from "@/lib/utils"; // Loot drop types type LootType = "ITEM" | "CURRENCY" | "XP"; interface LootDrop { type: LootType; itemId?: number; itemName?: string; amount?: number; minAmount?: number; maxAmount?: number; weight: number; } interface LootTableBuilderProps { pool: LootDrop[]; onChange: (pool: LootDrop[]) => void; } const LOOT_TYPES = [ { value: "ITEM" as LootType, label: "Item", icon: Package, color: "text-purple-400" }, { value: "CURRENCY" as LootType, label: "Currency", icon: Coins, color: "text-amber-400" }, { value: "XP" as LootType, label: "XP", icon: Sparkles, color: "text-blue-400" }, ]; const getDefaultDrop = (type: LootType): LootDrop => { switch (type) { case "ITEM": return { type: "ITEM", itemId: 0, itemName: "", weight: 10 }; case "CURRENCY": return { type: "CURRENCY", minAmount: 100, maxAmount: 500, weight: 30 }; case "XP": return { type: "XP", minAmount: 50, maxAmount: 200, weight: 20 }; } }; export function LootTableBuilder({ pool, onChange }: LootTableBuilderProps) { // Calculate total weight for probability display const totalWeight = pool.reduce((sum, drop) => sum + drop.weight, 0); const addDrop = useCallback(() => { onChange([...pool, getDefaultDrop("CURRENCY")]); }, [pool, onChange]); const removeDrop = useCallback((index: number) => { onChange(pool.filter((_, i) => i !== index)); }, [pool, onChange]); const updateDrop = useCallback((index: number, updates: Partial) => { onChange(pool.map((drop, i) => i === index ? { ...drop, ...updates } : drop)); }, [pool, onChange]); const changeDropType = useCallback((index: number, newType: LootType) => { onChange(pool.map((drop, i) => i === index ? getDefaultDrop(newType) : drop)); }, [pool, onChange]); const getProbability = (weight: number): string => { if (totalWeight === 0) return "0%"; return ((weight / totalWeight) * 100).toFixed(1) + "%"; }; if (pool.length === 0) { return (

No drops configured yet

); } return (
{/* Probability Summary Bar */} {pool.length > 0 && (
{pool.map((drop, index) => { const lootType = LOOT_TYPES.find(t => t.value === drop.type); const probability = totalWeight > 0 ? (drop.weight / totalWeight) * 100 : 0; if (probability < 1) return null; const colors: Record = { ITEM: "bg-purple-500", CURRENCY: "bg-amber-500", XP: "bg-blue-500", }; return (
{probability > 10 && ( {probability.toFixed(0)}% )}
); })}
)} {/* Drop List */}
{pool.map((drop, index) => { return ( {/* Header Row */}
{/* Type Selector */} {/* Probability Badge */}
{getProbability(drop.weight)}
{/* Delete Button */}
{/* Type-Specific Fields */}
{drop.type === "ITEM" && ( <>
updateDrop(index, { itemId: parseInt(e.target.value) || 0 })} placeholder="Item ID" className="bg-background/50 h-8 text-sm" />
updateDrop(index, { itemName: e.target.value })} placeholder="For reference only" className="bg-background/50 h-8 text-sm" />
)} {(drop.type === "CURRENCY" || drop.type === "XP") && ( <>
{ const min = parseInt(e.target.value) || 0; const currentMax = drop.maxAmount ?? drop.amount ?? min; updateDrop(index, { minAmount: min, maxAmount: Math.max(min, currentMax), amount: undefined // Clear amount }); }} className="bg-background/50 h-8 text-sm" />
{ const max = parseInt(e.target.value) || 0; const currentMin = drop.minAmount ?? drop.amount ?? max; updateDrop(index, { minAmount: Math.min(currentMin, max), maxAmount: max, amount: undefined // Clear amount }); }} className="bg-background/50 h-8 text-sm" />
)}
{/* Weight Slider */}
{drop.weight}
updateDrop(index, { weight: values[0] ?? drop.weight })} min={1} max={100} step={1} className="py-1" />
); })}
{/* Add Button */}
); } export default LootTableBuilder;