From 1664baef92b57dde55b9f5a02a23b5a81dc10609 Mon Sep 17 00:00:00 2001 From: billsonnn Date: Mon, 1 Apr 2024 23:19:57 -0400 Subject: [PATCH] Continue avatar editor changes --- .../avatar/AvatarEditorThumbnailsHelper.ts | 69 ++++++-- .../avatar-editor-new/AvatarEditorView.tsx | 7 +- .../AvatarEditorFigureSetItemView.tsx | 22 ++- .../figure-set/AvatarEditorFigureSetView.tsx | 4 +- .../AvatarEditorPaletteSetView.tsx | 4 +- src/hooks/avatar-editor/index.ts | 1 + src/hooks/avatar-editor/useAvatarEditor.ts | 165 ++++++++++-------- src/hooks/avatar-editor/useFigureData.ts | 114 ++++++++++++ 8 files changed, 284 insertions(+), 102 deletions(-) create mode 100644 src/hooks/avatar-editor/useFigureData.ts diff --git a/src/api/avatar/AvatarEditorThumbnailsHelper.ts b/src/api/avatar/AvatarEditorThumbnailsHelper.ts index 336d5763..6be9b9be 100644 --- a/src/api/avatar/AvatarEditorThumbnailsHelper.ts +++ b/src/api/avatar/AvatarEditorThumbnailsHelper.ts @@ -1,4 +1,4 @@ -import { AvatarFigurePartType, GetAssetManager, GetAvatarRenderManager, IFigurePart, IGraphicAsset, IPartColor, NitroAlphaFilter, NitroContainer, NitroSprite, TextureUtils } from '@nitrots/nitro-renderer'; +import { AvatarFigurePartType, AvatarScaleType, AvatarSetType, GetAssetManager, GetAvatarRenderManager, IFigurePart, IGraphicAsset, IPartColor, NitroAlphaFilter, NitroContainer, NitroSprite, TextureUtils } from '@nitrots/nitro-renderer'; import { FigureData } from './FigureData'; import { IAvatarEditorCategoryPartItem } from './IAvatarEditorCategoryPartItem'; @@ -40,7 +40,12 @@ export class AvatarEditorThumbnailsHelper return `${ setType }-${ part.partSet.id }`; } - public static async build(setType: string, part: IAvatarEditorCategoryPartItem, useColors: boolean, isDisabled: boolean = false): Promise + public static clearCache(): void + { + this.THUMBNAIL_CACHE.clear(); + } + + public static async build(setType: string, part: IAvatarEditorCategoryPartItem, useColors: boolean, partColors: IPartColor[], isDisabled: boolean = false): Promise { if(!setType || !setType.length || !part || !part.partSet || !part.partSet.parts || !part.partSet.parts.length) return null; @@ -49,7 +54,7 @@ export class AvatarEditorThumbnailsHelper if(cached) return cached; - const buildContainer = (part: IAvatarEditorCategoryPartItem, useColors: boolean, isDisabled: boolean = false) => + const buildContainer = (part: IAvatarEditorCategoryPartItem, useColors: boolean, partColors: IPartColor[], isDisabled: boolean = false) => { const container = new NitroContainer(); const parts = part.partSet.parts.concat().sort(this.sortByDrawOrder); @@ -82,20 +87,17 @@ export class AvatarEditorThumbnailsHelper const x = asset.offsetX; const y = asset.offsetY; - let partColor: IPartColor = null; - - if(useColors && (part.colorLayerIndex > 0)) - { - //const color = this._partColors[(part.colorLayerIndex - 1)]; - - //if(color) partColor = color; - } const sprite = new NitroSprite(asset.texture); sprite.position.set(x, y); - if(partColor) sprite.tint = partColor.rgb; + if(useColors && (part.colorLayerIndex > 0) && partColors && partColors.length) + { + const color = partColors[(part.colorLayerIndex - 1)]; + + if(color) sprite.tint = color.rgb; + } if(isDisabled) container.filters = [ AvatarEditorThumbnailsHelper.ALPHA_FILTER ]; @@ -109,12 +111,12 @@ export class AvatarEditorThumbnailsHelper { const resetFigure = async (figure: string) => { - const container = buildContainer(part, useColors, isDisabled); - const url = await TextureUtils.generateImageUrl(container); + const container = buildContainer(part, useColors, partColors, isDisabled); + const imageUrl = await TextureUtils.generateImageUrl(container); - AvatarEditorThumbnailsHelper.THUMBNAIL_CACHE.set(thumbnailKey, url); + AvatarEditorThumbnailsHelper.THUMBNAIL_CACHE.set(thumbnailKey, imageUrl); - resolve(url); + resolve(imageUrl); } const figureContainer = GetAvatarRenderManager().createFigureContainer(`${ setType }-${ part.partSet.id }`); @@ -134,6 +136,41 @@ export class AvatarEditorThumbnailsHelper }); } + public static async buildForFace(figureString: string, isDisabled: boolean = false): Promise + { + if(!figureString || !figureString.length) return null; + + const thumbnailKey = figureString; + const cached = this.THUMBNAIL_CACHE.get(thumbnailKey); + + if(cached) return cached; + + return new Promise(async (resolve, reject) => + { + const resetFigure = async (figure: string) => + { + const avatarImage = GetAvatarRenderManager().createAvatarImage(figure, AvatarScaleType.LARGE, null, { resetFigure, dispose: null, disposed: false }); + 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 + }); + + sprite.destroy(); + avatarImage.dispose(); + + if(!avatarImage.isPlaceholder()) AvatarEditorThumbnailsHelper.THUMBNAIL_CACHE.set(thumbnailKey, imageUrl); + + resolve(imageUrl); + } + + resetFigure(figureString); + }); + } + private static sortByDrawOrder(a: IFigurePart, b: IFigurePart): number { const indexA = AvatarEditorThumbnailsHelper.DRAW_ORDER.indexOf(a.type); diff --git a/src/components/avatar-editor-new/AvatarEditorView.tsx b/src/components/avatar-editor-new/AvatarEditorView.tsx index 1e6b0abd..eb1973d1 100644 --- a/src/components/avatar-editor-new/AvatarEditorView.tsx +++ b/src/components/avatar-editor-new/AvatarEditorView.tsx @@ -12,7 +12,7 @@ const DEFAULT_FEMALE_FIGURE: string = 'hr-515-33.hd-600-1.ch-635-70.lg-716-66-62 export const AvatarEditorNewView: FC<{}> = props => { const [ isVisible, setIsVisible ] = useState(false); - const { avatarModels, activeModelKey, setActiveModelKey } = useAvatarEditor(); + const { setIsVisible: setEditorVisibility, avatarModels, activeModelKey, setActiveModelKey } = useAvatarEditor(); const processAction = (action: string) => { @@ -59,6 +59,11 @@ export const AvatarEditorNewView: FC<{}> = props => return () => RemoveLinkEventTracker(linkTracker); }, []); + useEffect(() => + { + setEditorVisibility(isVisible) + }, [ isVisible, setEditorVisibility ]); + if(!isVisible) return null; return ( diff --git a/src/components/avatar-editor-new/views/figure-set/AvatarEditorFigureSetItemView.tsx b/src/components/avatar-editor-new/views/figure-set/AvatarEditorFigureSetItemView.tsx index a4407a97..1c00fc91 100644 --- a/src/components/avatar-editor-new/views/figure-set/AvatarEditorFigureSetItemView.tsx +++ b/src/components/avatar-editor-new/views/figure-set/AvatarEditorFigureSetItemView.tsx @@ -1,6 +1,7 @@ import { FC, useEffect, useState } from 'react'; -import { AvatarEditorThumbnailsHelper, GetConfigurationValue, IAvatarEditorCategoryPartItem } from '../../../../api'; +import { AvatarEditorThumbnailsHelper, FigureData, GetConfigurationValue, IAvatarEditorCategoryPartItem } from '../../../../api'; import { LayoutCurrencyIcon, LayoutGridItem, LayoutGridItemProps } from '../../../../common'; +import { useAvatarEditor } from '../../../../hooks'; import { AvatarEditorIcon } from '../AvatarEditorIcon'; export const AvatarEditorFigureSetItemView: FC<{ @@ -11,6 +12,7 @@ export const AvatarEditorFigureSetItemView: FC<{ { 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); @@ -21,18 +23,28 @@ export const AvatarEditorFigureSetItemView: FC<{ const loadImage = async () => { const isHC = !GetConfigurationValue('hc.disabled', false) && ((partItem.partSet?.clubLevel ?? 0) > 0); - const url = await AvatarEditorThumbnailsHelper.build(setType, partItem, partItem.usesColor, isHC); + + let url: string = null; + + if(setType === FigureData.FACE) + { + 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 ]); + }, [ setType, partItem, selectedColorParts, getFigureStringWithFace ]); - if(!partItem || !partItem.partSet) return null; + if(!partItem) return null; return ( - + { !partItem.isClear && isHC && } { partItem.isClear && } { !partItem.isClear && partItem.partSet.isSellable && } diff --git a/src/components/avatar-editor-new/views/figure-set/AvatarEditorFigureSetView.tsx b/src/components/avatar-editor-new/views/figure-set/AvatarEditorFigureSetView.tsx index 369ac95b..7db37430 100644 --- a/src/components/avatar-editor-new/views/figure-set/AvatarEditorFigureSetView.tsx +++ b/src/components/avatar-editor-new/views/figure-set/AvatarEditorFigureSetView.tsx @@ -9,7 +9,7 @@ export const AvatarEditorFigureSetView: FC<{ }> = props => { const { category = null } = props; - const { selectedParts = null, selectPart } = useAvatarEditor(); + const { selectedParts = null, selectEditorPart } = useAvatarEditor(); const elementRef = useRef(null); const isPartItemSelected = (partItem: IAvatarEditorCategoryPartItem) => @@ -29,7 +29,7 @@ export const AvatarEditorFigureSetView: FC<{ if(!item) return null; return ( - selectPart(category.setType, item.partSet?.id ?? -1) } style={ { width: ~~(100 / columnCount) + '%' } } /> + selectEditorPart(category.setType, item.partSet?.id ?? -1) } /> ) } } /> ); diff --git a/src/components/avatar-editor-new/views/palette-set/AvatarEditorPaletteSetView.tsx b/src/components/avatar-editor-new/views/palette-set/AvatarEditorPaletteSetView.tsx index a7479c18..2e72cd46 100644 --- a/src/components/avatar-editor-new/views/palette-set/AvatarEditorPaletteSetView.tsx +++ b/src/components/avatar-editor-new/views/palette-set/AvatarEditorPaletteSetView.tsx @@ -12,7 +12,7 @@ export const AvatarEditorPaletteSetView: FC<{ { const { category = null, paletteIndex = -1 } = props; const paletteSet = category?.colorItems[paletteIndex] ?? null; - const { selectedColors = null, selectColor } = useAvatarEditor(); + const { selectedColors = null, selectEditorColor } = useAvatarEditor(); const elementRef = useRef(null); const isPartColorSelected = (partColor: IPartColor) => @@ -27,7 +27,7 @@ export const AvatarEditorPaletteSetView: FC<{ return ( { (paletteSet.length > 0) && paletteSet.map(item => - selectColor(category.setType, paletteIndex, item.id) } />) } + selectEditorColor(category.setType, paletteIndex, item.id) } />) } ); } diff --git a/src/hooks/avatar-editor/index.ts b/src/hooks/avatar-editor/index.ts index 42e3f1b4..ab8a733f 100644 --- a/src/hooks/avatar-editor/index.ts +++ b/src/hooks/avatar-editor/index.ts @@ -1 +1,2 @@ export * from './useAvatarEditor'; +export * from './useFigureData'; diff --git a/src/hooks/avatar-editor/useAvatarEditor.ts b/src/hooks/avatar-editor/useAvatarEditor.ts index b5a06da6..b04f7ad9 100644 --- a/src/hooks/avatar-editor/useAvatarEditor.ts +++ b/src/hooks/avatar-editor/useAvatarEditor.ts @@ -1,23 +1,49 @@ -import { AvatarEditorFigureCategory, GetAvatarRenderManager, IFigurePartSet, IPartColor } from '@nitrots/nitro-renderer'; +import { AvatarEditorFigureCategory, FigureSetIdsMessageEvent, GetAvatarRenderManager, GetSessionDataManager, IFigurePartSet, IPartColor } from '@nitrots/nitro-renderer'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useBetween } from 'use-between'; -import { FigureData, GetClubMemberLevel, GetConfigurationValue, IAvatarEditorCategory, IAvatarEditorCategoryPartItem } from '../../api'; +import { AvatarEditorThumbnailsHelper, FigureData, GetClubMemberLevel, IAvatarEditorCategory, IAvatarEditorCategoryPartItem } from '../../api'; +import { useMessageEvent } from '../events'; +import { useFigureData } from './useFigureData'; const MAX_PALETTES: number = 2; const useAvatarEditorState = () => { + const [ isVisible, setIsVisible ] = useState(false); const [ avatarModels, setAvatarModels ] = useState<{ [index: string]: IAvatarEditorCategory[] }>({}); const [ activeModelKey, setActiveModelKey ] = useState(''); - const [ selectedParts, setSelectedParts ] = useState<{ [index: string]: number }>({}); - const [ selectedColors, setSelectedColors ] = useState<{ [index: string]: { [index: number]: number }}>({}); const [ maxPaletteCount, setMaxPaletteCount ] = useState(1); + const [ figureSetIds, setFigureSetIds ] = useState([]); + const [ boundFurnitureNames, setBoundFurnitureNames ] = useState([]); + const { gender, selectedParts, selectedColors, loadAvatarData, selectPart, selectColor, getFigureStringWithFace } = useFigureData(); - const clubItemsFirst = useMemo(() => GetConfigurationValue('avatareditor.show.clubitems.first', true), []); - const clubItemsDimmed = useMemo(() => GetConfigurationValue('avatareditor.show.clubitems.dimmed', true), []); const activeModel = useMemo(() => (avatarModels[activeModelKey] ?? null), [ activeModelKey, avatarModels ]); - const selectPart = useCallback((setType: string, partId: number) => + const selectedColorParts = useMemo(() => + { + const colorSets: { [index: string]: IPartColor[] } = {}; + + for(const setType of Object.keys(selectedColors)) + { + if(!selectedColors[setType]) continue; + + const parts: IPartColor[] = []; + + for(const paletteId of Object.keys(selectedColors[setType])) + { + const partColor = activeModel.find(category => (category.setType === setType))?.colorItems[paletteId]?.find(partColor => (partColor.id === selectedColors[setType][paletteId])); + + if(partColor) parts.push(partColor); + } + + colorSets[setType] = parts; + } + + return colorSets; + + }, [ activeModel, selectedColors ]); + + const selectEditorPart = useCallback((setType: string, partId: number) => { if(!setType || !setType.length) return; @@ -39,17 +65,10 @@ const useAvatarEditorState = () => setMaxPaletteCount(partItem.maxPaletteCount || 1); - setSelectedParts(prevValue => - { - const newValue = { ...prevValue }; + selectPart(setType, partId); + }, [ activeModel, selectPart ]); - newValue[setType] = partItem.id; - - return newValue; - }); - }, [ activeModel ]); - - const selectColor = useCallback((setType: string, paletteId: number, colorId: number) => + const selectEditorColor = useCallback((setType: string, paletteId: number, colorId: number) => { if(!setType || !setType.length) return; @@ -67,22 +86,26 @@ const useAvatarEditorState = () => if(GetClubMemberLevel() < partColor.clubLevel) return; - setSelectedColors(prevValue => - { - const newValue = { ...prevValue }; + selectColor(setType, paletteId, colorId); + }, [ activeModel, selectColor ]); - if(!newValue[setType]) newValue[setType] = {}; + useMessageEvent(FigureSetIdsMessageEvent, event => + { + const parser = event.getParser(); - if(!newValue[setType][paletteId]) newValue[setType][paletteId] = -1; - - newValue[setType][paletteId] = partColor.id; - - return newValue; - }) - }, [ activeModel ]); + setFigureSetIds(parser.figureSetIds); + setBoundFurnitureNames(parser.boundsFurnitureNames); + }); useEffect(() => { + AvatarEditorThumbnailsHelper.clearCache(); + }, [ selectedColorParts ]); + + useEffect(() => + { + if(!isVisible) return; + const newAvatarModels: { [index: string]: IAvatarEditorCategory[] } = {}; const buildCategory = (setType: string) => @@ -117,16 +140,7 @@ const useAvatarEditorState = () => } */ } - let mandatorySetIds: string[] = []; - - if(clubItemsDimmed) - { - //mandatorySetIds = GetAvatarRenderManager().getMandatoryAvatarPartSetIds(this.CURRENT_FIGURE.gender, 2); - } - else - { - //mandatorySetIds = GetAvatarRenderManager().getMandatoryAvatarPartSetIds(this.CURRENT_FIGURE.gender, clubMemberLevel); - } + let mandatorySetIds: string[] = GetAvatarRenderManager().getMandatoryAvatarPartSetIds(gender, GetClubMemberLevel()); const isntMandatorySet = (mandatorySetIds.indexOf(setType) === -1); @@ -139,7 +153,9 @@ const useAvatarEditorState = () => { const partSet = partSets.getWithIndex(i); - if(!partSet || !partSet.isSelectable) continue; + if(!partSet || !partSet.isSelectable || ((partSet.gender !== gender) && (partSet.gender !== FigureData.UNISEX))) continue; + + if(partSet.isSellable && figureSetIds.indexOf(partSet.id) === -1) continue; let maxPaletteCount = 0; @@ -148,7 +164,7 @@ const useAvatarEditorState = () => partItems.push({ id: partSet.id, partSet, usesColor, maxPaletteCount }); } - partItems.sort(clubItemsFirst ? clubSorter : noobSorter); + partItems.sort(partSorter(false)); for(let i = 0; i < MAX_PALETTES; i++) colorItems[i].sort(colorSorter); @@ -165,33 +181,52 @@ const useAvatarEditorState = () => setAvatarModels(newAvatarModels); setActiveModelKey(AvatarEditorFigureCategory.GENERIC); - }, [ clubItemsDimmed, clubItemsFirst ]); + }, [ isVisible, gender, figureSetIds ]); - return { avatarModels, activeModelKey, setActiveModelKey, selectedParts, selectedColors, maxPaletteCount, selectPart, selectColor }; + useEffect(() => + { + if(!isVisible) return; + + loadAvatarData(GetSessionDataManager().figure, GetSessionDataManager().gender); + }, [ isVisible, loadAvatarData ]); + + return { isVisible, setIsVisible, avatarModels, activeModelKey, setActiveModelKey, selectedParts, selectedColors, maxPaletteCount, selectedColorParts, selectEditorPart, selectEditorColor, getFigureStringWithFace }; } export const useAvatarEditor = () => useBetween(useAvatarEditorState); -const clubSorter = (a: { partSet: IFigurePartSet, usesColor: boolean, isClear?: boolean }, b: { partSet: IFigurePartSet, usesColor: boolean, isClear?: boolean }) => +const partSorter = (hcFirst: boolean) => { - const clubLevelA = (!a.partSet ? 9999999999 : a.partSet.clubLevel); - const clubLevelB = (!b.partSet ? 9999999999 : b.partSet.clubLevel); - const isSellableA = (!a.partSet ? false : a.partSet.isSellable); - const isSellableB = (!b.partSet ? false : b.partSet.isSellable); + 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(isSellableA && !isSellableB) return 1; - if(isSellableB && !isSellableA) return -1; + if(isSellableB && !isSellableA) return -1; - if(clubLevelA > clubLevelB) return -1; + if(hcFirst) + { + if(clubLevelA > clubLevelB) return -1; - if(clubLevelA < clubLevelB) return 1; + if(clubLevelA < clubLevelB) return 1; + } + else + { + if(clubLevelA < clubLevelB) return -1; - if(a.partSet.id > b.partSet.id) 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; + if(a.partSet.id > b.partSet.id) return 1; + + return 0; + } } const colorSorter = (a: IPartColor, b: IPartColor) => @@ -209,25 +244,3 @@ const colorSorter = (a: IPartColor, b: IPartColor) => return 0; } - -const noobSorter = (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(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/hooks/avatar-editor/useFigureData.ts b/src/hooks/avatar-editor/useFigureData.ts new file mode 100644 index 00000000..5d68730a --- /dev/null +++ b/src/hooks/avatar-editor/useFigureData.ts @@ -0,0 +1,114 @@ +import { useCallback, useState } from 'react'; +import { FigureData } from '../../api'; + +const useFigureDataState = () => +{ + const [ selectedParts, setSelectedParts ] = useState<{ [index: string]: number }>({}); + const [ selectedColors, setSelectedColors ] = useState<{ [index: string]: number[] }>({}); + const [ gender, setGender ] = useState(FigureData.MALE); + + const loadAvatarData = useCallback((figureString: string, gender: string) => + { + const parse = (figure: string) => + { + const sets = figure.split('.'); + + if(!sets || !sets.length) return; + + const partSets: { [index: string]: number } = {}; + const colorSets: { [index: string]: number[] } = {}; + + for(const set of sets) + { + const parts = set.split('-'); + + if(!parts.length) continue; + + const setType = parts[0]; + const partId = parseInt(parts[1]); + const colorIds: number[] = []; + + let offset = 2; + + while(offset < parts.length) + { + colorIds.push(parseInt(parts[offset])); + + offset++; + } + + if(!colorIds.length) colorIds.push(0); + + if(partId >= 0) partSets[setType] = partId; + + if(colorIds.length) colorSets[setType] = colorIds; + } + + return { partSets, colorSets }; + } + + const { partSets, colorSets } = parse(figureString); + + setSelectedParts(partSets); + setSelectedColors(colorSets); + setGender(gender); + }, []); + + const selectPart = useCallback((setType: string, partId: number) => + { + if(!setType || !setType.length) return; + + setSelectedParts(prevValue => + { + const newValue = { ...prevValue }; + + newValue[setType] = partId; + + return newValue; + }); + }, []); + + const selectColor = useCallback((setType: string, paletteId: number, colorId: number) => + { + if(!setType || !setType.length) return; + + setSelectedColors(prevValue => + { + const newValue = { ...prevValue }; + + if(!newValue[setType]) newValue[setType] = []; + + if(!newValue[setType][paletteId]) newValue[setType][paletteId] = 0; + + newValue[setType][paletteId] = colorId; + + return newValue; + }) + }, []); + + const getFigureStringWithFace = useCallback((overridePartId: number, override: boolean = true) => + { + const figureSets = [ FigureData.FACE ].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]; + 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(''); + } + + 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 }; +} + +export const useFigureData = useFigureDataState;