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 { AvatarEditorColorSorter, AvatarEditorPartSorter, AvatarEditorThumbnailsHelper, GetClubMemberLevel, GetConfigurationValue, IAvatarEditorCategory, IAvatarEditorCategoryPartItem, Randomizer, SendMessageComposer } 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 [ maxPaletteCount, setMaxPaletteCount ] = useState(1); const [ figureSetIds, setFigureSetIds ] = useState([]); const [ boundFurnitureNames, setBoundFurnitureNames ] = useState([]); 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 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; const category = activeModel.find(category => (category.setType === setType)); if(!category || !category.partItems || !category.partItems.length) return; const partItem = category.partItems.find(partItem => partItem.id === partId); if(!partItem) return; if(partItem.isClear) { selectPart(setType, -1); return; } if(GetClubMemberLevel() < partItem.partSet.clubLevel) return; setMaxPaletteCount(partItem.maxPaletteCount || 1); selectPart(setType, partId); }, [ activeModel, selectPart ]); const selectEditorColor = useCallback((setType: string, paletteId: number, colorId: number) => { if(!setType || !setType.length) return; const category = activeModel.find(category => (category.setType === setType)); if(!category || !category.colorItems || !category.colorItems.length) return; const palette = category.colorItems[paletteId]; if(!palette || !palette.length) return; const partColor = palette.find(partColor => (partColor.id === colorId)); if(!partColor) return; if(GetClubMemberLevel() < partColor.clubLevel) return; 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(); setFigureSetIds(parser.figureSetIds); 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(); }, [ selectedColorParts ]); useEffect(() => { if(!isVisible) return; const newAvatarModels: { [index: string]: IAvatarEditorCategory[] } = {}; const buildCategory = (setType: string) => { const partItems: IAvatarEditorCategoryPartItem[] = []; const colorItems: IPartColor[][] = []; for(let i = 0; i < MAX_PALETTES; i++) colorItems.push([]); const set = GetAvatarRenderManager().structureData.getSetType(setType); if(!set) return null; const palette = GetAvatarRenderManager().structureData.getPalette(set.paletteID); if(!palette) return null; for(const partColor of palette.colors.getValues()) { if(!partColor || !partColor.isSelectable) continue; for(let i = 0; i < MAX_PALETTES; i++) colorItems[i].push(partColor); } let mandatorySetIds: string[] = GetAvatarRenderManager().getMandatoryAvatarPartSetIds(gender, GetClubMemberLevel()); const isntMandatorySet = (mandatorySetIds.indexOf(setType) === -1); if(isntMandatorySet) partItems.push({ id: -1, isClear: true }); 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 !== AvatarFigurePartType.UNISEX))) continue; if(partSet.isSellable && figureSetIds.indexOf(partSet.id) === -1) continue; let maxPaletteCount = 0; for(const part of partSet.parts) maxPaletteCount = Math.max(maxPaletteCount, part.colorLayerIndex); partItems.push({ id: partSet.id, partSet, usesColor, maxPaletteCount }); } partItems.sort(AvatarEditorPartSorter(false)); for(let i = 0; i < MAX_PALETTES; i++) colorItems[i].sort(AvatarEditorColorSorter); return { setType, partItems, colorItems }; }; newAvatarModels[AvatarEditorFigureCategory.GENERIC] = [ AvatarFigurePartType.HEAD ].map(setType => buildCategory(setType)).filter(Boolean); newAvatarModels[AvatarEditorFigureCategory.HEAD] = [ AvatarFigurePartType.HAIR, AvatarFigurePartType.HEAD_ACCESSORY, AvatarFigurePartType.HEAD_ACCESSORY_EXTRA, AvatarFigurePartType.EYE_ACCESSORY, AvatarFigurePartType.FACE_ACCESSORY ].map(setType => buildCategory(setType)).filter(Boolean); newAvatarModels[AvatarEditorFigureCategory.TORSO] = [ AvatarFigurePartType.CHEST, AvatarFigurePartType.CHEST_PRINT, AvatarFigurePartType.COAT_CHEST, AvatarFigurePartType.CHEST_ACCESSORY ].map(setType => buildCategory(setType)).filter(Boolean); newAvatarModels[AvatarEditorFigureCategory.LEGS] = [ AvatarFigurePartType.LEGS, AvatarFigurePartType.SHOES, AvatarFigurePartType.WAIST_ACCESSORY ].map(setType => buildCategory(setType)).filter(Boolean); newAvatarModels[AvatarEditorFigureCategory.PETS] = [ AvatarFigurePartType.PET ].map(setType => buildCategory(setType)).filter(Boolean); newAvatarModels[AvatarEditorFigureCategory.WARDROBE] = []; setAvatarModels(newAvatarModels); setActiveModelKey(AvatarEditorFigureCategory.GENERIC); }, [ isVisible, gender, figureSetIds ]); useEffect(() => { if(!isVisible) return; loadAvatarData(GetSessionDataManager().figure, GetSessionDataManager().gender); }, [ isVisible, loadAvatarData ]); 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);