Merge pull request #55 from duckietm/Dev

🆕 🐶 have you favorite pet as your best pall next to you
This commit is contained in:
DuckieTM
2026-03-25 14:14:39 +01:00
committed by GitHub
7 changed files with 236 additions and 2 deletions
@@ -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;
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 188 B

@@ -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<string>(null);
const [ advancedColorMode, setAdvancedColorMode ] = useState<boolean>(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 (
<div className="flex flex-col items-center justify-center h-full text-sm text-[#888]">
No companion pets available.
</div>
);
}
return (
<div className="flex flex-col overflow-hidden h-full gap-2">
{ /* Equipped pet bar */ }
<div className="pet-equipped-bar">
<div className="pet-equipped-preview">
{ selectedPartItem
? <div
className="pet-equipped-thumb"
style={ slotThumbUrl ? { backgroundImage: `url(${ slotThumbUrl })` } : {} } />
: <div className="pet-paw-icon opacity-20" />
}
</div>
<div className="flex flex-col flex-1 min-w-0 justify-center">
{ selectedPartItem
? <>
<span className="text-xs font-bold text-white truncate">Companion Equipped</span>
<button className="pet-remove-btn" onClick={ removePet }>Remove</button>
</>
: <span className="text-xs text-[#ccc]">No companion selected</span>
}
</div>
</div>
{ /* Pet grid */ }
<div className="flex-1 min-h-0 overflow-hidden pet-grid-container">
<AvatarEditorFigureSetView category={ petCategory } columnCount={ 6 } />
</div>
{ /* Color palette */ }
{ (petCategory.colorItems && petCategory.colorItems[0] && petCategory.colorItems[0].length > 0) &&
<>
<div className="flex shrink-0 items-center justify-end px-2">
<button
className={ `flex items-center gap-1 text-xs px-2 py-0.5 rounded border cursor-pointer transition-colors ${ advancedColorMode ? 'bg-sky-400 border-sky-300 text-white' : 'bg-sky-900/30 border-sky-600/50 text-white hover:text-yellow-800' }` }
onClick={ () => hasHC ? setAdvancedColorMode(prev => !prev) : CreateLinkEvent('habboUI/open/hccenter') }
>
Advanced Color
<LayoutCurrencyIcon type="hc" />
</button>
</div>
<div className={ `flex shrink-0 overflow-hidden gap-2 ${ maxPaletteCount === 2 ? 'dual-palette' : '' }` } style={ { height: '80px' } }>
{ (maxPaletteCount >= 1) &&
<div className="flex-1 min-w-0 overflow-hidden avatar-editor-palette-set-view">
{ advancedColorMode
? <AvatarEditorAdvancedColorView category={ petCategory } paletteIndex={ 0 } />
: <AvatarEditorPaletteSetView category={ petCategory } columnCount={ 14 } paletteIndex={ 0 } /> }
</div> }
{ (maxPaletteCount === 2) &&
<div className="flex-1 min-w-0 overflow-hidden avatar-editor-palette-set-view">
{ advancedColorMode
? <AvatarEditorAdvancedColorView category={ petCategory } paletteIndex={ 1 } />
: <AvatarEditorPaletteSetView category={ petCategory } columnCount={ 14 } paletteIndex={ 1 } /> }
</div> }
</div>
</> }
</div>
);
};
@@ -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 (
<NitroCardTabsItemView key={ modelKey } isActive={ isActive } onClick={ event => setActiveModelKey(modelKey) }>
<div className={ isWardrobe ? 'tab-wardrobe' : `tab ${ modelKey }` } />
<div className={ tabClass } />
</NitroCardTabsItemView>
);
}) }
@@ -94,10 +101,12 @@ export const AvatarEditorView: FC<{}> = props =>
<div className="flex gap-2 overflow-hidden h-full">
{ /* left: model view or wardrobe */ }
<div className="flex flex-col flex-1 min-w-0 overflow-hidden">
{ (activeModelKey.length > 0 && !isWardrobeOpen) &&
{ (activeModelKey.length > 0 && !isWardrobeOpen && !isPetsOpen) &&
<AvatarEditorModelView categories={ avatarModels[activeModelKey] } name={ activeModelKey } /> }
{ isWardrobeOpen &&
<AvatarEditorWardrobeView /> }
{ isPetsOpen &&
<AvatarEditorPetView categories={ avatarModels[activeModelKey] } /> }
</div>
{ /* right: preview + actions */ }
<div className="flex flex-col shrink-0 w-[120px] gap-1 overflow-hidden">
+1
View File
@@ -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';
+72
View File
@@ -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;
@@ -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);