mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-20 07:26:19 +00:00
🆙 Init V3
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
export * from './useAvatarEditor';
|
||||
export * from './useFigureData';
|
||||
@@ -0,0 +1,318 @@
|
||||
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<boolean>(false);
|
||||
const [ avatarModels, setAvatarModels ] = useState<{ [index: string]: IAvatarEditorCategory[] }>({});
|
||||
const [ activeModelKey, setActiveModelKey ] = useState<string>('');
|
||||
const [ maxPaletteCount, setMaxPaletteCount ] = useState<number>(1);
|
||||
const [ figureSetIds, setFigureSetIds ] = useState<number[]>([]);
|
||||
const [ boundFurnitureNames, setBoundFurnitureNames ] = useState<string[]>([]);
|
||||
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>(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 < 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(() =>
|
||||
{
|
||||
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);
|
||||
const palette = GetAvatarRenderManager().structureData.getPalette(set.paletteID);
|
||||
|
||||
if(!set || !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));
|
||||
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);
|
||||
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<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);
|
||||
@@ -0,0 +1,150 @@
|
||||
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<string>(AvatarFigurePartType.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 };
|
||||
|
||||
if(partId === -1) delete newValue[setType];
|
||||
else 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 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 = [ AvatarFigurePartType.HEAD ].map(setType =>
|
||||
{
|
||||
let partId = (setType === AvatarFigurePartType.HEAD && override) ? overridePartId : selectedParts[setType];
|
||||
const colors = selectedColors[setType] || [];
|
||||
|
||||
let figureSet = `${ setType }-${ partId }`;
|
||||
|
||||
if(partId >= 0) figureSet += colors.map(color => `-${ color }`).join('');
|
||||
|
||||
return figureSet;
|
||||
});
|
||||
|
||||
return figureSets.join('.');
|
||||
}, [ selectedParts, selectedColors ]);
|
||||
|
||||
return { selectedParts, selectedColors, gender, setGender, loadAvatarData, selectPart, selectColor, getFigureString, getFigureStringWithFace };
|
||||
};
|
||||
|
||||
export const useFigureData = useFigureDataState;
|
||||
Reference in New Issue
Block a user