mirror of
https://github.com/billsonnn/nitro-react.git
synced 2024-11-26 15:40:51 +01:00
Begin new avatar editor
This commit is contained in:
parent
382cb665d8
commit
d675258adb
@ -10,7 +10,7 @@
|
||||
"eslint": "eslint src --ext .ts,.tsx"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/react-virtual": "3.0.0-alpha.0",
|
||||
"@tanstack/react-virtual": "3.2.0",
|
||||
"react": "^18.2.0",
|
||||
"react-bootstrap": "^2.2.2",
|
||||
"react-dom": "^18.2.0",
|
||||
|
152
src/api/avatar/AvatarEditorThumbnailsHelper.ts
Normal file
152
src/api/avatar/AvatarEditorThumbnailsHelper.ts
Normal file
@ -0,0 +1,152 @@
|
||||
import { AvatarFigurePartType, GetAssetManager, GetAvatarRenderManager, IFigurePart, IGraphicAsset, IPartColor, NitroAlphaFilter, NitroContainer, NitroSprite, TextureUtils } from '@nitrots/nitro-renderer';
|
||||
import { FigureData } from './FigureData';
|
||||
import { IAvatarEditorCategoryPartItem } from './IAvatarEditorCategoryPartItem';
|
||||
|
||||
export class AvatarEditorThumbnailsHelper
|
||||
{
|
||||
private static THUMBNAIL_CACHE: Map<string, string> = new Map();
|
||||
private static THUMB_DIRECTIONS: number[] = [ 2, 6, 0, 4, 3, 1 ];
|
||||
private static ALPHA_FILTER: NitroAlphaFilter = new NitroAlphaFilter({ alpha: 0.2 });
|
||||
private static DRAW_ORDER: string[] = [
|
||||
AvatarFigurePartType.LEFT_HAND_ITEM,
|
||||
AvatarFigurePartType.LEFT_HAND,
|
||||
AvatarFigurePartType.LEFT_SLEEVE,
|
||||
AvatarFigurePartType.LEFT_COAT_SLEEVE,
|
||||
AvatarFigurePartType.BODY,
|
||||
AvatarFigurePartType.SHOES,
|
||||
AvatarFigurePartType.LEGS,
|
||||
AvatarFigurePartType.CHEST,
|
||||
AvatarFigurePartType.CHEST_ACCESSORY,
|
||||
AvatarFigurePartType.COAT_CHEST,
|
||||
AvatarFigurePartType.CHEST_PRINT,
|
||||
AvatarFigurePartType.WAIST_ACCESSORY,
|
||||
AvatarFigurePartType.RIGHT_HAND,
|
||||
AvatarFigurePartType.RIGHT_SLEEVE,
|
||||
AvatarFigurePartType.RIGHT_COAT_SLEEVE,
|
||||
AvatarFigurePartType.HEAD,
|
||||
AvatarFigurePartType.FACE,
|
||||
AvatarFigurePartType.EYES,
|
||||
AvatarFigurePartType.HAIR,
|
||||
AvatarFigurePartType.HAIR_BIG,
|
||||
AvatarFigurePartType.FACE_ACCESSORY,
|
||||
AvatarFigurePartType.EYE_ACCESSORY,
|
||||
AvatarFigurePartType.HEAD_ACCESSORY,
|
||||
AvatarFigurePartType.HEAD_ACCESSORY_EXTRA,
|
||||
AvatarFigurePartType.RIGHT_HAND_ITEM,
|
||||
];
|
||||
|
||||
private static getThumbnailKey(setType: string, part: IAvatarEditorCategoryPartItem): string
|
||||
{
|
||||
return `${ setType }-${ part.partSet.id }`;
|
||||
}
|
||||
|
||||
public static async build(setType: string, part: IAvatarEditorCategoryPartItem, useColors: boolean, isDisabled: boolean = false): Promise<string>
|
||||
{
|
||||
if(!setType || !setType.length || !part || !part.partSet || !part.partSet.parts || !part.partSet.parts.length) return null;
|
||||
|
||||
const thumbnailKey = this.getThumbnailKey(setType, part);
|
||||
const cached = this.THUMBNAIL_CACHE.get(thumbnailKey);
|
||||
|
||||
if(cached) return cached;
|
||||
|
||||
const buildContainer = (part: IAvatarEditorCategoryPartItem, useColors: boolean, isDisabled: boolean = false) =>
|
||||
{
|
||||
const container = new NitroContainer();
|
||||
const parts = part.partSet.parts.concat().sort(this.sortByDrawOrder);
|
||||
|
||||
for(const part of parts)
|
||||
{
|
||||
if(!part) continue;
|
||||
|
||||
let asset: IGraphicAsset = null;
|
||||
let direction = 0;
|
||||
let hasAsset = false;
|
||||
|
||||
while(!hasAsset && (direction < AvatarEditorThumbnailsHelper.THUMB_DIRECTIONS.length))
|
||||
{
|
||||
const assetName = `${ FigureData.SCALE }_${ FigureData.STD }_${ part.type }_${ part.id }_${ AvatarEditorThumbnailsHelper.THUMB_DIRECTIONS[direction] }_${ FigureData.DEFAULT_FRAME }`;
|
||||
|
||||
asset = GetAssetManager().getAsset(assetName);
|
||||
|
||||
if(asset && asset.texture)
|
||||
{
|
||||
hasAsset = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
direction++;
|
||||
}
|
||||
}
|
||||
|
||||
if(!hasAsset) continue;
|
||||
|
||||
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(isDisabled) container.filters = [ AvatarEditorThumbnailsHelper.ALPHA_FILTER ];
|
||||
|
||||
container.addChild(sprite);
|
||||
}
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
return new Promise(async (resolve, reject) =>
|
||||
{
|
||||
const resetFigure = async (figure: string) =>
|
||||
{
|
||||
const container = buildContainer(part, useColors, isDisabled);
|
||||
const url = await TextureUtils.generateImageUrl(container);
|
||||
|
||||
AvatarEditorThumbnailsHelper.THUMBNAIL_CACHE.set(thumbnailKey, url);
|
||||
|
||||
resolve(url);
|
||||
}
|
||||
|
||||
const figureContainer = GetAvatarRenderManager().createFigureContainer(`${ setType }-${ part.partSet.id }`);
|
||||
|
||||
if(!GetAvatarRenderManager().isFigureContainerReady(figureContainer))
|
||||
{
|
||||
GetAvatarRenderManager().downloadAvatarFigure(figureContainer, {
|
||||
resetFigure,
|
||||
dispose: null,
|
||||
disposed: false
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
resetFigure(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static sortByDrawOrder(a: IFigurePart, b: IFigurePart): number
|
||||
{
|
||||
const indexA = AvatarEditorThumbnailsHelper.DRAW_ORDER.indexOf(a.type);
|
||||
const indexB = AvatarEditorThumbnailsHelper.DRAW_ORDER.indexOf(b.type);
|
||||
|
||||
if(indexA < indexB) return -1;
|
||||
|
||||
if(indexA > indexB) return 1;
|
||||
|
||||
if(a.index < b.index) return -1;
|
||||
|
||||
if(a.index > b.index) return 1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
9
src/api/avatar/IAvatarEditorCategory.ts
Normal file
9
src/api/avatar/IAvatarEditorCategory.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { IPartColor } from '@nitrots/nitro-renderer';
|
||||
import { IAvatarEditorCategoryPartItem } from './IAvatarEditorCategoryPartItem';
|
||||
|
||||
export interface IAvatarEditorCategory
|
||||
{
|
||||
setType: string;
|
||||
partItems: IAvatarEditorCategoryPartItem[];
|
||||
colorItems: IPartColor[][];
|
||||
}
|
10
src/api/avatar/IAvatarEditorCategoryPartItem.ts
Normal file
10
src/api/avatar/IAvatarEditorCategoryPartItem.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { IFigurePartSet } from '@nitrots/nitro-renderer';
|
||||
|
||||
export interface IAvatarEditorCategoryPartItem
|
||||
{
|
||||
id?: number;
|
||||
partSet?: IFigurePartSet;
|
||||
usesColor?: boolean;
|
||||
maxPaletteCount?: number;
|
||||
isClear?: boolean;
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
export * from './AvatarEditorAction';
|
||||
export * from './AvatarEditorGridColorItem';
|
||||
export * from './AvatarEditorGridPartItem';
|
||||
export * from './AvatarEditorThumbnailsHelper';
|
||||
export * from './AvatarEditorUtilities';
|
||||
export * from './BodyModel';
|
||||
export * from './CategoryBaseModel';
|
||||
@ -8,6 +9,8 @@ export * from './CategoryData';
|
||||
export * from './FigureData';
|
||||
export * from './FigureGenerator';
|
||||
export * from './HeadModel';
|
||||
export * from './IAvatarEditorCategory';
|
||||
export * from './IAvatarEditorCategoryModel';
|
||||
export * from './IAvatarEditorCategoryPartItem';
|
||||
export * from './LegModel';
|
||||
export * from './TorsoModel';
|
||||
|
69
src/common/InfiniteGrid.tsx
Normal file
69
src/common/InfiniteGrid.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import { FC, Fragment, ReactElement, useRef } from 'react';
|
||||
import { Base } from './Base';
|
||||
import { Flex } from './Flex';
|
||||
|
||||
interface InfiniteGridProps<T = any>
|
||||
{
|
||||
rows: T[];
|
||||
columnCount: number;
|
||||
overscan?: number;
|
||||
itemRender?: (item: T) => ReactElement;
|
||||
}
|
||||
|
||||
export const InfiniteGrid: FC<InfiniteGridProps> = props =>
|
||||
{
|
||||
const { rows = [], columnCount = 4, overscan = 5, itemRender = null } = props;
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const virtualizer = useVirtualizer({
|
||||
count: Math.ceil(rows.length / columnCount),
|
||||
overscan,
|
||||
getScrollElement: () => parentRef.current,
|
||||
estimateSize: () => 45,
|
||||
});
|
||||
const items = virtualizer.getVirtualItems();
|
||||
|
||||
return (
|
||||
<Base innerRef={ parentRef } fit position="relative" style={ { overflowY: 'auto' } }>
|
||||
<div
|
||||
style={ {
|
||||
height: virtualizer.getTotalSize(),
|
||||
width: '100%',
|
||||
position: 'relative'
|
||||
} }>
|
||||
<Flex
|
||||
column
|
||||
gap={ 1 }
|
||||
style={ {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
transform: `translateY(${ items[0]?.start ?? 0 }px)`
|
||||
} }>
|
||||
{ items.map(virtualRow => (
|
||||
<Flex
|
||||
gap={ 1 }
|
||||
key={ virtualRow.key + 'a' }
|
||||
data-index={ virtualRow.index }
|
||||
ref={ virtualizer.measureElement }
|
||||
style={ {
|
||||
minHeight: virtualRow.index === 0 ? 45 : virtualRow.size
|
||||
} }>
|
||||
{ Array.from(Array(columnCount)).map((e,i) =>
|
||||
{
|
||||
const item = rows[i + (virtualRow.index * columnCount)];
|
||||
|
||||
if(!item) return <Fragment
|
||||
key={ virtualRow.index + i + 'b' } />;
|
||||
|
||||
return itemRender(item);
|
||||
}) }
|
||||
</Flex>
|
||||
)) }
|
||||
</Flex>
|
||||
</div>
|
||||
</Base>
|
||||
);
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { useVirtual } from '@tanstack/react-virtual';
|
||||
import { FC, Fragment, ReactElement, useEffect, useRef, useState } from 'react';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import { FC, ReactElement, useRef, useState } from 'react';
|
||||
import { Base } from './Base';
|
||||
|
||||
interface InfiniteScrollProps<T = any>
|
||||
@ -14,50 +14,43 @@ export const InfiniteScroll: FC<InfiniteScrollProps> = props =>
|
||||
{
|
||||
const { rows = [], overscan = 5, scrollToBottom = false, rowRender = null } = props;
|
||||
const [ scrollIndex, setScrollIndex ] = useState<number>(rows.length - 1);
|
||||
const elementRef = useRef<HTMLDivElement>(null);
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { virtualItems = [], totalSize = 0, scrollToIndex = null } = useVirtual({
|
||||
parentRef: elementRef,
|
||||
size: rows.length,
|
||||
overscan
|
||||
|
||||
const virtualizer = useVirtualizer({
|
||||
count: rows.length,
|
||||
overscan,
|
||||
getScrollElement: () => parentRef.current,
|
||||
estimateSize: () => 45,
|
||||
});
|
||||
|
||||
const paddingTop = (virtualItems.length > 0) ? (virtualItems?.[0]?.start || 0) : 0
|
||||
const paddingBottom = (virtualItems.length > 0) ? (totalSize - (virtualItems?.[virtualItems.length - 1]?.end || 0)) : 0;
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!scrollToBottom) return;
|
||||
|
||||
scrollToIndex(scrollIndex);
|
||||
}, [ scrollToBottom, scrollIndex, scrollToIndex ]);
|
||||
const items = virtualizer.getVirtualItems();
|
||||
|
||||
return (
|
||||
<Base fit innerRef={ elementRef } position="relative" overflow="auto">
|
||||
{ (paddingTop > 0) &&
|
||||
<Base fit innerRef={ parentRef } position="relative" overflow="auto">
|
||||
<div
|
||||
style={ {
|
||||
height: virtualizer.getTotalSize(),
|
||||
width: '100%',
|
||||
position: 'relative'
|
||||
} }>
|
||||
<div
|
||||
style={ { minHeight: `${ paddingTop }px` } } /> }
|
||||
{ virtualItems.map(item =>
|
||||
{
|
||||
const row = rows[item.index];
|
||||
|
||||
if (!row) return (
|
||||
<Fragment
|
||||
key={ item.key } />
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={ item.key }
|
||||
data-index={ item.index }
|
||||
ref={ item.measureRef }>
|
||||
{ rowRender(row) }
|
||||
</div>
|
||||
)
|
||||
}) }
|
||||
{ (paddingBottom > 0) &&
|
||||
<div
|
||||
style={ { minHeight: `${ paddingBottom }px` } } /> }
|
||||
style={ {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
transform: `translateY(${ items[0]?.start ?? 0 }px)`
|
||||
} }>
|
||||
{ items.map((virtualRow) => (
|
||||
<div
|
||||
key={ virtualRow.key }
|
||||
data-index={ virtualRow.index }
|
||||
ref={ virtualizer.measureElement }>
|
||||
{ rowRender(rows[virtualRow.index]) }
|
||||
</div>
|
||||
)) }
|
||||
</div>
|
||||
</div>
|
||||
</Base>
|
||||
);
|
||||
}
|
||||
|
@ -2,21 +2,22 @@ export * from './AutoGrid';
|
||||
export * from './Base';
|
||||
export * from './Button';
|
||||
export * from './ButtonGroup';
|
||||
export * from './card';
|
||||
export * from './card/accordion';
|
||||
export * from './card/tabs';
|
||||
export * from './classNames';
|
||||
export * from './Column';
|
||||
export * from './draggable-window';
|
||||
export * from './Flex';
|
||||
export * from './FormGroup';
|
||||
export * from './Grid';
|
||||
export * from './GridContext';
|
||||
export * from './HorizontalRule';
|
||||
export * from './InfiniteGrid';
|
||||
export * from './InfiniteScroll';
|
||||
export * from './Text';
|
||||
export * from './card';
|
||||
export * from './card/accordion';
|
||||
export * from './card/tabs';
|
||||
export * from './classNames';
|
||||
export * from './draggable-window';
|
||||
export * from './layout';
|
||||
export * from './layout/limited-edition';
|
||||
export * from './Text';
|
||||
export * from './transitions';
|
||||
export * from './types';
|
||||
export * from './utils';
|
||||
|
336
src/components/avatar-editor-new/AvatarEditorView.scss
Normal file
336
src/components/avatar-editor-new/AvatarEditorView.scss
Normal file
@ -0,0 +1,336 @@
|
||||
.nitro-avatar-editor-spritesheet {
|
||||
background: url('@/assets/images/avatareditor/avatar-editor-spritesheet.png') transparent no-repeat;
|
||||
|
||||
&.arrow-left-icon {
|
||||
width: 28px;
|
||||
height: 21px;
|
||||
background-position: -226px -131px;
|
||||
}
|
||||
|
||||
&.arrow-right-icon {
|
||||
width: 28px;
|
||||
height: 21px;
|
||||
background-position: -226px -162px;
|
||||
}
|
||||
|
||||
&.ca-icon {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
background-position: -226px -61px;
|
||||
|
||||
&.selected {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
background-position: -226px -96px;
|
||||
}
|
||||
}
|
||||
|
||||
&.cc-icon {
|
||||
width: 31px;
|
||||
height: 29px;
|
||||
background-position: -145px -5px;
|
||||
|
||||
&.selected {
|
||||
width: 31px;
|
||||
height: 29px;
|
||||
background-position: -145px -44px;
|
||||
}
|
||||
}
|
||||
|
||||
&.ch-icon {
|
||||
width: 29px;
|
||||
height: 24px;
|
||||
background-position: -186px -39px;
|
||||
|
||||
&.selected {
|
||||
width: 29px;
|
||||
height: 24px;
|
||||
background-position: -186px -73px;
|
||||
}
|
||||
}
|
||||
|
||||
&.clear-icon {
|
||||
width: 27px;
|
||||
height: 27px;
|
||||
background-position: -145px -157px;
|
||||
}
|
||||
|
||||
&.cp-icon {
|
||||
width: 30px;
|
||||
height: 24px;
|
||||
background-position: -145px -264px;
|
||||
|
||||
&.selected {
|
||||
width: 30px;
|
||||
height: 24px;
|
||||
background-position: -186px -5px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&.ea-icon {
|
||||
width: 35px;
|
||||
height: 16px;
|
||||
background-position: -226px -193px;
|
||||
|
||||
&.selected {
|
||||
width: 35px;
|
||||
height: 16px;
|
||||
background-position: -226px -219px;
|
||||
}
|
||||
}
|
||||
|
||||
&.fa-icon {
|
||||
width: 27px;
|
||||
height: 20px;
|
||||
background-position: -186px -137px;
|
||||
|
||||
&.selected {
|
||||
width: 27px;
|
||||
height: 20px;
|
||||
background-position: -186px -107px;
|
||||
}
|
||||
}
|
||||
|
||||
&.female-icon {
|
||||
width: 18px;
|
||||
height: 27px;
|
||||
background-position: -186px -202px;
|
||||
|
||||
&.selected {
|
||||
width: 18px;
|
||||
height: 27px;
|
||||
background-position: -186px -239px;
|
||||
}
|
||||
}
|
||||
|
||||
&.ha-icon {
|
||||
width: 25px;
|
||||
height: 22px;
|
||||
background-position: -226px -245px;
|
||||
|
||||
&.selected {
|
||||
width: 25px;
|
||||
height: 22px;
|
||||
background-position: -226px -277px;
|
||||
}
|
||||
}
|
||||
|
||||
&.he-icon {
|
||||
width: 31px;
|
||||
height: 27px;
|
||||
background-position: -145px -83px;
|
||||
|
||||
&.selected {
|
||||
width: 31px;
|
||||
height: 27px;
|
||||
background-position: -145px -120px;
|
||||
}
|
||||
}
|
||||
|
||||
&.hr-icon {
|
||||
width: 29px;
|
||||
height: 25px;
|
||||
background-position: -145px -194px;
|
||||
|
||||
&.selected {
|
||||
width: 29px;
|
||||
height: 25px;
|
||||
background-position: -145px -229px;
|
||||
}
|
||||
}
|
||||
|
||||
&.lg-icon {
|
||||
width: 19px;
|
||||
height: 20px;
|
||||
background-position: -303px -45px;
|
||||
|
||||
&.selected {
|
||||
width: 19px;
|
||||
height: 20px;
|
||||
background-position: -303px -75px;
|
||||
}
|
||||
}
|
||||
|
||||
&.loading-icon {
|
||||
width: 21px;
|
||||
height: 25px;
|
||||
background-position: -186px -167px;
|
||||
}
|
||||
|
||||
|
||||
&.male-icon {
|
||||
width: 21px;
|
||||
height: 21px;
|
||||
background-position: -186px -276px;
|
||||
|
||||
&.selected {
|
||||
width: 21px;
|
||||
height: 21px;
|
||||
background-position: -272px -5px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&.sellable-icon {
|
||||
width: 17px;
|
||||
height: 15px;
|
||||
background-position: -303px -105px;
|
||||
}
|
||||
|
||||
|
||||
&.sh-icon {
|
||||
width: 37px;
|
||||
height: 10px;
|
||||
background-position: -303px -5px;
|
||||
|
||||
&.selected {
|
||||
width: 37px;
|
||||
height: 10px;
|
||||
background-position: -303px -25px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&.spotlight-icon {
|
||||
width: 130px;
|
||||
height: 305px;
|
||||
background-position: -5px -5px;
|
||||
}
|
||||
|
||||
|
||||
&.wa-icon {
|
||||
width: 36px;
|
||||
height: 18px;
|
||||
background-position: -226px -5px;
|
||||
|
||||
&.selected {
|
||||
width: 36px;
|
||||
height: 18px;
|
||||
background-position: -226px -33px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nitro-avatar-editor-wardrobe-figure-preview {
|
||||
background-color: $pale-sky;
|
||||
overflow: hidden;
|
||||
z-index: 1;
|
||||
|
||||
.avatar-image {
|
||||
position: absolute;
|
||||
bottom: -15px;
|
||||
margin: 0 auto;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.avatar-shadow {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 25px;
|
||||
width: 40px;
|
||||
height: 20px;
|
||||
margin: 0 auto;
|
||||
border-radius: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.20);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&:after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
top: 75%;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
border-radius: 50%;
|
||||
background-color: $pale-sky;
|
||||
box-shadow: 0 0 8px 2px rgba($white,.6);
|
||||
transform: scale(2);
|
||||
}
|
||||
|
||||
.button-container {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
z-index: 5;
|
||||
}
|
||||
}
|
||||
|
||||
.nitro-avatar-editor {
|
||||
width: $avatar-editor-width;
|
||||
height: $avatar-editor-height;
|
||||
|
||||
.category-item {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.figure-preview-container {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
background-color: $pale-sky;
|
||||
overflow: hidden;
|
||||
z-index: 1;
|
||||
|
||||
.arrow-container {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 0 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
bottom: 12px;
|
||||
z-index: 5;
|
||||
|
||||
.icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-image {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 50px;
|
||||
margin: 0 auto;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.avatar-spotlight {
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: 0 auto;
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.avatar-shadow {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 15px;
|
||||
width: 70px;
|
||||
height: 30px;
|
||||
margin: 0 auto;
|
||||
border-radius: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.20);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&:after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
top: 75%;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
border-radius: 50%;
|
||||
background-color: $pale-sky;
|
||||
box-shadow: 0 0 8px 2px rgba($white,.6);
|
||||
transform: scale(2);
|
||||
}
|
||||
}
|
||||
}
|
109
src/components/avatar-editor-new/AvatarEditorView.tsx
Normal file
109
src/components/avatar-editor-new/AvatarEditorView.tsx
Normal file
@ -0,0 +1,109 @@
|
||||
import { AddLinkEventTracker, AvatarEditorFigureCategory, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { FaDice, FaTrash, FaUndo } from 'react-icons/fa';
|
||||
import { AvatarEditorAction, LocalizeText } from '../../api';
|
||||
import { Button, ButtonGroup, Column, Grid, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../common';
|
||||
import { useAvatarEditor } from '../../hooks';
|
||||
import { AvatarEditorModelView } from './views/AvatarEditorModelView';
|
||||
|
||||
const DEFAULT_MALE_FIGURE: string = 'hr-100.hd-180-7.ch-215-66.lg-270-79.sh-305-62.ha-1002-70.wa-2007';
|
||||
const DEFAULT_FEMALE_FIGURE: string = 'hr-515-33.hd-600-1.ch-635-70.lg-716-66-62.sh-735-68';
|
||||
|
||||
export const AvatarEditorNewView: FC<{}> = props =>
|
||||
{
|
||||
const [ isVisible, setIsVisible ] = useState(false);
|
||||
const { avatarModels, activeModelKey, setActiveModelKey } = useAvatarEditor();
|
||||
|
||||
const processAction = (action: string) =>
|
||||
{
|
||||
switch(action)
|
||||
{
|
||||
case AvatarEditorAction.ACTION_CLEAR:
|
||||
return;
|
||||
case AvatarEditorAction.ACTION_RESET:
|
||||
return;
|
||||
case AvatarEditorAction.ACTION_RANDOMIZE:
|
||||
return;
|
||||
case AvatarEditorAction.ACTION_SAVE:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const linkTracker: ILinkEventTracker = {
|
||||
linkReceived: (url: string) =>
|
||||
{
|
||||
const parts = url.split('/');
|
||||
|
||||
if(parts.length < 2) return;
|
||||
|
||||
switch(parts[1])
|
||||
{
|
||||
case 'show':
|
||||
setIsVisible(true);
|
||||
return;
|
||||
case 'hide':
|
||||
setIsVisible(false);
|
||||
return;
|
||||
case 'toggle':
|
||||
setIsVisible(prevValue => !prevValue);
|
||||
return;
|
||||
}
|
||||
},
|
||||
eventUrlPrefix: 'avatar-editor/'
|
||||
};
|
||||
|
||||
AddLinkEventTracker(linkTracker);
|
||||
|
||||
return () => RemoveLinkEventTracker(linkTracker);
|
||||
}, []);
|
||||
|
||||
if(!isVisible) return null;
|
||||
|
||||
return (
|
||||
<NitroCardView uniqueKey="avatar-editor" className="nitro-avatar-editor">
|
||||
<NitroCardHeaderView headerText={ LocalizeText('avatareditor.title') } onCloseClick={ event => setIsVisible(false) } />
|
||||
<NitroCardTabsView>
|
||||
{ Object.keys(avatarModels).map(modelKey =>
|
||||
{
|
||||
const isActive = (activeModelKey === modelKey);
|
||||
|
||||
return (
|
||||
<NitroCardTabsItemView key={ modelKey } isActive={ isActive } onClick={ event => setActiveModelKey(modelKey) }>
|
||||
{ LocalizeText(`avatareditor.category.${ modelKey }`) }
|
||||
</NitroCardTabsItemView>
|
||||
);
|
||||
}) }
|
||||
</NitroCardTabsView>
|
||||
<NitroCardContentView>
|
||||
<Grid>
|
||||
<Column size={ 9 } overflow="hidden">
|
||||
{ ((activeModelKey.length > 0) && (activeModelKey !== AvatarEditorFigureCategory.WARDROBE)) &&
|
||||
<AvatarEditorModelView name={ activeModelKey } categories={ avatarModels[activeModelKey] } /> }
|
||||
{ (activeModelKey === AvatarEditorFigureCategory.WARDROBE) }
|
||||
</Column>
|
||||
<Column size={ 3 } overflow="hidden">
|
||||
{ /* <AvatarEditorFigurePreviewView figureData={ figureData } /> */ }
|
||||
<Column grow gap={ 1 }>
|
||||
<ButtonGroup>
|
||||
<Button variant="secondary" onClick={ event => processAction(AvatarEditorAction.ACTION_RESET) }>
|
||||
<FaUndo className="fa-icon" />
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={ event => processAction(AvatarEditorAction.ACTION_CLEAR) }>
|
||||
<FaTrash className="fa-icon" />
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={ event => processAction(AvatarEditorAction.ACTION_RANDOMIZE) }>
|
||||
<FaDice className="fa-icon" />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<Button className="w-100" variant="success" onClick={ event => processAction(AvatarEditorAction.ACTION_SAVE) }>
|
||||
{ LocalizeText('avatareditor.save') }
|
||||
</Button>
|
||||
</Column>
|
||||
</Column>
|
||||
</Grid>
|
||||
</NitroCardContentView>
|
||||
</NitroCardView>
|
||||
);
|
||||
}
|
30
src/components/avatar-editor-new/views/AvatarEditorIcon.tsx
Normal file
30
src/components/avatar-editor-new/views/AvatarEditorIcon.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import { FC, useMemo } from 'react';
|
||||
import { Base, BaseProps } from '../../../common';
|
||||
|
||||
type AvatarIconType = 'male' | 'female' | 'clear' | 'sellable' | string;
|
||||
|
||||
export interface AvatarEditorIconProps extends BaseProps<HTMLDivElement>
|
||||
{
|
||||
icon: AvatarIconType;
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
export const AvatarEditorIcon: FC<AvatarEditorIconProps> = props =>
|
||||
{
|
||||
const { icon = null, selected = false, classNames = [], children = null, ...rest } = props;
|
||||
|
||||
const getClassNames = useMemo(() =>
|
||||
{
|
||||
const newClassNames: string[] = [ 'nitro-avatar-editor-spritesheet' ];
|
||||
|
||||
if(icon && icon.length) newClassNames.push(icon + '-icon');
|
||||
|
||||
if(selected) newClassNames.push('selected');
|
||||
|
||||
if(classNames.length) newClassNames.push(...classNames);
|
||||
|
||||
return newClassNames;
|
||||
}, [ icon, selected, classNames ]);
|
||||
|
||||
return <Base classNames={ getClassNames } { ...rest } />
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
import { AvatarEditorFigureCategory } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect, useMemo, useState } from 'react';
|
||||
import { FigureData, IAvatarEditorCategory } from '../../../api';
|
||||
import { Column, Flex, Grid } from '../../../common';
|
||||
import { useAvatarEditor } from '../../../hooks';
|
||||
import { AvatarEditorIcon } from './AvatarEditorIcon';
|
||||
import { AvatarEditorFigureSetView } from './figure-set';
|
||||
import { AvatarEditorPaletteSetView } from './palette-set';
|
||||
|
||||
export const AvatarEditorModelView: FC<{
|
||||
name: string,
|
||||
categories: IAvatarEditorCategory[]
|
||||
}> = props =>
|
||||
{
|
||||
const { name = '', categories = [] } = props;
|
||||
const [ activeSetType, setActiveSetType ] = useState<string>('');
|
||||
const { maxPaletteCount = 1 } = useAvatarEditor();
|
||||
|
||||
const activeCategory = useMemo(() =>
|
||||
{
|
||||
return categories.find(category => category.setType === activeSetType) ?? null;
|
||||
}, [ categories, activeSetType ]);
|
||||
|
||||
const setGender = (gender: string) =>
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!activeCategory) return;
|
||||
|
||||
// we need to run this when we change which parts r selected
|
||||
/* for(const partItem of activeCategory.partItems)
|
||||
{
|
||||
if(!partItem || !part.isSelected) continue;
|
||||
|
||||
setMaxPaletteCount(part.maxColorIndex || 1);
|
||||
|
||||
break;
|
||||
} */
|
||||
}, [ activeCategory ])
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!categories || !categories.length) return;
|
||||
|
||||
setActiveSetType(categories[0]?.setType)
|
||||
}, [ categories ]);
|
||||
|
||||
if(!activeCategory) return null;
|
||||
|
||||
return (
|
||||
<Grid>
|
||||
<Column size={ 2 }>
|
||||
{ (name === AvatarEditorFigureCategory.GENERIC) &&
|
||||
<>
|
||||
<Flex center pointer className="category-item" onClick={ event => setGender(FigureData.MALE) }>
|
||||
<AvatarEditorIcon icon="male" selected={ false } />
|
||||
</Flex>
|
||||
<Flex center pointer className="category-item" onClick={ event => setGender(FigureData.FEMALE) }>
|
||||
<AvatarEditorIcon icon="female" selected={ false } />
|
||||
</Flex>
|
||||
</> }
|
||||
{ (name !== AvatarEditorFigureCategory.GENERIC) && (categories.length > 0) && categories.map(category =>
|
||||
{
|
||||
return (
|
||||
<Flex center pointer key={ category.setType } className="category-item" onClick={ event => setActiveSetType(category.setType) }>
|
||||
<AvatarEditorIcon icon={ category.setType } selected={ (activeSetType === category.setType) } />
|
||||
</Flex>
|
||||
);
|
||||
}) }
|
||||
</Column>
|
||||
<Column size={ 5 } overflow="hidden">
|
||||
<AvatarEditorFigureSetView category={ activeCategory } />
|
||||
</Column>
|
||||
<Column size={ 5 } overflow="hidden">
|
||||
{ (maxPaletteCount >= 1) &&
|
||||
<AvatarEditorPaletteSetView category={ activeCategory } paletteIndex={ 0 } /> }
|
||||
{ (maxPaletteCount === 2) &&
|
||||
<AvatarEditorPaletteSetView category={ activeCategory } paletteIndex={ 1 } /> }
|
||||
</Column>
|
||||
</Grid>
|
||||
);
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { AvatarEditorThumbnailsHelper, GetConfigurationValue, IAvatarEditorCategoryPartItem } from '../../../../api';
|
||||
import { LayoutCurrencyIcon, LayoutGridItem, LayoutGridItemProps } from '../../../../common';
|
||||
import { AvatarEditorIcon } from '../AvatarEditorIcon';
|
||||
|
||||
export const AvatarEditorFigureSetItemView: FC<{
|
||||
setType: string;
|
||||
partItem: IAvatarEditorCategoryPartItem;
|
||||
isSelected: boolean;
|
||||
} & LayoutGridItemProps> = props =>
|
||||
{
|
||||
const { setType = null, partItem = null, isSelected = false, ...rest } = props;
|
||||
const [ assetUrl, setAssetUrl ] = useState<string>('');
|
||||
|
||||
const isHC = !GetConfigurationValue<boolean>('hc.disabled', false) && ((partItem.partSet?.clubLevel ?? 0) > 0);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!setType || !setType.length || !partItem) return;
|
||||
|
||||
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);
|
||||
|
||||
if(url && url.length) setAssetUrl(url);
|
||||
}
|
||||
|
||||
loadImage();
|
||||
}, [ setType, partItem ]);
|
||||
|
||||
if(!partItem || !partItem.partSet) return null;
|
||||
|
||||
return (
|
||||
<LayoutGridItem itemImage={ (partItem.isClear ? undefined : assetUrl) } itemActive={ isSelected } style={ { width: '100%' } } { ...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" /> }
|
||||
</LayoutGridItem>
|
||||
);
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
import { FC, useRef } from 'react';
|
||||
import { IAvatarEditorCategory, IAvatarEditorCategoryPartItem } from '../../../../api';
|
||||
import { InfiniteGrid } from '../../../../common';
|
||||
import { useAvatarEditor } from '../../../../hooks';
|
||||
import { AvatarEditorFigureSetItemView } from './AvatarEditorFigureSetItemView';
|
||||
|
||||
export const AvatarEditorFigureSetView: FC<{
|
||||
category: IAvatarEditorCategory
|
||||
}> = props =>
|
||||
{
|
||||
const { category = null } = props;
|
||||
const { selectedParts = null, selectPart } = useAvatarEditor();
|
||||
const elementRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const isPartItemSelected = (partItem: IAvatarEditorCategoryPartItem) =>
|
||||
{
|
||||
if(!category || !category.setType || !selectedParts || !selectedParts[category.setType]) return false;
|
||||
|
||||
const partId = selectedParts[category.setType];
|
||||
|
||||
return (partId === partItem.id);
|
||||
}
|
||||
|
||||
const columnCount = 3;
|
||||
|
||||
return (
|
||||
<InfiniteGrid rows={ category.partItems } columnCount={ columnCount } overscan={ 5 } itemRender={ (item: IAvatarEditorCategoryPartItem) =>
|
||||
{
|
||||
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) + '%' } } />
|
||||
)
|
||||
} } />
|
||||
);
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
export * from './AvatarEditorFigureSetItemView';
|
||||
export * from './AvatarEditorFigureSetView';
|
@ -0,0 +1,27 @@
|
||||
import { ColorConverter, IPartColor } from '@nitrots/nitro-renderer';
|
||||
import { FC } from 'react';
|
||||
import { GetConfigurationValue } from '../../../../api';
|
||||
import { LayoutCurrencyIcon, LayoutGridItem, LayoutGridItemProps } from '../../../../common';
|
||||
|
||||
export interface AvatarEditorPaletteSetItemProps extends LayoutGridItemProps
|
||||
{
|
||||
setType: string;
|
||||
partColor: IPartColor;
|
||||
isSelected: boolean;
|
||||
}
|
||||
|
||||
// its disabled if its hc and you dont have it
|
||||
export const AvatarEditorPaletteSetItem: FC<AvatarEditorPaletteSetItemProps> = props =>
|
||||
{
|
||||
const { setType = null, partColor = null, isSelected = false, ...rest } = props;
|
||||
|
||||
if(!partColor) return null;
|
||||
|
||||
const isHC = !GetConfigurationValue<boolean>('hc.disabled', false) && (partColor.clubLevel > 0);
|
||||
|
||||
return (
|
||||
<LayoutGridItem itemHighlight itemColor={ ColorConverter.int2rgb(partColor.rgb) } itemActive={ isSelected } className="clear-bg" { ...rest }>
|
||||
{ isHC && <LayoutCurrencyIcon className="position-absolute end-1 bottom-1" type="hc" /> }
|
||||
</LayoutGridItem>
|
||||
);
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
import { IPartColor } from '@nitrots/nitro-renderer';
|
||||
import { FC, useRef } from 'react';
|
||||
import { IAvatarEditorCategory } from '../../../../api';
|
||||
import { AutoGrid } from '../../../../common';
|
||||
import { useAvatarEditor } from '../../../../hooks';
|
||||
import { AvatarEditorPaletteSetItem } from './AvatarEditorPaletteSetItemView';
|
||||
|
||||
export const AvatarEditorPaletteSetView: FC<{
|
||||
category: IAvatarEditorCategory,
|
||||
paletteIndex: number;
|
||||
}> = props =>
|
||||
{
|
||||
const { category = null, paletteIndex = -1 } = props;
|
||||
const paletteSet = category?.colorItems[paletteIndex] ?? null;
|
||||
const { selectedColors = null, selectColor } = useAvatarEditor();
|
||||
const elementRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const isPartColorSelected = (partColor: IPartColor) =>
|
||||
{
|
||||
if(!category || !category.setType || !selectedColors || !selectedColors[category.setType] || !selectedColors[category.setType][paletteIndex]) return false;
|
||||
|
||||
const colorId = selectedColors[category.setType][paletteIndex];
|
||||
|
||||
return (colorId === partColor.id);
|
||||
}
|
||||
|
||||
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) } />) }
|
||||
</AutoGrid>
|
||||
);
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
export * from './AvatarEditorPaletteSetItemView';
|
||||
export * from './AvatarEditorPaletteSetView';
|
@ -1,4 +1,4 @@
|
||||
import { AddLinkEventTracker, AvatarEditorFigureCategory, FigureSetIdsMessageEvent, GetAvatarRenderManager, GetSessionDataManager, GetWardrobeMessageComposer, IAvatarFigureContainer, ILinkEventTracker, RemoveLinkEventTracker, UserFigureComposer, UserWardrobePageEvent } from '@nitrots/nitro-renderer';
|
||||
import { AvatarEditorFigureCategory, FigureSetIdsMessageEvent, GetAvatarRenderManager, GetSessionDataManager, GetWardrobeMessageComposer, IAvatarFigureContainer, UserFigureComposer, UserWardrobePageEvent } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { FaDice, FaTrash, FaUndo } from 'react-icons/fa';
|
||||
import { AvatarEditorAction, AvatarEditorUtilities, BodyModel, FigureData, GetClubMemberLevel, GetConfigurationValue, HeadModel, IAvatarEditorCategoryModel, LegModel, LocalizeText, SendMessageComposer, TorsoModel, generateRandomFigure } from '../../api';
|
||||
@ -148,7 +148,7 @@ export const AvatarEditorView: FC<{}> = props =>
|
||||
setFigureData(figures.get(gender));
|
||||
}, [ figures ]);
|
||||
|
||||
useEffect(() =>
|
||||
/* useEffect(() =>
|
||||
{
|
||||
const linkTracker: ILinkEventTracker = {
|
||||
linkReceived: (url: string) =>
|
||||
@ -176,7 +176,7 @@ export const AvatarEditorView: FC<{}> = props =>
|
||||
AddLinkEventTracker(linkTracker);
|
||||
|
||||
return () => RemoveLinkEventTracker(linkTracker);
|
||||
}, []);
|
||||
}, []); */
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
|
@ -3,6 +3,7 @@ import { FC, useEffect, useState } from 'react';
|
||||
import { Base, TransitionAnimation, TransitionAnimationTypes } from '../../common';
|
||||
import { useNitroEvent } from '../../hooks';
|
||||
import { AchievementsView } from '../achievements/AchievementsView';
|
||||
import { AvatarEditorNewView } from '../avatar-editor-new/AvatarEditorView';
|
||||
import { AvatarEditorView } from '../avatar-editor/AvatarEditorView';
|
||||
import { CameraWidgetView } from '../camera/CameraWidgetView';
|
||||
import { CampaignView } from '../campaign/CampaignView';
|
||||
@ -89,6 +90,7 @@ export const MainView: FC<{}> = props =>
|
||||
<ChatHistoryView />
|
||||
<WiredView />
|
||||
<AvatarEditorView />
|
||||
<AvatarEditorNewView />
|
||||
<AchievementsView />
|
||||
<NavigatorView />
|
||||
<InventoryView />
|
||||
|
1
src/hooks/avatar-editor/index.ts
Normal file
1
src/hooks/avatar-editor/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './useAvatarEditor';
|
233
src/hooks/avatar-editor/useAvatarEditor.ts
Normal file
233
src/hooks/avatar-editor/useAvatarEditor.ts
Normal file
@ -0,0 +1,233 @@
|
||||
import { AvatarEditorFigureCategory, GetAvatarRenderManager, 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';
|
||||
|
||||
const MAX_PALETTES: number = 2;
|
||||
|
||||
const useAvatarEditorState = () =>
|
||||
{
|
||||
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 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) =>
|
||||
{
|
||||
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)
|
||||
{
|
||||
// clear the part
|
||||
return;
|
||||
}
|
||||
|
||||
if(GetClubMemberLevel() < partItem.partSet.clubLevel) return;
|
||||
|
||||
setMaxPaletteCount(partItem.maxPaletteCount || 1);
|
||||
|
||||
setSelectedParts(prevValue =>
|
||||
{
|
||||
const newValue = { ...prevValue };
|
||||
|
||||
newValue[setType] = partItem.id;
|
||||
|
||||
return newValue;
|
||||
});
|
||||
}, [ activeModel ]);
|
||||
|
||||
const selectColor = 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;
|
||||
|
||||
setSelectedColors(prevValue =>
|
||||
{
|
||||
const newValue = { ...prevValue };
|
||||
|
||||
if(!newValue[setType]) newValue[setType] = {};
|
||||
|
||||
if(!newValue[setType][paletteId]) newValue[setType][paletteId] = -1;
|
||||
|
||||
newValue[setType][paletteId] = partColor.id;
|
||||
|
||||
return newValue;
|
||||
})
|
||||
}, [ activeModel ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
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);
|
||||
|
||||
// TODO - check what this does
|
||||
/* if(setType !== FigureData.FACE)
|
||||
{
|
||||
let i = 0;
|
||||
|
||||
while(i < colorIds.length)
|
||||
{
|
||||
if(partColor.id === colorIds[i]) partColors[i] = partColor;
|
||||
|
||||
i++;
|
||||
}
|
||||
} */
|
||||
}
|
||||
|
||||
let mandatorySetIds: string[] = [];
|
||||
|
||||
if(clubItemsDimmed)
|
||||
{
|
||||
//mandatorySetIds = GetAvatarRenderManager().getMandatoryAvatarPartSetIds(this.CURRENT_FIGURE.gender, 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
//mandatorySetIds = GetAvatarRenderManager().getMandatoryAvatarPartSetIds(this.CURRENT_FIGURE.gender, clubMemberLevel);
|
||||
}
|
||||
|
||||
const isntMandatorySet = (mandatorySetIds.indexOf(setType) === -1);
|
||||
|
||||
if(isntMandatorySet) partItems.push({ id: -1, isClear: true });
|
||||
|
||||
const usesColor = (setType !== FigureData.FACE);
|
||||
const partSets = set.partSets;
|
||||
|
||||
for(let i = (partSets.length); i >= 0; i--)
|
||||
{
|
||||
const partSet = partSets.getWithIndex(i);
|
||||
|
||||
if(!partSet || !partSet.isSelectable) 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(clubItemsFirst ? clubSorter : noobSorter);
|
||||
|
||||
for(let i = 0; i < MAX_PALETTES; i++) colorItems[i].sort(colorSorter);
|
||||
|
||||
return { setType, partItems, colorItems };
|
||||
}
|
||||
|
||||
newAvatarModels[AvatarEditorFigureCategory.GENERIC] = [ FigureData.FACE ].map(setType => buildCategory(setType));
|
||||
newAvatarModels[AvatarEditorFigureCategory.HEAD] = [ FigureData.HAIR, FigureData.HAT, FigureData.HEAD_ACCESSORIES, FigureData.EYE_ACCESSORIES, FigureData.FACE_ACCESSORIES ].map(setType => buildCategory(setType));
|
||||
newAvatarModels[AvatarEditorFigureCategory.TORSO] = [ FigureData.SHIRT, FigureData.CHEST_PRINTS, FigureData.JACKET, FigureData.CHEST_ACCESSORIES ].map(setType => buildCategory(setType));
|
||||
newAvatarModels[AvatarEditorFigureCategory.LEGS] = [ FigureData.TROUSERS, FigureData.SHOES, FigureData.TROUSER_ACCESSORIES ].map(setType => buildCategory(setType));
|
||||
newAvatarModels[AvatarEditorFigureCategory.WARDROBE] = [];
|
||||
|
||||
console.log(newAvatarModels);
|
||||
|
||||
setAvatarModels(newAvatarModels);
|
||||
setActiveModelKey(AvatarEditorFigureCategory.GENERIC);
|
||||
}, [ clubItemsDimmed, clubItemsFirst ]);
|
||||
|
||||
return { avatarModels, activeModelKey, setActiveModelKey, selectedParts, selectedColors, maxPaletteCount, selectPart, selectColor };
|
||||
}
|
||||
|
||||
export const useAvatarEditor = () => useBetween(useAvatarEditorState);
|
||||
|
||||
const clubSorter = (a: { partSet: IFigurePartSet, usesColor: boolean, isClear?: boolean }, b: { partSet: IFigurePartSet, usesColor: boolean, isClear?: 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);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const colorSorter = (a: IPartColor, b: IPartColor) =>
|
||||
{
|
||||
const clubLevelA = (!a ? -1 : a.clubLevel);
|
||||
const clubLevelB = (!b ? -1 : b.clubLevel);
|
||||
|
||||
if(clubLevelA < clubLevelB) return -1;
|
||||
|
||||
if(clubLevelA > clubLevelB) return 1;
|
||||
|
||||
if(a.index < b.index) return -1;
|
||||
|
||||
if(a.index > b.index) return 1;
|
||||
|
||||
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;
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
export * from './UseMountEffect';
|
||||
export * from './achievements';
|
||||
export * from './avatar-editor';
|
||||
export * from './camera';
|
||||
export * from './catalog';
|
||||
export * from './chat-history';
|
||||
@ -14,6 +15,10 @@ export * from './navigator';
|
||||
export * from './notification';
|
||||
export * from './purse';
|
||||
export * from './rooms';
|
||||
export * from './rooms/engine';
|
||||
export * from './rooms/promotes';
|
||||
export * from './rooms/widgets';
|
||||
export * from './rooms/widgets/furniture';
|
||||
export * from './session';
|
||||
export * from './useLocalStorage';
|
||||
export * from './useSharedVisibility';
|
||||
|
23
yarn.lock
23
yarn.lock
@ -180,7 +180,7 @@
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.24.0"
|
||||
|
||||
"@babel/runtime@^7.16.7", "@babel/runtime@^7.21.0", "@babel/runtime@^7.22.5", "@babel/runtime@^7.23.2", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.8.7":
|
||||
"@babel/runtime@^7.21.0", "@babel/runtime@^7.22.5", "@babel/runtime@^7.23.2", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.8.7":
|
||||
version "7.24.1"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.1.tgz#431f9a794d173b53720e69a6464abc6f0e2a5c57"
|
||||
integrity sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==
|
||||
@ -445,11 +445,6 @@
|
||||
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f"
|
||||
integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==
|
||||
|
||||
"@reach/observe-rect@^1.1.0":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@reach/observe-rect/-/observe-rect-1.2.0.tgz#d7a6013b8aafcc64c778a0ccb83355a11204d3b2"
|
||||
integrity sha512-Ba7HmkFgfQxZqqaeIWWkNK0rEhpxVQHIoVyW1YDSkGsGIXzcaW4deC8B0pZrNSSyLTdIk7y+5olKt5+g0GmFIQ==
|
||||
|
||||
"@react-aria/ssr@^3.5.0":
|
||||
version "3.9.2"
|
||||
resolved "https://registry.yarnpkg.com/@react-aria/ssr/-/ssr-3.9.2.tgz#01b756965cd6e32b95217f968f513eb3bd6ee44b"
|
||||
@ -551,13 +546,17 @@
|
||||
dependencies:
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@tanstack/react-virtual@3.0.0-alpha.0":
|
||||
version "3.0.0-alpha.0"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.0.0-alpha.0.tgz#2ca5a75fa609eca2b2ba622024d3aa3ee097bc30"
|
||||
integrity sha512-WpHU/dt34NwZZ8qtiE05TF+nX/b1W6qrWZarO+s8jJFpPVicrTbJKp5Bjt4eSJuk7aYw272oEfsH3ABBRgj+3A==
|
||||
"@tanstack/react-virtual@3.2.0":
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.2.0.tgz#fb70f9c6baee753a5a0f7618ac886205d5a02af9"
|
||||
integrity sha512-OEdMByf2hEfDa6XDbGlZN8qO6bTjlNKqjM3im9JG+u3mCL8jALy0T/67oDI001raUUPh1Bdmfn4ZvPOV5knpcg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.16.7"
|
||||
"@reach/observe-rect" "^1.1.0"
|
||||
"@tanstack/virtual-core" "3.2.0"
|
||||
|
||||
"@tanstack/virtual-core@3.2.0":
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.2.0.tgz#874d36135e4badce2719e7bdc556ce240cbaff14"
|
||||
integrity sha512-P5XgYoAw/vfW65byBbJQCw+cagdXDT/qH6wmABiLt4v4YBT2q2vqCOhihe+D1Nt325F/S/0Tkv6C5z0Lv+VBQQ==
|
||||
|
||||
"@types/babel__core@^7.20.5":
|
||||
version "7.20.5"
|
||||
|
Loading…
Reference in New Issue
Block a user