mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 15:06:20 +00:00
🆕 🐶 have you favorite pet as your best pall next to you
This commit is contained in:
@@ -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,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';
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user