Fix: Avatar Editor

This commit is contained in:
duckietm 2024-03-05 15:30:27 +01:00
parent 35b5791dad
commit d7de6777be
33 changed files with 605 additions and 213 deletions

View File

@ -21,8 +21,8 @@ $toolbar-height: 55px;
$achievement-width: 375px;
$achievement-height: 405px;
$avatar-editor-width: 746px;
$avatar-editor-height: 445px;
$avatar-editor-width: 520px;
$avatar-editor-height: 553px;
$catalog-width: 650px;
$catalog-height: 480px;

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 B

View File

@ -1,5 +1,5 @@
$nitro-card-header-height: 33px;
$nitro-card-tabs-height: 33px;
$nitro-card-tabs-height: 42px;
.nitro-card {
resize: both;

View File

@ -0,0 +1,75 @@
import { FC, useMemo } from 'react';
import { Base } from '../Base';
import { Column, ColumnProps } from '../Column';
import { LayoutItemCountView } from './LayoutItemCountView';
import { LayoutLimitedEditionStyledNumberView } from './limited-edition';
export interface LayoutGridColorPickerItemProps extends ColumnProps
{
itemImage?: string;
itemColor?: string;
itemActive?: boolean;
itemCount?: number;
itemCountMinimum?: number;
itemUniqueSoldout?: boolean;
itemUniqueNumber?: number;
itemUnseen?: boolean;
itemHighlight?: boolean;
disabled?: boolean;
}
export const LayoutGridColorPickerItem: FC<LayoutGridColorPickerItemProps> = props =>
{
const { itemImage = undefined, itemColor = undefined, itemActive = false, itemCount = 1, itemCountMinimum = 1, itemUniqueSoldout = false, itemUniqueNumber = -2, itemUnseen = false, itemHighlight = false, disabled = false, center = true, column = true, style = {}, classNames = [], position = 'relative', overflow = 'hidden', children = null, ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'layout-grid-item', 'color-picker-frame' ];
if(itemActive) newClassNames.push('active');
if(itemUniqueSoldout || (itemUniqueNumber > 0)) newClassNames.push('unique-item');
if(itemUniqueSoldout) newClassNames.push('sold-out');
if(itemUnseen) newClassNames.push('unseen');
if(itemHighlight) newClassNames.push('has-highlight');
if(disabled) newClassNames.push('disabled')
if(itemImage === null) newClassNames.push('icon', 'loading-icon');
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ itemActive, itemUniqueSoldout, itemUniqueNumber, itemUnseen, itemHighlight, disabled, itemImage, classNames ]);
const getStyle = useMemo(() =>
{
let newStyle = { ...style };
if(itemImage) newStyle.backgroundImage = `url(${ itemImage })`;
if(itemColor) newStyle.backgroundColor = itemColor;
if(Object.keys(style).length) newStyle = { ...newStyle, ...style };
return newStyle;
}, [ style, itemImage, itemColor ]);
return (
<Column center={ center } pointer position={ position } overflow={ overflow } column={ column } classNames={ getClassNames } style={ getStyle } { ...rest }>
{ (itemCount > itemCountMinimum) &&
<LayoutItemCountView count={ itemCount } /> }
{ (itemUniqueNumber > 0) &&
<>
<Base fit className="unique-bg-override" style={ { backgroundImage: `url(${ itemImage })` } } />
<div className="position-absolute bottom-0 unique-item-counter">
<LayoutLimitedEditionStyledNumberView value={ itemUniqueNumber } />
</div>
</> }
{ children }
</Column>
);
}

View File

