Avatar editor works

This commit is contained in:
Bill 2021-07-19 13:26:54 -04:00
parent 1c2800bfe3
commit df167ba1d8
56 changed files with 1255 additions and 550 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 B

After

Width:  |  Height:  |  Size: 259 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 242 B

After

Width:  |  Height:  |  Size: 335 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 281 B

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 265 B

After

Width:  |  Height:  |  Size: 338 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 B

After

Width:  |  Height:  |  Size: 228 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 B

After

Width:  |  Height:  |  Size: 260 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 234 B

After

Width:  |  Height:  |  Size: 252 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 199 B

After

Width:  |  Height:  |  Size: 280 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 155 B

After

Width:  |  Height:  |  Size: 251 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 227 B

After

Width:  |  Height:  |  Size: 298 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 B

After

Width:  |  Height:  |  Size: 234 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 B

After

Width:  |  Height:  |  Size: 286 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 B

After

Width:  |  Height:  |  Size: 241 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 220 B

After

Width:  |  Height:  |  Size: 285 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 B

After

Width:  |  Height:  |  Size: 267 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 248 B

After

Width:  |  Height:  |  Size: 338 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 B

After

Width:  |  Height:  |  Size: 257 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 238 B

After

Width:  |  Height:  |  Size: 348 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 B

After

Width:  |  Height:  |  Size: 222 B

View File

@ -78,6 +78,8 @@ $mirage: #131e25 !default;
$aztec: #0d171d !default;
$cello-light: #21516e !default;
$cello-dark: #1e465e !default;
$pale-sky: #677181 !default;
$oslo-gray: #8F9297 !default;
$success: $green !default;
$info: $cyan !default;

View File

