Add NFT avatar tab and wired extras UI

This commit is contained in:
Lorenzune
2026-03-26 05:24:53 +01:00
parent a1b267d8f2
commit 3b3e91f6d9
20 changed files with 542 additions and 22 deletions
+50 -8
View File
@@ -15,6 +15,7 @@ const useAvatarEditorState = () =>
const [ maxPaletteCount, setMaxPaletteCount ] = useState<number>(1);
const [ figureSetIds, setFigureSetIds ] = useState<number[]>([]);
const [ boundFurnitureNames, setBoundFurnitureNames ] = useState<string[]>([]);
const [ figureSetNames, setFigureSetNames ] = useState<Record<number, string>>({});
const [ savedFigures, setSavedFigures ] = useState<[ IAvatarFigureContainer, string ][]>(null);
const { selectedColors, gender, setGender, loadAvatarData, selectPart, selectColor, getFigureString, getFigureStringWithFace, selectedParts } = useFigureData();
@@ -65,6 +66,8 @@ const useAvatarEditorState = () =>
if(GetClubMemberLevel() < partItem.partSet.clubLevel) return;
if(partItem.isSellableNotOwned) return;
setMaxPaletteCount(partItem.maxPaletteCount || 1);
selectPart(setType, partId);
@@ -194,12 +197,27 @@ const useAvatarEditorState = () =>
loadAvatarData(figureContainer.getFigureString(), gender);
}, [ figureSetIds, gender, loadAvatarData, selectedColors, selectedParts ]);
const nftFigureSetIds = useMemo(() =>
{
const nftSetIds = new Set<number>();
for(const [ setId, furnitureName ] of Object.entries(figureSetNames))
{
if(!furnitureName?.toLowerCase().includes('nft')) continue;
nftSetIds.add(Number(setId));
}
return nftSetIds;
}, [ figureSetNames ]);
useMessageEvent<FigureSetIdsMessageEvent>(FigureSetIdsMessageEvent, event =>
{
const parser = event.getParser();
setFigureSetIds(parser.figureSetIds);
setBoundFurnitureNames(parser.boundsFurnitureNames);
setFigureSetNames(parser.figureSetNameMap);
});
useMessageEvent<UserWardrobePageEvent>(UserWardrobePageEvent, event =>
@@ -236,8 +254,10 @@ const useAvatarEditorState = () =>
if(!isVisible) return;
const newAvatarModels: { [index: string]: IAvatarEditorCategory[] } = {};
const buildModeDefault = 'default';
const buildModeNft = 'nft';
const buildCategory = (setType: string) =>
const buildCategory = (setType: string, buildMode: string = buildModeDefault) =>
{
const partItems: IAvatarEditorCategoryPartItem[] = [];
const colorItems: IPartColor[][] = [];
@@ -271,13 +291,20 @@ const useAvatarEditorState = () =>
if(!partSet || !partSet.isSelectable || ((partSet.gender !== gender) && (partSet.gender !== AvatarFigurePartType.UNISEX))) continue;
if(partSet.isSellable && figureSetIds.indexOf(partSet.id) === -1) continue;
const isNftPartSet = nftFigureSetIds.has(partSet.id);
if((buildMode === buildModeDefault) && isNftPartSet) continue;
if((buildMode === buildModeNft) && !isNftPartSet) continue;
const isSellableNotOwned = partSet.isSellable && figureSetIds.indexOf(partSet.id) === -1;
if(isSellableNotOwned && (buildMode !== buildModeNft) && setType !== AvatarFigurePartType.PET) 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.push({ id: partSet.id, partSet, usesColor, maxPaletteCount, isSellableNotOwned });
}
partItems.sort(AvatarEditorPartSorter(false));
@@ -287,16 +314,31 @@ const useAvatarEditorState = () =>
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.GENERIC] = [ AvatarFigurePartType.HEAD ].map(setType => buildCategory(setType, buildModeDefault));
newAvatarModels[AvatarEditorFigureCategory.HEAD] = [ AvatarFigurePartType.HAIR, AvatarFigurePartType.HEAD_ACCESSORY, AvatarFigurePartType.HEAD_ACCESSORY_EXTRA, AvatarFigurePartType.EYE_ACCESSORY, AvatarFigurePartType.FACE_ACCESSORY ].map(setType => buildCategory(setType, buildModeDefault));
newAvatarModels[AvatarEditorFigureCategory.TORSO] = [ AvatarFigurePartType.CHEST, AvatarFigurePartType.CHEST_PRINT, AvatarFigurePartType.COAT_CHEST, AvatarFigurePartType.CHEST_ACCESSORY ].map(setType => buildCategory(setType, buildModeDefault));
newAvatarModels[AvatarEditorFigureCategory.LEGS] = [ AvatarFigurePartType.LEGS, AvatarFigurePartType.SHOES, AvatarFigurePartType.WAIST_ACCESSORY ].map(setType => buildCategory(setType, buildModeDefault));
newAvatarModels[AvatarEditorFigureCategory.PETS] = [ AvatarFigurePartType.PET ].map(setType => buildCategory(setType)).filter(Boolean);
newAvatarModels[AvatarEditorFigureCategory.NFT] = [
AvatarFigurePartType.HEAD,
AvatarFigurePartType.HAIR,
AvatarFigurePartType.HEAD_ACCESSORY,
AvatarFigurePartType.HEAD_ACCESSORY_EXTRA,
AvatarFigurePartType.EYE_ACCESSORY,
AvatarFigurePartType.FACE_ACCESSORY,
AvatarFigurePartType.CHEST,
AvatarFigurePartType.CHEST_PRINT,
AvatarFigurePartType.COAT_CHEST,
AvatarFigurePartType.CHEST_ACCESSORY,
AvatarFigurePartType.LEGS,
AvatarFigurePartType.SHOES,
AvatarFigurePartType.WAIST_ACCESSORY
].map(setType => buildCategory(setType, buildModeNft)).filter(Boolean);
newAvatarModels[AvatarEditorFigureCategory.WARDROBE] = [];
setAvatarModels(newAvatarModels);
setActiveModelKey(AvatarEditorFigureCategory.GENERIC);
}, [ isVisible, gender, figureSetIds ]);
}, [ isVisible, gender, figureSetIds, nftFigureSetIds ]);
useEffect(() =>
{
+26 -3
View File
@@ -75,14 +75,37 @@ const useWiredState = () =>
return rawValue.toLowerCase();
};
const getComparableInteractionNames = (furniData: any): string[] =>
{
if(!furniData) return [];
const values = [
getInteractionTypeName(furniData),
(typeof (furniData as any).className === 'string') ? (furniData as any).className.toLowerCase() : null,
(typeof (furniData as any).fullName === 'string') ? (furniData as any).fullName.toLowerCase() : null,
(typeof (furniData as any).name === 'string') ? (furniData as any).name.toLowerCase() : null
];
return values.filter((value, index, array): value is string => !!value && (array.indexOf(value) === index));
};
const matchesAllowedPattern = (value: string, pattern: string) =>
{
const normalizedPattern = pattern.toLowerCase();
if(normalizedPattern.endsWith('*')) return value.startsWith(normalizedPattern.slice(0, -1));
return (normalizedPattern === value);
};
const isAllowedInteraction = (furniData: any): boolean =>
{
if(!allowedInteractionTypes || !allowedInteractionTypes.length) return true;
const interactionType = getInteractionTypeName(furniData);
if(!interactionType) return true;
const comparableNames = getComparableInteractionNames(furniData);
if(!comparableNames.length) return true;
return allowedInteractionTypes.some(type => (type && type.toLowerCase() === interactionType));
return comparableNames.some(value => allowedInteractionTypes.some(type => !!type && matchesAllowedPattern(value, type)));
};
const handleDisallowedInteraction = () =>