From 9da0e32702926864a07e3824ae5ed469ef1df1ab Mon Sep 17 00:00:00 2001 From: duckietm Date: Thu, 4 Apr 2024 09:25:08 +0200 Subject: [PATCH] Added: Friendbar and updates Avatar Editor --- src/api/avatar/AvatarEditorColorSorter.ts | 17 ++ src/api/avatar/AvatarEditorPartSorter.ts | 35 +++ .../avatar/AvatarEditorThumbnailsHelper.ts | 13 +- src/api/avatar/index.ts | 14 +- src/common/InfiniteGrid.tsx | 17 +- .../AvatarEditorFigurePreviewView.tsx | 40 +++ .../avatar-editor/AvatarEditorIcon.tsx | 27 ++ .../avatar-editor/AvatarEditorModelView.tsx | 81 ++++++ .../avatar-editor/AvatarEditorView.tsx | 258 +++--------------- .../AvatarEditorWardrobeView.tsx | 60 ++++ .../AvatarEditorFigureSetItemView.tsx | 54 ++++ .../figure-set/AvatarEditorFigureSetView.tsx | 40 +++ .../avatar-editor/figure-set/index.ts | 2 + src/components/avatar-editor/index.ts | 7 + .../AvatarEditorPaletteSetItemView.tsx | 23 ++ .../AvatarEditorPaletteSetView.tsx | 35 +++ .../avatar-editor/palette-set/index.ts | 2 + src/components/friends/FriendsView.scss | 10 + .../views/friends-bar/FriendBarItemView.tsx | 6 +- src/components/main/MainView.tsx | 8 +- .../views/bubble-layouts/GetBubbleLayout.tsx | 6 +- .../PurchasableClothingConfirmView.tsx | 6 +- src/hooks/avatar-editor/useAvatarEditor.ts | 226 +++++++++------ src/hooks/avatar-editor/useFigureData.ts | 66 ++++- 24 files changed, 699 insertions(+), 354 deletions(-) create mode 100644 src/api/avatar/AvatarEditorColorSorter.ts create mode 100644 src/api/avatar/AvatarEditorPartSorter.ts create mode 100644 src/components/avatar-editor/AvatarEditorFigurePreviewView.tsx create mode 100644 src/components/avatar-editor/AvatarEditorIcon.tsx create mode 100644 src/components/avatar-editor/AvatarEditorModelView.tsx create mode 100644 src/components/avatar-editor/AvatarEditorWardrobeView.tsx create mode 100644 src/components/avatar-editor/figure-set/AvatarEditorFigureSetItemView.tsx create mode 100644 src/components/avatar-editor/figure-set/AvatarEditorFigureSetView.tsx create mode 100644 src/components/avatar-editor/figure-set/index.ts create mode 100644 src/components/avatar-editor/index.ts create mode 100644 src/components/avatar-editor/palette-set/AvatarEditorPaletteSetItemView.tsx create mode 100644 src/components/avatar-editor/palette-set/AvatarEditorPaletteSetView.tsx create mode 100644 src/components/avatar-editor/palette-set/index.ts diff --git a/src/api/avatar/AvatarEditorColorSorter.ts b/src/api/avatar/AvatarEditorColorSorter.ts new file mode 100644 index 0000000..d64057a --- /dev/null +++ b/src/api/avatar/AvatarEditorColorSorter.ts @@ -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; +} diff --git a/src/api/avatar/AvatarEditorPartSorter.ts b/src/api/avatar/AvatarEditorPartSorter.ts new file mode 100644 index 0000000..276108f --- /dev/null +++ b/src/api/avatar/AvatarEditorPartSorter.ts @@ -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; + } +} diff --git a/src/api/avatar/AvatarEditorThumbnailsHelper.ts b/src/api/avatar/AvatarEditorThumbnailsHelper.ts index 6be9b9b..c5eabaa 100644 --- a/src/api/avatar/AvatarEditorThumbnailsHelper.ts +++ b/src/api/avatar/AvatarEditorThumbnailsHelper.ts @@ -1,5 +1,4 @@ -import { AvatarFigurePartType, AvatarScaleType, AvatarSetType, GetAssetManager, GetAvatarRenderManager, IFigurePart, IGraphicAsset, IPartColor, NitroAlphaFilter, NitroContainer, NitroSprite, TextureUtils } from '@nitrots/nitro-renderer'; -import { FigureData } from './FigureData'; +import { AvatarFigurePartType, AvatarScaleType, AvatarSetType, GetAssetManager, GetAvatarRenderManager, IFigurePart, IGraphicAsset, IPartColor, NitroAlphaFilter, NitroContainer, NitroRectangle, NitroSprite, TextureUtils } from '@nitrots/nitro-renderer'; import { IAvatarEditorCategoryPartItem } from './IAvatarEditorCategoryPartItem'; export class AvatarEditorThumbnailsHelper @@ -69,7 +68,7 @@ export class AvatarEditorThumbnailsHelper 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); @@ -150,19 +149,23 @@ export class AvatarEditorThumbnailsHelper const resetFigure = async (figure: string) => { 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 sprite = new NitroSprite(texture); if(isDisabled) sprite.filters = [ AvatarEditorThumbnailsHelper.ALPHA_FILTER ]; const imageUrl = await TextureUtils.generateImageUrl({ - target: sprite + target: sprite, + frame: new NitroRectangle(0, 0, texture.width, texture.height) }); sprite.destroy(); avatarImage.dispose(); - if(!avatarImage.isPlaceholder()) AvatarEditorThumbnailsHelper.THUMBNAIL_CACHE.set(thumbnailKey, imageUrl); + AvatarEditorThumbnailsHelper.THUMBNAIL_CACHE.set(thumbnailKey, imageUrl); resolve(imageUrl); } diff --git a/src/api/avatar/index.ts b/src/api/avatar/index.ts index acf945b..415185e 100644 --- a/src/api/avatar/index.ts +++ b/src/api/avatar/index.ts @@ -1,16 +1,6 @@ export * from './AvatarEditorAction'; -export * from './AvatarEditorGridColorItem'; -export * from './AvatarEditorGridPartItem'; +export * from './AvatarEditorColorSorter'; +export * from './AvatarEditorPartSorter'; 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 './IAvatarEditorCategoryModel'; export * from './IAvatarEditorCategoryPartItem'; -export * from './LegModel'; -export * from './TorsoModel'; diff --git a/src/common/InfiniteGrid.tsx b/src/common/InfiniteGrid.tsx index 0103caf..4c791a3 100644 --- a/src/common/InfiniteGrid.tsx +++ b/src/common/InfiniteGrid.tsx @@ -8,19 +8,20 @@ interface InfiniteGridProps rows: T[]; columnCount: number; overscan?: number; - itemRender?: (item: T) => ReactElement; + estimateSize?: number; + itemRender?: (item: T, index?: number) => ReactElement; } export const InfiniteGrid: FC = props => { - const { rows = [], columnCount = 4, overscan = 5, itemRender = null } = props; + const { rows = [], columnCount = 4, overscan = 5, estimateSize = 45, itemRender = null } = props; const parentRef = useRef(null); const virtualizer = useVirtualizer({ count: Math.ceil(rows.length / columnCount), overscan, getScrollElement: () => parentRef.current, - estimateSize: () => 45, + estimateSize: () => estimateSize }); useEffect(() => @@ -58,7 +59,7 @@ export const InfiniteGrid: FC = props => style={ { display: 'grid', gap: '0.25rem', - minHeight: virtualRow.index === 0 ? 45 : virtualRow.size, + minHeight: virtualRow.index === 0 ? estimateSize : virtualRow.size, gridTemplateColumns: `repeat(${ columnCount }, 1fr)` } }> { Array.from(Array(columnCount)).map((e,i) => @@ -67,8 +68,12 @@ export const InfiniteGrid: FC = props => if(!item) return ; - - return itemRender(item); + + return ( + + { itemRender(item, i) } + + ); }) } )) } diff --git a/src/components/avatar-editor/AvatarEditorFigurePreviewView.tsx b/src/components/avatar-editor/AvatarEditorFigurePreviewView.tsx new file mode 100644 index 0000000..9efae75 --- /dev/null +++ b/src/components/avatar-editor/AvatarEditorFigurePreviewView.tsx @@ -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(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 ( + + + + + + rotateFigure(direction + 1) } /> + rotateFigure(direction - 1) } /> + + + ); +} diff --git a/src/components/avatar-editor/AvatarEditorIcon.tsx b/src/components/avatar-editor/AvatarEditorIcon.tsx new file mode 100644 index 0000000..878018c --- /dev/null +++ b/src/components/avatar-editor/AvatarEditorIcon.tsx @@ -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> = 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 +} diff --git a/src/components/avatar-editor/AvatarEditorModelView.tsx b/src/components/avatar-editor/AvatarEditorModelView.tsx new file mode 100644 index 0000000..876ca72 --- /dev/null +++ b/src/components/avatar-editor/AvatarEditorModelView.tsx @@ -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(false); + const [ activeSetType, setActiveSetType ] = useState(''); + 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 ( + + + { (name === AvatarEditorFigureCategory.GENERIC) && + <> + setGender(AvatarFigurePartType.MALE) }> + + + setGender(AvatarFigurePartType.FEMALE) }> + + + } + { (name !== AvatarEditorFigureCategory.GENERIC) && (categories.length > 0) && categories.map(category => + { + return ( + selectSet(category.setType) }> + + + ); + }) } + + + + + + { (maxPaletteCount >= 1) && + } + { (maxPaletteCount === 2) && + } + + + ); +} diff --git a/src/components/avatar-editor/AvatarEditorView.tsx b/src/components/avatar-editor/AvatarEditorView.tsx index f2b9c0b..db00b34 100644 --- a/src/components/avatar-editor/AvatarEditorView.tsx +++ b/src/components/avatar-editor/AvatarEditorView.tsx @@ -1,12 +1,12 @@ -import { AvatarEditorFigureCategory, FigureSetIdsMessageEvent, GetAvatarRenderManager, GetSessionDataManager, GetWardrobeMessageComposer, IAvatarFigureContainer, UserFigureComposer, UserWardrobePageEvent } from '@nitrots/nitro-renderer'; -import { FC, useCallback, useEffect, useMemo, useState } from 'react'; -import { FaDice, FaTrash, FaUndo } from 'react-icons/fa'; -import { AvatarEditorAction, AvatarEditorUtilities, BodyModel, FigureData, GetClubMemberLevel, GetConfigurationValue, HeadModel, IAvatarEditorCategoryModel, LegModel, LocalizeText, SendMessageComposer, TorsoModel, generateRandomFigure } from '../../api'; +import { AddLinkEventTracker, AvatarEditorFigureCategory, GetSessionDataManager, ILinkEventTracker, RemoveLinkEventTracker, UserFigureComposer } from '@nitrots/nitro-renderer'; +import { FC, useEffect, useState } from 'react'; +import { FaDice, FaRedo, FaTrash } from 'react-icons/fa'; +import { AvatarEditorAction, LocalizeText, SendMessageComposer } from '../../api'; import { Button, ButtonGroup, Column, Grid, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../common'; -import { useMessageEvent } from '../../hooks'; -import { AvatarEditorFigurePreviewView } from './views/AvatarEditorFigurePreviewView'; -import { AvatarEditorModelView } from './views/AvatarEditorModelView'; -import { AvatarEditorWardrobeView } from './views/AvatarEditorWardrobeView'; +import { useAvatarEditor } from '../../hooks'; +import { AvatarEditorFigurePreviewView } from './AvatarEditorFigurePreviewView'; +import { AvatarEditorModelView } from './AvatarEditorModelView'; +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_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 => { const [ isVisible, setIsVisible ] = useState(false); - const [ figures, setFigures ] = useState>(null); - const [ figureData, setFigureData ] = useState(null); - const [ categories, setCategories ] = useState>(null); - const [ activeCategory, setActiveCategory ] = useState(null); - const [ figureSetIds, setFigureSetIds ] = useState([]); - const [ boundFurnitureNames, setBoundFurnitureNames ] = useState([]); - const [ savedFigures, setSavedFigures ] = useState<[ IAvatarFigureContainer, string ][]>([]); - const [ isWardrobeVisible, setIsWardrobeVisible ] = useState(false); - const [ lastFigure, setLastFigure ] = useState(null); - const [ lastGender, setLastGender ] = useState(null); - const [ needsReset, setNeedsReset ] = useState(true); - const [ isInitalized, setIsInitalized ] = useState(false); + const { setIsVisible: setEditorVisibility, avatarModels, activeModelKey, setActiveModelKey, loadAvatarData, getFigureStringWithFace, gender, figureSetIds = [], randomizeCurrentFigure = null, getFigureString = null } = useAvatarEditor(); - const maxWardrobeSlots = useMemo(() => GetConfigurationValue('avatar.wardrobe.max.slots', 10), []); - - useMessageEvent(FigureSetIdsMessageEvent, event => - { - const parser = event.getParser(); - - setFigureSetIds(parser.figureSetIds); - setBoundFurnitureNames(parser.boundsFurnitureNames); - }); - - useMessageEvent(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 = 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) => + const processAction = (action: string) => { switch(action) { - case AvatarEditorAction.ACTION_CLEAR: - loadAvatarInEditor(figureData.getFigureStringWithFace(0, false), figureData.gender, false); - resetCategories(); - return; case AvatarEditorAction.ACTION_RESET: - loadAvatarInEditor(lastFigure, lastGender); - resetCategories(); + loadAvatarData(GetSessionDataManager().figure, GetSessionDataManager().gender); + return; + case AvatarEditorAction.ACTION_CLEAR: + loadAvatarData(getFigureStringWithFace(0, false), gender); return; case AvatarEditorAction.ACTION_RANDOMIZE: - const figure = generateRandomFigure(figureData, figureData.gender, GetClubMemberLevel(), figureSetIds, [ FigureData.FACE ]); - - loadAvatarInEditor(figure, figureData.gender, false); - resetCategories(); + randomizeCurrentFigure(); return; case AvatarEditorAction.ACTION_SAVE: - SendMessageComposer(new UserFigureComposer(figureData.gender, figureData.getFigureString())); + SendMessageComposer(new UserFigureComposer(gender, getFigureString)); setIsVisible(false); return; } - }, [ figureData, lastFigure, lastGender, figureSetIds, loadAvatarInEditor, resetCategories ]) + } - const setGender = useCallback((gender: string) => - { - gender = AvatarEditorUtilities.getGender(gender); - - setFigureData(figures.get(gender)); - }, [ figures ]); - - /* useEffect(() => + useEffect(() => { const linkTracker: ILinkEventTracker = { linkReceived: (url: string) => @@ -176,126 +64,44 @@ export const AvatarEditorView: FC<{}> = props => AddLinkEventTracker(linkTracker); return () => RemoveLinkEventTracker(linkTracker); - }, []); */ + }, []); useEffect(() => { - setSavedFigures(new Array(maxWardrobeSlots)); - }, [ maxWardrobeSlots ]); + setEditorVisibility(isVisible) + }, [ isVisible, setEditorVisibility ]); - useEffect(() => - { - if(!isWardrobeVisible) return; - - setActiveCategory(null); - SendMessageComposer(new GetWardrobeMessageComposer()); - }, [ isWardrobeVisible ]); - - useEffect(() => - { - if(!activeCategory) return; - - setIsWardrobeVisible(false); - }, [ activeCategory ]); - - useEffect(() => - { - if(!categories) return; - - selectCategory(AvatarEditorFigureCategory.GENERIC); - }, [ categories, selectCategory ]); - - 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; + if(!isVisible) return null; return ( setIsVisible(false) } /> - { 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 ( - selectCategory(category) }> - { LocalizeText(`avatareditor.category.${ category }`) } + setActiveModelKey(modelKey) }> + { LocalizeText(`avatareditor.category.${ modelKey }`) } ); }) } - setIsWardrobeVisible(true) }> - { LocalizeText('avatareditor.category.wardrobe') } - - { (activeCategory && !isWardrobeVisible) && - } - { isWardrobeVisible && - } + { ((activeModelKey.length > 0) && (activeModelKey !== AvatarEditorFigureCategory.WARDROBE)) && + } + { (activeModelKey === AvatarEditorFigureCategory.WARDROBE) && + } - + + { figureContainer && + } + + + ) + } } /> + ); +} diff --git a/src/components/avatar-editor/figure-set/AvatarEditorFigureSetItemView.tsx b/src/components/avatar-editor/figure-set/AvatarEditorFigureSetItemView.tsx new file mode 100644 index 0000000..955d7bc --- /dev/null +++ b/src/components/avatar-editor/figure-set/AvatarEditorFigureSetItemView.tsx @@ -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(''); + const { selectedColorParts = null, getFigureStringWithFace = null } = useAvatarEditor(); + + const isHC = !GetConfigurationValue('hc.disabled', false) && ((partItem.partSet?.clubLevel ?? 0) > 0); + + useEffect(() => + { + if(!setType || !setType.length || !partItem) return; + + const loadImage = async () => + { + const isHC = !GetConfigurationValue('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 ( + + { !partItem.isClear && isHC && } + { partItem.isClear && } + { !partItem.isClear && partItem.partSet.isSellable && } + + ); +} diff --git a/src/components/avatar-editor/figure-set/AvatarEditorFigureSetView.tsx b/src/components/avatar-editor/figure-set/AvatarEditorFigureSetView.tsx new file mode 100644 index 0000000..57413de --- /dev/null +++ b/src/components/avatar-editor/figure-set/AvatarEditorFigureSetView.tsx @@ -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 ( + + { + if(!item) return null; + + return ( + selectEditorPart(category.setType, item.partSet?.id ?? -1) } /> + ) + } } /> + ); +} diff --git a/src/components/avatar-editor/figure-set/index.ts b/src/components/avatar-editor/figure-set/index.ts new file mode 100644 index 0000000..0c5880b --- /dev/null +++ b/src/components/avatar-editor/figure-set/index.ts @@ -0,0 +1,2 @@ +export * from './AvatarEditorFigureSetItemView'; +export * from './AvatarEditorFigureSetView'; diff --git a/src/components/avatar-editor/index.ts b/src/components/avatar-editor/index.ts new file mode 100644 index 0000000..5ae66e5 --- /dev/null +++ b/src/components/avatar-editor/index.ts @@ -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'; diff --git a/src/components/avatar-editor/palette-set/AvatarEditorPaletteSetItemView.tsx b/src/components/avatar-editor/palette-set/AvatarEditorPaletteSetItemView.tsx new file mode 100644 index 0000000..0f43efb --- /dev/null +++ b/src/components/avatar-editor/palette-set/AvatarEditorPaletteSetItemView.tsx @@ -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('hc.disabled', false) && (partColor.clubLevel > 0); + + return ( + + { isHC && } + + ); +} diff --git a/src/components/avatar-editor/palette-set/AvatarEditorPaletteSetView.tsx b/src/components/avatar-editor/palette-set/AvatarEditorPaletteSetView.tsx new file mode 100644 index 0000000..45b32dd --- /dev/null +++ b/src/components/avatar-editor/palette-set/AvatarEditorPaletteSetView.tsx @@ -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 ( + + { + if(!item) return null; + + return ( + selectEditorColor(category.setType, paletteIndex, item.id) } /> + ) + } } /> + ); +} diff --git a/src/components/avatar-editor/palette-set/index.ts b/src/components/avatar-editor/palette-set/index.ts new file mode 100644 index 0000000..977e5b9 --- /dev/null +++ b/src/components/avatar-editor/palette-set/index.ts @@ -0,0 +1,2 @@ +export * from './AvatarEditorPaletteSetItemView'; +export * from './AvatarEditorPaletteSetView'; diff --git a/src/components/friends/FriendsView.scss b/src/components/friends/FriendsView.scss index b45fff4..2396400 100644 --- a/src/components/friends/FriendsView.scss +++ b/src/components/friends/FriendsView.scss @@ -109,6 +109,12 @@ .friend-bar-button { z-index: 2; + margin: 0 0.2rem; + background-color: #d3d3d3; + color: #212131; + border: 2px solid #fff; + border-radius: 8px; + width: 30px; } .friend-bar-item { @@ -135,6 +141,10 @@ pointer-events: none; } + + .avatar-image { + max-height: 80px; + } &.friend-bar-search { .friend-bar-item-head { diff --git a/src/components/friends/views/friends-bar/FriendBarItemView.tsx b/src/components/friends/views/friends-bar/FriendBarItemView.tsx index 52f7568..fe8624f 100644 --- a/src/components/friends/views/friends-bar/FriendBarItemView.tsx +++ b/src/components/friends/views/friends-bar/FriendBarItemView.tsx @@ -33,7 +33,7 @@ export const FriendBarItemView: FC<{ friend: MessengerFriend }> = props => if(!friend) { return ( -
+
{ LocalizeText('friend.bar.find.title') }
@@ -41,9 +41,9 @@ export const FriendBarItemView: FC<{ friend: MessengerFriend }> = props => } return ( -
setVisible(prevValue => !prevValue) }> +
setVisible(prevValue => !prevValue) }>
0 ? 'avatar': 'group' }` }> - { (friend.id > 0) && } + { (friend.id > 0) && } { (friend.id <= 0) && }
{ friend.name }
diff --git a/src/components/main/MainView.tsx b/src/components/main/MainView.tsx index 66ae798..1354c8e 100644 --- a/src/components/main/MainView.tsx +++ b/src/components/main/MainView.tsx @@ -3,8 +3,7 @@ import { FC, useEffect, useState } from 'react'; import { Base, TransitionAnimation, TransitionAnimationTypes } from '../../common'; import { useNitroEvent } from '../../hooks'; import { AchievementsView } from '../achievements/AchievementsView'; -import { AvatarEditorNewView } from '../avatar-editor-new/AvatarEditorView'; -import { AvatarEditorView } from '../avatar-editor/AvatarEditorView'; +import { AvatarEditorView } from '../avatar-editor'; import { CameraWidgetView } from '../camera/CameraWidgetView'; import { CampaignView } from '../campaign/CampaignView'; import { CatalogView } from '../catalog/CatalogView'; @@ -20,7 +19,7 @@ import { HotelView } from '../hotel-view/HotelView'; import { InventoryView } from '../inventory/InventoryView'; import { ModToolsView } from '../mod-tools/ModToolsView'; import { NavigatorView } from '../navigator/NavigatorView'; -import { NitrobubbleHiddenView } from '../nitrobubblehidden/NitrobubbleHiddenView'; +import { NitrobubbleHiddenView } from '../nitrobubblehidden/NitrobubbleHiddenView'; import { NitropediaView } from '../nitropedia/NitropediaView'; import { RightSideView } from '../right-side/RightSideView'; import { RoomView } from '../room/RoomView'; @@ -91,10 +90,9 @@ export const MainView: FC<{}> = props => - - + diff --git a/src/components/notification-center/views/bubble-layouts/GetBubbleLayout.tsx b/src/components/notification-center/views/bubble-layouts/GetBubbleLayout.tsx index b22a2f6..23b6e11 100644 --- a/src/components/notification-center/views/bubble-layouts/GetBubbleLayout.tsx +++ b/src/components/notification-center/views/bubble-layouts/GetBubbleLayout.tsx @@ -6,13 +6,13 @@ export const GetBubbleLayout = (item: NotificationBubbleItem, onClose: () => voi { if(!item) return null; - const props = { key: item.id, item, onClose }; + const props = { item, onClose }; switch(item.notificationType) { case NotificationBubbleType.CLUBGIFT: - return + return default: - return + return } } diff --git a/src/components/room/widgets/furniture/context-menu/PurchasableClothingConfirmView.tsx b/src/components/room/widgets/furniture/context-menu/PurchasableClothingConfirmView.tsx index 5ba9905..090c1ed 100644 --- a/src/components/room/widgets/furniture/context-menu/PurchasableClothingConfirmView.tsx +++ b/src/components/room/widgets/furniture/context-menu/PurchasableClothingConfirmView.tsx @@ -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 { 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 { useRoom } from '../../../../../hooks'; @@ -17,7 +17,7 @@ export const PurchasableClothingConfirmView: FC(FigureData.MALE); + const [ gender, setGender ] = useState(AvatarFigurePartType.MALE); const [ newFigure, setNewFigure ] = useState(null); const { roomSession = null } = useRoom(); diff --git a/src/hooks/avatar-editor/useAvatarEditor.ts b/src/hooks/avatar-editor/useAvatarEditor.ts index 82dd433..d35bb5f 100644 --- a/src/hooks/avatar-editor/useAvatarEditor.ts +++ b/src/hooks/avatar-editor/useAvatarEditor.ts @@ -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 { 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 { useFigureData } from './useFigureData'; @@ -15,7 +15,8 @@ const useAvatarEditorState = () => const [ maxPaletteCount, setMaxPaletteCount ] = useState(1); const [ figureSetIds, setFigureSetIds ] = useState([]); const [ boundFurnitureNames, setBoundFurnitureNames ] = useState([]); - 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 ]); @@ -57,7 +58,8 @@ const useAvatarEditorState = () => if(partItem.isClear) { - // clear the part + selectPart(setType, -1); + return; } @@ -89,6 +91,109 @@ const useAvatarEditorState = () => selectColor(setType, paletteId, colorId); }, [ 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, event => { const parser = event.getParser(); @@ -97,6 +202,30 @@ const useAvatarEditorState = () => setBoundFurnitureNames(parser.boundsFurnitureNames); }); + useMessageEvent(UserWardrobePageEvent, event => + { + const parser = event.getParser(); + const savedFigures: [ IAvatarFigureContainer, string ][] = []; + + let i = 0; + + while(i < GetConfigurationValue('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(() => { AvatarEditorThumbnailsHelper.clearCache(); @@ -125,19 +254,6 @@ const useAvatarEditorState = () => if(!partColor || !partColor.isSelectable) continue; 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()); @@ -146,14 +262,14 @@ const useAvatarEditorState = () => if(isntMandatorySet) partItems.push({ id: -1, isClear: true }); - const usesColor = (setType !== FigureData.FACE); + const usesColor = (setType !== AvatarFigurePartType.HEAD); const partSets = set.partSets; for(let i = (partSets.length); i >= 0; 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; @@ -164,17 +280,17 @@ const useAvatarEditorState = () => 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 }; } - newAvatarModels[AvatarEditorFigureCategory.GENERIC] = [ FigureData.FACE ].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.TORSO] = [ FigureData.SHIRT, FigureData.CHEST_PRINTS, FigureData.JACKET, FigureData.CHEST_ACCESSORIES ].map(setType => buildCategory(setType)); - newAvatarModels[AvatarEditorFigureCategory.LEGS] = [ FigureData.TROUSERS, FigureData.SHOES, FigureData.TROUSER_ACCESSORIES ].map(setType => buildCategory(setType)); + newAvatarModels[AvatarEditorFigureCategory.GENERIC] = [ AvatarFigurePartType.HEAD ].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] = [ AvatarFigurePartType.CHEST, AvatarFigurePartType.CHEST_PRINT, AvatarFigurePartType.COAT_CHEST, AvatarFigurePartType.CHEST_ACCESSORY ].map(setType => buildCategory(setType)); + newAvatarModels[AvatarEditorFigureCategory.LEGS] = [ AvatarFigurePartType.LEGS, AvatarFigurePartType.SHOES, AvatarFigurePartType.WAIST_ACCESSORY ].map(setType => buildCategory(setType)); newAvatarModels[AvatarEditorFigureCategory.WARDROBE] = []; setAvatarModels(newAvatarModels); @@ -188,57 +304,15 @@ const useAvatarEditorState = () => loadAvatarData(GetSessionDataManager().figure, GetSessionDataManager().gender); }, [ isVisible, loadAvatarData ]); - return { isVisible, setIsVisible, avatarModels, activeModelKey, setActiveModelKey, selectedParts, selectedColors, maxPaletteCount, selectedColorParts, selectEditorPart, selectEditorColor, getFigureStringWithFace }; + useEffect(() => + { + if(!isVisible || savedFigures) return; + + setSavedFigures(new Array(GetConfigurationValue('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); - -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; -} diff --git a/src/hooks/avatar-editor/useFigureData.ts b/src/hooks/avatar-editor/useFigureData.ts index 5d68730..0e1dbae 100644 --- a/src/hooks/avatar-editor/useFigureData.ts +++ b/src/hooks/avatar-editor/useFigureData.ts @@ -1,11 +1,11 @@ -import { useCallback, useState } from 'react'; -import { FigureData } from '../../api'; +import { AvatarFigurePartType } from '@nitrots/nitro-renderer'; +import { useCallback, useMemo, useState } from 'react'; const useFigureDataState = () => { const [ selectedParts, setSelectedParts ] = useState<{ [index: string]: number }>({}); const [ selectedColors, setSelectedColors ] = useState<{ [index: string]: number[] }>({}); - const [ gender, setGender ] = useState(FigureData.MALE); + const [ gender, setGender ] = useState(AvatarFigurePartType.MALE); const loadAvatarData = useCallback((figureString: string, gender: string) => { @@ -62,7 +62,8 @@ const useFigureDataState = () => { const newValue = { ...prevValue }; - newValue[setType] = partId; + if(partId === -1) delete newValue[setType]; + else newValue[setType] = partId; 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 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 === FigureData.FACE && override) ? overridePartId : selectedParts[setType]; + let partId = (setType === AvatarFigurePartType.HEAD && override) ? overridePartId : selectedParts[setType]; const colors = selectedColors[setType] || []; - // Construct the figure set string, including the type, part ID, and any colors. let figureSet = `${ setType }-${ partId }`; - if (partId >= 0) - { - figureSet += colors.map(color => `-${ color }`).join(''); - } + + if (partId >= 0) figureSet += colors.map(color => `-${ color }`).join(''); return figureSet; }); - - // Join all figure sets with '.', ensuring to only add '.' between items, not at the end. + return figureSets.join('.'); }, [ selectedParts, selectedColors ]); - return { selectedParts, selectedColors, gender, loadAvatarData, selectPart, selectColor, getFigureStringWithFace }; + return { selectedParts, selectedColors, gender, setGender, loadAvatarData, selectPart, selectColor, getFigureString, getFigureStringWithFace }; } export const useFigureData = useFigureDataState;