feat: Implement structured lootbox results with image support and display referenced items in shop listings.
All checks were successful
Deploy to Production / test (push) Successful in 42s

This commit is contained in:
syntaxbullet
2026-02-08 16:07:13 +01:00
parent ee088ad84b
commit 5e8683a19f
9 changed files with 359 additions and 64 deletions

View File

@@ -20,13 +20,15 @@ import { Plus, Trash2, Package, Coins, Sparkles, GripVertical, Percent } from "l
import { cn } from "@/lib/utils";
// Loot drop types
type LootType = "ITEM" | "BALANCE" | "XP";
type LootType = "ITEM" | "CURRENCY" | "XP";
interface LootDrop {
type: LootType;
itemId?: number;
itemName?: string;
amount?: number | [number, number]; // Single value or [min, max]
amount?: number;
minAmount?: number;
maxAmount?: number;
weight: number;
}
@@ -37,7 +39,7 @@ interface LootTableBuilderProps {
const LOOT_TYPES = [
{ value: "ITEM" as LootType, label: "Item", icon: Package, color: "text-purple-400" },
{ value: "BALANCE" as LootType, label: "Balance", icon: Coins, color: "text-amber-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" },
];
@@ -45,10 +47,10 @@ const getDefaultDrop = (type: LootType): LootDrop => {
switch (type) {
case "ITEM":
return { type: "ITEM", itemId: 0, itemName: "", weight: 10 };
case "BALANCE":
return { type: "BALANCE", amount: [100, 500], weight: 30 };
case "CURRENCY":
return { type: "CURRENCY", minAmount: 100, maxAmount: 500, weight: 30 };
case "XP":
return { type: "XP", amount: [50, 200], weight: 20 };
return { type: "XP", minAmount: 50, maxAmount: 200, weight: 20 };
}
};
@@ -57,7 +59,7 @@ export function LootTableBuilder({ pool, onChange }: LootTableBuilderProps) {
const totalWeight = pool.reduce((sum, drop) => sum + drop.weight, 0);
const addDrop = useCallback(() => {
onChange([...pool, getDefaultDrop("BALANCE")]);
onChange([...pool, getDefaultDrop("CURRENCY")]);
}, [pool, onChange]);
const removeDrop = useCallback((index: number) => {
@@ -105,7 +107,7 @@ export function LootTableBuilder({ pool, onChange }: LootTableBuilderProps) {
const colors: Record<LootType, string> = {
ITEM: "bg-purple-500",
BALANCE: "bg-amber-500",
CURRENCY: "bg-amber-500",
XP: "bg-blue-500",
};
@@ -206,17 +208,21 @@ export function LootTableBuilder({ pool, onChange }: LootTableBuilderProps) {
</>
)}
{(drop.type === "BALANCE" || drop.type === "XP") && (
{(drop.type === "CURRENCY" || drop.type === "XP") && (
<>
<div className="space-y-1.5">
<Label className="text-xs">Min Amount</Label>
<Input
type="number"
value={Array.isArray(drop.amount) ? drop.amount[0] : drop.amount || 0}
value={drop.minAmount ?? drop.amount ?? 0}
onChange={(e) => {
const min = parseInt(e.target.value) || 0;
const max = Array.isArray(drop.amount) ? drop.amount[1] : min;
updateDrop(index, { amount: [min, Math.max(min, max)] });
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"
/>
@@ -225,11 +231,15 @@ export function LootTableBuilder({ pool, onChange }: LootTableBuilderProps) {
<Label className="text-xs">Max Amount</Label>
<Input
type="number"
value={Array.isArray(drop.amount) ? drop.amount[1] : drop.amount || 0}
value={drop.maxAmount ?? drop.amount ?? 0}
onChange={(e) => {
const max = parseInt(e.target.value) || 0;
const min = Array.isArray(drop.amount) ? drop.amount[0] : max;
updateDrop(index, { amount: [Math.min(min, max), max] });
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"
/>