@ -2,15 +2,12 @@
line-height: 0 !important;
}
i {
&.icon {
.icon {
display: inline-block;
outline: 0;
background-color: transparent;
background-repeat: no-repeat;
background-position: center;
background-size: contain;
&.icon-nitro-light {
background-image: url('../images/nitro/nitro-n-light.svg');
@ -190,6 +187,7 @@ i {
background-image: url('../images/avatareditor/ca-icon.png');
width: 30px;
height: 24px;
background
&.selected {
background-image: url('../images/avatareditor/ca-selected-icon.png');
@ -297,9 +295,9 @@ i {
}
&.loading-icon {
background-image: url('../images/avatareditor/loading-icon.png');
width: 21px;
height: 25px;
background-image: url('../images/icons/loading-icon.png');
width: 17px;
height: 21px;
}
&.male-icon {
@ -500,4 +498,3 @@ i {
}
}
}
}

View File

@ -11,6 +11,17 @@
}
}
.row-cols-4 {
.col {
padding-right: 0.25rem;
&:nth-child(4n+4) {
padding-right: 0;
}
}
}
.row-cols-5 {
.col {

View File

@ -1,15 +1,18 @@
import { FC } from 'react';
import { NitroCardGridViewProps } from './NitroCardGridView.types';
import { NitroCardGridContextProvider } from './context/NitroCardGridContext';
import { NitroCardGridThemes, NitroCardGridViewProps } from './NitroCardGridView.types';
export const NitroCardGridView: FC<NitroCardGridViewProps> = props =>
{
const { columns = 5, children = null } = props;
const { columns = 5, theme = NitroCardGridThemes.THEME_DEFAULT, children = null } = props;
return (
<div className="h-100 overflow-hidden nitro-card-grid">
<NitroCardGridContextProvider value={ { theme } }>
<div className={ `h-100 overflow-hidden nitro-card-grid ${ theme }` }>
<div className={ `row row-cols-${ columns } align-content-start g-0 w-100 h-100 overflow-auto` }>
{ children }
</div>
</div>
</NitroCardGridContextProvider>
);
}

View File

@ -1,4 +1,11 @@
export interface NitroCardGridViewProps
{
columns?: number;
theme?: string;
}
export class NitroCardGridThemes
{
public static THEME_DEFAULT: string = 'theme-default';
public static THEME_SHADOWED: string = 'theme-shadowed';
}

View File

@ -0,0 +1,13 @@
import { createContext, FC, useContext } from 'react';
import { INitroCardGridContext, NitroCardGridContextProps } from './NitroCardGridContext.types';
const NitroCardGridContext = createContext<INitroCardGridContext>({
theme: null
});
export const NitroCardGridContextProvider: FC<NitroCardGridContextProps> = props =>
{
return <NitroCardGridContext.Provider value={ props.value }>{ props.children }</NitroCardGridContext.Provider>
}
export const useNitroCardGridContext = () => useContext(NitroCardGridContext);

View File

@ -0,0 +1,11 @@
import { ProviderProps } from 'react';
export interface INitroCardGridContext
{
theme: string;
}
export interface NitroCardGridContextProps extends ProviderProps<INitroCardGridContext>
{
}

View File

@ -0,0 +1,2 @@
export * from './NitroCardGridContext';
export * from './NitroCardGridContext.types';

View File

@ -1,16 +1,50 @@
.grid-item-container {
height: 48px;
max-height: 48px;
height: 50px;
max-height: 50px;
.grid-item {
position: relative;
width: 100%;
height: 100%;
border-color: $grid-border-color !important;
background-color: $grid-bg-color;
background-position: center;
background-repeat: no-repeat;
overflow: hidden;
&.theme-default {
border-radius: $border-radius;
border-color: $grid-border-color !important;
background-color: $grid-bg-color;
border: nth(map-values($border-widths), 2) solid;
}
&.theme-shadowed {
border-radius: $border-radius;
background-color: $light;
&::after {
position: absolute;
content: '';
top: 0;
bottom: 0;
left: 0;
right: 0;
border-radius: $border-radius;
border-bottom: 2px solid white;
border-right: 2px solid white;
box-shadow: -2px -2px rgba(0, 0, 0, .4), inset 3px 3px rgba(0, 0, 0, .2);
}
&.active {
border: nth(map-values($border-widths), 2) solid;
border-color: $oslo-gray !important;
background-color: #F5F5F5;
&:after {
content: unset;
}
}
}
&.active {
border-color: $grid-active-border-color !important;
background-color: $grid-active-bg-color !important;

View File

@ -1,16 +1,19 @@
import { FC } from 'react';
import { LimitedEditionStyledNumberView } from '../../../../views/shared/limited-edition/styled-number/LimitedEditionStyledNumberView';
import { useNitroCardGridContext } from '../context';
import { NitroCardGridThemes } from '../NitroCardGridView.types';
import { NitroCardGridItemViewProps } from './NitroCardGridItemView.types';
export const NitroCardGridItemView: FC<NitroCardGridItemViewProps> = props =>
{
const { itemImage = null, itemActive = false, itemCount = 1, itemUnique = false, itemUniqueNumber = 0, itemUnseen = false, className = '', style = {}, children = null, ...rest } = props;
const { itemImage = undefined, itemColor = undefined, itemActive = false, itemCount = 1, itemUnique = false, itemUniqueNumber = 0, itemUnseen = false, className = '', style = {}, children = null, ...rest } = props;
const { theme = NitroCardGridThemes.THEME_DEFAULT } = useNitroCardGridContext();
const imageUrl = `url(${ itemImage })`;
return (
<div className="col pb-1 grid-item-container">
<div className={ `position-relative border border-2 rounded grid-item cursor-pointer${ itemActive ? ' active' : '' }${ itemUnique ? ' unique-item' : '' }${ itemUnseen ? ' unseen' : ''} ${ className || '' }` } style={ itemImage ? { ...style, backgroundImage: imageUrl } : style } { ...rest }>
<div className={ `grid-item ${ theme } cursor-pointer${ itemActive ? ' active' : '' }${ itemUnique ? ' unique-item' : '' }${ itemUnseen ? ' unseen' : ''}${ (itemImage === null ? ' icon loading-icon': '')} ${ className || '' }` } style={ itemImage ? { ...style, backgroundImage: imageUrl } : (itemColor ? { ...style, backgroundColor: itemColor } : style) } { ...rest }>
{ (itemCount > 1) &&
<span className="position-absolute badge border bg-danger px-1 rounded-circle">{ itemCount }</span> }
{ itemUnique &&

View File

@ -3,6 +3,7 @@ import { DetailsHTMLAttributes } from 'react';
export interface NitroCardGridItemViewProps extends DetailsHTMLAttributes<HTMLDivElement>
{
itemImage?: string;
itemColor?: string;
itemActive?: boolean;
itemCount?: number;
itemUnique?: boolean;

View File

@ -1,8 +1,51 @@
.nitro-avatar-editor {
width: 550px;
width: 600px;
.content-area {
height: 300px;
max-height: 300px;
height: 330px;
max-height: 330px;
}
.figure-preview-container {
position: relative;
height: 100%;
background-color: $pale-sky;
overflow: hidden;
z-index: 1;
.avatar-image {
margin: 45px auto 0;
z-index: 2;
}
.arrow-container {
position: absolute;
width: 100%;
margin: 0 auto;
display: flex;
justify-content: center;
bottom: 12px;
z-index: 3;
.icon {
cursor: pointer;
}
.arrow-left-icon {
margin-right: 10px;
}
}
&:after {
position: absolute;
content: '';
top: 75%;
bottom: 0;
left: 0;
right: 0;
border-radius: 50%;
background-color: red;
transform: scale(2);
}
}
}

View File

@ -1,5 +1,6 @@
import { AvatarEditorFigureCategory } from 'nitro-renderer';
import { FC, useCallback, useEffect, useReducer, useState } from 'react';
import { GetSessionDataManager } from '../../api';
import { AvatarEditorEvent } from '../../events/avatar-editor';
import { useUiEvent } from '../../hooks/events/ui/ui-event';
import { NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../layout';
@ -7,6 +8,7 @@ import { LocalizeText } from '../../utils/LocalizeText';
import { AvatarEditorViewProps } from './AvatarEditorView.types';
import { AvatarEditor } from './common/AvatarEditor';
import { BodyModel } from './common/BodyModel';
import { FigureData } from './common/FigureData';
import { HeadModel } from './common/HeadModel';
import { IAvatarEditorCategoryModel } from './common/IAvatarEditorCategoryModel';
import { LegModel } from './common/LegModel';
@ -24,6 +26,79 @@ export const AvatarEditorView: FC<AvatarEditorViewProps> = props =>
const [ activeCategory, setActiveCategory ] = useState<IAvatarEditorCategoryModel>(null);
const [ isInitalized, setIsInitalized ] = useState(false);
const selectCategory = useCallback((name: string) =>
{
setActiveCategory(categories.get(name));
}, [ categories ]);
const resetCategories = useCallback((editor: AvatarEditor) =>
{
const categories = new Map();
categories.set(AvatarEditorFigureCategory.GENERIC, new BodyModel(editor));
categories.set(AvatarEditorFigureCategory.HEAD, new HeadModel(editor));
categories.set(AvatarEditorFigureCategory.TORSO, new TorsoModel(editor));
categories.set(AvatarEditorFigureCategory.LEGS, new LegModel(editor));
setCategories(categories);
setActiveCategory(categories.get(AvatarEditorFigureCategory.GENERIC));
}, []);
const selectGender = useCallback((gender: string) =>
{
if(gender === avatarEditor.gender) return;
avatarEditor.gender = gender;
resetCategories(avatarEditor);
}, [ avatarEditor, resetCategories ]);
const loadAvatarInEditor = useCallback((figure: string, gender: string, reset: boolean = true) =>
{
if(!avatarEditor) return;
switch(gender)
{
case FigureData.MALE:
case 'm':
case 'M':
gender = FigureData.MALE;
break;
case FigureData.FEMALE:
case 'f':
case 'F':
gender = FigureData.FEMALE;
break;
default:
gender = FigureData.MALE;
}
let update = false;
if(gender !== avatarEditor.gender)
{
avatarEditor.gender = gender;
update = true;
}
const figureData = avatarEditor.figureData;
if(!figureData) return;
if(figure !== figureData.getFigureString())
{
update = true;
}
figureData.loadAvatarData(figure, gender);
if(update)
{
resetCategories(avatarEditor);
}
}, [ avatarEditor, resetCategories ]);
const onAvatarEditorEvent = useCallback((event: AvatarEditorEvent) =>
{
switch(event.type)
@ -35,7 +110,7 @@ export const AvatarEditorView: FC<AvatarEditorViewProps> = props =>
setIsVisible(false);
return;
case AvatarEditorEvent.TOGGLE_EDITOR:
setIsVisible(value => !value);
setIsVisible(prevValue => !prevValue);
return;
}
}, []);
@ -44,11 +119,6 @@ export const AvatarEditorView: FC<AvatarEditorViewProps> = props =>
useUiEvent(AvatarEditorEvent.HIDE_EDITOR, onAvatarEditorEvent);
useUiEvent(AvatarEditorEvent.TOGGLE_EDITOR, onAvatarEditorEvent);
const selectCategory = useCallback((name: string) =>
{
setActiveCategory(categories.get(name));
}, [ categories ]);
useEffect(() =>
{
if(!isVisible || isInitalized) return;
@ -56,26 +126,23 @@ export const AvatarEditorView: FC<AvatarEditorViewProps> = props =>
const newEditor = new AvatarEditor();
setAvatarEditor(newEditor);
const categories = new Map();
categories.set(AvatarEditorFigureCategory.GENERIC, new BodyModel(newEditor));
categories.set(AvatarEditorFigureCategory.HEAD, new HeadModel(newEditor));
categories.set(AvatarEditorFigureCategory.TORSO, new TorsoModel(newEditor));
categories.set(AvatarEditorFigureCategory.LEGS, new LegModel(newEditor));
setCategories(categories);
setActiveCategory(categories.get(AvatarEditorFigureCategory.GENERIC));
setIsInitalized(true);
}, [ isVisible, isInitalized ]);
useEffect(() =>
{
if(!isVisible || !avatarEditor) return;
loadAvatarInEditor(GetSessionDataManager().figure, GetSessionDataManager().gender);
}, [ isVisible, avatarEditor, loadAvatarInEditor ]);
return (
<AvatarEditorContextProvider value={ { avatarEditorState, dispatchAvatarEditorState } }>
{ isVisible &&
<NitroCardView className="nitro-avatar-editor">
<NitroCardHeaderView headerText={ LocalizeText('avatareditor.title') } onCloseClick={ event => setIsVisible(false) } />
<NitroCardTabsView>
{ categories && Array.from(categories.keys()).map(category =>
{ categories && (categories.size > 0) && Array.from(categories.keys()).map(category =>
{
return (
<NitroCardTabsItemView key={ category } isActive={ (activeCategory.name === category) } onClick={ event => selectCategory(category) }>
@ -85,7 +152,7 @@ export const AvatarEditorView: FC<AvatarEditorViewProps> = props =>
})}
</NitroCardTabsView>
<NitroCardContentView>
{ activeCategory && <AvatarEditorModelView model={ activeCategory } editor={ avatarEditor } /> }
{ activeCategory && <AvatarEditorModelView model={ activeCategory } editor={ avatarEditor } selectGender={ selectGender } /> }
</NitroCardContentView>
</NitroCardView> }
</AvatarEditorContextProvider>

View File

@ -1,9 +1,10 @@
import { FigureData, IPalette, IPartColor, ISetType, IStructureData } from 'nitro-renderer';
import { IPalette, IPartColor, ISetType, IStructureData } from 'nitro-renderer';
import { GetAvatarRenderManager, GetConfiguration, GetSessionDataManager } from '../../../api';
import { AvatarEditorGridColorItem } from './AvatarEditorGridColorItem';
import { AvatarEditorGridPartItem } from './AvatarEditorGridPartItem';
import { CategoryBaseModel } from './CategoryBaseModel';
import { CategoryData } from './CategoryData';
import { FigureData } from './FigureData';
const MAX_PALETTES: number = 2;
const DEFAULT_MALE_FIGURE: string = 'hr-100.hd-180-7.ch-215-66.lg-270-79.sh-305-62.ha-1002-70.wa-2007';
@ -11,16 +12,13 @@ const DEFAULT_FEMALE_FIGURE: string = 'hr-515-33.hd-600-1.ch-635-70.lg-716-66-62
export class AvatarEditor
{
private _figureStructureData: IStructureData;
private _figures: Map<string, FigureData>;
private _gender: string;
private _figureStructureData: IStructureData = GetAvatarRenderManager().structureData;
private _figures: Map<string, FigureData> = new Map();
private _gender: string = FigureData.MALE;
private _notifier: () => void = null;
constructor()
{
this._figureStructureData = GetAvatarRenderManager().structureData;
this._figures = new Map();
this._gender = FigureData.MALE;
const maleFigure = new FigureData();
const femaleFigure = new FigureData();
@ -160,6 +158,7 @@ export class AvatarEditor
if(partSet.isSellable)
{
isValid = false;
//isValid = (this._inventoryService && this._inventoryService.hasFigureSetId(partSet.id));
}
@ -284,6 +283,24 @@ export class AvatarEditor
public set gender(gender: string)
{
if(this._gender === gender) return;
this._gender = gender;
if(this.figureData) this.figureData.notify = this.notify;
if(this.notify) this.notify();
}
public get notify(): () => void
{
return this._notifier;
}
public set notify(notifier: () => void)
{
if(this.figureData) this.figureData.notify = notifier;
this._notifier = notifier;
}
}

View File

@ -6,6 +6,7 @@ export class AvatarEditorGridColorItem
private _isDisabled: boolean;
private _isHC: boolean;
private _isSelected: boolean;
private _notifier: () => void;
constructor(partColor: IPartColor, isDisabled: boolean = false)
{
@ -48,5 +49,17 @@ export class AvatarEditorGridColorItem
public set isSelected(flag: boolean)
{
this._isSelected = flag;
if(this.notify) this.notify();
}
public get notify(): () => void
{
return this._notifier;
}
public set notify(notifier: () => void)
{
this._notifier = notifier;
}
}

View File

@ -1,5 +1,6 @@
import { AvatarFigurePartType, FigureData, IAvatarImageListener, IAvatarRenderManager, IFigurePart, IFigurePartSet, IGraphicAsset, IPartColor, NitroContainer, NitroSprite, TextureUtils } from 'nitro-renderer';
import { AvatarFigurePartType, IAvatarImageListener, IAvatarRenderManager, IFigurePart, IFigurePartSet, IGraphicAsset, IPartColor, NitroContainer, NitroSprite, TextureUtils } from 'nitro-renderer';
import { GetAvatarRenderManager } from '../../../api';
import { FigureData } from './FigureData';
export class AvatarEditorGridPartItem implements IAvatarImageListener
{
@ -47,6 +48,7 @@ export class AvatarEditorGridPartItem implements IAvatarImageListener
private _isSelected: boolean;
private _disposed: boolean;
private _isInitalized: boolean;
private _notifier: () => void;
constructor(partSet: IFigurePartSet, partColors: IPartColor[], useColors: boolean = true, isDisabled: boolean = false)
{
@ -210,6 +212,8 @@ export class AvatarEditorGridPartItem implements IAvatarImageListener
if(this._isDisabled) this.setAlpha(container, 0.2);
this._imageUrl = TextureUtils.generateImageUrl(container);
if(this.notify) this.notify();
}
private setAlpha(container: NitroContainer, alpha: number): NitroContainer
@ -259,21 +263,21 @@ export class AvatarEditorGridPartItem implements IAvatarImageListener
return this._partSet;
}
public set colors(partColors: IPartColor[])
public set partColors(partColors: IPartColor[])
{
this._partColors = partColors;
this.update();
}
public get isDisabledForWearing(): boolean
public get isDisabled(): boolean
{
return this._isDisabled;
}
public set iconImage(k: NitroContainer)
public set thumbContainer(container: NitroContainer)
{
this._thumbContainer = k;
this._thumbContainer = container;
this.update();
}
@ -283,7 +287,7 @@ export class AvatarEditorGridPartItem implements IAvatarImageListener
return this._imageUrl;
}
public get colorLayerCount(): number
public get maxColorIndex(): number
{
return this._maxColorIndex;
}
@ -316,5 +320,17 @@ export class AvatarEditorGridPartItem implements IAvatarImageListener
public set isSelected(flag: boolean)
{
this._isSelected = flag;
if(this.notify) this.notify();
}
public get notify(): () => void
{
return this._notifier;
}
public set notify(notifier: () => void)
{
this._notifier = notifier;
}
}

View File

@ -1,6 +1,7 @@
import { AvatarEditorFigureCategory, AvatarScaleType, AvatarSetType, FigureData, IAvatarImageListener } from 'nitro-renderer';
import { AvatarEditorFigureCategory, AvatarScaleType, AvatarSetType, IAvatarImageListener } from 'nitro-renderer';
import { GetAvatarRenderManager } from '../../../api';
import { CategoryBaseModel } from './CategoryBaseModel';
import { FigureData } from './FigureData';
export class BodyModel extends CategoryBaseModel implements IAvatarImageListener
{
@ -50,7 +51,7 @@ export class BodyModel extends CategoryBaseModel implements IAvatarImageListener
{
sprite.y = 10;
part.iconImage = sprite;
part.thumbContainer = sprite;
setTimeout(() => avatarImage.dispose(), 0);
}

View File

@ -174,7 +174,7 @@ export class CategoryBaseModel implements IAvatarEditorCategoryModel
if(!partItem) return;
if(partItem.isDisabledForWearing)
if(partItem.isDisabled)
{
categoryData.selectPartIndex(selectedPartIndex);
@ -183,7 +183,7 @@ export class CategoryBaseModel implements IAvatarEditorCategoryModel
return;
}
this._maxPaletteCount = partItem.colorLayerCount;
this._maxPaletteCount = partItem.maxColorIndex;
this._editor.figureData.savePartData(category, partItem.id, categoryData.getSelectedColorIds(), true);
}
@ -233,7 +233,7 @@ export class CategoryBaseModel implements IAvatarEditorCategoryModel
public get maxPaletteCount(): number
{
return this._maxPaletteCount;
return (this._maxPaletteCount || 1);
}
public set maxPaletteCount(count: number)

View File

@ -266,7 +266,7 @@ export class CategoryData
if(!partItem) return null;
return colorIds.slice(0, Math.max(partItem.colorLayerCount, 1));
return colorIds.slice(0, Math.max(partItem.maxColorIndex, 1));
}
private getSelectedColors(): IPartColor[]
@ -333,7 +333,7 @@ export class CategoryData
for(const partItem of this._parts)
{
if(partItem) partItem.colors = partColors;
if(partItem) partItem.partColors = partColors;
}
}

View File

@ -0,0 +1,271 @@
export class FigureData
{
public static MALE: string = 'M';
public static FEMALE: string = 'F';
public static UNISEX: string = 'U';
public static SCALE: string = 'h';
public static STD: string = 'std';
public static DEFAULT_FRAME: string = '0';
public static FACE: string = 'hd';
public static HAIR: string = 'hr';
public static HAT: string = 'ha';
public static HEAD_ACCESSORIES: string = 'he';
public static EYE_ACCESSORIES: string = 'ea';
public static FACE_ACCESSORIES: string = 'fa';
public static JACKET: string = 'cc';
public static SHIRT: string = 'ch';
public static CHEST_ACCESSORIES: string = 'ca';
public static CHEST_PRINTS: string = 'cp';
public static TROUSERS: string = 'lg';
public static SHOES: string = 'sh';
public static TROUSER_ACCESSORIES: string = 'wa';
public static PREVIEW_AVATAR_DIRECTION: number = 4;
private _data: Map<string, number>;
private _colors: Map<string, number[]>;
private _gender: string = 'M';
private _avatarEffectType: number = -1;
private _notifier: () => void = null;
public loadAvatarData(figureString: string, gender: string): void
{
this._data = new Map();
this._colors = new Map();
this._gender = gender;
this.parseFigureString(figureString);
this.updateView();
}
private parseFigureString(figure: string): void
{
if(!figure) return;
const sets = figure.split('.');
if(!sets || !sets.length) return;
for(const set of sets)
{
const parts = set.split('-');
if(!parts.length) continue;
const setType = parts[0];
const setId = parseInt(parts[1]);
const colorIds: number[] = [];
let offset = 2;
while(offset < parts.length)
{
colorIds.push(parseInt(parts[offset]));
offset++;
}
if(!colorIds.length) colorIds.push(0);
this.savePartSetId(setType, setId, false);
this.savePartSetColourId(setType, colorIds, false);
}
}
public getPartSetId(partSetId: string): number
{
const existing = this._data.get(partSetId);
if(existing !== undefined) return existing;
return -1;
}
public getColourIds(colorId: string): number[]
{
const existing = this._colors.get(colorId);
if(existing !== undefined) return existing;
return [];
// return [this._avatarEditor._Str_24919(k)];
}
public getFigureString(): string
{
let figureString = '';
const setParts: string[] = [];
for(const [ setType, setId ] of this._data.entries())
{
const colorIds = this._colors.get(setType);
let setPart = ((setType + '-') + setId);
if(colorIds && colorIds.length)
{
let i = 0;
while(i < colorIds.length)
{
setPart = (setPart + ('-' + colorIds[i]));
i++;
}
}
setParts.push(setPart);
}
let i = 0;
while(i < setParts.length)
{
figureString = (figureString + setParts[i]);
if(i < (setParts.length - 1)) figureString = (figureString + '.');
i++;
}
return figureString;
}
public savePartData(k: string, _arg_2: number, _arg_3: number[], _arg_4: boolean = false): void
{
this.savePartSetId(k, _arg_2, _arg_4);
this.savePartSetColourId(k, _arg_3, _arg_4);
}
private savePartSetId(k: string, _arg_2: number, _arg_3: boolean = true): void
{
switch(k)
{
case FigureData.FACE:
case FigureData.HAIR:
case FigureData.HAT:
case FigureData.HEAD_ACCESSORIES:
case FigureData.EYE_ACCESSORIES:
case FigureData.FACE_ACCESSORIES:
case FigureData.SHIRT:
case FigureData.JACKET:
case FigureData.CHEST_ACCESSORIES:
case FigureData.CHEST_PRINTS:
case FigureData.TROUSERS:
case FigureData.SHOES:
case FigureData.TROUSER_ACCESSORIES:
if(_arg_2 >= 0)
{
this._data.set(k, _arg_2);
}
else
{
this._data.delete(k);
}
break;
}
if(_arg_3) this.updateView();
}
public savePartSetColourId(k: string, _arg_2: number[], _arg_3: boolean = true): void
{
switch(k)
{
case FigureData.FACE:
case FigureData.HAIR:
case FigureData.HAT:
case FigureData.HEAD_ACCESSORIES:
case FigureData.EYE_ACCESSORIES:
case FigureData.FACE_ACCESSORIES:
case FigureData.SHIRT:
case FigureData.JACKET:
case FigureData.CHEST_ACCESSORIES:
case FigureData.CHEST_PRINTS:
case FigureData.TROUSERS:
case FigureData.SHOES:
case FigureData.TROUSER_ACCESSORIES:
this._colors.set(k, _arg_2);
break;
}
if(_arg_3) this.updateView();
}
public getFigureStringWithFace(k: number, override = true): string
{
let figureString = '';
const setTypes: string[] = [ FigureData.FACE ];
const figureSets: string[] = [];
for(const setType of setTypes)
{
const colors = this._colors.get(setType);
if(colors === undefined) continue;
let setId = this._data.get(setType);
if((setType === FigureData.FACE) && override) setId = k;
let figureSet = ((setType + '-') + setId);
if(setId >= 0)
{
let i = 0;
while(i < colors.length)
{
figureSet = (figureSet + ('-' + colors[i]));
i++;
}
}
figureSets.push(figureSet);
}
let i = 0;
while(i < figureSets.length)
{
figureString = (figureString + figureSets[i]);
if(i < (figureSets.length - 1)) figureString = (figureString + '.');
i++;
}
return figureString;
}
public updateView(): void
{
if(this.notify) this.notify();
}
public get gender(): string
{
return this._gender;
}
public set avatarEffectType(k: number)
{
this._avatarEffectType = k;
}
public get avatarEffectType(): number
{
return this._avatarEffectType;
}
public get notify(): () => void
{
return this._notifier;
}
public set notify(notifier: () => void)
{
this._notifier = notifier;
}
}

View File

@ -1,5 +1,6 @@
import { AvatarEditorFigureCategory, FigureData } from 'nitro-renderer';
import { AvatarEditorFigureCategory } from 'nitro-renderer';
import { CategoryBaseModel } from './CategoryBaseModel';
import { FigureData } from './FigureData';
export class HeadModel extends CategoryBaseModel
{

View File

@ -1,5 +1,6 @@
import { AvatarEditorFigureCategory, FigureData } from 'nitro-renderer';
import { AvatarEditorFigureCategory } from 'nitro-renderer';
import { CategoryBaseModel } from './CategoryBaseModel';
import { FigureData } from './FigureData';
export class LegModel extends CategoryBaseModel
{

View File

@ -1,5 +1,6 @@
import { AvatarEditorFigureCategory, FigureData } from 'nitro-renderer';
import { AvatarEditorFigureCategory } from 'nitro-renderer';
import { CategoryBaseModel } from './CategoryBaseModel';
import { FigureData } from './FigureData';
export class TorsoModel extends CategoryBaseModel
{

View File

@ -0,0 +1,28 @@
import { FC, useCallback, useEffect, useState } from 'react';
import { AvatarImageView } from '../../../shared/avatar-image/AvatarImageView';
import { AvatarEditorFigurePreviewViewProps } from './AvatarEditorFigurePreviewView.types';
export const AvatarEditorFigurePreviewView: FC<AvatarEditorFigurePreviewViewProps> = props =>
{
const { editor = null } = props;
const [ updateId, setUpdateId ] = useState(-1);
const rerender = useCallback(() =>
{
setUpdateId(prevValue => (prevValue + 1));
}, []);
useEffect(() =>
{
if(!editor) return;
editor.notify = rerender;
return () =>
{
editor.notify = null;
}
}, [ editor, rerender ] );
return <AvatarImageView figure={ editor.figureData.getFigureString() } direction={ 4 } scale={ 2 } />
}

View File

@ -0,0 +1,6 @@
import { AvatarEditor } from '../../common/AvatarEditor';
export interface AvatarEditorFigurePreviewViewProps
{
editor: AvatarEditor;
}

View File

@ -1,16 +1,31 @@
import { FC, useEffect, useState } from 'react';
import { FC, useCallback, useEffect, useState } from 'react';
import { NitroCardGridItemView } from '../../../../layout/card/grid/item/NitroCardGridItemView';
import { CurrencyIcon } from '../../../shared/currency-icon/CurrencyIcon';
import { AvatarEditorFigureSetItemViewProps } from './AvatarEditorFigureSetItemView.types';
export const AvatarEditorFigureSetItemView: FC<AvatarEditorFigureSetItemViewProps> = props =>
{
const { partItem = null, onClick = null } = props;
const [ imageUrl, setImageUrl ] = useState<string>(null);
const [ updateId, setUpdateId ] = useState(-1);
const rerender = useCallback(() =>
{
setUpdateId(prevValue => (prevValue + 1));
}, []);
useEffect(() =>
{
setImageUrl(partItem.imageUrl);
}, [ partItem.imageUrl ]);
partItem.notify = rerender;
return <NitroCardGridItemView itemImage={ imageUrl } onClick={ () => onClick(partItem) } />
return () =>
{
partItem.notify = null;
}
})
return (
<NitroCardGridItemView itemImage={ partItem.imageUrl } itemActive={ partItem.isSelected } onClick={ () => onClick(partItem) }>
{ partItem.isHC && <CurrencyIcon type={ 'hc' } /> }
</NitroCardGridItemView>
);
}

View File

@ -1,28 +1,33 @@
import { FC, useCallback } from 'react';
import { NitroCardGridView } from '../../../../layout/card/grid/NitroCardGridView';
import { NitroCardGridThemes } from '../../../../layout/card/grid/NitroCardGridView.types';
import { AvatarEditorGridPartItem } from '../../common/AvatarEditorGridPartItem';
import { AvatarEditorFigureSetItemView } from '../figure-set-item/AvatarEditorFigureSetItemView';
import { AvatarEditorFigureSetViewProps } from './AvatarEditorFigureSetView.types';
export const AvatarEditorFigureSetView: FC<AvatarEditorFigureSetViewProps> = props =>
{
const { model = null, category = null } = props;
const { model = null, category = null, setMaxPaletteCount = null } = props;
const selectPart = useCallback((part: AvatarEditorGridPartItem) =>
const selectPart = useCallback((item: AvatarEditorGridPartItem) =>
{
const index = category.parts.indexOf(part);
const index = category.parts.indexOf(item);
if(index === -1) return;
model.selectPart(category.name, index);
}, [ model, category ]);
const partItem = category.getCurrentPart();
setMaxPaletteCount(partItem.maxColorIndex || 1);
}, [ model, category, setMaxPaletteCount ]);
return (
<NitroCardGridView columns={ 3 }>
<NitroCardGridView columns={ 3 } theme={ NitroCardGridThemes.THEME_SHADOWED }>
{ (category.parts.length > 0) && category.parts.map((item, index) =>
{
return <AvatarEditorFigureSetItemView key={ index } partItem={ item } onClick={ selectPart } />;
}) }
</NitroCardGridView>
)
);
}

View File

@ -1,3 +1,4 @@
import { Dispatch, SetStateAction } from 'react';
import { CategoryData } from '../../common/CategoryData';
import { IAvatarEditorCategoryModel } from '../../common/IAvatarEditorCategoryModel';
@ -5,4 +6,5 @@ export interface AvatarEditorFigureSetViewProps
{
model: IAvatarEditorCategoryModel;
category: CategoryData;
setMaxPaletteCount: Dispatch<SetStateAction<number>>;
}

View File

@ -1,17 +1,19 @@
import { UserFigureComposer } from 'nitro-renderer';
import { FC, useCallback, useEffect, useState } from 'react';
import { SendMessageHook } from '../../../../hooks';
import { LocalizeText } from '../../../../utils/LocalizeText';
import { CategoryData } from '../../common/CategoryData';
import { FigureData } from '../../common/FigureData';
import { AvatarEditorFigurePreviewView } from '../figure-preview/AvatarEditorFigurePreviewView';
import { AvatarEditorFigureSetView } from '../figure-set/AvatarEditorFigureSetView';
import { AvatarEditorPaletteSetView } from '../palette-set/AvatarEditorPaletteSetView';
import { AvatarEditorModelViewProps } from './AvatarEditorModelView.types';
export const AvatarEditorModelView: FC<AvatarEditorModelViewProps> = props =>
{
const { model = null, editor = null } = props;
const { model = null, editor = null, selectGender = null } = props;
const [ activeCategory, setActiveCategory ] = useState<CategoryData>(null);
const selectGender = useCallback((gender: string) =>
{
editor.gender = gender;
}, [ editor ]);
const [ maxPaletteCount, setMaxPaletteCount ] = useState(1);
const selectCategory = useCallback((name: string) =>
{
@ -27,12 +29,19 @@ export const AvatarEditorModelView: FC<AvatarEditorModelViewProps> = props =>
{
if(!part || !part.isSelected) continue;
model.maxPaletteCount = part.colorLayerCount;
setMaxPaletteCount(part.maxColorIndex || 1);
break;
}
}, [ model ]);
const saveFigure = useCallback(() =>
{
const figureData = editor.figureData;
SendMessageHook(new UserFigureComposer(figureData.gender, figureData.getFigureString()));
}, [ editor ]);
useEffect(() =>
{
model.init();
@ -45,16 +54,54 @@ export const AvatarEditorModelView: FC<AvatarEditorModelViewProps> = props =>
}
}, [ model, selectCategory ]);
if(!activeCategory) return null;
if(!model || !activeCategory) return null;
return (
<div className="row h-100">
<div className="col-2 d-flex flex-column h-100"></div>
<div className="col-3 d-flex flex-column h-100">
<AvatarEditorFigureSetView model={ model } category={ activeCategory } />
<div className="col-1 d-flex flex-column align-items-center h-100 pe-0">
{ model.canSetGender &&
<>
<i className={ `icon male-icon ${ (editor.gender === FigureData.MALE) ? ' selected' : ''}` } onClick={ event => selectGender(FigureData.MALE) } />
<i className={ `icon female-icon ${ (editor.gender === FigureData.FEMALE) ? ' selected' : ''}` } onClick={ event => selectGender(FigureData.FEMALE) } />
</> }
{ !model.canSetGender && model.categories && (model.categories.size > 0) && Array.from(model.categories.keys()).map(name =>
{
const category = model.categories.get(name);
return (
<i className={ `icon ${ category.name }-icon mb-2 ${ (activeCategory === category) ? ' selected' : ''}` } onClick={ event => selectCategory(name) } />
);
})}
</div>
<div className="col-4 d-flex flex-column h-100">
<AvatarEditorFigureSetView model={ model } category={ activeCategory } setMaxPaletteCount={ setMaxPaletteCount } />
</div>
<div className="col-3 d-flex flex-column h-100">
<div className="figure-preview-container mb-2">
<AvatarEditorFigurePreviewView editor={ editor } />
<div className="arrow-container">
<i className="icon arrow-left-icon" />
<i className="icon arrow-right-icon" />
</div>
</div>
<div className="d-flex flex-column">
<div className="btn-group mb-1">
<button type="button" className="btn btn-sm btn-secondary">
<i className="fas fa-undo" />
</button>
<button type="button" className="btn btn-sm btn-secondary">
<i className="fas fa-trash" />
</button>
</div>
<button type="button" className="btn btn-success btn-sm w-100" onClick={ saveFigure }>{ LocalizeText('avatareditor.save') }</button>
</div>
</div>
<div className="col-4 d-flex flex-column h-100">
{ (maxPaletteCount >= 1) &&
<AvatarEditorPaletteSetView model={ model } category={ activeCategory } paletteSet={ activeCategory.getPalette(0) } paletteIndex={ 0 } /> }
{ (maxPaletteCount === 2) &&
<AvatarEditorPaletteSetView model={ model } category={ activeCategory } paletteSet={ activeCategory.getPalette(1) } paletteIndex={ 1 } /> }
</div>
<div className="col-3 d-flex flex-column h-100"></div>
<div className="col-4 d-flex flex-column h-100"></div>
</div>
);
}

View File

@ -5,4 +5,5 @@ export interface AvatarEditorModelViewProps
{
model: IAvatarEditorCategoryModel;
editor: AvatarEditor;
selectGender: (gender: string) => void;
}

View File

@ -0,0 +1,26 @@
import { FC, useCallback, useEffect, useState } from 'react';
import { NitroCardGridItemView } from '../../../../layout/card/grid/item/NitroCardGridItemView';
import { AvatarEditorPaletteSetItemProps } from './AvatarEditorPaletteSetItem.types';
export const AvatarEditorPaletteSetItem: FC<AvatarEditorPaletteSetItemProps> = props =>
{
const { colorItem = null, onClick = null } = props;
const [ updateId, setUpdateId ] = useState(-1);
const rerender = useCallback(() =>
{
setUpdateId(prevValue => (prevValue + 1));
}, []);
useEffect(() =>
{
colorItem.notify = rerender;
return () =>
{
colorItem.notify = null;
}
})
return <NitroCardGridItemView itemColor={ colorItem.color } itemActive={ colorItem.isSelected } onClick={ () => onClick(colorItem) } />
}

View File

@ -0,0 +1,7 @@
import { AvatarEditorGridColorItem } from '../../common/AvatarEditorGridColorItem';
export interface AvatarEditorPaletteSetItemProps
{
colorItem: AvatarEditorGridColorItem;
onClick: (item: AvatarEditorGridColorItem) => void;
}

View File

@ -0,0 +1,29 @@
import { FC, useCallback } from 'react';
import { NitroCardGridView } from '../../../../layout/card/grid/NitroCardGridView';
import { NitroCardGridThemes } from '../../../../layout/card/grid/NitroCardGridView.types';
import { AvatarEditorGridColorItem } from '../../common/AvatarEditorGridColorItem';
import { AvatarEditorPaletteSetItem } from '../palette-set-item/AvatarEditorPaletteSetItem';
import { AvatarEditorPaletteSetViewProps } from './AvatarEditorPaletteSetView.types';
export const AvatarEditorPaletteSetView: FC<AvatarEditorPaletteSetViewProps> = props =>
{
const { model = null, category = null, paletteSet = [], paletteIndex = -1 } = props;
const selectColor = useCallback((item: AvatarEditorGridColorItem) =>
{
const index = paletteSet.indexOf(item);
if(index === -1) return;
model.selectColor(category.name, index, paletteIndex);
}, [ model, category, paletteSet, paletteIndex ]);
return (
<NitroCardGridView columns={ 3 } theme={ NitroCardGridThemes.THEME_SHADOWED }>
{ (paletteSet.length > 0) && paletteSet.map((item, index) =>
{
return <AvatarEditorPaletteSetItem key={ index } colorItem={ item } onClick={ selectColor } />;
}) }
</NitroCardGridView>
);
}

View File

@ -0,0 +1,11 @@
import { AvatarEditorGridColorItem } from '../../common/AvatarEditorGridColorItem';
import { CategoryData } from '../../common/CategoryData';
import { IAvatarEditorCategoryModel } from '../../common/IAvatarEditorCategoryModel';
export interface AvatarEditorPaletteSetViewProps
{
model: IAvatarEditorCategoryModel;
category: CategoryData;
paletteSet: AvatarEditorGridColorItem[];
paletteIndex: number;
}

View File

@ -1,8 +1,9 @@
import { FigureData, RedeemItemClothingComposer, RoomObjectCategory, UserFigureComposer } from 'nitro-renderer';
import { RedeemItemClothingComposer, RoomObjectCategory, UserFigureComposer } from 'nitro-renderer';
import { FC, useCallback, useEffect, useState } from 'react';
import { GetAvatarRenderManager, GetConnection, GetFurnitureDataForRoomObject, GetSessionDataManager } from '../../../../../../../api';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../../../../layout';
import { LocalizeText } from '../../../../../../../utils/LocalizeText';
import { FigureData } from '../../../../../../avatar-editor/common/FigureData';
import { FurniCategory } from '../../../../../../inventory/common/FurniCategory';
import { AvatarImageView } from '../../../../../../shared/avatar-image/AvatarImageView';
import { useRoomContext } from '../../../../../context/RoomContext';

View File

@ -5,4 +5,5 @@
background-repeat: no-repeat;
background-position-x: center;
background-position-y: -8px !important;
pointer-events: none;
}

View File

@ -1,4 +1,4 @@
import { Dispose, DropBounce, EaseOut, JumpBy, Motions, NitroToolbarAnimateIconEvent, Queue, Wait } from 'nitro-renderer';
import { Dispose, DropBounce, EaseOut, JumpBy, Motions, NitroToolbarAnimateIconEvent, Queue, UserFigureEvent, Wait } from 'nitro-renderer';
import { UserInfoEvent } from 'nitro-renderer/src/nitro/communication/messages/incoming/user/data/UserInfoEvent';
import { UserInfoDataParser } from 'nitro-renderer/src/nitro/communication/messages/parser/user/data/UserInfoDataParser';
import { FC, useCallback, useState } from 'react';
@ -17,6 +17,7 @@ export const ToolbarView: FC<ToolbarViewProps> = props =>
const { isInRoom } = props;
const [ userInfo, setUserInfo ] = useState<UserInfoDataParser>(null);
const [ userFigure, setUserFigure ] = useState<string>(null);
const [ isMeExpanded, setMeExpanded ] = useState(false);
const [ unseenInventoryCount, setUnseenInventoryCount ] = useState(0);
@ -28,10 +29,20 @@ export const ToolbarView: FC<ToolbarViewProps> = props =>
const parser = event.getParser();
setUserInfo(parser.userInfo);
setUserFigure(parser.userInfo.figure);
}, []);
CreateMessageHook(UserInfoEvent, onUserInfoEvent);
const onUserFigureEvent = useCallback((event: UserFigureEvent) =>
{
const parser = event.getParser();
setUserFigure(parser.figure);
}, []);
CreateMessageHook(UserFigureEvent, onUserFigureEvent);
const onUnseenItemTrackerUpdateEvent = useCallback((event: UnseenItemTrackerUpdateEvent) =>
{
setUnseenInventoryCount(event.count);
@ -116,7 +127,7 @@ export const ToolbarView: FC<ToolbarViewProps> = props =>
<div className="navigation-items navigation-avatar pe-1 me-2">
<div className="navigation-item">
<div className={ 'toolbar-avatar ' + (isMeExpanded ? 'active ' : '') } onClick={ event => setMeExpanded(!isMeExpanded) }>
{ userInfo && <AvatarImageView figure={ userInfo.figure } direction={ 2 } /> }
{ userFigure && <AvatarImageView figure={ userFigure } direction={ 2 } /> }
</div>
</div>
{ (unseenAchievementsCount > 0) && (