- Change tooltip background from bg-foreground to bg-popover (dark) - Increase text size from text-xs to text-sm - Add border border-border for subtle definition - Use font-semibold for slot name label
131 lines
4.1 KiB
TypeScript
131 lines
4.1 KiB
TypeScript
"use client";
|
|
|
|
import { useMemo } from "react";
|
|
import {
|
|
Backpack,
|
|
Crosshair,
|
|
Eye,
|
|
Glasses,
|
|
Rocket,
|
|
Shield,
|
|
Shirt,
|
|
Target,
|
|
User,
|
|
} from "lucide-react";
|
|
import type { Asset, Loadout } from "@/payload-types";
|
|
import {
|
|
WARDROBE_SLOT_LAYOUT,
|
|
getLoadoutSlot,
|
|
type WardrobeSlotIcon,
|
|
type LoadoutSlots,
|
|
type LockerGridItem,
|
|
} from "@/lib/locker";
|
|
import { cn } from "@/lib/utils";
|
|
import { getAssetIcon } from "./LockerGrid";
|
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
|
|
|
function slotIcon(icon: WardrobeSlotIcon, filled: boolean) {
|
|
const cls = filled ? "size-3.5" : "size-3";
|
|
switch (icon) {
|
|
case "head":
|
|
return <User className={cls} />;
|
|
case "eye":
|
|
return <Eye className={cls} />;
|
|
case "face":
|
|
return <Glasses className={cls} />;
|
|
case "chest":
|
|
return <Shield className={cls} />;
|
|
case "torso":
|
|
return <Shirt className={cls} />;
|
|
case "back":
|
|
return <Backpack className={cls} />;
|
|
case "weapon":
|
|
return <Crosshair className={cls} />;
|
|
case "sidearm":
|
|
return <Target className={cls} />;
|
|
case "heavy":
|
|
return <Rocket className={cls} />;
|
|
}
|
|
}
|
|
|
|
function resolveSlotAsset(
|
|
value: LoadoutSlots[keyof LoadoutSlots],
|
|
items: LockerGridItem[],
|
|
): Asset | null {
|
|
if (value == null) return null;
|
|
if (typeof value === "object" && "id" in value) return value as Asset;
|
|
const id = value as number;
|
|
return items.find((i) => i.assetId === id)?.asset ?? null;
|
|
}
|
|
|
|
export function Wardrobe({
|
|
loadout,
|
|
items,
|
|
onSlotClick,
|
|
}: {
|
|
loadout: Loadout;
|
|
items: LockerGridItem[];
|
|
onSlotClick: (slotName: string) => void;
|
|
}) {
|
|
const slots = (loadout.slots ?? {}) as LoadoutSlots;
|
|
|
|
const filled = useMemo(() => {
|
|
const set = new Set<string>();
|
|
for (const layout of WARDROBE_SLOT_LAYOUT) {
|
|
const value = slots[layout.name];
|
|
if (value != null) set.add(layout.zone);
|
|
}
|
|
return set;
|
|
}, [slots]);
|
|
|
|
return (
|
|
<div className="relative mx-auto w-full max-w-100">
|
|
<div className="relative" style={{ aspectRatio: "260 / 380" }}>
|
|
<img
|
|
src="/images/soldier-pose-alpha.png"
|
|
alt="Character wardrobe reflecting the equipped loadout"
|
|
className="h-full w-full object-contain pointer-events-none select-none"
|
|
draggable={false}
|
|
/>
|
|
|
|
{WARDROBE_SLOT_LAYOUT.map((layout) => {
|
|
const value = slots[layout.name];
|
|
const asset = resolveSlotAsset(value, items);
|
|
return (
|
|
<Tooltip key={layout.name}>
|
|
<TooltipTrigger asChild>
|
|
<button
|
|
type="button"
|
|
aria-label={`${layout.label} slot: ${asset ? asset.name : "empty"}`}
|
|
onClick={() => onSlotClick(layout.name)}
|
|
className={cn(
|
|
"absolute flex items-center justify-center rounded-full border transition-colors",
|
|
"-translate-x-1/2 -translate-y-1/2 cursor-pointer",
|
|
asset
|
|
? "size-7 border-amber-400/70 bg-amber-500/25 text-amber-200 hover:bg-amber-500/40"
|
|
: "size-6 border-border bg-background/80 text-muted-foreground hover:border-primary hover:text-foreground",
|
|
)}
|
|
style={{ left: `${layout.x}%`, top: `${layout.y}%` }}
|
|
>
|
|
{asset ? getAssetIcon(asset.assetType, "size-3.5") : slotIcon(layout.icon, false)}
|
|
</button>
|
|
</TooltipTrigger>
|
|
<TooltipContent
|
|
side="right"
|
|
sideOffset={8}
|
|
className="bg-popover text-popover-foreground border border-border text-sm px-3.5 py-2"
|
|
>
|
|
<div className="flex flex-col gap-0.5">
|
|
<span className="font-semibold">{layout.label}</span>
|
|
<span className="text-muted-foreground">
|
|
{asset ? asset.name : "Empty"}
|
|
</span>
|
|
</div>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|