- Add EquipmentEditorDialog for per-category attachments, skins, and custom names - Add Wardrobe component with soldier image and clickable equipment slots - Add WardrobePanel with loadout selector and current equipment display - Add WardrobeSlotPicker dialog for equipping items from locker - Add equip/unequip loadout functionality with equipped badge - Add equipment schema to LockerStorages (attachments, skin, customName) - Add wardrobe slot layout with hotspot positions for each equipment type - Add locker:item-attach, locker:item-detach, locker:item-skin event types - Update LoadoutCard to show equipped status and equipment list - Update LoadoutsPanel to pass ownedItems for equipment display
91 lines
3.3 KiB
TypeScript
91 lines
3.3 KiB
TypeScript
"use client";
|
|
|
|
import { CheckCircle2Icon, PencilIcon, TrashIcon } from "lucide-react";
|
|
import type { Asset, Loadout } from "@/payload-types";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Item, ItemContent, ItemHeader, ItemTitle } from "@/components/ui/item";
|
|
import { getAssetTypeLabel, LOADOUT_SLOTS, type LockerGridItem, type LoadoutSlots } from "@/lib/locker";
|
|
|
|
function resolveSlotValue(value: LoadoutSlots[keyof LoadoutSlots]): Asset | null {
|
|
if (value == null) return null;
|
|
if (typeof value === "object" && "id" in value) return value as Asset;
|
|
return null;
|
|
}
|
|
|
|
function slotDisplayName(asset: Asset, ownedItems: LockerGridItem[]): string {
|
|
const custom = ownedItems.find((i) => i.assetId === asset.id)?.equipment?.customName?.trim();
|
|
return custom || asset.name;
|
|
}
|
|
|
|
interface LoadoutCardProps {
|
|
loadout: Loadout;
|
|
ownedItems: LockerGridItem[];
|
|
onEdit: () => void;
|
|
onDelete: () => void;
|
|
onEquip: () => void;
|
|
}
|
|
|
|
export function LoadoutCard({ loadout, ownedItems, onEdit, onDelete, onEquip }: LoadoutCardProps) {
|
|
const slots = (loadout.slots ?? {}) as LoadoutSlots;
|
|
const filled = LOADOUT_SLOTS.filter((slot) => {
|
|
const v = slots[slot.name];
|
|
return v != null && (typeof v !== "object" || (typeof v === "object" && "id" in v));
|
|
});
|
|
|
|
return (
|
|
<Item variant="outline">
|
|
<ItemHeader>
|
|
<ItemTitle className="flex items-center gap-2">
|
|
{loadout.name}
|
|
{loadout.equipped && (
|
|
<span className="inline-flex items-center gap-1 text-xs font-medium text-amber-400">
|
|
<CheckCircle2Icon className="size-3.5" />
|
|
Equipped
|
|
</span>
|
|
)}
|
|
</ItemTitle>
|
|
{loadout.description && (
|
|
<span className="text-xs text-muted-foreground">{loadout.description}</span>
|
|
)}
|
|
<div className="flex items-center gap-1">
|
|
<Button
|
|
variant={loadout.equipped ? "default" : "outline"}
|
|
size="sm"
|
|
onClick={onEquip}
|
|
>
|
|
{loadout.equipped ? "Unequip" : "Equip"}
|
|
</Button>
|
|
<Button variant="outline" size="sm" onClick={onEdit}>
|
|
<PencilIcon className="size-3.5" />
|
|
Edit
|
|
</Button>
|
|
<Button variant="outline" size="sm" onClick={onDelete}>
|
|
<TrashIcon className="size-3.5" />
|
|
</Button>
|
|
</div>
|
|
</ItemHeader>
|
|
<ItemContent>
|
|
<div className="flex flex-wrap gap-1.5">
|
|
{filled.length === 0 && (
|
|
<span className="text-xs text-muted-foreground">No equipment assigned.</span>
|
|
)}
|
|
{filled.map((slot) => {
|
|
const value = slots[slot.name];
|
|
const asset = resolveSlotValue(value);
|
|
if (!asset) return null;
|
|
return (
|
|
<span
|
|
key={slot.name}
|
|
className="inline-flex items-center gap-1.5 text-xs bg-muted px-2.5 py-1 rounded-md"
|
|
>
|
|
<span className="text-muted-foreground">{slot.label}:</span>
|
|
<span className="font-medium">{slotDisplayName(asset, ownedItems)}</span>
|
|
<span className="text-muted-foreground">({getAssetTypeLabel(asset.assetType)})</span>
|
|
</span>
|
|
);
|
|
})}
|
|
</div>
|
|
</ItemContent>
|
|
</Item>
|
|
);
|
|
}
|