"use client"; import { useCallback, useState } from "react"; import { CharacterColorPresets } from "@/components/character/character-color-presets"; import { CharacterColorSliders } from "@/components/character/character-color-sliders"; import { CharacterLayerPreview } from "@/components/character/character-layer-preview"; import { CharacterLayerTabs } from "@/components/character/character-layer-tabs"; import { CharacterPieceSelector } from "@/components/character/character-piece-selector"; import { ParallaxRoomBackground } from "@/components/pet/parallax-room-background"; import { RoomStyleSelector } from "@/components/pet/room-style-selector"; import { ShopItemPreviewModal } from "@/components/shop/shop-item-preview-modal"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { useToast } from "@/components/ui/toast-provider"; import { DEFAULT_AVATAR_NAME, type AvatarCustomization, } from "@/lib/avatar-customization-storage"; import { clampHsl } from "@/lib/character/color-utils"; import { COLOR_PRESETS, DEFAULT_GRAY_COLOR, LAYER_DEFAULT_COLORS, NONE_VARIANT_ID, buildDefaultVariants, getLayerById, type CharacterCreatorTabId, type ColorPreset, } from "@/lib/character/presets"; import type { LayerColorState, LayerVariantState } from "@/lib/character/types"; import { isVariantUnlocked } from "@/lib/character/variant-access"; import { isRoomBackgroundUnlocked, normalizeRoomBackgroundId, type RoomBackgroundId, } from "@/lib/room-backgrounds"; import { findShopItemById, parseShopStyleItem, type ShopItemRecord, } from "@/lib/shop-catalog"; import { buildShopItemPreviewCustomization } from "@/lib/shop-preview"; import { equipShopItem, purchaseShopItem, unequipShopItem, } from "@/lib/shop-storage"; function SectionLabel({ children }: { children: React.ReactNode }) { return (

{children}

); } type CharacterCreatorProps = { initialCustomization?: AvatarCustomization; name?: string; onNameChange?: (name: string) => void; showNameField?: boolean; saveLabel?: string; isSaving?: boolean; ownedVariantIds?: string[]; shopItems?: ShopItemRecord[]; coins?: number | null; equippedItems?: string[]; onShopDataChange?: () => void | Promise; onSave?: (customization: AvatarCustomization) => Promise; }; export function CharacterCreator({ initialCustomization, name: controlledName, onNameChange, ownedVariantIds = [], shopItems = [], coins = null, equippedItems: initialEquippedItems = [], onShopDataChange, showNameField = false, saveLabel = "Save avatar", isSaving = false, onSave, }: CharacterCreatorProps) { const { toast } = useToast(); const [internalName, setInternalName] = useState( initialCustomization?.name ?? DEFAULT_AVATAR_NAME, ); const name = controlledName ?? internalName; const setName = (nextName: string) => { if (onNameChange) { onNameChange(nextName); return; } setInternalName(nextName); }; const [colors, setColors] = useState( initialCustomization?.colors ?? { ...LAYER_DEFAULT_COLORS }, ); const [variants, setVariants] = useState( initialCustomization?.variants ?? buildDefaultVariants(), ); const [roomBackground, setRoomBackground] = useState( normalizeRoomBackgroundId(initialCustomization?.roomBackground), ); const [equippedItems, setEquippedItems] = useState( initialCustomization?.equippedItems ?? initialEquippedItems, ); const [activePresetId, setActivePresetId] = useState( DEFAULT_GRAY_COLOR.id, ); const [activeTabId, setActiveTabId] = useState("skin"); const [saveError, setSaveError] = useState(null); const [previewItem, setPreviewItem] = useState(null); const [pendingItemId, setPendingItemId] = useState(null); const isRoomTab = activeTabId === "room"; const isSkinTab = activeTabId === "skin"; const showStyleSection = !isSkinTab; const showColorAdjust = !isRoomTab; const activeLayerId = isRoomTab ? "skin" : activeTabId; const activeLayer = getLayerById(activeLayerId); const activeColor = colors[activeLayerId]; const ownedSet = new Set(ownedVariantIds); const pieceVariants = activeLayer.allowVariants ? activeLayer.variants : []; const lockedVariantIds = new Set( pieceVariants .filter( (variant) => variant.id !== NONE_VARIANT_ID && !isVariantUnlocked(variant.id, ownedSet), ) .map((variant) => variant.id), ); const applyPreset = (preset: ColorPreset) => { setActivePresetId(preset.id); setColors((current) => ({ ...current, [activeLayerId]: { ...preset.hsl }, })); }; const updateActiveColor = (nextColor: typeof activeColor) => { setActivePresetId(""); setColors((current) => ({ ...current, [activeLayerId]: clampHsl(nextColor), })); }; const handleTabChange = (tabId: CharacterCreatorTabId) => { setActiveTabId(tabId); if (tabId === "room") { return; } const layerColor = colors[tabId]; const matchingPreset = COLOR_PRESETS.find( (preset) => preset.hsl.h === layerColor.h && preset.hsl.s === layerColor.s && preset.hsl.l === layerColor.l, ); setActivePresetId(matchingPreset?.id ?? ""); }; const handleVariantChange = (variantId: string) => { if (!isVariantUnlocked(variantId, ownedSet)) { return; } setVariants((current) => ({ ...current, [activeLayerId]: variantId, })); }; const handleRoomChange = (roomId: RoomBackgroundId) => { if (!isRoomBackgroundUnlocked(roomId, ownedVariantIds)) { return; } setRoomBackground(roomId); }; const openLockedItemPreview = useCallback( (itemId: string) => { const item = findShopItemById(shopItems, itemId); if (!item) { toast("This item is not available in the shop yet.", "error"); return; } setPreviewItem(item); }, [shopItems, toast], ); const buildCustomization = (): AvatarCustomization => ({ name, colors, variants, customized: true, equippedItems, roomBackground, }); const displayCustomization = previewItem ? buildShopItemPreviewCustomization(buildCustomization(), previewItem) : buildCustomization(); const previewOwned = previewItem ? ownedVariantIds.includes(previewItem.id) : false; const previewEquipped = previewItem ? previewItem.type === "room" ? roomBackground === previewItem.id : equippedItems.includes(previewItem.id) : false; const previewCanAfford = previewItem ? coins !== null && coins >= previewItem.price : false; const applyOwnedItemToEditor = (item: ShopItemRecord) => { if (item.type === "room") { setRoomBackground(normalizeRoomBackgroundId(item.id)); return; } const style = parseShopStyleItem(item.id, item.type); if (style) { setVariants((current) => ({ ...current, [style.layerId]: style.variantId, })); } }; const handlePreviewPurchase = async () => { if (!previewItem) { return; } setPendingItemId(previewItem.id); try { const purchased = await purchaseShopItem(previewItem.id); const nextEquipped = await equipShopItem(previewItem.id); setEquippedItems(nextEquipped); applyOwnedItemToEditor(purchased); toast(`Purchased and equipped ${purchased.name}.`, "success"); setPreviewItem(null); await onShopDataChange?.(); } catch (error) { toast( error instanceof Error ? error.message : "Purchase failed.", "error", ); } finally { setPendingItemId(null); } }; const handlePreviewEquipToggle = async () => { if (!previewItem) { return; } setPendingItemId(previewItem.id); try { const isEquipped = previewItem.type === "room" ? roomBackground === previewItem.id : equippedItems.includes(previewItem.id); if (previewItem.type !== "room" && isEquipped) { const nextEquipped = await unequipShopItem(previewItem.id); setEquippedItems(nextEquipped); toast(`Unequipped ${previewItem.name}.`, "default"); } else if (!isEquipped) { const nextEquipped = await equipShopItem(previewItem.id); setEquippedItems(nextEquipped); applyOwnedItemToEditor(previewItem); toast(`Equipped ${previewItem.name}.`, "success"); setPreviewItem(null); } await onShopDataChange?.(); } catch (error) { toast( error instanceof Error ? error.message : "Could not equip item.", "error", ); } finally { setPendingItemId(null); } }; const previewStyleLayerId = previewItem && previewItem.type !== "room" ? parseShopStyleItem(previewItem.id, previewItem.type)?.layerId : null; const activePieceId = previewStyleLayerId === activeLayerId && previewItem ? previewItem.id : variants[activeLayerId]; const activeRoomId: RoomBackgroundId = previewItem?.type === "room" ? normalizeRoomBackgroundId(previewItem.id) : roomBackground; const handleSave = async () => { if (!onSave) { return; } setSaveError(null); try { await onSave(buildCustomization()); } catch (error) { setSaveError( error instanceof Error ? error.message : "Could not save avatar.", ); } }; return ( <> setPreviewItem(null)} onPurchase={() => { void handlePreviewPurchase(); }} onEquipToggle={() => { void handlePreviewEquipToggle(); }} />
{showNameField ? (
setName(event.target.value)} />
) : null}
{showStyleSection ? (
Style
{isRoomTab ? ( ) : ( )}
) : null} {showColorAdjust ? ( <>
Color
Adjust
) : null} {onSave ? (
{saveError ? (

{saveError}

) : null}
) : null}
); }