@ -6,146 +6,146 @@
height: 21px;
background-position: -226px -131px;
}
&.arrow-right-icon {
width: 28px;
height: 21px;
background-position: -226px -162px;
}
&.ca-icon {
width: 25px;
height: 25px;
background-position: -226px -61px;
&.selected {
&.selected, &:hover {
width: 25px;
height: 25px;
background-position: -226px -96px;
}
}
&.cc-icon {
width: 31px;
height: 29px;
background-position: -145px -5px;
&.selected {
&.selected, &:hover {
width: 31px;
height: 29px;
background-position: -145px -44px;
}
}
&.ch-icon {
width: 29px;
height: 24px;
background-position: -186px -39px;
&.selected {
&.selected, &:hover {
width: 29px;
height: 24px;
background-position: -186px -73px;
}
}
&.clear-icon {
width: 27px;
height: 27px;
background-position: -145px -157px;
}
&.cp-icon {
width: 30px;
height: 24px;
background-position: -145px -264px;
&.selected {
&.selected, &:hover {
width: 30px;
height: 24px;
background-position: -186px -5px;
}
}
&.ea-icon {
width: 35px;
height: 16px;
background-position: -226px -193px;
&.selected {
&.selected, &:hover {
width: 35px;
height: 16px;
background-position: -226px -219px;
}
}
&.fa-icon {
width: 27px;
height: 20px;
background-position: -186px -137px;
&.selected {
&.selected, &:hover {
width: 27px;
height: 20px;
background-position: -186px -107px;
}
}
&.female-icon {
width: 18px;
height: 27px;
background-position: -186px -202px;
&.selected {
&.selected, &:hover {
width: 18px;
height: 27px;
background-position: -186px -239px;
}
}
&.ha-icon {
width: 25px;
height: 22px;
background-position: -226px -245px;
&.selected {
&.selected, &:hover {
width: 25px;
height: 22px;
background-position: -226px -277px;
}
}
&.he-icon {
width: 31px;
height: 27px;
background-position: -145px -83px;
&.selected {
&.selected, &:hover {
width: 31px;
height: 27px;
background-position: -145px -120px;
}
}
&.hr-icon {
width: 29px;
height: 25px;
background-position: -145px -194px;
&.selected {
&.selected, &:hover {
width: 29px;
height: 25px;
background-position: -145px -229px;
}
}
&.lg-icon {
width: 19px;
height: 20px;
background-position: -303px -45px;
&.selected {
&.selected, &:hover {
width: 19px;
height: 20px;
background-position: -303px -75px;
@ -157,54 +157,54 @@
height: 25px;
background-position: -186px -167px;
}
&.male-icon {
width: 21px;
height: 21px;
background-position: -186px -276px;
&.selected {
&.selected, &:hover {
width: 21px;
height: 21px;
background-position: -272px -5px;
}
}
&.sellable-icon {
width: 17px;
height: 15px;
background-position: -303px -105px;
}
&.sh-icon {
width: 37px;
height: 10px;
background-position: -303px -5px;
&.selected {
&.selected, &:hover {
width: 37px;
height: 10px;
background-position: -303px -25px;
}
}
&.spotlight-icon {
width: 130px;
height: 305px;
background-position: -5px -5px;
}
&.wa-icon {
width: 36px;
height: 18px;
background-position: -226px -5px;
&.selected {
&.selected, &:hover {
width: 36px;
height: 18px;
background-position: -226px -33px;
@ -212,18 +212,64 @@
}
}
.nitro-avatar-editor-wardrobe-figure-preview {
background-color: $pale-sky;
overflow: hidden;
z-index: 1;
.saved-outfits-title {
color: #a7a6a2;
font-weight: bold;
}
.saved-outfit-container {
display: flex;
width: 100% !important;
height: 91.5%;
.avatar-image {
position: absolute;
bottom: -15px;
margin: 0 auto;
width: 40px !important;
z-index: 4;
transform: scale(0.5);
}
.avatar-figure {
margin-top: -46px;
margin-left: -9px;
image-rendering: auto !important;
}
.nitro-avatar-editor-wardrobe-container {
background-color: #cacaca;
border-radius: 0.3rem;
border: solid 1px #000;
height: 386px;
width: 100%;
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-gap: 10px;
padding: 10px 12px 10px 0;
overflow-y: auto;
}
.avatar-container {
height: 50px;
border-radius: 0.3rem;
background-color: #a7a6a2;
width: 30px;
}
.saved-outfit-button {
margin-top: -3px;
background-color: transparent;
border: none;
}
}
.nitro-avatar-editor-wardrobe-figure-preview {
border-image-source: url(@/assets/images/avatareditor/wardrobe_user_bg.png);
border-image-slice: 4 4 4 4 fill;
border-image-width: 4px 4px 4px 4px;
background-color: transparent;
overflow: hidden;
z-index: 1;
.avatar-shadow {
position: absolute;
left: 0;
@ -237,19 +283,6 @@
z-index: 2;
}
&:after {
position: absolute;
content: '';
top: 75%;
bottom: 0;
left: 0;
right: 0;
border-radius: 50%;
background-color: $pale-sky;
box-shadow: 0 0 8px 2px rgba($white,.6);
transform: scale(2);
}
.button-container {
position: absolute;
bottom: 0;
@ -258,8 +291,11 @@
}
.nitro-avatar-editor {
width: $avatar-editor-width;
height: $avatar-editor-height;
min-width: $avatar-editor-width;
min-height: $avatar-editor-height;
max-width: $avatar-editor-width;
max-height: $avatar-editor-height;
.category-item {
height: 40px;
@ -268,7 +304,6 @@
.figure-preview-container {
position: relative;
height: 100%;
background-color: #b69b83;
overflow: hidden;
z-index: 1;
@ -279,9 +314,17 @@
padding: 0 10px;
display: flex;
justify-content: space-between;
bottom: 12px;
bottom: 50px;
z-index: 5;
.arrow-left {
background-image: url(@/assets/images/avatareditor/rotation_arrow.png);
width: 44px;
height: 29px;
margin-left: auto;
margin-right: auto;
}
.icon {
cursor: pointer;
}
@ -291,46 +334,194 @@
position: absolute;
left: 0;
right: 0;
bottom: 50px;
bottom: 125px;
margin: 0 auto;
z-index: 4;
}
.avatar-spotlight {
position: absolute;
top: -10px;
left: 0;
right: 0;
margin: 0 auto;
opacity: 0.3;
pointer-events: none;
z-index: 3;
}
.avatar-shadow {
position: absolute;
left: 0;
right: 0;
bottom: 15px;
width: 70px;
height: 30px;
bottom: 88px;
width: 68px;
height: 34px;
margin: 0 auto;
border-radius: 100%;
background-color: rgba(0, 0, 0, 0.20);
background: url(@/assets/images/avatareditor/avatar_shadow.png);
z-index: 2;
}
&:after {
position: absolute;
content: '';
top: 75%;
bottom: 0;
left: 0;
right: 0;
border-radius: 50%;
background-color: #a68d76;
box-shadow: 0 0 8px 2px rgba($white,.6);
transform: scale(2);
}
}
}
.nitro-avatar-editor.expanded {
max-width: $avatar-editor-width + 264px;
min-width: $avatar-editor-width + 164px;
}
.choose-clothing {
width: 320px;
}
.color-picker-frame {
border-image-source: url(@/assets/images/avatareditor/color_frame.png);
border-image-slice: 6 6 6 6 fill;
border-image-width: 6px 6px 6px 6px;
width: 14px;
height: 21px;
border-radius: 4px;
&.active {
border-image-source: url(@/assets/images/avatareditor/color_frame_active.png);
height: 21px;
margin-top: -2px;
}
&:hover {
border-image-source: url(@/assets/images/avatareditor/color_frame_active.png);
height: 21px;
margin-top: -2px;
}
}
.hc-icon {
background-image: url(@/assets/images/avatareditor/hc_icon.png);
height: 9px;
width: 10px;
bottom: 2px;
left: 2px;
}
.avatar-editor-tabs {
position: relative;
.tab {
background-position-x: 0;
background-position-y: 0;
width: 34px;
height: 22px;
}
.hd {
background: url(@/assets/images/wardrobe/hd.png) no-repeat center;
}
.head {
background: url(@/assets/images/wardrobe/head.png) no-repeat center;
}
.torso {
background: url(@/assets/images/wardrobe/torso.png) no-repeat center;
}
.legs {
background: url(@/assets/images/wardrobe/legs.png) no-repeat center;
}
.tab-wardrobe {
width: 40px;
height: 28px;
background-size: 38px 28px;
background-image: url(@/assets/images/wardrobe/wardrobe.png);
background-repeat: no-repeat;
background-position: center;
filter: contrast(1.2) brightness(1.05);
}
.nav-tabs .nav-link {
position: relative;
border-image-source: url(@/assets/images/boxes/card/tabs_avatareditor.png);
border-image-slice: 7 7 7 7 fill;
border-image-width: 7px 7px 7px 7px;
border-image-outset: 0px 0px 0px 0px;
border-image-repeat: repeat repeat;
margin-bottom: -2px;
margin-left: -2px;
&:hover {
border-image-source: url(@/assets/images/boxes/card/tabs_active.png);
}
}
.nav-tabs .nav-link.active {
border-image-source: url(@/assets/images/boxes/card/tabs_active.png);
}
}
.randomize-container {
bottom: 95px;
left: 330px;
z-index: 2;
}
.randomize-icon {
background-image: url(@/assets/images/avatareditor/randomize_transparent.png);
height: 33px;
width: 39px;
&:hover {
background-image: url(@/assets/images/avatareditor/randomize.png);
}
}
.avatar-wardrobe {
border-image-source: url(@/assets/images/avatareditor/wardrobe_bg.png);
border-image-slice: 6 6 6 6 fill;
border-image-width: 6px 6px 6px 6px;
}
.avatar-container {
padding: 3px;
}
.avatar-parts {
border: none !important;
height: 42px;
width: 42px;
background-position: center;
background-repeat: no-repeat;
border-radius: 2rem !important;
overflow: visible !important;
background-color: transparent;
&:hover {
box-shadow: 0 0 0 3px #dbdad5 !important;
background-color: #cecdc8 !important;
}
&:active,
&.part-selected {
box-shadow: 0 0 0 3px #c5c3c0 !important;
background-color: #b1b1b1 !important;
}
}
.avatar-parts-container {
height: 70%;
padding-left: 10px;
}
.avatar-color-palette-container {
height: 30%;
width: 100%!important;
padding-left: 10px;
}
.dual-palette {
display: flex !important;
flex-direction: row !important;
}
.avatar-editor-palette-set-view {
padding-right: 15px !important;
flex-grow: 1;
}
.clothing-container {
padding-right: 15px !important;
}
.action-buttons {
gap: 5px;
}

View File

@ -1,8 +1,8 @@
import { AvatarEditorFigureCategory, FigureSetIdsMessageEvent, GetWardrobeMessageComposer, IAvatarFigureContainer, ILinkEventTracker, UserFigureComposer, UserWardrobePageEvent } from '@nitrots/nitro-renderer';
import { AvatarEditorFigureCategory, FigureSetIdsMessageEvent, GetWardrobeMessageComposer, IAvatarFigureContainer, ILinkEventTracker, SetClothingChangeDataMessageComposer, UserFigureComposer, UserWardrobePageEvent } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { FaDice, FaTrash, FaUndo } from 'react-icons/fa';
import { AddEventLinkTracker, AvatarEditorAction, AvatarEditorUtilities, BodyModel, FigureData, generateRandomFigure, GetAvatarRenderManager, GetClubMemberLevel, GetConfiguration, GetSessionDataManager, HeadModel, IAvatarEditorCategoryModel, LegModel, LocalizeText, RemoveLinkEventTracker, SendMessageComposer, TorsoModel } from '../../api';
import { Button, ButtonGroup, Column, Grid, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../common';
import { AddEventLinkTracker, AvatarEditorAction, AvatarEditorUtilities, BodyModel, FigureData, GetAvatarRenderManager, GetClubMemberLevel, GetConfiguration, GetSessionDataManager, HeadModel, IAvatarEditorCategoryModel, LegModel, LocalizeText, RemoveLinkEventTracker, SendMessageComposer, SetLocalStorage, TorsoModel, generateRandomFigure } from '../../api';
import { Button, ButtonGroup, Column, Flex, Grid, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../common';
import { useMessageEvent } from '../../hooks';
import { AvatarEditorFigurePreviewView } from './views/AvatarEditorFigurePreviewView';
import { AvatarEditorModelView } from './views/AvatarEditorModelView';
@ -26,8 +26,19 @@ export const AvatarEditorView: FC<{}> = props =>
const [ lastGender, setLastGender ] = useState<string>(null);
const [ needsReset, setNeedsReset ] = useState(true);
const [ isInitalized, setIsInitalized ] = useState(false);
const [ genderFootballGate, setGenderFootballGate ] = useState<string>(null);
const [ objectFootballGate, setObjectFootballGate ] = useState<number>(null);
const DEFAULT_MALE_FOOTBALL_GATE = JSON.parse(window.localStorage.getItem('nitro.look.footballgate.M')) || 'ch-3109-92-1408.lg-3116-82-1408.sh-3115-1408-1408';
const DEFAULT_FEMALE_FOOTBALL_GATE = JSON.parse(window.localStorage.getItem('nitro.look.footballgate.F')) || 'ch-3112-1408-1408.lg-3116-71-1408.sh-3115-1408-1408';
const maxWardrobeSlots = useMemo(() => GetConfiguration<number>('avatar.wardrobe.max.slots', 10), []);
const onClose = () =>
{
setGenderFootballGate(null);
setObjectFootballGate(null);
setIsVisible(false);
}
useMessageEvent<FigureSetIdsMessageEvent>(FigureSetIdsMessageEvent, event =>
{
@ -50,7 +61,7 @@ export const AvatarEditorView: FC<{}> = props =>
i++;
}
for(let [ index, [ look, gender ] ] of parser.looks.entries())
{
const container = GetAvatarRenderManager().createFigureContainer(look);
@ -64,7 +75,7 @@ export const AvatarEditorView: FC<{}> = props =>
const selectCategory = useCallback((name: string) =>
{
if(!categories) return;
setActiveCategory(categories.get(name));
}, [ categories ]);
@ -72,13 +83,21 @@ export const AvatarEditorView: FC<{}> = props =>
{
const categories = new Map();
categories.set(AvatarEditorFigureCategory.GENERIC, new BodyModel());
categories.set(AvatarEditorFigureCategory.HEAD, new HeadModel());
categories.set(AvatarEditorFigureCategory.TORSO, new TorsoModel());
categories.set(AvatarEditorFigureCategory.LEGS, new LegModel());
if (!genderFootballGate)
{
categories.set(AvatarEditorFigureCategory.GENERIC, new BodyModel());
categories.set(AvatarEditorFigureCategory.HEAD, new HeadModel());
categories.set(AvatarEditorFigureCategory.TORSO, new TorsoModel());
categories.set(AvatarEditorFigureCategory.LEGS, new LegModel());
}
else
{
categories.set(AvatarEditorFigureCategory.TORSO, new TorsoModel());
categories.set(AvatarEditorFigureCategory.LEGS, new LegModel());
}
setCategories(categories);
}, []);
}, [ genderFootballGate ]);
const setupFigures = useCallback(() =>
{
@ -135,11 +154,12 @@ export const AvatarEditorView: FC<{}> = props =>
resetCategories();
return;
case AvatarEditorAction.ACTION_SAVE:
SendMessageComposer(new UserFigureComposer(figureData.gender, figureData.getFigureString()));
setIsVisible(false);
!genderFootballGate ? SendMessageComposer(new UserFigureComposer(figureData.gender, figureData.getFigureString())) : SendMessageComposer(new SetClothingChangeDataMessageComposer(objectFootballGate, genderFootballGate, figureData.getFigureString()));
SetLocalStorage(`nitro.look.footballgate.${ genderFootballGate }`, figureData.getFigureString());
onClose();
return;
}
}, [ figureData, lastFigure, lastGender, figureSetIds, loadAvatarInEditor, resetCategories ])
}, [ loadAvatarInEditor, figureData, resetCategories, lastFigure, lastGender, figureSetIds, genderFootballGate, objectFootballGate ])
const setGender = useCallback((gender: string) =>
{
@ -154,9 +174,12 @@ export const AvatarEditorView: FC<{}> = props =>
linkReceived: (url: string) =>
{
const parts = url.split('/');
setGenderFootballGate(parts[2] ? parts[2] : null);
setObjectFootballGate(parts[3] ? Number(parts[3]) : null);
if(parts.length < 2) return;
switch(parts[1])
{
case 'show':
@ -185,25 +208,15 @@ export const AvatarEditorView: FC<{}> = props =>
useEffect(() =>
{
if(!isWardrobeVisible) return;
setActiveCategory(null);
SendMessageComposer(new GetWardrobeMessageComposer());
}, [ isWardrobeVisible ]);
useEffect(() =>
{
if(!activeCategory) return;
setIsWardrobeVisible(false);
}, [ activeCategory ]);
}, []);
useEffect(() =>
{
if(!categories) return;
selectCategory(AvatarEditorFigureCategory.GENERIC);
}, [ categories, selectCategory ]);
selectCategory(!genderFootballGate ? AvatarEditorFigureCategory.GENERIC : AvatarEditorFigureCategory.TORSO);
}, [ categories, genderFootballGate, selectCategory ]);
useEffect(() =>
{
@ -248,9 +261,22 @@ export const AvatarEditorView: FC<{}> = props =>
{
if(!isVisible || !isInitalized || !needsReset) return;
loadAvatarInEditor(GetSessionDataManager().figure, GetSessionDataManager().gender);
if (!genderFootballGate) loadAvatarInEditor(GetSessionDataManager().figure, GetSessionDataManager().gender);
if (genderFootballGate) loadAvatarInEditor(genderFootballGate === FigureData.MALE ? DEFAULT_MALE_FOOTBALL_GATE : DEFAULT_FEMALE_FOOTBALL_GATE, genderFootballGate);
setNeedsReset(false);
}, [ isVisible, isInitalized, needsReset, loadAvatarInEditor ]);
}, [ isVisible, isInitalized, needsReset, loadAvatarInEditor, genderFootballGate, DEFAULT_MALE_FOOTBALL_GATE, DEFAULT_FEMALE_FOOTBALL_GATE ]);
useEffect(() => // This is so when you have the look editor open and you change the mode to Boy or Girl
{
if(!isVisible) return;
return () =>
{
setupFigures();
setIsWardrobeVisible(false);
setNeedsReset(true);
}
}, [ isVisible, genderFootballGate, setupFigures ]);
useEffect(() =>
{
@ -264,50 +290,64 @@ export const AvatarEditorView: FC<{}> = props =>
if(!isVisible || !figureData) return null;
const avatarEditorClasses = `nitro-avatar-editor no-resize ${ isWardrobeVisible ? 'expanded' : '' }`;
return (
<NitroCardView uniqueKey="avatar-editor" className="nitro-avatar-editor">
<NitroCardHeaderView headerText={ LocalizeText('avatareditor.title') } onCloseClick={ event => setIsVisible(false) } />
<NitroCardTabsView>
<NitroCardView uniqueKey="avatar-editor" className={ avatarEditorClasses }>
<NitroCardHeaderView headerText={ !genderFootballGate ? LocalizeText('avatareditor.title') : LocalizeText('widget.furni.clothingchange.editor.title') } onCloseClick={ onClose } />
<NitroCardTabsView className="avatar-editor-tabs">
{ categories && (categories.size > 0) && Array.from(categories.keys()).map(category =>
{
const isActive = (activeCategory && (activeCategory.name === category));
return (
<NitroCardTabsItemView key={ category } isActive={ isActive } onClick={ event => selectCategory(category) }>
{ LocalizeText(`avatareditor.category.${ category }`) }
<div className={ `tab ${ category }` }></div>
</NitroCardTabsItemView>
);
}) }
<NitroCardTabsItemView isActive={ isWardrobeVisible } onClick={ event => setIsWardrobeVisible(true) }>
{ LocalizeText('avatareditor.category.wardrobe') }
</NitroCardTabsItemView>
{ (!genderFootballGate) &&
<NitroCardTabsItemView onClick={ event => setIsWardrobeVisible(!isWardrobeVisible) }>
<div className="tab-wardrobe"></div>
</NitroCardTabsItemView>
}
</NitroCardTabsView>
<NitroCardContentView>
<Grid>
<Column size={ 9 } overflow="hidden">
{ (activeCategory && !isWardrobeVisible) &&
<AvatarEditorModelView model={ activeCategory } gender={ figureData.gender } setGender={ setGender } /> }
{ isWardrobeVisible &&
<AvatarEditorWardrobeView figureData={ figureData } savedFigures={ savedFigures } setSavedFigures={ setSavedFigures } loadAvatarInEditor={ loadAvatarInEditor } /> }
<Column size={ isWardrobeVisible ? 6 : 8 } overflow="hidden">
{ (activeCategory) &&
<AvatarEditorModelView model={ activeCategory } gender={ figureData.gender } setGender={ setGender } />
}
</Column>
<Column size={ 3 } overflow="hidden">
<AvatarEditorFigurePreviewView figureData={ figureData } />
<Column grow gap={ 1 }>
<ButtonGroup>
<Button variant="secondary" onClick={ event => processAction(AvatarEditorAction.ACTION_RESET) }>
<FaUndo className="fa-icon" />
</Button>
<Button variant="secondary" onClick={ event => processAction(AvatarEditorAction.ACTION_CLEAR) }>
<FaTrash className="fa-icon" />
</Button>
<Button variant="secondary" onClick={ event => processAction(AvatarEditorAction.ACTION_RANDOMIZE) }>
<FaDice className="fa-icon" />
</Button>
</ButtonGroup>
<Button className="w-100" variant="success" onClick={ event => processAction(AvatarEditorAction.ACTION_SAVE) }>
{ LocalizeText('avatareditor.save') }
</Button>
</Column>
<Column size={ isWardrobeVisible ? 6 : 4 } overflow="hidden">
<Flex gap={ 2 } className="w-100 h-100">
<Flex column={ true } className="w-100">
<AvatarEditorFigurePreviewView figureData={ figureData } />
<Column grow gap={ 1 }>
{ (!genderFootballGate) &&
<ButtonGroup className="action-buttons w-100">
<Button variant="secondary" onClick={ event => processAction(AvatarEditorAction.ACTION_RESET) }>
<FaUndo className="fa-icon" />
</Button>
<Button variant="secondary" onClick={ event => processAction(AvatarEditorAction.ACTION_CLEAR) }>
<FaTrash className="fa-icon" />
</Button>
<Button variant="secondary" onClick={ event => processAction(AvatarEditorAction.ACTION_RANDOMIZE) }>
<FaDice className="fa-icon" />
</Button>
</ButtonGroup>
}
<Button className="w-10" variant="success" onClick={ event => processAction(AvatarEditorAction.ACTION_SAVE) }>
{ LocalizeText('avatareditor.save') }
</Button>
</Column>
</Flex>
{ isWardrobeVisible &&
<Column overflow="hidden" className="w-100">
<AvatarEditorWardrobeView figureData={ figureData } savedFigures={ savedFigures } setSavedFigures={ setSavedFigures } loadAvatarInEditor={ loadAvatarInEditor } />
</Column>
}
</Flex>
</Column>
</Grid>
</NitroCardContentView>

View File

@ -44,11 +44,9 @@ export const AvatarEditorFigurePreviewView: FC<AvatarEditorFigurePreviewViewProp
return (
<Column className="figure-preview-container" overflow="hidden" position="relative">
<LayoutAvatarImageView figure={ figureData.getFigureString() } direction={ figureData.direction } scale={ 2 } />
<AvatarEditorIcon className="avatar-spotlight" icon="spotlight" />
<Base className="avatar-shadow" />
<Base className="arrow-container">
<AvatarEditorIcon pointer icon="arrow-left" onClick={ event => rotateFigure(figureData.direction + 1) } />
<AvatarEditorIcon pointer icon="arrow-right" onClick={ event => rotateFigure(figureData.direction - 1) } />
<i className="icon arrow-left" onClick={ event => rotateFigure(figureData.direction + 1) } />
</Base>
</Column>
);

View File

@ -1,9 +1,12 @@
import { Dispatch, FC, SetStateAction, useCallback, useEffect, useState } from 'react';
import { CategoryData, FigureData, IAvatarEditorCategoryModel } from '../../../api';
import { Column, Flex, Grid } from '../../../common';
import { CategoryData, FigureData, IAvatarEditorCategoryModel, LocalizeText } from '../../../api';
import { Column, Flex, Grid, Text } from '../../../common';
import { AvatarEditorIcon } from './AvatarEditorIcon';
import { AvatarEditorFigureSetView } from './figure-set/AvatarEditorFigureSetView';
import { AvatarEditorPaletteSetView } from './palette-set/AvatarEditorPaletteSetView';
const CATEGORY_FOOTBALL_GATE = [ 'ch', 'cp', 'lg', 'sh' ];
export interface AvatarEditorModelViewProps
{
model: IAvatarEditorCategoryModel;
@ -13,7 +16,7 @@ export interface AvatarEditorModelViewProps
export const AvatarEditorModelView: FC<AvatarEditorModelViewProps> = props =>
{
const { model = null, gender = null, setGender = null } = props;
const { model = null, gender = null, isFromFootballGate = false, setGender = null } = props;
const [ activeCategory, setActiveCategory ] = useState<CategoryData>(null);
const [ maxPaletteCount, setMaxPaletteCount ] = useState(1);
@ -53,35 +56,48 @@ export const AvatarEditorModelView: FC<AvatarEditorModelViewProps> = props =>
return (
<Grid>
<Column size={ 2 }>
{ model.canSetGender &&
<Column className="choose-clothing overflow-y-auto overflow-x-hidden">
<Flex className="px-3" gap={ 4 }>
{ model.canSetGender &&
<>
<Flex center pointer className="category-item" onClick={ event => setGender(FigureData.MALE) }>
<Flex center pointer className="category-item" gap={ 3 } onClick={ event => setGender(FigureData.MALE) }>
<AvatarEditorIcon icon="male" selected={ (gender === FigureData.MALE) } />
<Text bold>{ LocalizeText('avatareditor.generic.boy') }</Text>
</Flex>
<Flex center pointer className="category-item" onClick={ event => setGender(FigureData.FEMALE) }>
<Flex center pointer className="category-item" gap={ 3 } onClick={ event => setGender(FigureData.FEMALE) }>
<AvatarEditorIcon icon="female" selected={ (gender === FigureData.FEMALE) } />
<Text bold>{ LocalizeText('avatareditor.generic.girl') }</Text>
</Flex>
</> }
{ !model.canSetGender && model.categories && (model.categories.size > 0) && Array.from(model.categories.keys()).map(name =>
{
const category = model.categories.get(name);
{ !model.canSetGender && model.categories && (model.categories.size > 0) && Array.from(model.categories.keys()).map(name =>
{
const category = model.categories.get(name);
return (
<Flex center pointer key={ name } className="category-item" onClick={ event => selectCategory(name) }>
<AvatarEditorIcon icon={ category.name } selected={ (activeCategory === category) } />
</Flex>
);
}) }
</Column>
<Column size={ 5 } overflow="hidden">
<AvatarEditorFigureSetView model={ model } category={ activeCategory } setMaxPaletteCount={ setMaxPaletteCount } />
</Column>
<Column size={ 5 } overflow="hidden">
{ (maxPaletteCount >= 1) &&
return (
<div key={ name }>
<Flex center pointer className="category-item" onClick={ event => selectCategory(name) }>
{ (isFromFootballGate && CATEGORY_FOOTBALL_GATE.includes(category.name)) &&
<AvatarEditorIcon icon={ category.name } selected={ (activeCategory === category) } />
}
{ (!isFromFootballGate) &&
<AvatarEditorIcon icon={ category.name } selected={ (activeCategory === category) } />
}
</Flex>
</div>
);
}) }
</Flex>
<Column className="avatar-parts-container" size={ 5 } overflow="hidden">
<AvatarEditorFigureSetView model={ model } category={ activeCategory } isFromFootballGate={ isFromFootballGate } setMaxPaletteCount={ setMaxPaletteCount } />
</Column>
<Column overflow="hidden" className={
maxPaletteCount === 2 ? 'avatar-color-palette-container dual-palette' : 'avatar-color-palette-container'
}>
{ (maxPaletteCount >= 1) &&
<AvatarEditorPaletteSetView model={ model } category={ activeCategory } paletteSet={ activeCategory.getPalette(0) } paletteIndex={ 0 } /> }
{ (maxPaletteCount === 2) &&
{ (maxPaletteCount === 2) &&
<AvatarEditorPaletteSetView model={ model } category={ activeCategory } paletteSet={ activeCategory.getPalette(1) } paletteIndex={ 1 } /> }
</Column>
</Column>
</Grid>
);

View File

@ -1,8 +1,8 @@
import { IAvatarFigureContainer, SaveWardrobeOutfitMessageComposer } from '@nitrots/nitro-renderer';
import { HabboClubLevelEnum, IAvatarFigureContainer, SaveWardrobeOutfitMessageComposer } from '@nitrots/nitro-renderer';
import { Dispatch, FC, SetStateAction, useCallback, useMemo } from 'react';
import { FigureData, GetAvatarRenderManager, GetClubMemberLevel, GetConfiguration, LocalizeText, SendMessageComposer } from '../../../api';
import { AutoGrid, Base, Button, Flex, LayoutAvatarImageView, LayoutCurrencyIcon, LayoutGridItem } from '../../../common';
import { MdKeyboardArrowLeft, MdKeyboardArrowRight } from 'react-icons/md';
import { CreateLinkEvent, FigureData, GetAvatarRenderManager, GetClubMemberLevel, GetConfiguration, GetSessionDataManager, LocalizeText, SendMessageComposer } from '../../../api';
import { Flex, LayoutAvatarImageView, LayoutCurrencyIcon } from '../../../common';
export interface AvatarEditorWardrobeViewProps
{
figureData: FigureData;
@ -30,6 +30,8 @@ export const AvatarEditorWardrobeView: FC<AvatarEditorWardrobeViewProps> = props
{
if(!figureData || (index >= savedFigures.length) || (index < 0)) return;
if (GetSessionDataManager().clubLevel === HabboClubLevelEnum.NO_CLUB) return CreateLinkEvent('habboUI/open/hccenter');
const newFigures = [ ...savedFigures ];
const figure = figureData.getFigureString();
@ -41,6 +43,22 @@ export const AvatarEditorWardrobeView: FC<AvatarEditorWardrobeViewProps> = props
SendMessageComposer(new SaveWardrobeOutfitMessageComposer((index + 1), figure, gender));
}, [ figureData, savedFigures, setSavedFigures ]);
const getClubLevel = useCallback(() =>
{
let highestClubLevel = 0;
savedFigures.forEach(([ figureContainer, gender ]) =>
{
if (figureContainer)
{
const clubLevel = GetAvatarRenderManager().getFigureClubLevel(figureContainer, gender);
highestClubLevel = Math.max(highestClubLevel, clubLevel);
}
});
return highestClubLevel;
}, [ savedFigures ]);
const figures = useMemo(() =>
{
if(!savedFigures || !savedFigures.length) return [];
@ -54,26 +72,52 @@ export const AvatarEditorWardrobeView: FC<AvatarEditorWardrobeViewProps> = props
if(figureContainer) clubLevel = GetAvatarRenderManager().getFigureClubLevel(figureContainer, gender);
items.push(
<LayoutGridItem key={ index } position="relative" overflow="hidden" className="nitro-avatar-editor-wardrobe-figure-preview">
{ figureContainer &&
<LayoutAvatarImageView figure={ figureContainer.getFigureString() } gender={ gender } direction={ 2 } /> }
<Base className="avatar-shadow" />
{ !hcDisabled && (clubLevel > 0) && <LayoutCurrencyIcon className="position-absolute top-1 start-1" type="hc" /> }
<Flex gap={ 1 } className="button-container">
<Button variant="link" fullWidth onClick={ event => saveFigureAtWardrobeIndex(index) }>{ LocalizeText('avatareditor.wardrobe.save') }</Button>
{ figureContainer &&
<Button variant="link" fullWidth onClick={ event => wearFigureAtIndex(index) } disabled={ (clubLevel > GetClubMemberLevel()) }>{ LocalizeText('generic_usable.button.use') }</Button> }
<Flex key={ index } alignItems={ 'center' } justifyContent={ 'center' }>
<Flex gap={ 1 } column={ true } className="button-container">
<button
className="saved-outfit-button"
onClick={ event => saveFigureAtWardrobeIndex(index) }
disabled={ clubLevel > GetClubMemberLevel() && !hcDisabled }>
<MdKeyboardArrowRight />
</button>
{ figureContainer && (
<button
className="saved-outfit-button"
onClick={ event => wearFigureAtIndex(index) }
disabled={ clubLevel > GetClubMemberLevel() && !hcDisabled }
>
<MdKeyboardArrowLeft />
</button>
) }
</Flex>
</LayoutGridItem>
<div className="avatar-container">
{ figureContainer && (
<LayoutAvatarImageView className="avatar-figure" figure={ figureContainer.getFigureString() } gender={ gender } direction={ 4 } />
) }
</div>
</Flex>
);
});
return items;
}, [ savedFigures, hcDisabled, saveFigureAtWardrobeIndex, wearFigureAtIndex ]);
}, [ savedFigures, saveFigureAtWardrobeIndex, wearFigureAtIndex ]);
return (
<AutoGrid columnCount={ 5 } columnMinWidth={ 80 } columnMinHeight={ 140 }>
{ figures }
</AutoGrid>
<div>
<div className="d-flex flex-column align-items-center">
<span className="saved-outfits-title">
{ LocalizeText('avatareditor.wardrobe.title') }
</span>
<span className="mt-2">
{ !hcDisabled && getClubLevel() > 0 && (
<LayoutCurrencyIcon type="hc" />
) }
</span>
</div>
<div className="saved-outfit-container mt-2">
<div className="nitro-avatar-editor-wardrobe-container">{ figures }</div>
</div>
</div>
);
}

View File

@ -25,11 +25,13 @@ export const AvatarEditorFigureSetItemView: FC<AvatarEditorFigureSetItemViewProp
}, [ partItem ]);
return (
<LayoutGridItem itemImage={ (partItem.isClear ? undefined : partItem.imageUrl) } itemActive={ partItem.isSelected } { ...rest }>
{ !hcDisabled && partItem.isHC && <LayoutCurrencyIcon className="position-absolute end-1 bottom-1" type="hc" /> }
{ partItem.isClear && <AvatarEditorIcon icon="clear" /> }
{ partItem.isSellable && <AvatarEditorIcon icon="sellable" position="absolute" className="end-1 bottom-1" /> }
{ children }
</LayoutGridItem>
<div className="avatar-container">
<LayoutGridItem className={ `avatar-parts ${ partItem.isSelected ? 'part-selected' : '' }` } itemImage={ (partItem.isClear ? undefined : partItem.imageUrl) } { ...rest }>
{ !hcDisabled && partItem.isHC && <i className="icon hc-icon position-absolute" /> }
{ partItem.isClear && <AvatarEditorIcon icon="clear" /> }
{ partItem.isSellable && <AvatarEditorIcon icon="sellable" position="absolute" className="end-1 bottom-1" /> }
{ children }
</LayoutGridItem>
</div>
);
}

View File

@ -1,8 +1,14 @@
import { HabboClubLevelEnum } from '@nitrots/nitro-renderer';
import { Dispatch, FC, SetStateAction, useCallback, useEffect, useRef } from 'react';
import { AvatarEditorGridPartItem, CategoryData, IAvatarEditorCategoryModel } from '../../../../api';
import { AvatarEditorGridPartItem, CategoryData, CreateLinkEvent, GetSessionDataManager, IAvatarEditorCategoryModel } from '../../../../api';
import { AutoGrid } from '../../../../common';
import { AvatarEditorFigureSetItemView } from './AvatarEditorFigureSetItemView';
const TSHIRT_FOOTBALL_GATE = [ 3111, 3110, 3109, 3030, 3114, 266, 265, 262, 3113, 3112, 691, 690, 667 ];
const NUMBER_BEHIND_FOOTBALL_GATE = [ 3128, 3127, 3126, 3125, 3124, 3123, 3122, 3121, 3120, 3119 ];
const PANTS_FOOTBALL_GATE = [ 3116, 281, 275, 715, 700, 696, 3006 ];
const SHOES_FOOTBALL_GATE = [ 3115, 3068, 906 ];
export interface AvatarEditorFigureSetViewProps
{
model: IAvatarEditorCategoryModel;
@ -12,7 +18,7 @@ export interface AvatarEditorFigureSetViewProps
export const AvatarEditorFigureSetView: FC<AvatarEditorFigureSetViewProps> = props =>
{
const { model = null, category = null, setMaxPaletteCount = null } = props;
const { model = null, category = null, isFromFootballGate = false, setMaxPaletteCount = null } = props;
const elementRef = useRef<HTMLDivElement>(null);
const selectPart = useCallback((item: AvatarEditorGridPartItem) =>
@ -21,6 +27,8 @@ export const AvatarEditorFigureSetView: FC<AvatarEditorFigureSetViewProps> = pro
if(index === -1) return;
if (item.isHC && GetSessionDataManager().clubLevel === HabboClubLevelEnum.NO_CLUB) return CreateLinkEvent('habboUI/open/hccenter');
model.selectPart(category.name, index);
const partItem = category.getCurrentPart();
@ -36,9 +44,11 @@ export const AvatarEditorFigureSetView: FC<AvatarEditorFigureSetViewProps> = pro
}, [ model, category ]);
return (
<AutoGrid innerRef={ elementRef } columnCount={ 3 } columnMinHeight={ 50 }>
{ (category.parts.length > 0) && category.parts.map((item, index) =>
<AvatarEditorFigureSetItemView key={ index } partItem={ item } onClick={ event => selectPart(item) } />) }
</AutoGrid>
<AutoGrid className="clothing-container" innerRef={ elementRef } columnCount={ 3 } columnMinHeight={ 50 }>
{ (category.parts.length > 0) && category.parts.map(item =>
(!isFromFootballGate || (isFromFootballGate && TSHIRT_FOOTBALL_GATE.includes(item.id) || NUMBER_BEHIND_FOOTBALL_GATE.includes(item.id) || PANTS_FOOTBALL_GATE.includes(item.id) || SHOES_FOOTBALL_GATE.includes(item.id))) &&
<AvatarEditorFigureSetItemView key={ item.id } partItem={ item } onClick={ event => selectPart(item) } />)
}
</AutoGrid>
);
}

View File

@ -1,6 +1,7 @@
import { FC, useEffect, useState } from 'react';
import { AvatarEditorGridColorItem, GetConfiguration } from '../../../../api';
import { LayoutCurrencyIcon, LayoutGridItem, LayoutGridItemProps } from '../../../../common';
import { LayoutGridColorPickerItem } from '../../../../common/layout/LayoutGridColorPickerItem';
export interface AvatarEditorPaletteSetItemProps extends LayoutGridItemProps
{
@ -24,9 +25,9 @@ export const AvatarEditorPaletteSetItem: FC<AvatarEditorPaletteSetItemProps> = p
}, [ colorItem ]);
return (
<LayoutGridItem itemHighlight itemColor={ colorItem.color } itemActive={ colorItem.isSelected } className="clear-bg" { ...rest }>
{ !hcDisabled && colorItem.isHC && <LayoutCurrencyIcon className="position-absolute end-1 bottom-1" type="hc" /> }
<LayoutGridColorPickerItem itemHighlight itemColor={ colorItem.color } itemActive={ colorItem.isSelected } className="color-picker-frame clear-bg" { ...rest }>
{ !hcDisabled && colorItem.isHC && <i className="icon hc-icon position-absolute" /> }
{ children }
</LayoutGridItem>
</LayoutGridColorPickerItem>
);
}

View File

@ -1,5 +1,6 @@
import { HabboClubLevelEnum } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useRef } from 'react';
import { AvatarEditorGridColorItem, CategoryData, IAvatarEditorCategoryModel } from '../../../../api';
import { AvatarEditorGridColorItem, CategoryData, CreateLinkEvent, GetSessionDataManager, IAvatarEditorCategoryModel } from '../../../../api';
import { AutoGrid } from '../../../../common';
import { AvatarEditorPaletteSetItem } from './AvatarEditorPaletteSetItemView';
@ -21,6 +22,8 @@ export const AvatarEditorPaletteSetView: FC<AvatarEditorPaletteSetViewProps> = p
const index = paletteSet.indexOf(item);
if(index === -1) return;
if (item.isHC && GetSessionDataManager().clubLevel === HabboClubLevelEnum.NO_CLUB) return CreateLinkEvent('habboUI/open/hccenter');
model.selectColor(category.name, index, paletteIndex);
}, [ model, category, paletteSet, paletteIndex ]);
@ -33,9 +36,21 @@ export const AvatarEditorPaletteSetView: FC<AvatarEditorPaletteSetViewProps> = p
}, [ model, category ]);
return (
<AutoGrid innerRef={ elementRef } gap={ 1 } columnCount={ 5 } columnMinWidth={ 30 }>
{ (paletteSet.length > 0) && paletteSet.map((item, index) =>
<AvatarEditorPaletteSetItem key={ index } colorItem={ item } onClick={ event => selectColor(item) } />) }
<AutoGrid
className="py-1 avatar-editor-palette-set-view"
innerRef={ elementRef }
gap={ 1 }
columnCount={ 8 }
columnMinWidth={ 14 }
>
{ paletteSet.length > 0 &&
paletteSet.map((item, index) => (
<AvatarEditorPaletteSetItem
key={ index }
colorItem={ item }
onClick={ (event) => selectColor(item) }
/>
)) }
</AutoGrid>
);
}