From 303b75d37804fac32334d7b436d49e428dd0dfc0 Mon Sep 17 00:00:00 2001 From: duckietm Date: Wed, 25 Mar 2026 14:11:53 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=86=95=20=F0=9F=90=B6=20have=20you=20favo?= =?UTF-8?q?rite=20pet=20as=20your=20best=20pall=20next=20to=20you?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../avatar/AvatarEditorThumbnailsHelper.ts | 19 +++ src/assets/images/wardrobe/pets.png | Bin 0 -> 188 bytes .../avatar-editor/AvatarEditorPetView.tsx | 132 ++++++++++++++++++ .../avatar-editor/AvatarEditorView.tsx | 13 +- src/components/avatar-editor/index.ts | 1 + src/css/index.css | 72 ++++++++++ src/hooks/avatar-editor/useAvatarEditor.ts | 1 + 7 files changed, 236 insertions(+), 2 deletions(-) create mode 100644 src/assets/images/wardrobe/pets.png create mode 100644 src/components/avatar-editor/AvatarEditorPetView.tsx diff --git a/src/api/avatar/AvatarEditorThumbnailsHelper.ts b/src/api/avatar/AvatarEditorThumbnailsHelper.ts index 2c21ebd..f9ce18f 100644 --- a/src/api/avatar/AvatarEditorThumbnailsHelper.ts +++ b/src/api/avatar/AvatarEditorThumbnailsHelper.ts @@ -32,6 +32,9 @@ export class AvatarEditorThumbnailsHelper AvatarFigurePartType.HEAD_ACCESSORY, AvatarFigurePartType.HEAD_ACCESSORY_EXTRA, AvatarFigurePartType.RIGHT_HAND_ITEM, + AvatarFigurePartType.PET, + 'ptl', + 'ptr', ]; private static getThumbnailKey(setType: string, part: IAvatarEditorCategoryPartItem): string @@ -106,6 +109,22 @@ export class AvatarEditorThumbnailsHelper container.addChild(sprite); } + if(container.children.length > 0) + { + const isPetPart = parts.some(p => p.type === 'pt' || p.type === 'ptl' || p.type === 'ptr'); + + if(isPetPart) + { + const bounds = container.getBounds(); + + for(const child of container.children) + { + (child as NitroSprite).position.x -= bounds.x; + (child as NitroSprite).position.y -= bounds.y; + } + } + } + return container; }; diff --git a/src/assets/images/wardrobe/pets.png b/src/assets/images/wardrobe/pets.png new file mode 100644 index 0000000000000000000000000000000000000000..2c36bff9b801c93514df4e15fca22c656492cac9 GIT binary patch literal 188 zcmeAS@N?(olHy`uVBq!ia0vp^B0wz0!3HD`IPL*a&7LlfAr*6y6C_x-u>Id|vhy$d zMT;Lxq{}ihx$X#**m>$PU2HK>6i+S{+?7^h=O-@wl4+0Kfmco`hdVgsoL+tOOcGT# zy_VQD(R9+(9FAiqE7=a4HYEwlP8K+KWM^MUxKOHu=DH=jxC3Ku3g@>pdh$9iit+4v lZNQVZz{%=Js>NSMhJEv6gv`$V%m6xv!PC{xWt~$(69B7gK{EgV literal 0 HcmV?d00001 diff --git a/src/components/avatar-editor/AvatarEditorPetView.tsx b/src/components/avatar-editor/AvatarEditorPetView.tsx new file mode 100644 index 0000000..a127392 --- /dev/null +++ b/src/components/avatar-editor/AvatarEditorPetView.tsx @@ -0,0 +1,132 @@ +import { AvatarFigurePartType } from '@nitrots/nitro-renderer'; +import { FC, useCallback, useEffect, useMemo, useState } from 'react'; +import { AvatarEditorThumbnailsHelper, CreateLinkEvent, GetClubMemberLevel, IAvatarEditorCategory, IAvatarEditorCategoryPartItem } from '../../api'; +import { LayoutCurrencyIcon } from '../../common'; +import { useAvatarEditor } from '../../hooks'; +import { AvatarEditorFigureSetView } from './figure-set'; +import { AvatarEditorAdvancedColorView, AvatarEditorPaletteSetView } from './palette-set'; + +export const AvatarEditorPetView: FC<{ + categories: IAvatarEditorCategory[]; +}> = props => +{ + const { categories = [] } = props; + const [ slotThumbUrl, setSlotThumbUrl ] = useState(null); + const [ advancedColorMode, setAdvancedColorMode ] = useState(false); + const { selectedParts = null, selectEditorPart = null, selectedColorParts = null, maxPaletteCount = 1, getFirstSelectableColor = null, selectEditorColor = null } = useAvatarEditor(); + const hasHC = GetClubMemberLevel() > 0; + + const petCategory = categories.length ? categories[0] : null; + const selectedPetSetId = selectedParts?.['pt'] ?? null; + + const selectedPartItem = useMemo(() => + { + if(!selectedPetSetId || !petCategory?.partItems) return null; + + return petCategory.partItems.find(item => item.partSet?.id === selectedPetSetId) ?? null; + }, [ selectedPetSetId, petCategory ]); + + // Ensure color is initialized when pet tab loads + useEffect(() => + { + if(!petCategory) return; + + const selectedPalettes = selectedColorParts?.['pt']; + + if(!selectedPalettes || !selectedPalettes.length) selectEditorColor('pt', 0, getFirstSelectableColor('pt')); + }, [ petCategory, selectedColorParts, selectEditorColor, getFirstSelectableColor ]); + + useEffect(() => + { + if(!selectedPartItem || !selectedPartItem.partSet) + { + setSlotThumbUrl(null); + return; + } + + const loadThumb = async () => + { + const url = await AvatarEditorThumbnailsHelper.build( + AvatarFigurePartType.PET, + selectedPartItem, + selectedPartItem.usesColor, + selectedColorParts?.['pt'] ?? null + ); + + if(url) setSlotThumbUrl(url); + }; + + loadThumb(); + }, [ selectedPartItem, selectedColorParts ]); + + const removePet = useCallback(() => + { + selectEditorPart('pt', -1); + setSlotThumbUrl(null); + }, [ selectEditorPart ]); + + if(!petCategory || !petCategory.partItems || !petCategory.partItems.length) + { + return ( +
+ No companion pets available. +
+ ); + } + + return ( +
+ { /* Equipped pet bar */ } +
+
+ { selectedPartItem + ?
+ :
+ } +
+
+ { selectedPartItem + ? <> + Companion Equipped + + + : No companion selected + } +
+
+ { /* Pet grid */ } +
+ +
+ { /* Color palette */ } + { (petCategory.colorItems && petCategory.colorItems[0] && petCategory.colorItems[0].length > 0) && + <> +
+ +
+
+ { (maxPaletteCount >= 1) && +
+ { advancedColorMode + ? + : } +
} + { (maxPaletteCount === 2) && +
+ { advancedColorMode + ? + : } +
} +
+ } +
+ ); +}; diff --git a/src/components/avatar-editor/AvatarEditorView.tsx b/src/components/avatar-editor/AvatarEditorView.tsx index 1945a17..f080e21 100644 --- a/src/components/avatar-editor/AvatarEditorView.tsx +++ b/src/components/avatar-editor/AvatarEditorView.tsx @@ -6,6 +6,7 @@ import { Button, ButtonGroup, NitroCardContentView, NitroCardHeaderView, NitroCa import { useAvatarEditor } from '../../hooks'; import { AvatarEditorFigurePreviewView } from './AvatarEditorFigurePreviewView'; import { AvatarEditorModelView } from './AvatarEditorModelView'; +import { AvatarEditorPetView } from './AvatarEditorPetView'; import { AvatarEditorWardrobeView } from './AvatarEditorWardrobeView'; export const AvatarEditorView: FC<{}> = props => @@ -14,6 +15,7 @@ export const AvatarEditorView: FC<{}> = props => const { setIsVisible: setEditorVisibility, avatarModels, activeModelKey, setActiveModelKey, loadAvatarData, getFigureStringWithFace, gender, randomizeCurrentFigure = null, getFigureString = null } = useAvatarEditor(); const isWardrobeOpen = (activeModelKey === AvatarEditorFigureCategory.WARDROBE); + const isPetsOpen = (activeModelKey === AvatarEditorFigureCategory.PETS); const processAction = (action: string) => { @@ -82,10 +84,15 @@ export const AvatarEditorView: FC<{}> = props => { const isActive = (activeModelKey === modelKey); const isWardrobe = (modelKey === AvatarEditorFigureCategory.WARDROBE); + const isPets = (modelKey === AvatarEditorFigureCategory.PETS); + + let tabClass = `tab ${ modelKey }`; + if(isWardrobe) tabClass = 'tab-wardrobe'; + else if(isPets) tabClass = 'tab-pets'; return ( setActiveModelKey(modelKey) }> -
+
); }) } @@ -94,10 +101,12 @@ export const AvatarEditorView: FC<{}> = props =>
{ /* left: model view or wardrobe */ }
- { (activeModelKey.length > 0 && !isWardrobeOpen) && + { (activeModelKey.length > 0 && !isWardrobeOpen && !isPetsOpen) && } { isWardrobeOpen && } + { isPetsOpen && + }
{ /* right: preview + actions */ }
diff --git a/src/components/avatar-editor/index.ts b/src/components/avatar-editor/index.ts index 5ae66e5..30769a7 100644 --- a/src/components/avatar-editor/index.ts +++ b/src/components/avatar-editor/index.ts @@ -1,6 +1,7 @@ export * from './AvatarEditorFigurePreviewView'; export * from './AvatarEditorIcon'; export * from './AvatarEditorModelView'; +export * from './AvatarEditorPetView'; export * from './AvatarEditorView'; export * from './AvatarEditorWardrobeView'; export * from './figure-set'; diff --git a/src/css/index.css b/src/css/index.css index 1303d6b..0e0dc01 100644 --- a/src/css/index.css +++ b/src/css/index.css @@ -1171,11 +1171,83 @@ body { background-position: center; filter: contrast(1.2) brightness(1.05); } + + .tab-pets { + width: 34px; + height: 22px; + background-image: url('@/assets/images/wardrobe/pets.png'); + background-repeat: no-repeat; + background-position: center; + background-size: 22px 22px; + } } /* ── Avatar Editor misc ─────────────────────────────────────────────────── */ +/* ── Pet Companion ─────────────────────────────────────────────────────── */ + +.pet-equipped-bar { + display: flex; + align-items: center; + gap: 10px; + padding: 6px 10px; + background-color: rgba(0, 0, 0, 0.25); + border-radius: 0.3rem; + margin: 0 2px; + min-height: 48px; +} + +.pet-equipped-preview { + width: 42px; + height: 42px; + border-radius: 4px; + background-color: rgba(255, 255, 255, 0.1); + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + flex-shrink: 0; +} + +.pet-equipped-thumb { + width: 100%; + height: 100%; + background-repeat: no-repeat; + background-position: center; + background-size: contain; +} + +.pet-paw-icon { + width: 24px; + height: 24px; + background-image: url('@/assets/images/wardrobe/pets.png'); + background-repeat: no-repeat; + background-position: center; + background-size: contain; +} + +.pet-grid-container .avatar-parts { + background-size: contain; + overflow: hidden !important; +} + +.pet-remove-btn { + background: none; + border: none; + color: #e57373; + font-size: 11px; + cursor: pointer; + padding: 0; + text-align: left; + width: fit-content; + + &:hover { + color: #ff5252; + text-decoration: underline; + } +} + .saved-outfits-title { color: #a7a6a2; font-weight: bold; diff --git a/src/hooks/avatar-editor/useAvatarEditor.ts b/src/hooks/avatar-editor/useAvatarEditor.ts index e92ec7c..ae401de 100644 --- a/src/hooks/avatar-editor/useAvatarEditor.ts +++ b/src/hooks/avatar-editor/useAvatarEditor.ts @@ -291,6 +291,7 @@ const useAvatarEditorState = () => newAvatarModels[AvatarEditorFigureCategory.HEAD] = [ AvatarFigurePartType.HAIR, AvatarFigurePartType.HEAD_ACCESSORY, AvatarFigurePartType.HEAD_ACCESSORY_EXTRA, AvatarFigurePartType.EYE_ACCESSORY, AvatarFigurePartType.FACE_ACCESSORY ].map(setType => buildCategory(setType)); newAvatarModels[AvatarEditorFigureCategory.TORSO] = [ AvatarFigurePartType.CHEST, AvatarFigurePartType.CHEST_PRINT, AvatarFigurePartType.COAT_CHEST, AvatarFigurePartType.CHEST_ACCESSORY ].map(setType => buildCategory(setType)); newAvatarModels[AvatarEditorFigureCategory.LEGS] = [ AvatarFigurePartType.LEGS, AvatarFigurePartType.SHOES, AvatarFigurePartType.WAIST_ACCESSORY ].map(setType => buildCategory(setType)); + newAvatarModels[AvatarEditorFigureCategory.PETS] = [ AvatarFigurePartType.PET ].map(setType => buildCategory(setType)).filter(Boolean); newAvatarModels[AvatarEditorFigureCategory.WARDROBE] = []; setAvatarModels(newAvatarModels);