Continue avatar editor changes

This commit is contained in:
billsonnn 2024-04-01 23:19:57 -04:00
parent c659074eb3
commit 1664baef92
8 changed files with 284 additions and 102 deletions

View File

@ -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<string>
public static clearCache(): void
{
this.THUMBNAIL_CACHE.clear();
}
public static async build(setType: string, part: IAvatarEditorCategoryPartItem, useColors: boolean, partColors: IPartColor[], isDisabled: boolean = false): Promise<string>
{
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<string>
{
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);

View File

@ -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 (

View File

@ -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<string>('');
const { selectedColorParts = null, getFigureStringWithFace = null } = useAvatarEditor();
const isHC = !GetConfigurationValue<boolean>('hc.disabled', false) && ((partItem.partSet?.clubLevel ?? 0) > 0);
@ -21,18 +23,28 @@ export const AvatarEditorFigureSetItemView: FC<{
const loadImage = async () =>
{
const isHC = !GetConfigurationValue<boolean>('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 (
<LayoutGridItem itemImage={ (partItem.isClear ? undefined : assetUrl) } itemActive={ isSelected } style={ { width: '100%' } } { ...rest }>
<LayoutGridItem itemImage={ (partItem.isClear ? undefined : assetUrl) } itemActive={ isSelected } style={ { width: '100%', 'flex': '1' } } { ...rest }>
{ !partItem.isClear && isHC && <LayoutCurrencyIcon className="position-absolute end-1 bottom-1" type="hc" /> }
{ partItem.isClear && <AvatarEditorIcon icon="clear" /> }
{ !partItem.isClear && partItem.partSet.isSellable && <AvatarEditorIcon icon="sellable" position="absolute" className="end-1 bottom-1" /> }

View File

@ -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<HTMLDivElement>(null);
const isPartItemSelected = (partItem: IAvatarEditorCategoryPartItem) =>
@ -29,7 +29,7 @@ export const AvatarEditorFigureSetView: FC<{
if(!item) return null;
return (
<AvatarEditorFigureSetItemView key={ item.id } setType={ category.setType } partItem={ item } isSelected={ isPartItemSelected(item) } onClick={ event => selectPart(category.setType, item.partSet?.id ?? -1) } style={ { width: ~~(100 / columnCount) + '%' } } />
<AvatarEditorFigureSetItemView key={ item.id } setType={ category.setType } partItem={ item } isSelected={ isPartItemSelected(item) } onClick={ event => selectEditorPart(category.setType, item.partSet?.id ?? -1) } />
)
} } />
);

View File

@ -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<HTMLDivElement>(null);
const isPartColorSelected = (partColor: IPartColor) =>
@ -27,7 +27,7 @@ export const AvatarEditorPaletteSetView: FC<{
return (
<AutoGrid innerRef={ elementRef } gap={ 1 } columnCount={ 5 } columnMinWidth={ 30 }>
{ (paletteSet.length > 0) && paletteSet.map(item =>
<AvatarEditorPaletteSetItem key={ item.id } setType={ category.setType } partColor={ item } isSelected={ isPartColorSelected(item) } onClick={ event => selectColor(category.setType, paletteIndex, item.id) } />) }
<AvatarEditorPaletteSetItem key={ item.id } setType={ category.setType } partColor={ item } isSelected={ isPartColorSelected(item) } onClick={ event => selectEditorColor(category.setType, paletteIndex, item.id) } />) }
</AutoGrid>
);
}

View File

@ -1 +1,2 @@
export * from './useAvatarEditor';
export * from './useFigureData';

View File

@ -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<boolean>(false);
const [ avatarModels, setAvatarModels ] = useState<{ [index: string]: IAvatarEditorCategory[] }>({});
const [ activeModelKey, setActiveModelKey ] = useState<string>('');
const [ selectedParts, setSelectedParts ] = useState<{ [index: string]: number }>({});
const [ selectedColors, setSelectedColors ] = useState<{ [index: string]: { [index: number]: number }}>({});
const [ maxPaletteCount, setMaxPaletteCount ] = useState<number>(1);
const [ figureSetIds, setFigureSetIds ] = useState<number[]>([]);
const [ boundFurnitureNames, setBoundFurnitureNames ] = useState<string[]>([]);
const { gender, selectedParts, selectedColors, loadAvatarData, selectPart, selectColor, getFigureStringWithFace } = useFigureData();
const clubItemsFirst = useMemo(() => GetConfigurationValue<boolean>('avatareditor.show.clubitems.first', true), []);
const clubItemsDimmed = useMemo(() => GetConfigurationValue<boolean>('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>(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;
}

View File

@ -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<string>(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;