mirror of
https://github.com/duckietm/Nitro-Cool-UI.git
synced 2025-06-21 22:36:58 +00:00
Added: Friendbar and updates Avatar Editor
This commit is contained in:
parent
0f858212d8
commit
9da0e32702
17
src/api/avatar/AvatarEditorColorSorter.ts
Normal file
17
src/api/avatar/AvatarEditorColorSorter.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { IPartColor } from '@nitrots/nitro-renderer';
|
||||||
|
|
||||||
|
export const AvatarEditorColorSorter = (a: IPartColor, b: IPartColor) =>
|
||||||
|
{
|
||||||
|
const clubLevelA = (!a ? -1 : a.clubLevel);
|
||||||
|
const clubLevelB = (!b ? -1 : b.clubLevel);
|
||||||
|
|
||||||
|
if(clubLevelA < clubLevelB) return -1;
|
||||||
|
|
||||||
|
if(clubLevelA > clubLevelB) return 1;
|
||||||
|
|
||||||
|
if(a.index < b.index) return -1;
|
||||||
|
|
||||||
|
if(a.index > b.index) return 1;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
35
src/api/avatar/AvatarEditorPartSorter.ts
Normal file
35
src/api/avatar/AvatarEditorPartSorter.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { IFigurePartSet } from '@nitrots/nitro-renderer';
|
||||||
|
|
||||||
|
export const AvatarEditorPartSorter = (hcFirst: boolean) =>
|
||||||
|
{
|
||||||
|
return (a: { partSet: IFigurePartSet, usesColor: boolean, isClear?: boolean }, b: { partSet: IFigurePartSet, usesColor: boolean, isClear?: boolean }) =>
|
||||||
|
{
|
||||||
|
const clubLevelA = (!a.partSet ? -1 : a.partSet.clubLevel);
|
||||||
|
const clubLevelB = (!b.partSet ? -1 : b.partSet.clubLevel);
|
||||||
|
const isSellableA = (!a.partSet ? false : a.partSet.isSellable);
|
||||||
|
const isSellableB = (!b.partSet ? false : b.partSet.isSellable);
|
||||||
|
|
||||||
|
if(isSellableA && !isSellableB) return 1;
|
||||||
|
|
||||||
|
if(isSellableB && !isSellableA) return -1;
|
||||||
|
|
||||||
|
if(hcFirst)
|
||||||
|
{
|
||||||
|
if(clubLevelA > clubLevelB) return -1;
|
||||||
|
|
||||||
|
if(clubLevelA < clubLevelB) return 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if(clubLevelA < clubLevelB) return -1;
|
||||||
|
|
||||||
|
if(clubLevelA > clubLevelB) return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(a.partSet.id < b.partSet.id) return -1;
|
||||||
|
|
||||||
|
if(a.partSet.id > b.partSet.id) return 1;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,4 @@
|
|||||||
import { AvatarFigurePartType, AvatarScaleType, AvatarSetType, GetAssetManager, GetAvatarRenderManager, IFigurePart, IGraphicAsset, IPartColor, NitroAlphaFilter, NitroContainer, NitroSprite, TextureUtils } from '@nitrots/nitro-renderer';
|
import { AvatarFigurePartType, AvatarScaleType, AvatarSetType, GetAssetManager, GetAvatarRenderManager, IFigurePart, IGraphicAsset, IPartColor, NitroAlphaFilter, NitroContainer, NitroRectangle, NitroSprite, TextureUtils } from '@nitrots/nitro-renderer';
|
||||||
import { FigureData } from './FigureData';
|
|
||||||
import { IAvatarEditorCategoryPartItem } from './IAvatarEditorCategoryPartItem';
|
import { IAvatarEditorCategoryPartItem } from './IAvatarEditorCategoryPartItem';
|
||||||
|
|
||||||
export class AvatarEditorThumbnailsHelper
|
export class AvatarEditorThumbnailsHelper
|
||||||
@ -69,7 +68,7 @@ export class AvatarEditorThumbnailsHelper
|
|||||||
|
|
||||||
while(!hasAsset && (direction < AvatarEditorThumbnailsHelper.THUMB_DIRECTIONS.length))
|
while(!hasAsset && (direction < AvatarEditorThumbnailsHelper.THUMB_DIRECTIONS.length))
|
||||||
{
|
{
|
||||||
const assetName = `${ FigureData.SCALE }_${ FigureData.STD }_${ part.type }_${ part.id }_${ AvatarEditorThumbnailsHelper.THUMB_DIRECTIONS[direction] }_${ FigureData.DEFAULT_FRAME }`;
|
const assetName = `${ AvatarFigurePartType.SCALE }_${ AvatarFigurePartType.STD }_${ part.type }_${ part.id }_${ AvatarEditorThumbnailsHelper.THUMB_DIRECTIONS[direction] }_${ AvatarFigurePartType.DEFAULT_FRAME }`;
|
||||||
|
|
||||||
asset = GetAssetManager().getAsset(assetName);
|
asset = GetAssetManager().getAsset(assetName);
|
||||||
|
|
||||||
@ -150,19 +149,23 @@ export class AvatarEditorThumbnailsHelper
|
|||||||
const resetFigure = async (figure: string) =>
|
const resetFigure = async (figure: string) =>
|
||||||
{
|
{
|
||||||
const avatarImage = GetAvatarRenderManager().createAvatarImage(figure, AvatarScaleType.LARGE, null, { resetFigure, dispose: null, disposed: false });
|
const avatarImage = GetAvatarRenderManager().createAvatarImage(figure, AvatarScaleType.LARGE, null, { resetFigure, dispose: null, disposed: false });
|
||||||
|
|
||||||
|
if(avatarImage.isPlaceholder()) return;
|
||||||
|
|
||||||
const texture = avatarImage.processAsTexture(AvatarSetType.HEAD, false);
|
const texture = avatarImage.processAsTexture(AvatarSetType.HEAD, false);
|
||||||
const sprite = new NitroSprite(texture);
|
const sprite = new NitroSprite(texture);
|
||||||
|
|
||||||
if(isDisabled) sprite.filters = [ AvatarEditorThumbnailsHelper.ALPHA_FILTER ];
|
if(isDisabled) sprite.filters = [ AvatarEditorThumbnailsHelper.ALPHA_FILTER ];
|
||||||
|
|
||||||
const imageUrl = await TextureUtils.generateImageUrl({
|
const imageUrl = await TextureUtils.generateImageUrl({
|
||||||
target: sprite
|
target: sprite,
|
||||||
|
frame: new NitroRectangle(0, 0, texture.width, texture.height)
|
||||||
});
|
});
|
||||||
|
|
||||||
sprite.destroy();
|
sprite.destroy();
|
||||||
avatarImage.dispose();
|
avatarImage.dispose();
|
||||||
|
|
||||||
if(!avatarImage.isPlaceholder()) AvatarEditorThumbnailsHelper.THUMBNAIL_CACHE.set(thumbnailKey, imageUrl);
|
AvatarEditorThumbnailsHelper.THUMBNAIL_CACHE.set(thumbnailKey, imageUrl);
|
||||||
|
|
||||||
resolve(imageUrl);
|
resolve(imageUrl);
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,6 @@
|
|||||||
export * from './AvatarEditorAction';
|
export * from './AvatarEditorAction';
|
||||||
export * from './AvatarEditorGridColorItem';
|
export * from './AvatarEditorColorSorter';
|
||||||
export * from './AvatarEditorGridPartItem';
|
export * from './AvatarEditorPartSorter';
|
||||||
export * from './AvatarEditorThumbnailsHelper';
|
export * from './AvatarEditorThumbnailsHelper';
|
||||||
export * from './AvatarEditorUtilities';
|
|
||||||
export * from './BodyModel';
|
|
||||||
export * from './CategoryBaseModel';
|
|
||||||
export * from './CategoryData';
|
|
||||||
export * from './FigureData';
|
|
||||||
export * from './FigureGenerator';
|
|
||||||
export * from './HeadModel';
|
|
||||||
export * from './IAvatarEditorCategory';
|
export * from './IAvatarEditorCategory';
|
||||||
export * from './IAvatarEditorCategoryModel';
|
|
||||||
export * from './IAvatarEditorCategoryPartItem';
|
export * from './IAvatarEditorCategoryPartItem';
|
||||||
export * from './LegModel';
|
|
||||||
export * from './TorsoModel';
|
|
||||||
|
@ -8,19 +8,20 @@ interface InfiniteGridProps<T = any>
|
|||||||
rows: T[];
|
rows: T[];
|
||||||
columnCount: number;
|
columnCount: number;
|
||||||
overscan?: number;
|
overscan?: number;
|
||||||
itemRender?: (item: T) => ReactElement;
|
estimateSize?: number;
|
||||||
|
itemRender?: (item: T, index?: number) => ReactElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InfiniteGrid: FC<InfiniteGridProps> = props =>
|
export const InfiniteGrid: FC<InfiniteGridProps> = props =>
|
||||||
{
|
{
|
||||||
const { rows = [], columnCount = 4, overscan = 5, itemRender = null } = props;
|
const { rows = [], columnCount = 4, overscan = 5, estimateSize = 45, itemRender = null } = props;
|
||||||
const parentRef = useRef<HTMLDivElement>(null);
|
const parentRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const virtualizer = useVirtualizer({
|
const virtualizer = useVirtualizer({
|
||||||
count: Math.ceil(rows.length / columnCount),
|
count: Math.ceil(rows.length / columnCount),
|
||||||
overscan,
|
overscan,
|
||||||
getScrollElement: () => parentRef.current,
|
getScrollElement: () => parentRef.current,
|
||||||
estimateSize: () => 45,
|
estimateSize: () => estimateSize
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
@ -58,7 +59,7 @@ export const InfiniteGrid: FC<InfiniteGridProps> = props =>
|
|||||||
style={ {
|
style={ {
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gap: '0.25rem',
|
gap: '0.25rem',
|
||||||
minHeight: virtualRow.index === 0 ? 45 : virtualRow.size,
|
minHeight: virtualRow.index === 0 ? estimateSize : virtualRow.size,
|
||||||
gridTemplateColumns: `repeat(${ columnCount }, 1fr)`
|
gridTemplateColumns: `repeat(${ columnCount }, 1fr)`
|
||||||
} }>
|
} }>
|
||||||
{ Array.from(Array(columnCount)).map((e,i) =>
|
{ Array.from(Array(columnCount)).map((e,i) =>
|
||||||
@ -67,8 +68,12 @@ export const InfiniteGrid: FC<InfiniteGridProps> = props =>
|
|||||||
|
|
||||||
if(!item) return <Fragment
|
if(!item) return <Fragment
|
||||||
key={ virtualRow.index + i + 'b' } />;
|
key={ virtualRow.index + i + 'b' } />;
|
||||||
|
|
||||||
return itemRender(item);
|
return (
|
||||||
|
<Fragment key={ i }>
|
||||||
|
{ itemRender(item, i) }
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
}) }
|
}) }
|
||||||
</div>
|
</div>
|
||||||
)) }
|
)) }
|
||||||
|
@ -0,0 +1,40 @@
|
|||||||
|
import { AvatarDirectionAngle } from '@nitrots/nitro-renderer';
|
||||||
|
import { FC, useState } from 'react';
|
||||||
|
import { Base, Column, LayoutAvatarImageView } from '../../common';
|
||||||
|
import { useAvatarEditor } from '../../hooks';
|
||||||
|
import { AvatarEditorIcon } from './AvatarEditorIcon';
|
||||||
|
|
||||||
|
const DEFAULT_DIRECTION: number = 4;
|
||||||
|
|
||||||
|
export const AvatarEditorFigurePreviewView: FC<{}> = props =>
|
||||||
|
{
|
||||||
|
const [ direction, setDirection ] = useState<number>(DEFAULT_DIRECTION);
|
||||||
|
const { getFigureString = null } = useAvatarEditor();
|
||||||
|
|
||||||
|
const rotateFigure = (newDirection: number) =>
|
||||||
|
{
|
||||||
|
if(direction < AvatarDirectionAngle.MIN_DIRECTION)
|
||||||
|
{
|
||||||
|
newDirection = (AvatarDirectionAngle.MAX_DIRECTION + (direction + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if(direction > AvatarDirectionAngle.MAX_DIRECTION)
|
||||||
|
{
|
||||||
|
newDirection = (direction - (AvatarDirectionAngle.MAX_DIRECTION + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
setDirection(newDirection);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column className="figure-preview-container" overflow="hidden" position="relative">
|
||||||
|
<LayoutAvatarImageView figure={ getFigureString } direction={ 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(direction + 1) } />
|
||||||
|
<AvatarEditorIcon pointer icon="arrow-right" onClick={ event => rotateFigure(direction - 1) } />
|
||||||
|
</Base>
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
}
|
27
src/components/avatar-editor/AvatarEditorIcon.tsx
Normal file
27
src/components/avatar-editor/AvatarEditorIcon.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { FC, useMemo } from 'react';
|
||||||
|
import { Base, BaseProps } from '../../common';
|
||||||
|
|
||||||
|
type AvatarIconType = 'male' | 'female' | 'clear' | 'sellable' | string;
|
||||||
|
|
||||||
|
export const AvatarEditorIcon: FC<{
|
||||||
|
icon: AvatarIconType;
|
||||||
|
selected?: boolean;
|
||||||
|
} & BaseProps<HTMLDivElement>> = props =>
|
||||||
|
{
|
||||||
|
const { icon = null, selected = false, classNames = [], children = null, ...rest } = props;
|
||||||
|
|
||||||
|
const getClassNames = useMemo(() =>
|
||||||
|
{
|
||||||
|
const newClassNames: string[] = [ 'nitro-avatar-editor-spritesheet' ];
|
||||||
|
|
||||||
|
if(icon && icon.length) newClassNames.push(icon + '-icon');
|
||||||
|
|
||||||
|
if(selected) newClassNames.push('selected');
|
||||||
|
|
||||||
|
if(classNames.length) newClassNames.push(...classNames);
|
||||||
|
|
||||||
|
return newClassNames;
|
||||||
|
}, [ icon, selected, classNames ]);
|
||||||
|
|
||||||
|
return <Base classNames={ getClassNames } { ...rest } />
|
||||||
|
}
|
81
src/components/avatar-editor/AvatarEditorModelView.tsx
Normal file
81
src/components/avatar-editor/AvatarEditorModelView.tsx
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import { AvatarEditorFigureCategory, AvatarFigurePartType, FigureDataContainer } from '@nitrots/nitro-renderer';
|
||||||
|
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { IAvatarEditorCategory } from '../../api';
|
||||||
|
import { Column, Flex, Grid } from '../../common';
|
||||||
|
import { useAvatarEditor } from '../../hooks';
|
||||||
|
import { AvatarEditorIcon } from './AvatarEditorIcon';
|
||||||
|
import { AvatarEditorFigureSetView } from './figure-set';
|
||||||
|
import { AvatarEditorPaletteSetView } from './palette-set';
|
||||||
|
|
||||||
|
export const AvatarEditorModelView: FC<{
|
||||||
|
name: string,
|
||||||
|
categories: IAvatarEditorCategory[]
|
||||||
|
}> = props =>
|
||||||
|
{
|
||||||
|
const { name = '', categories = [] } = props;
|
||||||
|
const [ didChange, setDidChange ] = useState<boolean>(false);
|
||||||
|
const [ activeSetType, setActiveSetType ] = useState<string>('');
|
||||||
|
const { maxPaletteCount = 1, gender = null, setGender = null, selectedColorParts = null, getFirstSelectableColor = null, selectEditorColor = null } = useAvatarEditor();
|
||||||
|
|
||||||
|
const activeCategory = useMemo(() =>
|
||||||
|
{
|
||||||
|
return categories.find(category => category.setType === activeSetType) ?? null;
|
||||||
|
}, [ categories, activeSetType ]);
|
||||||
|
|
||||||
|
const selectSet = useCallback((setType: string) =>
|
||||||
|
{
|
||||||
|
const selectedPalettes = selectedColorParts[setType];
|
||||||
|
|
||||||
|
if(!selectedPalettes || !selectedPalettes.length) selectEditorColor(setType, 0, getFirstSelectableColor(setType));
|
||||||
|
|
||||||
|
setActiveSetType(setType);
|
||||||
|
}, [ getFirstSelectableColor, selectEditorColor, selectedColorParts ]);
|
||||||
|
|
||||||
|
useEffect(() =>
|
||||||
|
{
|
||||||
|
if(!categories || !categories.length || !didChange) return;
|
||||||
|
|
||||||
|
selectSet(categories[0]?.setType);
|
||||||
|
setDidChange(false);
|
||||||
|
}, [ categories, didChange, selectSet ]);
|
||||||
|
|
||||||
|
useEffect(() =>
|
||||||
|
{
|
||||||
|
setDidChange(true);
|
||||||
|
}, [ categories ]);
|
||||||
|
|
||||||
|
if(!activeCategory) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid>
|
||||||
|
<Column size={ 2 }>
|
||||||
|
{ (name === AvatarEditorFigureCategory.GENERIC) &&
|
||||||
|
<>
|
||||||
|
<Flex center pointer className="category-item" onClick={ event => setGender(AvatarFigurePartType.MALE) }>
|
||||||
|
<AvatarEditorIcon icon="male" selected={ gender === FigureDataContainer.MALE } />
|
||||||
|
</Flex>
|
||||||
|
<Flex center pointer className="category-item" onClick={ event => setGender(AvatarFigurePartType.FEMALE) }>
|
||||||
|
<AvatarEditorIcon icon="female" selected={ gender === FigureDataContainer.FEMALE } />
|
||||||
|
</Flex>
|
||||||
|
</> }
|
||||||
|
{ (name !== AvatarEditorFigureCategory.GENERIC) && (categories.length > 0) && categories.map(category =>
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
<Flex center pointer key={ category.setType } className="category-item" onClick={ event => selectSet(category.setType) }>
|
||||||
|
<AvatarEditorIcon icon={ category.setType } selected={ (activeSetType === category.setType) } />
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}) }
|
||||||
|
</Column>
|
||||||
|
<Column size={ 5 } overflow="hidden">
|
||||||
|
<AvatarEditorFigureSetView category={ activeCategory } />
|
||||||
|
</Column>
|
||||||
|
<Column size={ 5 } overflow="hidden">
|
||||||
|
{ (maxPaletteCount >= 1) &&
|
||||||
|
<AvatarEditorPaletteSetView category={ activeCategory } paletteIndex={ 0 } /> }
|
||||||
|
{ (maxPaletteCount === 2) &&
|
||||||
|
<AvatarEditorPaletteSetView category={ activeCategory } paletteIndex={ 1 } /> }
|
||||||
|
</Column>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
}
|
@ -1,12 +1,12 @@
|
|||||||
import { AvatarEditorFigureCategory, FigureSetIdsMessageEvent, GetAvatarRenderManager, GetSessionDataManager, GetWardrobeMessageComposer, IAvatarFigureContainer, UserFigureComposer, UserWardrobePageEvent } from '@nitrots/nitro-renderer';
|
import { AddLinkEventTracker, AvatarEditorFigureCategory, GetSessionDataManager, ILinkEventTracker, RemoveLinkEventTracker, UserFigureComposer } from '@nitrots/nitro-renderer';
|
||||||
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
import { FC, useEffect, useState } from 'react';
|
||||||
import { FaDice, FaTrash, FaUndo } from 'react-icons/fa';
|
import { FaDice, FaRedo, FaTrash } from 'react-icons/fa';
|
||||||
import { AvatarEditorAction, AvatarEditorUtilities, BodyModel, FigureData, GetClubMemberLevel, GetConfigurationValue, HeadModel, IAvatarEditorCategoryModel, LegModel, LocalizeText, SendMessageComposer, TorsoModel, generateRandomFigure } from '../../api';
|
import { AvatarEditorAction, LocalizeText, SendMessageComposer } from '../../api';
|
||||||
import { Button, ButtonGroup, Column, Grid, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../common';
|
import { Button, ButtonGroup, Column, Grid, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../common';
|
||||||
import { useMessageEvent } from '../../hooks';
|
import { useAvatarEditor } from '../../hooks';
|
||||||
import { AvatarEditorFigurePreviewView } from './views/AvatarEditorFigurePreviewView';
|
import { AvatarEditorFigurePreviewView } from './AvatarEditorFigurePreviewView';
|
||||||
import { AvatarEditorModelView } from './views/AvatarEditorModelView';
|
import { AvatarEditorModelView } from './AvatarEditorModelView';
|
||||||
import { AvatarEditorWardrobeView } from './views/AvatarEditorWardrobeView';
|
import { AvatarEditorWardrobeView } from './AvatarEditorWardrobeView';
|
||||||
|
|
||||||
const DEFAULT_MALE_FIGURE: string = 'hr-100.hd-180-7.ch-215-66.lg-270-79.sh-305-62.ha-1002-70.wa-2007';
|
const DEFAULT_MALE_FIGURE: string = 'hr-100.hd-180-7.ch-215-66.lg-270-79.sh-305-62.ha-1002-70.wa-2007';
|
||||||
const DEFAULT_FEMALE_FIGURE: string = 'hr-515-33.hd-600-1.ch-635-70.lg-716-66-62.sh-735-68';
|
const DEFAULT_FEMALE_FIGURE: string = 'hr-515-33.hd-600-1.ch-635-70.lg-716-66-62.sh-735-68';
|
||||||
@ -14,141 +14,29 @@ const DEFAULT_FEMALE_FIGURE: string = 'hr-515-33.hd-600-1.ch-635-70.lg-716-66-62
|
|||||||
export const AvatarEditorView: FC<{}> = props =>
|
export const AvatarEditorView: FC<{}> = props =>
|
||||||
{
|
{
|
||||||
const [ isVisible, setIsVisible ] = useState(false);
|
const [ isVisible, setIsVisible ] = useState(false);
|
||||||
const [ figures, setFigures ] = useState<Map<string, FigureData>>(null);
|
const { setIsVisible: setEditorVisibility, avatarModels, activeModelKey, setActiveModelKey, loadAvatarData, getFigureStringWithFace, gender, figureSetIds = [], randomizeCurrentFigure = null, getFigureString = null } = useAvatarEditor();
|
||||||
const [ figureData, setFigureData ] = useState<FigureData>(null);
|
|
||||||
const [ categories, setCategories ] = useState<Map<string, IAvatarEditorCategoryModel>>(null);
|
|
||||||
const [ activeCategory, setActiveCategory ] = useState<IAvatarEditorCategoryModel>(null);
|
|
||||||
const [ figureSetIds, setFigureSetIds ] = useState<number[]>([]);
|
|
||||||
const [ boundFurnitureNames, setBoundFurnitureNames ] = useState<string[]>([]);
|
|
||||||
const [ savedFigures, setSavedFigures ] = useState<[ IAvatarFigureContainer, string ][]>([]);
|
|
||||||
const [ isWardrobeVisible, setIsWardrobeVisible ] = useState(false);
|
|
||||||
const [ lastFigure, setLastFigure ] = useState<string>(null);
|
|
||||||
const [ lastGender, setLastGender ] = useState<string>(null);
|
|
||||||
const [ needsReset, setNeedsReset ] = useState(true);
|
|
||||||
const [ isInitalized, setIsInitalized ] = useState(false);
|
|
||||||
|
|
||||||
const maxWardrobeSlots = useMemo(() => GetConfigurationValue<number>('avatar.wardrobe.max.slots', 10), []);
|
const processAction = (action: string) =>
|
||||||
|
|
||||||
useMessageEvent<FigureSetIdsMessageEvent>(FigureSetIdsMessageEvent, event =>
|
|
||||||
{
|
|
||||||
const parser = event.getParser();
|
|
||||||
|
|
||||||
setFigureSetIds(parser.figureSetIds);
|
|
||||||
setBoundFurnitureNames(parser.boundsFurnitureNames);
|
|
||||||
});
|
|
||||||
|
|
||||||
useMessageEvent<UserWardrobePageEvent>(UserWardrobePageEvent, event =>
|
|
||||||
{
|
|
||||||
const parser = event.getParser();
|
|
||||||
const savedFigures: [ IAvatarFigureContainer, string ][] = [];
|
|
||||||
|
|
||||||
let i = 0;
|
|
||||||
|
|
||||||
while(i < maxWardrobeSlots)
|
|
||||||
{
|
|
||||||
savedFigures.push([ null, null ]);
|
|
||||||
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
for(let [ index, [ look, gender ] ] of parser.looks.entries())
|
|
||||||
{
|
|
||||||
const container = GetAvatarRenderManager().createFigureContainer(look);
|
|
||||||
|
|
||||||
savedFigures[(index - 1)] = [ container, gender ];
|
|
||||||
}
|
|
||||||
|
|
||||||
setSavedFigures(savedFigures);
|
|
||||||
});
|
|
||||||
|
|
||||||
const selectCategory = useCallback((name: string) =>
|
|
||||||
{
|
|
||||||
if(!categories) return;
|
|
||||||
|
|
||||||
setActiveCategory(categories.get(name));
|
|
||||||
}, [ categories ]);
|
|
||||||
|
|
||||||
const resetCategories = useCallback(() =>
|
|
||||||
{
|
|
||||||
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());
|
|
||||||
|
|
||||||
setCategories(categories);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const setupFigures = useCallback(() =>
|
|
||||||
{
|
|
||||||
const figures: Map<string, FigureData> = new Map();
|
|
||||||
|
|
||||||
const maleFigure = new FigureData();
|
|
||||||
const femaleFigure = new FigureData();
|
|
||||||
|
|
||||||
maleFigure.loadAvatarData(DEFAULT_MALE_FIGURE, FigureData.MALE);
|
|
||||||
femaleFigure.loadAvatarData(DEFAULT_FEMALE_FIGURE, FigureData.FEMALE);
|
|
||||||
|
|
||||||
figures.set(FigureData.MALE, maleFigure);
|
|
||||||
figures.set(FigureData.FEMALE, femaleFigure);
|
|
||||||
|
|
||||||
setFigures(figures);
|
|
||||||
setFigureData(figures.get(FigureData.MALE));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const loadAvatarInEditor = useCallback((figure: string, gender: string, reset: boolean = true) =>
|
|
||||||
{
|
|
||||||
gender = AvatarEditorUtilities.getGender(gender);
|
|
||||||
|
|
||||||
let newFigureData = figureData;
|
|
||||||
|
|
||||||
if(gender !== newFigureData.gender) newFigureData = figures.get(gender);
|
|
||||||
|
|
||||||
if(figure !== newFigureData.getFigureString()) newFigureData.loadAvatarData(figure, gender);
|
|
||||||
|
|
||||||
if(newFigureData !== figureData) setFigureData(newFigureData);
|
|
||||||
|
|
||||||
if(reset)
|
|
||||||
{
|
|
||||||
setLastFigure(figureData.getFigureString());
|
|
||||||
setLastGender(figureData.gender);
|
|
||||||
}
|
|
||||||
}, [ figures, figureData ]);
|
|
||||||
|
|
||||||
const processAction = useCallback((action: string) =>
|
|
||||||
{
|
{
|
||||||
switch(action)
|
switch(action)
|
||||||
{
|
{
|
||||||
case AvatarEditorAction.ACTION_CLEAR:
|
|
||||||
loadAvatarInEditor(figureData.getFigureStringWithFace(0, false), figureData.gender, false);
|
|
||||||
resetCategories();
|
|
||||||
return;
|
|
||||||
case AvatarEditorAction.ACTION_RESET:
|
case AvatarEditorAction.ACTION_RESET:
|
||||||
loadAvatarInEditor(lastFigure, lastGender);
|
loadAvatarData(GetSessionDataManager().figure, GetSessionDataManager().gender);
|
||||||
resetCategories();
|
return;
|
||||||
|
case AvatarEditorAction.ACTION_CLEAR:
|
||||||
|
loadAvatarData(getFigureStringWithFace(0, false), gender);
|
||||||
return;
|
return;
|
||||||
case AvatarEditorAction.ACTION_RANDOMIZE:
|
case AvatarEditorAction.ACTION_RANDOMIZE:
|
||||||
const figure = generateRandomFigure(figureData, figureData.gender, GetClubMemberLevel(), figureSetIds, [ FigureData.FACE ]);
|
randomizeCurrentFigure();
|
||||||
|
|
||||||
loadAvatarInEditor(figure, figureData.gender, false);
|
|
||||||
resetCategories();
|
|
||||||
return;
|
return;
|
||||||
case AvatarEditorAction.ACTION_SAVE:
|
case AvatarEditorAction.ACTION_SAVE:
|
||||||
SendMessageComposer(new UserFigureComposer(figureData.gender, figureData.getFigureString()));
|
SendMessageComposer(new UserFigureComposer(gender, getFigureString));
|
||||||
setIsVisible(false);
|
setIsVisible(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}, [ figureData, lastFigure, lastGender, figureSetIds, loadAvatarInEditor, resetCategories ])
|
}
|
||||||
|
|
||||||
const setGender = useCallback((gender: string) =>
|
useEffect(() =>
|
||||||
{
|
|
||||||
gender = AvatarEditorUtilities.getGender(gender);
|
|
||||||
|
|
||||||
setFigureData(figures.get(gender));
|
|
||||||
}, [ figures ]);
|
|
||||||
|
|
||||||
/* useEffect(() =>
|
|
||||||
{
|
{
|
||||||
const linkTracker: ILinkEventTracker = {
|
const linkTracker: ILinkEventTracker = {
|
||||||
linkReceived: (url: string) =>
|
linkReceived: (url: string) =>
|
||||||
@ -176,126 +64,44 @@ export const AvatarEditorView: FC<{}> = props =>
|
|||||||
AddLinkEventTracker(linkTracker);
|
AddLinkEventTracker(linkTracker);
|
||||||
|
|
||||||
return () => RemoveLinkEventTracker(linkTracker);
|
return () => RemoveLinkEventTracker(linkTracker);
|
||||||
}, []); */
|
}, []);
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
setSavedFigures(new Array(maxWardrobeSlots));
|
setEditorVisibility(isVisible)
|
||||||
}, [ maxWardrobeSlots ]);
|
}, [ isVisible, setEditorVisibility ]);
|
||||||
|
|
||||||
useEffect(() =>
|
if(!isVisible) return null;
|
||||||
{
|
|
||||||
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 ]);
|
|
||||||
|
|
||||||
useEffect(() =>
|
|
||||||
{
|
|
||||||
if(!figureData) return;
|
|
||||||
|
|
||||||
AvatarEditorUtilities.CURRENT_FIGURE = figureData;
|
|
||||||
|
|
||||||
resetCategories();
|
|
||||||
|
|
||||||
return () => AvatarEditorUtilities.CURRENT_FIGURE = null;
|
|
||||||
}, [ figureData, resetCategories ]);
|
|
||||||
|
|
||||||
useEffect(() =>
|
|
||||||
{
|
|
||||||
AvatarEditorUtilities.FIGURE_SET_IDS = figureSetIds;
|
|
||||||
AvatarEditorUtilities.BOUND_FURNITURE_NAMES = boundFurnitureNames;
|
|
||||||
|
|
||||||
resetCategories();
|
|
||||||
|
|
||||||
return () =>
|
|
||||||
{
|
|
||||||
AvatarEditorUtilities.FIGURE_SET_IDS = null;
|
|
||||||
AvatarEditorUtilities.BOUND_FURNITURE_NAMES = null;
|
|
||||||
}
|
|
||||||
}, [ figureSetIds, boundFurnitureNames, resetCategories ]);
|
|
||||||
|
|
||||||
useEffect(() =>
|
|
||||||
{
|
|
||||||
if(!isVisible) return;
|
|
||||||
|
|
||||||
if(!figures)
|
|
||||||
{
|
|
||||||
setupFigures();
|
|
||||||
|
|
||||||
setIsInitalized(true);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}, [ isVisible, figures, setupFigures ]);
|
|
||||||
|
|
||||||
useEffect(() =>
|
|
||||||
{
|
|
||||||
if(!isVisible || !isInitalized || !needsReset) return;
|
|
||||||
|
|
||||||
loadAvatarInEditor(GetSessionDataManager().figure, GetSessionDataManager().gender);
|
|
||||||
setNeedsReset(false);
|
|
||||||
}, [ isVisible, isInitalized, needsReset, loadAvatarInEditor ]);
|
|
||||||
|
|
||||||
useEffect(() =>
|
|
||||||
{
|
|
||||||
if(isVisible) return;
|
|
||||||
|
|
||||||
return () =>
|
|
||||||
{
|
|
||||||
setNeedsReset(true);
|
|
||||||
}
|
|
||||||
}, [ isVisible ]);
|
|
||||||
|
|
||||||
if(!isVisible || !figureData) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NitroCardView uniqueKey="avatar-editor" className="nitro-avatar-editor">
|
<NitroCardView uniqueKey="avatar-editor" className="nitro-avatar-editor">
|
||||||
<NitroCardHeaderView headerText={ LocalizeText('avatareditor.title') } onCloseClick={ event => setIsVisible(false) } />
|
<NitroCardHeaderView headerText={ LocalizeText('avatareditor.title') } onCloseClick={ event => setIsVisible(false) } />
|
||||||
<NitroCardTabsView>
|
<NitroCardTabsView>
|
||||||
{ categories && (categories.size > 0) && Array.from(categories.keys()).map(category =>
|
{ Object.keys(avatarModels).map(modelKey =>
|
||||||
{
|
{
|
||||||
const isActive = (activeCategory && (activeCategory.name === category));
|
const isActive = (activeModelKey === modelKey);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NitroCardTabsItemView key={ category } isActive={ isActive } onClick={ event => selectCategory(category) }>
|
<NitroCardTabsItemView key={ modelKey } isActive={ isActive } onClick={ event => setActiveModelKey(modelKey) }>
|
||||||
{ LocalizeText(`avatareditor.category.${ category }`) }
|
{ LocalizeText(`avatareditor.category.${ modelKey }`) }
|
||||||
</NitroCardTabsItemView>
|
</NitroCardTabsItemView>
|
||||||
);
|
);
|
||||||
}) }
|
}) }
|
||||||
<NitroCardTabsItemView isActive={ isWardrobeVisible } onClick={ event => setIsWardrobeVisible(true) }>
|
|
||||||
{ LocalizeText('avatareditor.category.wardrobe') }
|
|
||||||
</NitroCardTabsItemView>
|
|
||||||
</NitroCardTabsView>
|
</NitroCardTabsView>
|
||||||
<NitroCardContentView>
|
<NitroCardContentView>
|
||||||
<Grid>
|
<Grid>
|
||||||
<Column size={ 9 } overflow="hidden">
|
<Column size={ 9 } overflow="hidden">
|
||||||
{ (activeCategory && !isWardrobeVisible) &&
|
{ ((activeModelKey.length > 0) && (activeModelKey !== AvatarEditorFigureCategory.WARDROBE)) &&
|
||||||
<AvatarEditorModelView model={ activeCategory } gender={ figureData.gender } setGender={ setGender } /> }
|
<AvatarEditorModelView name={ activeModelKey } categories={ avatarModels[activeModelKey] } /> }
|
||||||
{ isWardrobeVisible &&
|
{ (activeModelKey === AvatarEditorFigureCategory.WARDROBE) &&
|
||||||
<AvatarEditorWardrobeView figureData={ figureData } savedFigures={ savedFigures } setSavedFigures={ setSavedFigures } loadAvatarInEditor={ loadAvatarInEditor } /> }
|
<AvatarEditorWardrobeView /> }
|
||||||
</Column>
|
</Column>
|
||||||
<Column size={ 3 } overflow="hidden">
|
<Column size={ 3 } overflow="hidden">
|
||||||
<AvatarEditorFigurePreviewView figureData={ figureData } />
|
<AvatarEditorFigurePreviewView />
|
||||||
<Column grow gap={ 1 }>
|
<Column grow gap={ 1 }>
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
<Button variant="secondary" onClick={ event => processAction(AvatarEditorAction.ACTION_RESET) }>
|
<Button variant="secondary" onClick={ event => processAction(AvatarEditorAction.ACTION_RESET) }>
|
||||||
<FaUndo className="fa-icon" />
|
<FaRedo className="fa-icon" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="secondary" onClick={ event => processAction(AvatarEditorAction.ACTION_CLEAR) }>
|
<Button variant="secondary" onClick={ event => processAction(AvatarEditorAction.ACTION_CLEAR) }>
|
||||||
<FaTrash className="fa-icon" />
|
<FaTrash className="fa-icon" />
|
||||||
|
60
src/components/avatar-editor/AvatarEditorWardrobeView.tsx
Normal file
60
src/components/avatar-editor/AvatarEditorWardrobeView.tsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { GetAvatarRenderManager, IAvatarFigureContainer, SaveWardrobeOutfitMessageComposer } from '@nitrots/nitro-renderer';
|
||||||
|
import { FC, useCallback } from 'react';
|
||||||
|
import { GetClubMemberLevel, GetConfigurationValue, LocalizeText, SendMessageComposer } from '../../api';
|
||||||
|
import { Base, Button, Flex, InfiniteGrid, LayoutAvatarImageView, LayoutCurrencyIcon, LayoutGridItem } from '../../common';
|
||||||
|
import { useAvatarEditor } from '../../hooks';
|
||||||
|
|
||||||
|
export const AvatarEditorWardrobeView: FC<{}> = props =>
|
||||||
|
{
|
||||||
|
const { savedFigures = [], setSavedFigures = null, loadAvatarData = null, getFigureString = null, gender = null } = useAvatarEditor();
|
||||||
|
|
||||||
|
const hcDisabled = GetConfigurationValue<boolean>('hc.disabled', false);
|
||||||
|
|
||||||
|
const wearFigureAtIndex = useCallback((index: number) =>
|
||||||
|
{
|
||||||
|
if((index >= savedFigures.length) || (index < 0)) return;
|
||||||
|
|
||||||
|
const [ figure, gender ] = savedFigures[index];
|
||||||
|
|
||||||
|
loadAvatarData(figure.getFigureString(), gender);
|
||||||
|
}, [ savedFigures, loadAvatarData ]);
|
||||||
|
|
||||||
|
const saveFigureAtWardrobeIndex = useCallback((index: number) =>
|
||||||
|
{
|
||||||
|
if((index >= savedFigures.length) || (index < 0)) return;
|
||||||
|
|
||||||
|
const newFigures = [ ...savedFigures ];
|
||||||
|
|
||||||
|
const figure = getFigureString;
|
||||||
|
|
||||||
|
newFigures[index] = [ GetAvatarRenderManager().createFigureContainer(figure), gender ];
|
||||||
|
|
||||||
|
setSavedFigures(newFigures);
|
||||||
|
SendMessageComposer(new SaveWardrobeOutfitMessageComposer((index + 1), figure, gender));
|
||||||
|
}, [ getFigureString, gender, savedFigures, setSavedFigures ]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InfiniteGrid rows={ savedFigures } columnCount={ 5 } overscan={ 5 } estimateSize={ 140 } itemRender={ (item: [ IAvatarFigureContainer, string ], index: number) =>
|
||||||
|
{
|
||||||
|
const [ figureContainer, gender ] = item;
|
||||||
|
|
||||||
|
let clubLevel = 0;
|
||||||
|
|
||||||
|
if(figureContainer) clubLevel = GetAvatarRenderManager().getFigureClubLevel(figureContainer, gender);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LayoutGridItem 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('widget.generic_usable.button.use') }</Button> }
|
||||||
|
</Flex>
|
||||||
|
</LayoutGridItem>
|
||||||
|
)
|
||||||
|
} } />
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,54 @@
|
|||||||
|
import { AvatarFigurePartType } from '@nitrots/nitro-renderer';
|
||||||
|
import { FC, useEffect, useState } from 'react';
|
||||||
|
import { AvatarEditorThumbnailsHelper, GetConfigurationValue, IAvatarEditorCategoryPartItem } from '../../../api';
|
||||||
|
import { LayoutCurrencyIcon, LayoutGridItem, LayoutGridItemProps } from '../../../common';
|
||||||
|
import { useAvatarEditor } from '../../../hooks';
|
||||||
|
import { AvatarEditorIcon } from '../AvatarEditorIcon';
|
||||||
|
|
||||||
|
export const AvatarEditorFigureSetItemView: FC<{
|
||||||
|
setType: string;
|
||||||
|
partItem: IAvatarEditorCategoryPartItem;
|
||||||
|
isSelected: boolean;
|
||||||
|
} & LayoutGridItemProps> = props =>
|
||||||
|
{
|
||||||
|
const { setType = null, partItem = null, isSelected = false, ...rest } = props;
|
||||||
|
const [ assetUrl, setAssetUrl ] = useState<string>('');
|
||||||
|
const { selectedColorParts = null, getFigureStringWithFace = null } = useAvatarEditor();
|
||||||
|
|
||||||
|
const isHC = !GetConfigurationValue<boolean>('hc.disabled', false) && ((partItem.partSet?.clubLevel ?? 0) > 0);
|
||||||
|
|
||||||
|
useEffect(() =>
|
||||||
|
{
|
||||||
|
if(!setType || !setType.length || !partItem) return;
|
||||||
|
|
||||||
|
const loadImage = async () =>
|
||||||
|
{
|
||||||
|
const isHC = !GetConfigurationValue<boolean>('hc.disabled', false) && ((partItem.partSet?.clubLevel ?? 0) > 0);
|
||||||
|
|
||||||
|
let url: string = null;
|
||||||
|
|
||||||
|
if(setType === AvatarFigurePartType.HEAD)
|
||||||
|
{
|
||||||
|
url = await AvatarEditorThumbnailsHelper.buildForFace(getFigureStringWithFace(partItem.id), isHC);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
url = await AvatarEditorThumbnailsHelper.build(setType, partItem, partItem.usesColor, selectedColorParts[setType] ?? null, isHC);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(url && url.length) setAssetUrl(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadImage();
|
||||||
|
}, [ setType, partItem, selectedColorParts, getFigureStringWithFace ]);
|
||||||
|
|
||||||
|
if(!partItem) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LayoutGridItem itemImage={ (partItem.isClear ? undefined : assetUrl) } itemActive={ isSelected } style={ { width: '100%', flex: '1', backgroundPosition: (setType === AvatarFigurePartType.HEAD) ? 'center -35px' : 'center' } } { ...rest }>
|
||||||
|
{ !partItem.isClear && isHC && <LayoutCurrencyIcon className="position-absolute end-1 bottom-1" type="hc" /> }
|
||||||
|
{ partItem.isClear && <AvatarEditorIcon icon="clear" /> }
|
||||||
|
{ !partItem.isClear && partItem.partSet.isSellable && <AvatarEditorIcon icon="sellable" position="absolute" className="end-1 bottom-1" /> }
|
||||||
|
</LayoutGridItem>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
import { FC } from 'react';
|
||||||
|
import { IAvatarEditorCategory, IAvatarEditorCategoryPartItem } from '../../../api';
|
||||||
|
import { InfiniteGrid } from '../../../common';
|
||||||
|
import { useAvatarEditor } from '../../../hooks';
|
||||||
|
import { AvatarEditorFigureSetItemView } from './AvatarEditorFigureSetItemView';
|
||||||
|
|
||||||
|
export const AvatarEditorFigureSetView: FC<{
|
||||||
|
category: IAvatarEditorCategory
|
||||||
|
}> = props =>
|
||||||
|
{
|
||||||
|
const { category = null } = props;
|
||||||
|
const { selectedParts = null, selectEditorPart } = useAvatarEditor();
|
||||||
|
|
||||||
|
const isPartItemSelected = (partItem: IAvatarEditorCategoryPartItem) =>
|
||||||
|
{
|
||||||
|
if(!category || !category.setType || !selectedParts) return false;
|
||||||
|
|
||||||
|
if(!selectedParts[category.setType])
|
||||||
|
{
|
||||||
|
if(partItem.isClear) return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const partId = selectedParts[category.setType];
|
||||||
|
|
||||||
|
return (partId === partItem.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InfiniteGrid rows={ category.partItems } columnCount={ 3 } overscan={ 5 } itemRender={ (item: IAvatarEditorCategoryPartItem) =>
|
||||||
|
{
|
||||||
|
if(!item) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AvatarEditorFigureSetItemView setType={ category.setType } partItem={ item } isSelected={ isPartItemSelected(item) } onClick={ event => selectEditorPart(category.setType, item.partSet?.id ?? -1) } />
|
||||||
|
)
|
||||||
|
} } />
|
||||||
|
);
|
||||||
|
}
|
2
src/components/avatar-editor/figure-set/index.ts
Normal file
2
src/components/avatar-editor/figure-set/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './AvatarEditorFigureSetItemView';
|
||||||
|
export * from './AvatarEditorFigureSetView';
|
7
src/components/avatar-editor/index.ts
Normal file
7
src/components/avatar-editor/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export * from './AvatarEditorFigurePreviewView';
|
||||||
|
export * from './AvatarEditorIcon';
|
||||||
|
export * from './AvatarEditorModelView';
|
||||||
|
export * from './AvatarEditorView';
|
||||||
|
export * from './AvatarEditorWardrobeView';
|
||||||
|
export * from './figure-set';
|
||||||
|
export * from './palette-set';
|
@ -0,0 +1,23 @@
|
|||||||
|
import { ColorConverter, IPartColor } from '@nitrots/nitro-renderer';
|
||||||
|
import { FC } from 'react';
|
||||||
|
import { GetConfigurationValue } from '../../../api';
|
||||||
|
import { LayoutCurrencyIcon, LayoutGridItem, LayoutGridItemProps } from '../../../common';
|
||||||
|
|
||||||
|
export const AvatarEditorPaletteSetItem: FC<{
|
||||||
|
setType: string;
|
||||||
|
partColor: IPartColor;
|
||||||
|
isSelected: boolean;
|
||||||
|
} & LayoutGridItemProps> = props =>
|
||||||
|
{
|
||||||
|
const { setType = null, partColor = null, isSelected = false, ...rest } = props;
|
||||||
|
|
||||||
|
if(!partColor) return null;
|
||||||
|
|
||||||
|
const isHC = !GetConfigurationValue<boolean>('hc.disabled', false) && (partColor.clubLevel > 0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LayoutGridItem itemHighlight itemColor={ ColorConverter.int2rgb(partColor.rgb) } itemActive={ isSelected } className="clear-bg" { ...rest }>
|
||||||
|
{ isHC && <LayoutCurrencyIcon className="position-absolute end-1 bottom-1" type="hc" /> }
|
||||||
|
</LayoutGridItem>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
import { IPartColor } from '@nitrots/nitro-renderer';
|
||||||
|
import { FC } from 'react';
|
||||||
|
import { IAvatarEditorCategory } from '../../../api';
|
||||||
|
import { InfiniteGrid } from '../../../common';
|
||||||
|
import { useAvatarEditor } from '../../../hooks';
|
||||||
|
import { AvatarEditorPaletteSetItem } from './AvatarEditorPaletteSetItemView';
|
||||||
|
|
||||||
|
export const AvatarEditorPaletteSetView: FC<{
|
||||||
|
category: IAvatarEditorCategory,
|
||||||
|
paletteIndex: number;
|
||||||
|
}> = props =>
|
||||||
|
{
|
||||||
|
const { category = null, paletteIndex = -1 } = props;
|
||||||
|
const { selectedColorParts = null, selectEditorColor = null } = useAvatarEditor();
|
||||||
|
|
||||||
|
const isPartColorSelected = (partColor: IPartColor) =>
|
||||||
|
{
|
||||||
|
if(!category || !category.setType || !selectedColorParts || !selectedColorParts[category.setType] || !selectedColorParts[category.setType][paletteIndex]) return false;
|
||||||
|
|
||||||
|
const selectedColorPart = selectedColorParts[category.setType][paletteIndex];
|
||||||
|
|
||||||
|
return (selectedColorPart.id === partColor.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InfiniteGrid rows={ category.colorItems[paletteIndex] } columnCount={ 5 } overscan={ 5 } itemRender={ (item: IPartColor) =>
|
||||||
|
{
|
||||||
|
if(!item) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AvatarEditorPaletteSetItem setType={ category.setType } partColor={ item } isSelected={ isPartColorSelected(item) } onClick={ event => selectEditorColor(category.setType, paletteIndex, item.id) } />
|
||||||
|
)
|
||||||
|
} } />
|
||||||
|
);
|
||||||
|
}
|
2
src/components/avatar-editor/palette-set/index.ts
Normal file
2
src/components/avatar-editor/palette-set/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './AvatarEditorPaletteSetItemView';
|
||||||
|
export * from './AvatarEditorPaletteSetView';
|
@ -109,6 +109,12 @@
|
|||||||
|
|
||||||
.friend-bar-button {
|
.friend-bar-button {
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
margin: 0 0.2rem;
|
||||||
|
background-color: #d3d3d3;
|
||||||
|
color: #212131;
|
||||||
|
border: 2px solid #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
width: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.friend-bar-item {
|
.friend-bar-item {
|
||||||
@ -135,6 +141,10 @@
|
|||||||
|
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.avatar-image {
|
||||||
|
max-height: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
&.friend-bar-search {
|
&.friend-bar-search {
|
||||||
.friend-bar-item-head {
|
.friend-bar-item-head {
|
||||||
|
@ -33,7 +33,7 @@ export const FriendBarItemView: FC<{ friend: MessengerFriend }> = props =>
|
|||||||
if(!friend)
|
if(!friend)
|
||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
<div ref={ elementRef } className="btn btn-primary friend-bar-item friend-bar-search">
|
<div ref={ elementRef } className="btn btn-friendsgen friend-bar-item friend-bar-search">
|
||||||
<div className="friend-bar-item-head position-absolute"/>
|
<div className="friend-bar-item-head position-absolute"/>
|
||||||
<div className="text-truncate">{ LocalizeText('friend.bar.find.title') }</div>
|
<div className="text-truncate">{ LocalizeText('friend.bar.find.title') }</div>
|
||||||
</div>
|
</div>
|
||||||
@ -41,9 +41,9 @@ export const FriendBarItemView: FC<{ friend: MessengerFriend }> = props =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ elementRef } className={ 'btn btn-success friend-bar-item ' + (isVisible ? 'friend-bar-item-active' : '') } onClick={ event => setVisible(prevValue => !prevValue) }>
|
<div ref={ elementRef } className={ 'btn btn-friendsgensuccess friend-bar-item ' + (isVisible ? 'friend-bar-item-active' : '') } onClick={ event => setVisible(prevValue => !prevValue) }>
|
||||||
<div className={ `friend-bar-item-head position-absolute ${ friend.id > 0 ? 'avatar': 'group' }` }>
|
<div className={ `friend-bar-item-head position-absolute ${ friend.id > 0 ? 'avatar': 'group' }` }>
|
||||||
{ (friend.id > 0) && <LayoutAvatarImageView headOnly={ true } figure={ friend.figure } direction={ 2 } /> }
|
{ (friend.id > 0) && <LayoutAvatarImageView headOnly={ !isVisible } figure={ friend.figure } direction={ 2 } /> }
|
||||||
{ (friend.id <= 0) && <LayoutBadgeImageView isGroup={ true } badgeCode={ friend.figure } /> }
|
{ (friend.id <= 0) && <LayoutBadgeImageView isGroup={ true } badgeCode={ friend.figure } /> }
|
||||||
</div>
|
</div>
|
||||||
<div className="text-truncate">{ friend.name }</div>
|
<div className="text-truncate">{ friend.name }</div>
|
||||||
|
@ -3,8 +3,7 @@ import { FC, useEffect, useState } from 'react';
|
|||||||
import { Base, TransitionAnimation, TransitionAnimationTypes } from '../../common';
|
import { Base, TransitionAnimation, TransitionAnimationTypes } from '../../common';
|
||||||
import { useNitroEvent } from '../../hooks';
|
import { useNitroEvent } from '../../hooks';
|
||||||
import { AchievementsView } from '../achievements/AchievementsView';
|
import { AchievementsView } from '../achievements/AchievementsView';
|
||||||
import { AvatarEditorNewView } from '../avatar-editor-new/AvatarEditorView';
|
import { AvatarEditorView } from '../avatar-editor';
|
||||||
import { AvatarEditorView } from '../avatar-editor/AvatarEditorView';
|
|
||||||
import { CameraWidgetView } from '../camera/CameraWidgetView';
|
import { CameraWidgetView } from '../camera/CameraWidgetView';
|
||||||
import { CampaignView } from '../campaign/CampaignView';
|
import { CampaignView } from '../campaign/CampaignView';
|
||||||
import { CatalogView } from '../catalog/CatalogView';
|
import { CatalogView } from '../catalog/CatalogView';
|
||||||
@ -20,7 +19,7 @@ import { HotelView } from '../hotel-view/HotelView';
|
|||||||
import { InventoryView } from '../inventory/InventoryView';
|
import { InventoryView } from '../inventory/InventoryView';
|
||||||
import { ModToolsView } from '../mod-tools/ModToolsView';
|
import { ModToolsView } from '../mod-tools/ModToolsView';
|
||||||
import { NavigatorView } from '../navigator/NavigatorView';
|
import { NavigatorView } from '../navigator/NavigatorView';
|
||||||
import { NitrobubbleHiddenView } from '../nitrobubblehidden/NitrobubbleHiddenView';
|
import { NitrobubbleHiddenView } from '../nitrobubblehidden/NitrobubbleHiddenView';
|
||||||
import { NitropediaView } from '../nitropedia/NitropediaView';
|
import { NitropediaView } from '../nitropedia/NitropediaView';
|
||||||
import { RightSideView } from '../right-side/RightSideView';
|
import { RightSideView } from '../right-side/RightSideView';
|
||||||
import { RoomView } from '../room/RoomView';
|
import { RoomView } from '../room/RoomView';
|
||||||
@ -91,10 +90,9 @@ export const MainView: FC<{}> = props =>
|
|||||||
<ChatHistoryView />
|
<ChatHistoryView />
|
||||||
<WiredView />
|
<WiredView />
|
||||||
<AvatarEditorView />
|
<AvatarEditorView />
|
||||||
<AvatarEditorNewView />
|
|
||||||
<AchievementsView />
|
<AchievementsView />
|
||||||
<NavigatorView />
|
<NavigatorView />
|
||||||
<NitrobubbleHiddenView />
|
<NitrobubbleHiddenView />
|
||||||
<InventoryView />
|
<InventoryView />
|
||||||
<CatalogView />
|
<CatalogView />
|
||||||
<FriendsView />
|
<FriendsView />
|
||||||
|
@ -6,13 +6,13 @@ export const GetBubbleLayout = (item: NotificationBubbleItem, onClose: () => voi
|
|||||||
{
|
{
|
||||||
if(!item) return null;
|
if(!item) return null;
|
||||||
|
|
||||||
const props = { key: item.id, item, onClose };
|
const props = { item, onClose };
|
||||||
|
|
||||||
switch(item.notificationType)
|
switch(item.notificationType)
|
||||||
{
|
{
|
||||||
case NotificationBubbleType.CLUBGIFT:
|
case NotificationBubbleType.CLUBGIFT:
|
||||||
return <NotificationClubGiftBubbleView { ...props } />
|
return <NotificationClubGiftBubbleView key={ item.id } { ...props } />
|
||||||
default:
|
default:
|
||||||
return <NotificationDefaultBubbleView { ...props } />
|
return <NotificationDefaultBubbleView key={ item.id } { ...props } />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { GetAvatarRenderManager, GetSessionDataManager, RedeemItemClothingComposer, RoomObjectCategory, UserFigureComposer } from '@nitrots/nitro-renderer';
|
import { AvatarFigurePartType, GetAvatarRenderManager, GetSessionDataManager, RedeemItemClothingComposer, RoomObjectCategory, UserFigureComposer } from '@nitrots/nitro-renderer';
|
||||||
import { FC, useEffect, useState } from 'react';
|
import { FC, useEffect, useState } from 'react';
|
||||||
import { FigureData, FurniCategory, GetFurnitureDataForRoomObject, LocalizeText, SendMessageComposer } from '../../../../../api';
|
import { FurniCategory, GetFurnitureDataForRoomObject, LocalizeText, SendMessageComposer } from '../../../../../api';
|
||||||
import { Base, Button, Column, Flex, LayoutAvatarImageView, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../../common';
|
import { Base, Button, Column, Flex, LayoutAvatarImageView, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../../common';
|
||||||
import { useRoom } from '../../../../../hooks';
|
import { useRoom } from '../../../../../hooks';
|
||||||
|
|
||||||
@ -17,7 +17,7 @@ export const PurchasableClothingConfirmView: FC<PurchasableClothingConfirmViewPr
|
|||||||
{
|
{
|
||||||
const { objectId = -1, onClose = null } = props;
|
const { objectId = -1, onClose = null } = props;
|
||||||
const [ mode, setMode ] = useState(MODE_DEFAULT);
|
const [ mode, setMode ] = useState(MODE_DEFAULT);
|
||||||
const [ gender, setGender ] = useState<string>(FigureData.MALE);
|
const [ gender, setGender ] = useState<string>(AvatarFigurePartType.MALE);
|
||||||
const [ newFigure, setNewFigure ] = useState<string>(null);
|
const [ newFigure, setNewFigure ] = useState<string>(null);
|
||||||
const { roomSession = null } = useRoom();
|
const { roomSession = null } = useRoom();
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { AvatarEditorFigureCategory, FigureSetIdsMessageEvent, GetAvatarRenderManager, GetSessionDataManager, IFigurePartSet, IPartColor } from '@nitrots/nitro-renderer';
|
import { AvatarEditorFigureCategory, AvatarFigureContainer, AvatarFigurePartType, FigureSetIdsMessageEvent, GetAvatarRenderManager, GetSessionDataManager, GetWardrobeMessageComposer, IAvatarFigureContainer, IFigurePartSet, IPalette, IPartColor, SetType, UserWardrobePageEvent } from '@nitrots/nitro-renderer';
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useBetween } from 'use-between';
|
import { useBetween } from 'use-between';
|
||||||
import { AvatarEditorThumbnailsHelper, FigureData, GetClubMemberLevel, IAvatarEditorCategory, IAvatarEditorCategoryPartItem } from '../../api';
|
import { AvatarEditorColorSorter, AvatarEditorPartSorter, AvatarEditorThumbnailsHelper, GetClubMemberLevel, GetConfigurationValue, IAvatarEditorCategory, IAvatarEditorCategoryPartItem, Randomizer, SendMessageComposer } from '../../api';
|
||||||
import { useMessageEvent } from '../events';
|
import { useMessageEvent } from '../events';
|
||||||
import { useFigureData } from './useFigureData';
|
import { useFigureData } from './useFigureData';
|
||||||
|
|
||||||
@ -15,7 +15,8 @@ const useAvatarEditorState = () =>
|
|||||||
const [ maxPaletteCount, setMaxPaletteCount ] = useState<number>(1);
|
const [ maxPaletteCount, setMaxPaletteCount ] = useState<number>(1);
|
||||||
const [ figureSetIds, setFigureSetIds ] = useState<number[]>([]);
|
const [ figureSetIds, setFigureSetIds ] = useState<number[]>([]);
|
||||||
const [ boundFurnitureNames, setBoundFurnitureNames ] = useState<string[]>([]);
|
const [ boundFurnitureNames, setBoundFurnitureNames ] = useState<string[]>([]);
|
||||||
const { gender, selectedParts, selectedColors, loadAvatarData, selectPart, selectColor, getFigureStringWithFace } = useFigureData();
|
const [ savedFigures, setSavedFigures ] = useState<[ IAvatarFigureContainer, string ][]>(null);
|
||||||
|
const { selectedColors, gender, setGender, loadAvatarData, selectPart, selectColor, getFigureString, getFigureStringWithFace, selectedParts } = useFigureData();
|
||||||
|
|
||||||
const activeModel = useMemo(() => (avatarModels[activeModelKey] ?? null), [ activeModelKey, avatarModels ]);
|
const activeModel = useMemo(() => (avatarModels[activeModelKey] ?? null), [ activeModelKey, avatarModels ]);
|
||||||
|
|
||||||
@ -57,7 +58,8 @@ const useAvatarEditorState = () =>
|
|||||||
|
|
||||||
if(partItem.isClear)
|
if(partItem.isClear)
|
||||||
{
|
{
|
||||||
// clear the part
|
selectPart(setType, -1);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,6 +91,109 @@ const useAvatarEditorState = () =>
|
|||||||
selectColor(setType, paletteId, colorId);
|
selectColor(setType, paletteId, colorId);
|
||||||
}, [ activeModel, selectColor ]);
|
}, [ activeModel, selectColor ]);
|
||||||
|
|
||||||
|
const getFirstSelectableColor = useCallback((setType: string) =>
|
||||||
|
{
|
||||||
|
const set = GetAvatarRenderManager().structureData.getSetType(setType);
|
||||||
|
|
||||||
|
if(!setType) return -1;
|
||||||
|
|
||||||
|
const palette = GetAvatarRenderManager().structureData.getPalette(set.paletteID);
|
||||||
|
|
||||||
|
if(!palette) return -1;
|
||||||
|
|
||||||
|
for(const color of palette.colors.getValues())
|
||||||
|
{
|
||||||
|
if(!color.isSelectable || (GetClubMemberLevel() < color.clubLevel)) continue;
|
||||||
|
|
||||||
|
return color.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const randomizeCurrentFigure = useCallback((ignoredSets: string[] = []) =>
|
||||||
|
{
|
||||||
|
const structure = GetAvatarRenderManager().structure;
|
||||||
|
const figureContainer = new AvatarFigureContainer('');
|
||||||
|
|
||||||
|
const getRandomSetTypes = (requiredSets: string[], options: string[]) =>
|
||||||
|
{
|
||||||
|
options = options.filter(option => (requiredSets.indexOf(option) === -1));
|
||||||
|
|
||||||
|
return [ ...requiredSets, ...Randomizer.getRandomElements(options, (Randomizer.getRandomNumber(options.length) + 1)) ];
|
||||||
|
}
|
||||||
|
|
||||||
|
const requiredSets = getRandomSetTypes(structure.getMandatorySetTypeIds(gender, GetClubMemberLevel()), AvatarFigurePartType.FIGURE_SETS);
|
||||||
|
|
||||||
|
const getRandomPartSet = (setType: SetType, gender: string, clubLevel: number, figureSetIds: number[]) =>
|
||||||
|
{
|
||||||
|
const options = setType.partSets.getValues().filter(option =>
|
||||||
|
{
|
||||||
|
if(!option.isSelectable || ((option.gender !== 'U') && (option.gender !== gender)) || (option.clubLevel > clubLevel) || (option.isSellable && (figureSetIds.indexOf(option.id) === -1))) return null;
|
||||||
|
|
||||||
|
return option;
|
||||||
|
});
|
||||||
|
|
||||||
|
if(!options || !options.length) return null;
|
||||||
|
|
||||||
|
return Randomizer.getRandomElement(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
const getRandomColors = (palette: IPalette, partSet: IFigurePartSet, clubLevel: number) =>
|
||||||
|
{
|
||||||
|
const options = palette.colors.getValues().filter(option =>
|
||||||
|
{
|
||||||
|
if(!option.isSelectable || (option.clubLevel > clubLevel)) return null;
|
||||||
|
|
||||||
|
return option;
|
||||||
|
});
|
||||||
|
|
||||||
|
if(!options || !options.length) return null;
|
||||||
|
|
||||||
|
const getTotalColors = (partSet: IFigurePartSet) =>
|
||||||
|
{
|
||||||
|
const parts = partSet.parts;
|
||||||
|
|
||||||
|
let totalColors = 0;
|
||||||
|
|
||||||
|
for(const part of parts) totalColors = Math.max(totalColors, part.colorLayerIndex);
|
||||||
|
|
||||||
|
return totalColors;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Randomizer.getRandomElements(options, getTotalColors(partSet));
|
||||||
|
}
|
||||||
|
|
||||||
|
for(const setType of ignoredSets)
|
||||||
|
{
|
||||||
|
const partSetId = selectedParts[setType];
|
||||||
|
const colors = selectedColors[setType];
|
||||||
|
|
||||||
|
figureContainer.updatePart(setType, partSetId, colors);
|
||||||
|
}
|
||||||
|
|
||||||
|
for(const type of requiredSets)
|
||||||
|
{
|
||||||
|
if(figureContainer.hasPartType(type)) continue;
|
||||||
|
|
||||||
|
const setType = (structure.figureData.getSetType(type) as SetType);
|
||||||
|
const selectedSet = getRandomPartSet(setType, gender, GetClubMemberLevel(), figureSetIds);
|
||||||
|
|
||||||
|
if(!selectedSet) continue;
|
||||||
|
|
||||||
|
let selectedColors: number[] = [];
|
||||||
|
|
||||||
|
if(selectedSet.isColorable)
|
||||||
|
{
|
||||||
|
selectedColors = getRandomColors(structure.figureData.getPalette(setType.paletteID), selectedSet, GetClubMemberLevel()).map(color => color.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
figureContainer.updatePart(setType.type, selectedSet.id, selectedColors);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadAvatarData(figureContainer.getFigureString(), gender);
|
||||||
|
}, [ figureSetIds, gender, loadAvatarData, selectedColors, selectedParts ]);
|
||||||
|
|
||||||
useMessageEvent<FigureSetIdsMessageEvent>(FigureSetIdsMessageEvent, event =>
|
useMessageEvent<FigureSetIdsMessageEvent>(FigureSetIdsMessageEvent, event =>
|
||||||
{
|
{
|
||||||
const parser = event.getParser();
|
const parser = event.getParser();
|
||||||
@ -97,6 +202,30 @@ const useAvatarEditorState = () =>
|
|||||||
setBoundFurnitureNames(parser.boundsFurnitureNames);
|
setBoundFurnitureNames(parser.boundsFurnitureNames);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useMessageEvent<UserWardrobePageEvent>(UserWardrobePageEvent, event =>
|
||||||
|
{
|
||||||
|
const parser = event.getParser();
|
||||||
|
const savedFigures: [ IAvatarFigureContainer, string ][] = [];
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
while(i < GetConfigurationValue<number>('avatar.wardrobe.max.slots', 10))
|
||||||
|
{
|
||||||
|
savedFigures.push([ null, null ]);
|
||||||
|
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
for(let [ index, [ look, gender ] ] of parser.looks.entries())
|
||||||
|
{
|
||||||
|
const container = GetAvatarRenderManager().createFigureContainer(look);
|
||||||
|
|
||||||
|
savedFigures[(index - 1)] = [ container, gender ];
|
||||||
|
}
|
||||||
|
|
||||||
|
setSavedFigures(savedFigures);
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
AvatarEditorThumbnailsHelper.clearCache();
|
AvatarEditorThumbnailsHelper.clearCache();
|
||||||
@ -125,19 +254,6 @@ const useAvatarEditorState = () =>
|
|||||||
if(!partColor || !partColor.isSelectable) continue;
|
if(!partColor || !partColor.isSelectable) continue;
|
||||||
|
|
||||||
for(let i = 0; i < MAX_PALETTES; i++) colorItems[i].push(partColor);
|
for(let i = 0; i < MAX_PALETTES; i++) colorItems[i].push(partColor);
|
||||||
|
|
||||||
// TODO - check what this does
|
|
||||||
/* if(setType !== FigureData.FACE)
|
|
||||||
{
|
|
||||||
let i = 0;
|
|
||||||
|
|
||||||
while(i < colorIds.length)
|
|
||||||
{
|
|
||||||
if(partColor.id === colorIds[i]) partColors[i] = partColor;
|
|
||||||
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
} */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mandatorySetIds: string[] = GetAvatarRenderManager().getMandatoryAvatarPartSetIds(gender, GetClubMemberLevel());
|
let mandatorySetIds: string[] = GetAvatarRenderManager().getMandatoryAvatarPartSetIds(gender, GetClubMemberLevel());
|
||||||
@ -146,14 +262,14 @@ const useAvatarEditorState = () =>
|
|||||||
|
|
||||||
if(isntMandatorySet) partItems.push({ id: -1, isClear: true });
|
if(isntMandatorySet) partItems.push({ id: -1, isClear: true });
|
||||||
|
|
||||||
const usesColor = (setType !== FigureData.FACE);
|
const usesColor = (setType !== AvatarFigurePartType.HEAD);
|
||||||
const partSets = set.partSets;
|
const partSets = set.partSets;
|
||||||
|
|
||||||
for(let i = (partSets.length); i >= 0; i--)
|
for(let i = (partSets.length); i >= 0; i--)
|
||||||
{
|
{
|
||||||
const partSet = partSets.getWithIndex(i);
|
const partSet = partSets.getWithIndex(i);
|
||||||
|
|
||||||
if(!partSet || !partSet.isSelectable || ((partSet.gender !== gender) && (partSet.gender !== FigureData.UNISEX))) continue;
|
if(!partSet || !partSet.isSelectable || ((partSet.gender !== gender) && (partSet.gender !== AvatarFigurePartType.UNISEX))) continue;
|
||||||
|
|
||||||
if(partSet.isSellable && figureSetIds.indexOf(partSet.id) === -1) continue;
|
if(partSet.isSellable && figureSetIds.indexOf(partSet.id) === -1) continue;
|
||||||
|
|
||||||
@ -164,17 +280,17 @@ const useAvatarEditorState = () =>
|
|||||||
partItems.push({ id: partSet.id, partSet, usesColor, maxPaletteCount });
|
partItems.push({ id: partSet.id, partSet, usesColor, maxPaletteCount });
|
||||||
}
|
}
|
||||||
|
|
||||||
partItems.sort(partSorter(false));
|
partItems.sort(AvatarEditorPartSorter(false));
|
||||||
|
|
||||||
for(let i = 0; i < MAX_PALETTES; i++) colorItems[i].sort(colorSorter);
|
for(let i = 0; i < MAX_PALETTES; i++) colorItems[i].sort(AvatarEditorColorSorter);
|
||||||
|
|
||||||
return { setType, partItems, colorItems };
|
return { setType, partItems, colorItems };
|
||||||
}
|
}
|
||||||
|
|
||||||
newAvatarModels[AvatarEditorFigureCategory.GENERIC] = [ FigureData.FACE ].map(setType => buildCategory(setType));
|
newAvatarModels[AvatarEditorFigureCategory.GENERIC] = [ AvatarFigurePartType.HEAD ].map(setType => buildCategory(setType));
|
||||||
newAvatarModels[AvatarEditorFigureCategory.HEAD] = [ FigureData.HAIR, FigureData.HAT, FigureData.HEAD_ACCESSORIES, FigureData.EYE_ACCESSORIES, FigureData.FACE_ACCESSORIES ].map(setType => buildCategory(setType));
|
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] = [ FigureData.SHIRT, FigureData.CHEST_PRINTS, FigureData.JACKET, FigureData.CHEST_ACCESSORIES ].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] = [ FigureData.TROUSERS, FigureData.SHOES, FigureData.TROUSER_ACCESSORIES ].map(setType => buildCategory(setType));
|
newAvatarModels[AvatarEditorFigureCategory.LEGS] = [ AvatarFigurePartType.LEGS, AvatarFigurePartType.SHOES, AvatarFigurePartType.WAIST_ACCESSORY ].map(setType => buildCategory(setType));
|
||||||
newAvatarModels[AvatarEditorFigureCategory.WARDROBE] = [];
|
newAvatarModels[AvatarEditorFigureCategory.WARDROBE] = [];
|
||||||
|
|
||||||
setAvatarModels(newAvatarModels);
|
setAvatarModels(newAvatarModels);
|
||||||
@ -188,57 +304,15 @@ const useAvatarEditorState = () =>
|
|||||||
loadAvatarData(GetSessionDataManager().figure, GetSessionDataManager().gender);
|
loadAvatarData(GetSessionDataManager().figure, GetSessionDataManager().gender);
|
||||||
}, [ isVisible, loadAvatarData ]);
|
}, [ isVisible, loadAvatarData ]);
|
||||||
|
|
||||||
return { isVisible, setIsVisible, avatarModels, activeModelKey, setActiveModelKey, selectedParts, selectedColors, maxPaletteCount, selectedColorParts, selectEditorPart, selectEditorColor, getFigureStringWithFace };
|
useEffect(() =>
|
||||||
|
{
|
||||||
|
if(!isVisible || savedFigures) return;
|
||||||
|
|
||||||
|
setSavedFigures(new Array(GetConfigurationValue<number>('avatar.wardrobe.max.slots', 10)));
|
||||||
|
SendMessageComposer(new GetWardrobeMessageComposer());
|
||||||
|
}, [ isVisible, savedFigures ]);
|
||||||
|
|
||||||
|
return { isVisible, setIsVisible, avatarModels, activeModelKey, setActiveModelKey, maxPaletteCount, selectedColorParts, selectEditorColor, selectEditorPart, loadAvatarData, getFigureString, getFigureStringWithFace, selectedParts, gender, setGender, figureSetIds, randomizeCurrentFigure, savedFigures, setSavedFigures, getFirstSelectableColor };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useAvatarEditor = () => useBetween(useAvatarEditorState);
|
export const useAvatarEditor = () => useBetween(useAvatarEditorState);
|
||||||
|
|
||||||
const partSorter = (hcFirst: boolean) =>
|
|
||||||
{
|
|
||||||
return (a: { partSet: IFigurePartSet, usesColor: boolean, isClear?: boolean }, b: { partSet: IFigurePartSet, usesColor: boolean, isClear?: boolean }) =>
|
|
||||||
{
|
|
||||||
const clubLevelA = (!a.partSet ? -1 : a.partSet.clubLevel);
|
|
||||||
const clubLevelB = (!b.partSet ? -1 : b.partSet.clubLevel);
|
|
||||||
const isSellableA = (!a.partSet ? false : a.partSet.isSellable);
|
|
||||||
const isSellableB = (!b.partSet ? false : b.partSet.isSellable);
|
|
||||||
|
|
||||||
if(isSellableA && !isSellableB) return 1;
|
|
||||||
|
|
||||||
if(isSellableB && !isSellableA) return -1;
|
|
||||||
|
|
||||||
if(hcFirst)
|
|
||||||
{
|
|
||||||
if(clubLevelA > clubLevelB) return -1;
|
|
||||||
|
|
||||||
if(clubLevelA < clubLevelB) return 1;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if(clubLevelA < clubLevelB) return -1;
|
|
||||||
|
|
||||||
if(clubLevelA > clubLevelB) return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(a.partSet.id < b.partSet.id) return -1;
|
|
||||||
|
|
||||||
if(a.partSet.id > b.partSet.id) return 1;
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const colorSorter = (a: IPartColor, b: IPartColor) =>
|
|
||||||
{
|
|
||||||
const clubLevelA = (!a ? -1 : a.clubLevel);
|
|
||||||
const clubLevelB = (!b ? -1 : b.clubLevel);
|
|
||||||
|
|
||||||
if(clubLevelA < clubLevelB) return -1;
|
|
||||||
|
|
||||||
if(clubLevelA > clubLevelB) return 1;
|
|
||||||
|
|
||||||
if(a.index < b.index) return -1;
|
|
||||||
|
|
||||||
if(a.index > b.index) return 1;
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { useCallback, useState } from 'react';
|
import { AvatarFigurePartType } from '@nitrots/nitro-renderer';
|
||||||
import { FigureData } from '../../api';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
|
|
||||||
const useFigureDataState = () =>
|
const useFigureDataState = () =>
|
||||||
{
|
{
|
||||||
const [ selectedParts, setSelectedParts ] = useState<{ [index: string]: number }>({});
|
const [ selectedParts, setSelectedParts ] = useState<{ [index: string]: number }>({});
|
||||||
const [ selectedColors, setSelectedColors ] = useState<{ [index: string]: number[] }>({});
|
const [ selectedColors, setSelectedColors ] = useState<{ [index: string]: number[] }>({});
|
||||||
const [ gender, setGender ] = useState<string>(FigureData.MALE);
|
const [ gender, setGender ] = useState<string>(AvatarFigurePartType.MALE);
|
||||||
|
|
||||||
const loadAvatarData = useCallback((figureString: string, gender: string) =>
|
const loadAvatarData = useCallback((figureString: string, gender: string) =>
|
||||||
{
|
{
|
||||||
@ -62,7 +62,8 @@ const useFigureDataState = () =>
|
|||||||
{
|
{
|
||||||
const newValue = { ...prevValue };
|
const newValue = { ...prevValue };
|
||||||
|
|
||||||
newValue[setType] = partId;
|
if(partId === -1) delete newValue[setType];
|
||||||
|
else newValue[setType] = partId;
|
||||||
|
|
||||||
return newValue;
|
return newValue;
|
||||||
});
|
});
|
||||||
@ -86,29 +87,64 @@ const useFigureDataState = () =>
|
|||||||
})
|
})
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const getFigureString = useMemo(() =>
|
||||||
|
{
|
||||||
|
let figureString = '';
|
||||||
|
|
||||||
|
const partSets: string[] = [];
|
||||||
|
const setTypes = Object.keys(selectedParts);
|
||||||
|
|
||||||
|
for(const setType of setTypes)
|
||||||
|
{
|
||||||
|
const partId = selectedParts[setType];
|
||||||
|
|
||||||
|
if(!partId) continue;
|
||||||
|
|
||||||
|
let setPart = `${ setType }-${ partId }`;
|
||||||
|
|
||||||
|
if(selectedColors[setType] && selectedColors[setType].length)
|
||||||
|
{
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
while(i < selectedColors[setType].length)
|
||||||
|
{
|
||||||
|
setPart += `-${ selectedColors[setType][i] }`;
|
||||||
|
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
partSets.push(setPart);
|
||||||
|
}
|
||||||
|
|
||||||
|
for(const partSet of partSets)
|
||||||
|
{
|
||||||
|
figureString += partSet;
|
||||||
|
|
||||||
|
if(partSets.indexOf(partSet) < (partSets.length - 1)) figureString += '.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return figureString;
|
||||||
|
}, [ selectedParts, selectedColors ]);
|
||||||
|
|
||||||
const getFigureStringWithFace = useCallback((overridePartId: number, override: boolean = true) =>
|
const getFigureStringWithFace = useCallback((overridePartId: number, override: boolean = true) =>
|
||||||
{
|
{
|
||||||
const figureSets = [ FigureData.FACE ].map(setType =>
|
const figureSets = [ AvatarFigurePartType.HEAD ].map(setType =>
|
||||||
{
|
{
|
||||||
// Determine the part ID, with an option to override if the set type matches.
|
let partId = (setType === AvatarFigurePartType.HEAD && override) ? overridePartId : selectedParts[setType];
|
||||||
let partId = (setType === FigureData.FACE && override) ? overridePartId : selectedParts[setType];
|
|
||||||
const colors = selectedColors[setType] || [];
|
const colors = selectedColors[setType] || [];
|
||||||
|
|
||||||
// Construct the figure set string, including the type, part ID, and any colors.
|
|
||||||
let figureSet = `${ setType }-${ partId }`;
|
let figureSet = `${ setType }-${ partId }`;
|
||||||
if (partId >= 0)
|
|
||||||
{
|
if (partId >= 0) figureSet += colors.map(color => `-${ color }`).join('');
|
||||||
figureSet += colors.map(color => `-${ color }`).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
return figureSet;
|
return figureSet;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Join all figure sets with '.', ensuring to only add '.' between items, not at the end.
|
|
||||||
return figureSets.join('.');
|
return figureSets.join('.');
|
||||||
}, [ selectedParts, selectedColors ]);
|
}, [ selectedParts, selectedColors ]);
|
||||||
|
|
||||||
return { selectedParts, selectedColors, gender, loadAvatarData, selectPart, selectColor, getFigureStringWithFace };
|
return { selectedParts, selectedColors, gender, setGender, loadAvatarData, selectPart, selectColor, getFigureString, getFigureStringWithFace };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useFigureData = useFigureDataState;
|
export const useFigureData = useFigureDataState;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user