From 9dbe36044890da1699a2c6eb68c35c9ac27dc15b Mon Sep 17 00:00:00 2001 From: duckietm Date: Wed, 15 Apr 2026 13:09:27 +0200 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=86=99=20Fixed=20Buddy=20Pets=20showi?= =?UTF-8?q?ng=20in=20catalogue?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../avatar/BuildPurchasableClothingFigure.ts | 64 +++++++++++++++++++ src/api/avatar/index.ts | 1 + .../widgets/CatalogViewProductWidgetView.tsx | 27 ++++++-- .../PurchasableClothingConfirmView.tsx | 26 ++++---- 4 files changed, 99 insertions(+), 19 deletions(-) create mode 100644 src/api/avatar/BuildPurchasableClothingFigure.ts 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 Date: Wed, 15 Apr 2026 22:21:11 +0200 Subject: [PATCH 2/3] Modernize HC Center UI with Tailwind classes Replace missing SCSS styles with inline Tailwind utilities and image imports. Use design-system components (Column, Flex, Text, Button) for consistent look across the client. - Import hc-center images as modules (hc_logo, payday, clock, benefits) - Replace custom CSS classes with Tailwind (w-[], h-[], bg-*, rounded, etc.) - Use Text bold/small/variant props instead of raw h4/h5/h6 tags - Add hover:underline on links, border cards, rounded sections - Remove dead SCSS import from index.scss --- src/components/hc-center/HcCenterView.tsx | 109 +++++++++++----------- src/components/index.scss | 1 - 2 files changed, 56 insertions(+), 54 deletions(-) diff --git a/src/components/hc-center/HcCenterView.tsx b/src/components/hc-center/HcCenterView.tsx index 79bc0aa..2519197 100644 --- a/src/components/hc-center/HcCenterView.tsx +++ b/src/components/hc-center/HcCenterView.tsx @@ -1,6 +1,10 @@ import { AddLinkEventTracker, ClubGiftInfoEvent, CreateLinkEvent, GetClubGiftInfo, ILinkEventTracker, RemoveLinkEventTracker, ScrGetKickbackInfoMessageComposer, ScrKickbackData, ScrSendKickbackInfoMessageEvent } from '@nitrots/nitro-renderer'; import { FC, useEffect, useState } from 'react'; import { ClubStatus, FriendlyTime, GetClubBadge, GetConfigurationValue, LocalizeText, SendMessageComposer } from '../../api'; +import hcLogo from '../../assets/images/hc-center/hc_logo.gif'; +import paydayBg from '../../assets/images/hc-center/payday.png'; +import clockIcon from '../../assets/images/hc-center/clock.png'; +import benefitsBg from '../../assets/images/hc-center/benefits.png'; import { Button, Column, Flex, LayoutAvatarImageView, LayoutBadgeImageView, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../common'; import { useInventoryBadges, useMessageEvent, usePurse, useSessionInfo } from '../../hooks'; @@ -126,73 +130,72 @@ export const HcCenterView: FC<{}> = props => ); return ( - + setIsVisible(false) } /> - -
-
- - - -
-
+ + +
+ + +
-
- - - { LocalizeText('hccenter.status.' + clubStatus) } - + + + + { LocalizeText('hccenter.status.' + clubStatus) } + -
+ { GetConfigurationValue('hc.center')['payday.info'] && - - - -

{ LocalizeText('hccenter.special.title') }

-
{ LocalizeText('hccenter.special.info') }
-
CreateLinkEvent('habbopages/' + GetConfigurationValue('hc.center')['payday.habbopage']) }>{ LocalizeText('hccenter.special.infolink') }
-
-
-
{ LocalizeText('hccenter.special.time.title') }
-
-
-
{ getHcPaydayTime() }
+ + + { LocalizeText('hccenter.special.title') } + { LocalizeText('hccenter.special.info') } +
+ CreateLinkEvent('habbopages/' + GetConfigurationValue('hc.center')['payday.habbopage']) }> + { LocalizeText('hccenter.special.infolink') } +
+
+ + { LocalizeText('hccenter.special.time.title') } + +
+ { getHcPaydayTime() } + { clubStatus === ClubStatus.ACTIVE && -
-
{ LocalizeText('hccenter.special.amount.title') }
-
-
{ getHcPaydayAmount() }
-
- { LocalizeText('hccenter.breakdown.infolink') } -
-
-
} -
+ + { LocalizeText('hccenter.special.amount.title') } + { getHcPaydayAmount() } + CreateLinkEvent('habbopages/' + GetConfigurationValue('hc.center')['payday.habbopage']) }> + { LocalizeText('hccenter.breakdown.infolink') } + + } +
} { GetConfigurationValue('hc.center')['gift.info'] && -
-
-

{ LocalizeText('hccenter.gift.title') }

-
0 ? LocalizeText('hccenter.unclaimedgifts', [ 'unclaimedgifts' ], [ unclaimedGifts.toString() ]) : LocalizeText('hccenter.gift.info') } }>
-
- -
} + + } { GetConfigurationValue('hc.center')['benefits.info'] && -
-
{ LocalizeText('hccenter.general.title') }
-
- -
} + + } ); diff --git a/src/components/index.scss b/src/components/index.scss index 8be9445..a432c27 100644 --- a/src/components/index.scss +++ b/src/components/index.scss @@ -9,7 +9,6 @@ @import './friends/FriendsView'; @import './groups/GroupView'; @import './guide-tool/GuideToolView'; -@import './hc-center/HcCenterView'; @import './help/HelpView'; @import './hotel-view/HotelView'; @import './loading/LoadingView'; From b0967d7eafab5cd977527f85d4ce85e6102616f9 Mon Sep 17 00:00:00 2001 From: duckietm Date: Thu, 16 Apr 2026 13:34:53 +0200 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=86=95=20New=20Misc=20clothing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/avatar/AvatarEditorThumbnailsHelper.ts | 3 +++ src/assets/images/wardrobe/misc.png | Bin 0 -> 294 bytes src/components/avatar-editor/AvatarEditorView.tsx | 2 ++ src/css/index.css | 9 +++++++++ src/hooks/avatar-editor/useAvatarEditor.ts | 5 +++++ 5 files changed, 19 insertions(+) create mode 100644 src/assets/images/wardrobe/misc.png diff --git a/src/api/avatar/AvatarEditorThumbnailsHelper.ts b/src/api/avatar/AvatarEditorThumbnailsHelper.ts index ad0db38..c319d2a 100644 --- a/src/api/avatar/AvatarEditorThumbnailsHelper.ts +++ b/src/api/avatar/AvatarEditorThumbnailsHelper.ts @@ -87,6 +87,9 @@ export class AvatarEditorThumbnailsHelper AvatarFigurePartType.PET, 'ptl', 'ptr', + AvatarFigurePartType.MISC, + 'mcl', + 'mcr', ]; private static getThumbnailKey(setType: string, part: IAvatarEditorCategoryPartItem, partColors?: IPartColor[], isDisabled?: boolean): string diff --git a/src/assets/images/wardrobe/misc.png b/src/assets/images/wardrobe/misc.png new file mode 100644 index 0000000000000000000000000000000000000000..ea88b7f3605b8a0b1bbd8696e39716807e4a5780 GIT binary patch literal 294 zcmV+>0oneEP)~=_y1W1+~C7olo`%mif1^y zMVaC77G=1)|IaG^n+4;2r?U{2Brrcpa>F^Du@BdmSLo#El~kDHA+_SGkkk=gyG{dU7TusmTf@c zV^| = props => const isWardrobe = (modelKey === AvatarEditorFigureCategory.WARDROBE); const isPets = (modelKey === AvatarEditorFigureCategory.PETS); const isNft = (modelKey === AvatarEditorFigureCategory.NFT); + const isMisc = (modelKey === AvatarEditorFigureCategory.MISC); let tabClass = `tab ${ modelKey }`; if(isWardrobe) tabClass = 'tab-wardrobe'; else if(isPets) tabClass = 'tab-pets'; else if(isNft) tabClass = 'tab-nft'; + else if(isMisc) tabClass = 'tab-misc'; return ( setActiveModelKey(modelKey) }> diff --git a/src/css/index.css b/src/css/index.css index af06402..dc4905c 100644 --- a/src/css/index.css +++ b/src/css/index.css @@ -1916,6 +1916,15 @@ body { background-position: center; background-size: 22px 22px; } + + .tab-misc { + width: 34px; + height: 22px; + background-image: url('@/assets/images/wardrobe/misc.png'); + background-repeat: no-repeat; + background-position: center; + background-size: 24px 22px; + } } .nitro-wired__variable-picker-portal { diff --git a/src/hooks/avatar-editor/useAvatarEditor.ts b/src/hooks/avatar-editor/useAvatarEditor.ts index bb1cedf..06285c7 100644 --- a/src/hooks/avatar-editor/useAvatarEditor.ts +++ b/src/hooks/avatar-editor/useAvatarEditor.ts @@ -71,6 +71,10 @@ const useAvatarEditorState = () => setMaxPaletteCount(partItem.maxPaletteCount || 1); selectPart(setType, partId); + + // Pet (pt) and Misc (mc) cannot be equipped together — equipping one unequips the other. + if(setType === AvatarFigurePartType.PET) selectPart(AvatarFigurePartType.MISC, -1); + else if(setType === AvatarFigurePartType.MISC) selectPart(AvatarFigurePartType.PET, -1); }, [ activeModel, selectPart ]); const selectEditorColor = useCallback((setType: string, paletteId: number, colorId: number) => @@ -316,6 +320,7 @@ const useAvatarEditorState = () => newAvatarModels[AvatarEditorFigureCategory.TORSO] = [ AvatarFigurePartType.CHEST, AvatarFigurePartType.CHEST_PRINT, AvatarFigurePartType.COAT_CHEST, AvatarFigurePartType.CHEST_ACCESSORY ].map(setType => buildCategory(setType, buildModeDefault)); newAvatarModels[AvatarEditorFigureCategory.LEGS] = [ AvatarFigurePartType.LEGS, AvatarFigurePartType.SHOES, AvatarFigurePartType.WAIST_ACCESSORY ].map(setType => buildCategory(setType, buildModeDefault)); newAvatarModels[AvatarEditorFigureCategory.PETS] = [ AvatarFigurePartType.PET ].map(setType => buildCategory(setType)).filter(Boolean); + newAvatarModels[AvatarEditorFigureCategory.MISC] = [ AvatarFigurePartType.MISC ].map(setType => buildCategory(setType)).filter(Boolean); newAvatarModels[AvatarEditorFigureCategory.NFT] = [ AvatarFigurePartType.HEAD, AvatarFigurePartType.HAIR,