diff --git a/src/api/avatar/BuildPurchasableClothingFigure.ts b/src/api/avatar/BuildPurchasableClothingFigure.ts new file mode 100644 index 0000000..10aff65 --- /dev/null +++ b/src/api/avatar/BuildPurchasableClothingFigure.ts @@ -0,0 +1,64 @@ +import { AvatarFigureContainer, GetAvatarRenderManager, IFigurePartSet } from '@nitrots/nitro-renderer'; + +const getFirstSelectableColorForSetType = (setType: string): number => +{ + const structure = GetAvatarRenderManager()?.structureData; + + if(!structure) return -1; + + const set = structure.getSetType(setType); + + if(!set) return -1; + + const palette = structure.getPalette(set.paletteID); + + if(!palette) return -1; + + for(const color of palette.colors.getValues()) + { + if(!color || !color.isSelectable) continue; + + return color.id; + } + + return -1; +}; + +/** + * Builds a new figure string starting from the base figure and applying the + * provided figure part set IDs (e.g. a purchasable clothing set or pet set). + * + * When the base figure does not already define colours for the set type being + * applied (common for pet "pt" sets on an avatar that has never worn one), the + * first selectable palette colour is used so the part still renders instead of + * being dropped. + */ +export const BuildPurchasableClothingFigure = (baseFigure: string, setIds: number[]): string => +{ + const manager = GetAvatarRenderManager(); + + if(!manager) return baseFigure; + + const container = new AvatarFigureContainer(baseFigure ?? ''); + const structure = manager.structureData; + + for(const setId of setIds) + { + const partSet: IFigurePartSet = structure?.getFigurePartSet(setId); + + if(!partSet) continue; + + let colorIds = container.getPartColorIds(partSet.type) ?? []; + + if(!colorIds.length) + { + const defaultColor = getFirstSelectableColorForSetType(partSet.type); + + if(defaultColor >= 0) colorIds = [ defaultColor ]; + } + + container.updatePart(partSet.type, partSet.id, colorIds); + } + + return container.getFigureString(); +}; diff --git a/src/api/avatar/index.ts b/src/api/avatar/index.ts index 415185e..6049e7a 100644 --- a/src/api/avatar/index.ts +++ b/src/api/avatar/index.ts @@ -2,5 +2,6 @@ export * from './AvatarEditorAction'; export * from './AvatarEditorColorSorter'; export * from './AvatarEditorPartSorter'; export * from './AvatarEditorThumbnailsHelper'; +export * from './BuildPurchasableClothingFigure'; export * from './IAvatarEditorCategory'; export * from './IAvatarEditorCategoryPartItem'; diff --git a/src/components/catalog/views/page/widgets/CatalogViewProductWidgetView.tsx b/src/components/catalog/views/page/widgets/CatalogViewProductWidgetView.tsx index d64854e..14b4516 100644 --- a/src/components/catalog/views/page/widgets/CatalogViewProductWidgetView.tsx +++ b/src/components/catalog/views/page/widgets/CatalogViewProductWidgetView.tsx @@ -1,6 +1,6 @@ import { GetAvatarRenderManager, GetSessionDataManager, Vector3d } from '@nitrots/nitro-renderer'; import { FC, useEffect } from 'react'; -import { FurniCategory, Offer, ProductTypeEnum } from '../../../../../api'; +import { BuildPurchasableClothingFigure, FurniCategory, Offer, ProductTypeEnum } from '../../../../../api'; import { AutoGrid, Column, LayoutGridItem, LayoutRoomPreviewerView } from '../../../../../common'; import { useCatalog } from '../../../../../hooks'; @@ -24,18 +24,37 @@ export const CatalogViewProductWidgetView: FC<{}> = props => case ProductTypeEnum.FLOOR: { if(!product.furnitureData) return; - if(product.furnitureData.specialType === FurniCategory.FIGURE_PURCHASABLE_SET) + const furniData = GetSessionDataManager().getFloorItemData(product.furnitureData.id); + const isPurchasableClothing = (product.furnitureData.specialType === FurniCategory.FIGURE_PURCHASABLE_SET); + const hasResolvableFigureSets = (() => + { + if(!furniData || !furniData.customParams || !furniData.customParams.length) return false; + + const parts = furniData.customParams.split(',').map(value => parseInt(value)); + + for(const part of parts) + { + if(isNaN(part)) continue; + + if(GetAvatarRenderManager().structureData?.getFigurePartSet(part)) return true; + } + + return false; + })(); + + if(isPurchasableClothing || hasResolvableFigureSets) { - const furniData = GetSessionDataManager().getFloorItemData(product.furnitureData.id); const customParts = furniData.customParams.split(',').map(value => parseInt(value)); const figureSets: number[] = []; for(const part of customParts) { + if(isNaN(part)) continue; + if(GetAvatarRenderManager().isValidFigureSetForGender(part, GetSessionDataManager().gender)) figureSets.push(part); } - const figureString = GetAvatarRenderManager().getFigureStringWithFigureIds(GetSessionDataManager().figure, GetSessionDataManager().gender, figureSets); + const figureString = BuildPurchasableClothingFigure(GetSessionDataManager().figure, figureSets); roomPreviewer.addAvatarIntoRoom(figureString, product.productClassId); } diff --git a/src/components/room/widgets/furniture/context-menu/PurchasableClothingConfirmView.tsx b/src/components/room/widgets/furniture/context-menu/PurchasableClothingConfirmView.tsx index 85a6f80..60a0676 100644 --- a/src/components/room/widgets/furniture/context-menu/PurchasableClothingConfirmView.tsx +++ b/src/components/room/widgets/furniture/context-menu/PurchasableClothingConfirmView.tsx @@ -1,6 +1,6 @@ import { AvatarFigurePartType, GetAvatarRenderManager, GetSessionDataManager, RedeemItemClothingComposer, RoomObjectCategory, UserFigureComposer } from '@nitrots/nitro-renderer'; import { FC, useEffect, useState } from 'react'; -import { FurniCategory, GetFurnitureDataForRoomObject, LocalizeText, SendMessageComposer } from '../../../../../api'; +import { BuildPurchasableClothingFigure, GetFurnitureDataForRoomObject, LocalizeText, SendMessageComposer } from '../../../../../api'; import { Button, Column, LayoutAvatarImageView, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../../common'; import { useRoom } from '../../../../../hooks'; @@ -41,22 +41,18 @@ export const PurchasableClothingConfirmView: FC parseInt(part)) + .filter(id => !isNaN(id)); + + for(const setId of setIds) { - case FurniCategory.FIGURE_PURCHASABLE_SET: - mode = MODE_PURCHASABLE_CLOTHING; - - const setIds = furniData.customParams.split(',').map(part => parseInt(part)); - - for(const setId of setIds) - { - if(GetAvatarRenderManager().isValidFigureSetForGender(setId, gender)) validSets.push(setId); - } - - break; + if(GetAvatarRenderManager().isValidFigureSetForGender(setId, gender)) validSets.push(setId); } + + if(validSets.length) mode = MODE_PURCHASABLE_CLOTHING; } } @@ -68,7 +64,7 @@ export const PurchasableClothingConfirmView: FC