Avatar editor updates

This commit is contained in:
Bill 2021-07-21 03:09:03 -04:00
parent df167ba1d8
commit 918d2eb6bd
36 changed files with 407 additions and 300 deletions

View File

@ -0,0 +1,7 @@
import { IPalette } from 'nitro-renderer';
import { GetAvatarRenderManager } from './GetAvatarRenderManager';
export function GetAvatarPalette(paletteId: number): IPalette
{
return GetAvatarRenderManager().structureData.getPalette(paletteId);
}

View File

@ -0,0 +1,7 @@
import { ISetType } from 'nitro-renderer';
import { GetAvatarRenderManager } from './GetAvatarRenderManager';
export function GetAvatarSetType(setType: string): ISetType
{
return GetAvatarRenderManager().structureData.getSetType(setType);
}

View File

@ -1 +1,3 @@
export * from './GetAvatarPalette';
export * from './GetAvatarRenderManager';
export * from './GetAvatarSetType';

View File

@ -0,0 +1,6 @@
import { GetSessionDataManager } from './GetSessionDataManager';
export function GetClubMemberLevel(): number
{
return GetSessionDataManager().clubLevel;
}

View File

@ -1,6 +1,7 @@
export * from './CanManipulateFurniture';
export * from './GetCanStandUp';
export * from './GetCanUseExpression';
export * from './GetClubMemberLevel';
export * from './GetFurnitureDataForProductOffer';
export * from './GetFurnitureDataForRoomObject';
export * from './GetOwnPosture';

Binary file not shown.

Before

Width:  |  Height:  |  Size: 263 B

After

Width:  |  Height:  |  Size: 272 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 B

After

Width:  |  Height:  |  Size: 236 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 B

After

Width:  |  Height:  |  Size: 270 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 B

After

Width:  |  Height:  |  Size: 196 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 B

After

Width:  |  Height:  |  Size: 236 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 B

After

Width:  |  Height:  |  Size: 228 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 B

After

Width:  |  Height:  |  Size: 256 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 B

After

Width:  |  Height:  |  Size: 208 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 165 B

After

Width:  |  Height:  |  Size: 266 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 B

After

Width:  |  Height:  |  Size: 257 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 199 B

After

Width:  |  Height:  |  Size: 308 B

View File

@ -179,15 +179,14 @@
&.clear-icon {
background-image: url('../images/avatareditor/clear-icon.png');
width: 16px;
height: 16px;
width: 27px;
height: 27px;
}
&.ca-icon {
background-image: url('../images/avatareditor/ca-icon.png');
width: 30px;
height: 24px;
background
width: 25px;
height: 25px;
&.selected {
background-image: url('../images/avatareditor/ca-selected-icon.png');
@ -216,8 +215,8 @@
&.cp-icon {
background-image: url('../images/avatareditor/cp-icon.png');
width: 25px;
height: 25px;
width: 30px;
height: 24px;
&.selected {
background-image: url('../images/avatareditor/cp-selected-icon.png');

View File

@ -1,6 +1,8 @@
.content-area {
padding-top: $container-padding-x;
padding-bottom: $container-padding-x;
resize: vertical;
overflow: auto;
&.simple {
padding-left: ($container-padding-x + 25px);

View File

@ -4,11 +4,11 @@ import { NitroCardGridThemes, NitroCardGridViewProps } from './NitroCardGridView
export const NitroCardGridView: FC<NitroCardGridViewProps> = props =>
{
const { columns = 5, theme = NitroCardGridThemes.THEME_DEFAULT, children = null } = props;
const { columns = 5, theme = NitroCardGridThemes.THEME_DEFAULT, className = '', children = null, ...rest } = props;
return (
<NitroCardGridContextProvider value={ { theme } }>
<div className={ `h-100 overflow-hidden nitro-card-grid ${ theme }` }>
<div className={ `h-100 overflow-hidden nitro-card-grid ${ theme } ${ className || '' }` } { ...rest }>
<div className={ `row row-cols-${ columns } align-content-start g-0 w-100 h-100 overflow-auto` }>
{ children }
</div>

View File

@ -1,4 +1,6 @@
export interface NitroCardGridViewProps
import { DetailsHTMLAttributes } from 'react';
export interface NitroCardGridViewProps extends DetailsHTMLAttributes<HTMLDivElement>
{
columns?: number;
theme?: string;

View File

@ -3,6 +3,9 @@
max-height: 50px;
.grid-item {
display: flex;
justify-content: center;
align-items: center;
position: relative;
width: 100%;
height: 100%;

View File

@ -1,9 +1,13 @@
.nitro-avatar-editor {
width: 600px;
width: 620px;
.content-area {
height: 330px;
max-height: 330px;
min-height: 300px;
height: 300px;
}
.category-item {
height: 40px;
}
.figure-preview-container {
@ -13,27 +17,53 @@
overflow: hidden;
z-index: 1;
.avatar-image {
margin: 45px auto 0;
z-index: 2;
}
.arrow-container {
position: absolute;
width: 100%;
margin: 0 auto;
padding: 0 10px;
display: flex;
justify-content: center;
justify-content: space-between;
bottom: 12px;
z-index: 3;
z-index: 5;
.icon {
cursor: pointer;
}
}
.arrow-left-icon {
margin-right: 10px;
}
.avatar-image {
position: absolute;
left: 0;
right: 0;
bottom: 50px;
margin: 0 auto;
z-index: 4;
}
.avatar-spotlight {
position: absolute;
top: -10px;
width: 100%;
height: 305px;
margin: 0 auto;
background: transparent url('../../assets/images/avatareditor/spotlight.png') no-repeat center;
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 {
@ -44,7 +74,8 @@
left: 0;
right: 0;
border-radius: 50%;
background-color: red;
background-color: $pale-sky;
box-shadow: 0 0 8px 2px rgba($white,.6);
transform: scale(2);
}
}

View File

@ -1,103 +1,91 @@
import { AvatarEditorFigureCategory } from 'nitro-renderer';
import { FC, useCallback, useEffect, useReducer, useState } from 'react';
import { AvatarDirectionAngle, AvatarEditorFigureCategory, UserFigureComposer } from 'nitro-renderer';
import { FC, useCallback, useEffect, useState } from 'react';
import { GetSessionDataManager } from '../../api';
import { AvatarEditorEvent } from '../../events/avatar-editor';
import { SendMessageHook } from '../../hooks';
import { useUiEvent } from '../../hooks/events/ui/ui-event';
import { NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../layout';
import { LocalizeText } from '../../utils/LocalizeText';
import { AvatarEditorViewProps } from './AvatarEditorView.types';
import { AvatarEditor } from './common/AvatarEditor';
import { AvatarEditorUtilities } from './common/AvatarEditorUtilities';
import { BodyModel } from './common/BodyModel';
import { FigureData } from './common/FigureData';
import { HeadModel } from './common/HeadModel';
import { IAvatarEditorCategoryModel } from './common/IAvatarEditorCategoryModel';
import { LegModel } from './common/LegModel';
import { TorsoModel } from './common/TorsoModel';
import { AvatarEditorContextProvider } from './context/AvatarEditorContext';
import { AvatarEditorReducer, initialAvatarEditor } from './reducers/AvatarEditorReducer';
import { AvatarEditorFigurePreviewView } from './views/figure-preview/AvatarEditorFigurePreviewView';
import { AvatarEditorModelView } from './views/model/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 AvatarEditorView: FC<AvatarEditorViewProps> = props =>
{
const [ isVisible, setIsVisible ] = useState(false);
const [ avatarEditorState, dispatchAvatarEditorState ] = useReducer(AvatarEditorReducer, initialAvatarEditor);
const [ avatarEditor, setAvatarEditor ] = useState<AvatarEditor>(null);
const [ figures, setFigures ] = useState<Map<string, FigureData>>(null);
const [ figureData, setFigureData ] = useState<FigureData>(null);
const [ categories, setCategories ] = useState<Map<string, IAvatarEditorCategoryModel>>(null);
const [ activeCategory, setActiveCategory ] = useState<IAvatarEditorCategoryModel>(null);
const [ lastFigure, setLastFigure ] = useState<string>(null);
const [ lastGender, setLastGender ] = useState<string>(null);
const [ needsReset, setNeedsReset ] = useState(false);
const [ isInitalized, setIsInitalized ] = useState(false);
const selectCategory = useCallback((name: string) =>
{
if(!categories) return;
setActiveCategory(categories.get(name));
}, [ categories ]);
const resetCategories = useCallback((editor: AvatarEditor) =>
const resetCategories = useCallback(() =>
{
const categories = new Map();
categories.set(AvatarEditorFigureCategory.GENERIC, new BodyModel(editor));
categories.set(AvatarEditorFigureCategory.HEAD, new HeadModel(editor));
categories.set(AvatarEditorFigureCategory.TORSO, new TorsoModel(editor));
categories.set(AvatarEditorFigureCategory.LEGS, new LegModel(editor));
categories.set(AvatarEditorFigureCategory.GENERIC, new BodyModel());
categories.set(AvatarEditorFigureCategory.HEAD, new HeadModel());
categories.set(AvatarEditorFigureCategory.TORSO, new TorsoModel());
categories.set(AvatarEditorFigureCategory.LEGS, new LegModel());
setCategories(categories);
setActiveCategory(categories.get(AvatarEditorFigureCategory.GENERIC));
}, []);
const selectGender = useCallback((gender: string) =>
const setupFigures = useCallback(() =>
{
if(gender === avatarEditor.gender) return;
const figures: Map<string, FigureData> = new Map();
avatarEditor.gender = gender;
const maleFigure = new FigureData();
const femaleFigure = new FigureData();
resetCategories(avatarEditor);
}, [ avatarEditor, resetCategories ]);
maleFigure.loadAvatarData(DEFAULT_MALE_FIGURE, FigureData.MALE);
femaleFigure.loadAvatarData(DEFAULT_FEMALE_FIGURE, FigureData.FEMALE);
figures.set(FigureData.MALE, maleFigure);
figures.set(FigureData.FEMALE, femaleFigure);
setFigures(figures);
setFigureData(figures.get(FigureData.MALE));
}, []);
const loadAvatarInEditor = useCallback((figure: string, gender: string, reset: boolean = true) =>
{
if(!avatarEditor) return;
gender = AvatarEditorUtilities.getGender(gender);
switch(gender)
let newFigureData = figureData;
if(gender !== newFigureData.gender) newFigureData = figures.get(gender);
if(figure !== newFigureData.getFigureString()) newFigureData.loadAvatarData(figure, gender);
if(newFigureData !== figureData) setFigureData(newFigureData);
if(reset)
{
case FigureData.MALE:
case 'm':
case 'M':
gender = FigureData.MALE;
break;
case FigureData.FEMALE:
case 'f':
case 'F':
gender = FigureData.FEMALE;
break;
default:
gender = FigureData.MALE;
setLastFigure(figureData.getFigureString());
setLastGender(figureData.gender);
}
let update = false;
if(gender !== avatarEditor.gender)
{
avatarEditor.gender = gender;
update = true;
}
const figureData = avatarEditor.figureData;
if(!figureData) return;
if(figure !== figureData.getFigureString())
{
update = true;
}
figureData.loadAvatarData(figure, gender);
if(update)
{
resetCategories(avatarEditor);
}
}, [ avatarEditor, resetCategories ]);
}, [ figures, figureData ]);
const onAvatarEditorEvent = useCallback((event: AvatarEditorEvent) =>
{
@ -105,12 +93,20 @@ export const AvatarEditorView: FC<AvatarEditorViewProps> = props =>
{
case AvatarEditorEvent.SHOW_EDITOR:
setIsVisible(true);
setNeedsReset(true);
return;
case AvatarEditorEvent.HIDE_EDITOR:
setIsVisible(false);
return;
case AvatarEditorEvent.TOGGLE_EDITOR:
setIsVisible(prevValue => !prevValue);
setIsVisible(prevValue =>
{
const flag = !prevValue;
if(flag) setNeedsReset(true);
return flag;
});
return;
}
}, []);
@ -119,42 +115,130 @@ export const AvatarEditorView: FC<AvatarEditorViewProps> = props =>
useUiEvent(AvatarEditorEvent.HIDE_EDITOR, onAvatarEditorEvent);
useUiEvent(AvatarEditorEvent.TOGGLE_EDITOR, onAvatarEditorEvent);
useEffect(() =>
const clearFigure = useCallback(() =>
{
if(!isVisible || isInitalized) return;
loadAvatarInEditor(figureData.getFigureStringWithFace(0, false), figureData.gender, false);
resetCategories();
}, [ figureData, loadAvatarInEditor, resetCategories ]);
const newEditor = new AvatarEditor();
const resetFigure = useCallback(() =>
{
loadAvatarInEditor(lastFigure, lastGender);
resetCategories();
}, [ lastFigure, lastGender, loadAvatarInEditor, resetCategories ]);
setAvatarEditor(newEditor);
setIsInitalized(true);
}, [ isVisible, isInitalized ]);
const rotateFigure = useCallback((direction: number) =>
{
if(direction < AvatarDirectionAngle.MIN_DIRECTION)
{
direction = (AvatarDirectionAngle.MAX_DIRECTION + (direction + 1));
}
if(direction > AvatarDirectionAngle.MAX_DIRECTION)
{
direction = (direction - (AvatarDirectionAngle.MAX_DIRECTION + 1));
}
figureData.direction = direction;
}, [ figureData ]);
const saveFigure = useCallback(() =>
{
SendMessageHook(new UserFigureComposer(figureData.gender, figureData.getFigureString()));
}, [ figureData ]);
const setGender = useCallback((gender: string) =>
{
gender = AvatarEditorUtilities.getGender(gender);
setFigureData(figures.get(gender));
}, [ figures ]);
useEffect(() =>
{
if(!isVisible || !avatarEditor) return;
if(!categories) return;
selectCategory(AvatarEditorFigureCategory.GENERIC);
}, [ categories, selectCategory ]);
useEffect(() =>
{
if(!figureData) return;
AvatarEditorUtilities.CURRENT_FIGURE = figureData;
resetCategories();
return () => AvatarEditorUtilities.CURRENT_FIGURE = null;
}, [ figureData, resetCategories ]);
useEffect(() =>
{
if(!isVisible) return;
if(!figures)
{
setupFigures();
setIsInitalized(true);
return;
}
}, [ isVisible, figures, setupFigures ]);
useEffect(() =>
{
if(!isVisible || !isInitalized || !needsReset) return;
loadAvatarInEditor(GetSessionDataManager().figure, GetSessionDataManager().gender);
}, [ isVisible, avatarEditor, loadAvatarInEditor ]);
setNeedsReset(false);
}, [ isVisible, isInitalized, needsReset, loadAvatarInEditor ]);
if(!isVisible) return null;
return (
<AvatarEditorContextProvider value={ { avatarEditorState, dispatchAvatarEditorState } }>
{ isVisible &&
<NitroCardView className="nitro-avatar-editor">
<NitroCardHeaderView headerText={ LocalizeText('avatareditor.title') } onCloseClick={ event => setIsVisible(false) } />
<NitroCardTabsView>
{ categories && (categories.size > 0) && Array.from(categories.keys()).map(category =>
{
return (
<NitroCardTabsItemView key={ category } isActive={ (activeCategory.name === category) } onClick={ event => selectCategory(category) }>
{ LocalizeText(`avatareditor.category.${ category }`) }
</NitroCardTabsItemView>
);
})}
</NitroCardTabsView>
<NitroCardContentView>
{ activeCategory && <AvatarEditorModelView model={ activeCategory } editor={ avatarEditor } selectGender={ selectGender } /> }
</NitroCardContentView>
</NitroCardView> }
</AvatarEditorContextProvider>
<NitroCardView className="nitro-avatar-editor">
<NitroCardHeaderView headerText={ LocalizeText('avatareditor.title') } onCloseClick={ event => setIsVisible(false) } />
<NitroCardTabsView>
{ categories && (categories.size > 0) && activeCategory && Array.from(categories.keys()).map(category =>
{
return (
<NitroCardTabsItemView key={ category } isActive={ (activeCategory.name === category) } onClick={ event => selectCategory(category) }>
{ LocalizeText(`avatareditor.category.${ category }`) }
</NitroCardTabsItemView>
);
})}
</NitroCardTabsView>
<NitroCardContentView>
<div className="row h-100">
<div className="col-9 d-flex flex-column h-100">
{ activeCategory && <AvatarEditorModelView model={ activeCategory } gender={ figureData.gender } setGender={ setGender } /> }
</div>
<div className="col-3 d-flex flex-column h-100">
{ figureData &&
<div className="figure-preview-container">
<AvatarEditorFigurePreviewView figureData={ figureData } />
<div className="avatar-spotlight" />
<div className="avatar-shadow" />
<div className="arrow-container">
<i className="icon arrow-left-icon" onClick={ event => rotateFigure(figureData.direction + 1) } />
<i className="icon arrow-right-icon" onClick={ event => rotateFigure(figureData.direction - 1) } />
</div>
</div> }
<div className="d-flex flex-column mt-1">
<div className="btn-group mb-1">
<button type="button" className="btn btn-sm btn-secondary" onClick={ resetFigure }>
<i className="fas fa-undo" />
</button>
<button type="button" className="btn btn-sm btn-secondary" onClick={ clearFigure }>
<i className="fas fa-trash" />
</button>
</div>
<button type="button" className="btn btn-success btn-sm w-100" onClick={ saveFigure }>{ LocalizeText('avatareditor.save') }</button>
</div>
</div>
</div>
</NitroCardContentView>
</NitroCardView>
);
}

View File

@ -1,88 +1,79 @@
import { IPalette, IPartColor, ISetType, IStructureData } from 'nitro-renderer';
import { GetAvatarRenderManager, GetConfiguration, GetSessionDataManager } from '../../../api';
import { IPartColor } from 'nitro-renderer';
import { GetAvatarPalette, GetAvatarRenderManager, GetAvatarSetType, GetClubMemberLevel, GetConfiguration } from '../../../api';
import { AvatarEditorGridColorItem } from './AvatarEditorGridColorItem';
import { AvatarEditorGridPartItem } from './AvatarEditorGridPartItem';
import { CategoryBaseModel } from './CategoryBaseModel';
import { CategoryData } from './CategoryData';
import { FigureData } from './FigureData';
const MAX_PALETTES: number = 2;
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 class AvatarEditor
export class AvatarEditorUtilities
{
private _figureStructureData: IStructureData = GetAvatarRenderManager().structureData;
private _figures: Map<string, FigureData> = new Map();
private _gender: string = FigureData.MALE;
private _notifier: () => void = null;
private static MAX_PALETTES: number = 2;
constructor()
public static CURRENT_FIGURE: FigureData = null;
public static getGender(gender: string): string
{
const maleFigure = new FigureData();
const femaleFigure = new FigureData();
switch(gender)
{
case FigureData.MALE:
case 'm':
case 'M':
gender = FigureData.MALE;
break;
case FigureData.FEMALE:
case 'f':
case 'F':
gender = FigureData.FEMALE;
break;
default:
gender = FigureData.MALE;
}
maleFigure.loadAvatarData(DEFAULT_MALE_FIGURE, FigureData.MALE);
femaleFigure.loadAvatarData(DEFAULT_FEMALE_FIGURE, FigureData.FEMALE);
this._figures.set(FigureData.MALE, maleFigure);
this._figures.set(FigureData.FEMALE, femaleFigure);
return gender;
}
public getSetType(setType: string): ISetType
public static createCategory(model: CategoryBaseModel, name: string): CategoryData
{
if(!this._figureStructureData) return null;
return this._figureStructureData.getSetType(setType);
}
public getPalette(paletteId: number): IPalette
{
if(!this._figureStructureData) return null;
return this._figureStructureData.getPalette(paletteId);
}
public createCategory(model: CategoryBaseModel, name: string): CategoryData
{
if(!model || !name) return null;
if(!model || !name || !this.CURRENT_FIGURE) return null;
const partItems: AvatarEditorGridPartItem[] = [];
const colorItems: AvatarEditorGridColorItem[][] = [];
let i = 0;
while(i < MAX_PALETTES)
while(i < this.MAX_PALETTES)
{
colorItems.push([]);
i++;
}
const setType = this.getSetType(name);
const setType = GetAvatarSetType(name);
if(!setType) return null;
const palette = this.getPalette(setType.paletteID);
const palette = GetAvatarPalette(setType.paletteID);
if(!palette) return null;
let colorIds = this.figureData.getColourIds(name);
let colorIds = this.CURRENT_FIGURE.getColorIds(name);
if(!colorIds) colorIds = [];
const partColors: IPartColor[] = new Array(colorIds.length);
const clubItemsDimmed = this.clubItemsDimmed;
const clubMemberLevel = GetClubMemberLevel();
for(const partColor of palette.colors.values())
{
if(partColor.isSelectable && (clubItemsDimmed || (this.clubMemberLevel >= partColor.clubLevel)))
if(partColor.isSelectable && (clubItemsDimmed || (clubMemberLevel >= partColor.clubLevel)))
{
let i = 0;
while(i < MAX_PALETTES)
while(i < this.MAX_PALETTES)
{
const isDisabled = (this.clubMemberLevel < partColor.clubLevel);
const isDisabled = (clubMemberLevel < partColor.clubLevel);
const colorItem = new AvatarEditorGridColorItem(partColor, isDisabled);
colorItems[i].push(colorItem);
@ -108,11 +99,11 @@ export class AvatarEditor
if(clubItemsDimmed)
{
mandatorySetIds = GetAvatarRenderManager().getMandatoryAvatarPartSetIds(this._gender, 2);
mandatorySetIds = GetAvatarRenderManager().getMandatoryAvatarPartSetIds(this.CURRENT_FIGURE.gender, 2);
}
else
{
mandatorySetIds = GetAvatarRenderManager().getMandatoryAvatarPartSetIds(this._gender, this.clubMemberLevel);
mandatorySetIds = GetAvatarRenderManager().getMandatoryAvatarPartSetIds(this.CURRENT_FIGURE.gender, clubMemberLevel);
}
const isntMandatorySet = (mandatorySetIds.indexOf(name) === -1);
@ -142,17 +133,15 @@ export class AvatarEditor
{
isValidGender = true;
}
else
else if(partSet.gender === this.CURRENT_FIGURE.gender)
{
if(partSet.gender === this._gender)
{
isValidGender = true;
}
isValidGender = true;
}
if(partSet.isSelectable && isValidGender && (clubItemsDimmed || (this.clubMemberLevel >= partSet.clubLevel)))
if(partSet.isSelectable && isValidGender && (clubItemsDimmed || (clubMemberLevel >= partSet.clubLevel)))
{
const isDisabled = (this.clubMemberLevel < partSet.clubLevel);
const isDisabled = (clubMemberLevel < partSet.clubLevel);
let isValid = true;
@ -186,7 +175,7 @@ export class AvatarEditor
i = 0;
while(i < MAX_PALETTES)
while(i < this.MAX_PALETTES)
{
colorItems[i].sort(this.colorSorter);
@ -196,7 +185,7 @@ export class AvatarEditor
return new CategoryData(name, partItems, colorItems);
}
private clubSorter(a: AvatarEditorGridPartItem, b: AvatarEditorGridPartItem): number
public static clubSorter(a: AvatarEditorGridPartItem, b: AvatarEditorGridPartItem): number
{
const clubLevelA = (!a.partSet ? 9999999999 : a.partSet.clubLevel);
const clubLevelB = (!b.partSet ? 9999999999 : b.partSet.clubLevel);
@ -218,7 +207,23 @@ export class AvatarEditor
return 0;
}
private noobSorter(a: AvatarEditorGridPartItem, b: AvatarEditorGridPartItem): number
public static colorSorter(a: AvatarEditorGridColorItem, b: AvatarEditorGridColorItem): number
{
const clubLevelA = (!a.partColor ? -1 : a.partColor.clubLevel);
const clubLevelB = (!b.partColor ? -1 : b.partColor.clubLevel);
if(clubLevelA < clubLevelB) return -1;
if(clubLevelA > clubLevelB) return 1;
if(a.partColor.index < b.partColor.index) return -1;
if(a.partColor.index > b.partColor.index) return 1;
return 0;
}
public static noobSorter(a: AvatarEditorGridPartItem, b: AvatarEditorGridPartItem): number
{
const clubLevelA = (!a.partSet ? -1 : a.partSet.clubLevel);
const clubLevelB = (!b.partSet ? -1 : b.partSet.clubLevel);
@ -240,67 +245,33 @@ export class AvatarEditor
return 0;
}
private colorSorter(a: AvatarEditorGridColorItem, b: AvatarEditorGridColorItem): number
public static avatarSetFirstSelectableColor(name: string): number
{
const clubLevelA = (!a.partColor ? -1 : a.partColor.clubLevel);
const clubLevelB = (!b.partColor ? -1 : b.partColor.clubLevel);
const setType = GetAvatarSetType(name);
if(clubLevelA < clubLevelB) return -1;
if(!setType) return -1;
if(clubLevelA > clubLevelB) return 1;
const palette = GetAvatarPalette(setType.paletteID);
if(a.partColor.index < b.partColor.index) return -1;
if(!palette) return -1;
if(a.partColor.index > b.partColor.index) return 1;
for(const color of palette.colors.values())
{
if(!color.isSelectable || (GetClubMemberLevel() < color.clubLevel)) continue;
return 0;
return color.id;
}
return -1;
}
public get clubMemberLevel(): number
{
return GetSessionDataManager().clubLevel;
}
private get clubItemsFirst(): boolean
public static get clubItemsFirst(): boolean
{
return GetConfiguration<boolean>('avatareditor.show.clubitems.first', true);
}
private get clubItemsDimmed(): boolean
public static get clubItemsDimmed(): boolean
{
return GetConfiguration<boolean>('avatareditor.show.clubitems.dimmed', true);
}
public get figureData(): FigureData
{
return this._figures.get(this._gender);
}
public get gender(): string
{
return this._gender;
}
public set gender(gender: string)
{
if(this._gender === gender) return;
this._gender = gender;
if(this.figureData) this.figureData.notify = this.notify;
if(this.notify) this.notify();
}
public get notify(): () => void
{
return this._notifier;
}
public set notify(notifier: () => void)
{
if(this.figureData) this.figureData.notify = notifier;
this._notifier = notifier;
}
}

View File

@ -1,5 +1,6 @@
import { AvatarEditorFigureCategory, AvatarScaleType, AvatarSetType, IAvatarImageListener } from 'nitro-renderer';
import { GetAvatarRenderManager } from '../../../api';
import { AvatarEditorUtilities } from './AvatarEditorUtilities';
import { CategoryBaseModel } from './CategoryBaseModel';
import { FigureData } from './FigureData';
@ -25,15 +26,15 @@ export class BodyModel extends CategoryBaseModel implements IAvatarImageListener
protected updateSelectionsFromFigure(name: string): void
{
if(!this._categories || !this._editor || !this._editor.figureData) return;
if(!this._categories || !AvatarEditorUtilities.CURRENT_FIGURE) return;
const category = this._categories.get(name);
if(!category) return;
const setId = this._editor.figureData.getPartSetId(name);
const setId = AvatarEditorUtilities.CURRENT_FIGURE.getPartSetId(name);
let colorIds = this._editor.figureData.getColourIds(name);
let colorIds = AvatarEditorUtilities.CURRENT_FIGURE.getColorIds(name);
if(!colorIds) colorIds = [];
@ -42,7 +43,7 @@ export class BodyModel extends CategoryBaseModel implements IAvatarImageListener
for(const part of category.parts)
{
const figure = this._editor.figureData.getFigureStringWithFace(part.id);
const figure = AvatarEditorUtilities.CURRENT_FIGURE.getFigureStringWithFace(part.id);
const avatarImage = GetAvatarRenderManager().createAvatarImage(figure, AvatarScaleType.LARGE, null, this);
const sprite = avatarImage.getImageAsSprite(AvatarSetType.HEAD);

View File

@ -1,18 +1,16 @@
import { AvatarEditor } from './AvatarEditor';
import { AvatarEditorUtilities } from './AvatarEditorUtilities';
import { CategoryData } from './CategoryData';
import { IAvatarEditorCategoryModel } from './IAvatarEditorCategoryModel';
export class CategoryBaseModel implements IAvatarEditorCategoryModel
{
protected _editor: AvatarEditor;
protected _categories: Map<string, CategoryData>;
protected _isInitalized: boolean;
protected _maxPaletteCount: number;
private _disposed: boolean;
constructor(editor: AvatarEditor)
constructor()
{
this._editor = editor;
this._isInitalized = false;
this._maxPaletteCount = 0;
}
@ -51,7 +49,7 @@ export class CategoryBaseModel implements IAvatarEditorCategoryModel
if(existing) return;
existing = this._editor.createCategory(this, name);
existing = AvatarEditorUtilities.createCategory(this, name);
if(!existing) return;
@ -66,9 +64,9 @@ export class CategoryBaseModel implements IAvatarEditorCategoryModel
if(!category) return;
const setId = this._editor.figureData.getPartSetId(figure);
const setId = AvatarEditorUtilities.CURRENT_FIGURE.getPartSetId(figure);
let colorIds = this._editor.figureData.getColourIds(figure);
let colorIds = AvatarEditorUtilities.CURRENT_FIGURE.getColorIds(figure);
if(!colorIds) colorIds = [];
@ -120,9 +118,9 @@ export class CategoryBaseModel implements IAvatarEditorCategoryModel
{
const partItem = category.getCurrentPart();
if(partItem && this._editor && this._editor.figureData)
if(partItem && AvatarEditorUtilities.CURRENT_FIGURE)
{
this._editor.figureData.savePartData(name, partItem.id, category.getSelectedColorIds(), true);
AvatarEditorUtilities.CURRENT_FIGURE.savePartData(name, partItem.id, category.getSelectedColorIds(), true);
}
didStrip = true;
@ -148,9 +146,9 @@ export class CategoryBaseModel implements IAvatarEditorCategoryModel
{
const partItem = category.getCurrentPart();
if(partItem && this._editor && this._editor.figureData)
if(partItem && AvatarEditorUtilities.CURRENT_FIGURE)
{
this._editor.figureData.savePartData(name, partItem.id, category.getSelectedColorIds(), true);
AvatarEditorUtilities.CURRENT_FIGURE.savePartData(name, partItem.id, category.getSelectedColorIds(), true);
}
didStrip = true;
@ -185,7 +183,7 @@ export class CategoryBaseModel implements IAvatarEditorCategoryModel
this._maxPaletteCount = partItem.maxColorIndex;
this._editor.figureData.savePartData(category, partItem.id, categoryData.getSelectedColorIds(), true);
AvatarEditorUtilities.CURRENT_FIGURE.savePartData(category, partItem.id, categoryData.getSelectedColorIds(), true);
}
public selectColor(category: string, colorIndex: number, paletteId: number): void
@ -209,7 +207,7 @@ export class CategoryBaseModel implements IAvatarEditorCategoryModel
return;
}
this._editor.figureData.savePartSetColourId(category, categoryData.getSelectedColorIds(), true);
AvatarEditorUtilities.CURRENT_FIGURE.savePartSetColourId(category, categoryData.getSelectedColorIds(), true);
}
public getCategoryData(category: string): CategoryData

View File

@ -1,5 +1,9 @@
export class FigureData
import { AvatarEditorUtilities } from './AvatarEditorUtilities';
export class FigureData
{
private static DEFAULT_DIRECTION: number = 4;
public static MALE: string = 'M';
public static FEMALE: string = 'F';
public static UNISEX: string = 'U';
@ -24,6 +28,7 @@
private _data: Map<string, number>;
private _colors: Map<string, number[]>;
private _gender: string = 'M';
private _direction: number = FigureData.DEFAULT_DIRECTION;
private _avatarEffectType: number = -1;
private _notifier: () => void = null;
@ -80,14 +85,13 @@
return -1;
}
public getColourIds(colorId: string): number[]
public getColorIds(setType: string): number[]
{
const existing = this._colors.get(colorId);
const existing = this._colors.get(setType);
if(existing !== undefined) return existing;
return [];
// return [this._avatarEditor._Str_24919(k)];
return [ AvatarEditorUtilities.avatarSetFirstSelectableColor(setType) ];
}
public getFigureString(): string
@ -167,9 +171,9 @@
if(_arg_3) this.updateView();
}
public savePartSetColourId(k: string, _arg_2: number[], _arg_3: boolean = true): void
public savePartSetColourId(setType: string, colorIds: number[], update: boolean = true): void
{
switch(k)
switch(setType)
{
case FigureData.FACE:
case FigureData.HAIR:
@ -184,11 +188,11 @@
case FigureData.TROUSERS:
case FigureData.SHOES:
case FigureData.TROUSER_ACCESSORIES:
this._colors.set(k, _arg_2);
this._colors.set(setType, colorIds);
break;
}
if(_arg_3) this.updateView();
if(update) this.updateView();
}
public getFigureStringWithFace(k: number, override = true): string
@ -249,6 +253,18 @@
return this._gender;
}
public get direction(): number
{
return this._direction;
}
public set direction(direction: number)
{
this._direction = direction;
this.updateView();
}
public set avatarEffectType(k: number)
{
this._avatarEffectType = k;

View File

@ -4,7 +4,7 @@ import { AvatarEditorFigurePreviewViewProps } from './AvatarEditorFigurePreviewV
export const AvatarEditorFigurePreviewView: FC<AvatarEditorFigurePreviewViewProps> = props =>
{
const { editor = null } = props;
const { figureData = null } = props;
const [ updateId, setUpdateId ] = useState(-1);
const rerender = useCallback(() =>
@ -14,15 +14,15 @@ export const AvatarEditorFigurePreviewView: FC<AvatarEditorFigurePreviewViewProp
useEffect(() =>
{
if(!editor) return;
if(!figureData) return;
editor.notify = rerender;
figureData.notify = rerender;
return () =>
{
editor.notify = null;
figureData.notify = null;
}
}, [ editor, rerender ] );
}, [ figureData, rerender ] );
return <AvatarImageView figure={ editor.figureData.getFigureString() } direction={ 4 } scale={ 2 } />
return <AvatarImageView figure={ figureData.getFigureString() } direction={ figureData.direction } scale={ 2 } />
}

View File

@ -1,6 +1,6 @@
import { AvatarEditor } from '../../common/AvatarEditor';
import { FigureData } from '../../common/FigureData';
export interface AvatarEditorFigurePreviewViewProps
{
editor: AvatarEditor;
figureData: FigureData;
}

View File

@ -24,8 +24,9 @@ export const AvatarEditorFigureSetItemView: FC<AvatarEditorFigureSetItemViewProp
})
return (
<NitroCardGridItemView itemImage={ partItem.imageUrl } itemActive={ partItem.isSelected } onClick={ () => onClick(partItem) }>
<NitroCardGridItemView itemImage={ (partItem.isClear ? undefined : partItem.imageUrl) } itemActive={ partItem.isSelected } onClick={ () => onClick(partItem) }>
{ partItem.isHC && <CurrencyIcon type={ 'hc' } /> }
{ partItem.isClear && <i className="icon clear-icon" /> }
</NitroCardGridItemView>
);
}

View File

@ -1,17 +1,13 @@
import { UserFigureComposer } from 'nitro-renderer';
import { FC, useCallback, useEffect, useState } from 'react';
import { SendMessageHook } from '../../../../hooks';
import { LocalizeText } from '../../../../utils/LocalizeText';
import { CategoryData } from '../../common/CategoryData';
import { FigureData } from '../../common/FigureData';
import { AvatarEditorFigurePreviewView } from '../figure-preview/AvatarEditorFigurePreviewView';
import { AvatarEditorFigureSetView } from '../figure-set/AvatarEditorFigureSetView';
import { AvatarEditorPaletteSetView } from '../palette-set/AvatarEditorPaletteSetView';
import { AvatarEditorModelViewProps } from './AvatarEditorModelView.types';
export const AvatarEditorModelView: FC<AvatarEditorModelViewProps> = props =>
{
const { model = null, editor = null, selectGender = null } = props;
const { model = null, gender = null, setGender = null } = props;
const [ activeCategory, setActiveCategory ] = useState<CategoryData>(null);
const [ maxPaletteCount, setMaxPaletteCount ] = useState(1);
@ -35,13 +31,6 @@ export const AvatarEditorModelView: FC<AvatarEditorModelViewProps> = props =>
}
}, [ model ]);
const saveFigure = useCallback(() =>
{
const figureData = editor.figureData;
SendMessageHook(new UserFigureComposer(figureData.gender, figureData.getFigureString()));
}, [ editor ]);
useEffect(() =>
{
model.init();
@ -58,49 +47,35 @@ export const AvatarEditorModelView: FC<AvatarEditorModelViewProps> = props =>
return (
<div className="row h-100">
<div className="col-1 d-flex flex-column align-items-center h-100 pe-0">
<div className="col-2 d-flex flex-column align-items-center h-100">
{ model.canSetGender &&
<>
<i className={ `icon male-icon ${ (editor.gender === FigureData.MALE) ? ' selected' : ''}` } onClick={ event => selectGender(FigureData.MALE) } />
<i className={ `icon female-icon ${ (editor.gender === FigureData.FEMALE) ? ' selected' : ''}` } onClick={ event => selectGender(FigureData.FEMALE) } />
<div className="d-flex justify-content-center align-items-center category-item cursor-pointer" onClick={ event => setGender(FigureData.MALE) }>
<i className={ `icon male-icon ${ (gender === FigureData.MALE) ? ' selected' : ''}` } />
</div>
<div className="d-flex justify-content-center align-items-center category-item cursor-pointer" onClick={ event => setGender(FigureData.FEMALE) }>
<i className={ `icon female-icon ${ (gender === FigureData.FEMALE) ? ' selected' : ''}` } />
</div>
</> }
{ !model.canSetGender && model.categories && (model.categories.size > 0) && Array.from(model.categories.keys()).map(name =>
{
const category = model.categories.get(name);
return (
<i className={ `icon ${ category.name }-icon mb-2 ${ (activeCategory === category) ? ' selected' : ''}` } onClick={ event => selectCategory(name) } />
<div key={ name } className="d-flex justify-content-center align-items-center category-item cursor-pointer" onClick={ event => selectCategory(name) }>
<i className={ `icon ${ category.name }-icon ${ (activeCategory === category) ? ' selected' : ''}` } />
</div>
);
})}
</div>
<div className="col-4 d-flex flex-column h-100">
<div className="col-5 d-flex flex-column h-100">
<AvatarEditorFigureSetView model={ model } category={ activeCategory } setMaxPaletteCount={ setMaxPaletteCount } />
</div>
<div className="col-3 d-flex flex-column h-100">
<div className="figure-preview-container mb-2">
<AvatarEditorFigurePreviewView editor={ editor } />
<div className="arrow-container">
<i className="icon arrow-left-icon" />
<i className="icon arrow-right-icon" />
</div>
</div>
<div className="d-flex flex-column">
<div className="btn-group mb-1">
<button type="button" className="btn btn-sm btn-secondary">
<i className="fas fa-undo" />
</button>
<button type="button" className="btn btn-sm btn-secondary">
<i className="fas fa-trash" />
</button>
</div>
<button type="button" className="btn btn-success btn-sm w-100" onClick={ saveFigure }>{ LocalizeText('avatareditor.save') }</button>
</div>
</div>
<div className="col-4 d-flex flex-column h-100">
<div className="col-5 d-flex flex-column h-100">
{ (maxPaletteCount >= 1) &&
<AvatarEditorPaletteSetView model={ model } category={ activeCategory } paletteSet={ activeCategory.getPalette(0) } paletteIndex={ 0 } /> }
{ (maxPaletteCount === 2) &&
<AvatarEditorPaletteSetView model={ model } category={ activeCategory } paletteSet={ activeCategory.getPalette(1) } paletteIndex={ 1 } /> }
<AvatarEditorPaletteSetView model={ model } category={ activeCategory } paletteSet={ activeCategory.getPalette(1) } paletteIndex={ 1 } className="mt-1" /> }
</div>
</div>
);

View File

@ -1,9 +1,9 @@
import { AvatarEditor } from '../../common/AvatarEditor';
import { Dispatch, SetStateAction } from 'react';
import { IAvatarEditorCategoryModel } from '../../common/IAvatarEditorCategoryModel';
export interface AvatarEditorModelViewProps
{
model: IAvatarEditorCategoryModel;
editor: AvatarEditor;
selectGender: (gender: string) => void;
gender: string;
setGender: Dispatch<SetStateAction<string>>;
}

View File

@ -4,7 +4,7 @@ import { AvatarEditorPaletteSetItemProps } from './AvatarEditorPaletteSetItem.ty
export const AvatarEditorPaletteSetItem: FC<AvatarEditorPaletteSetItemProps> = props =>
{
const { colorItem = null, onClick = null } = props;
const { colorItem = null, ...rest } = props;
const [ updateId, setUpdateId ] = useState(-1);
const rerender = useCallback(() =>
@ -22,5 +22,5 @@ export const AvatarEditorPaletteSetItem: FC<AvatarEditorPaletteSetItemProps> = p
}
})
return <NitroCardGridItemView itemColor={ colorItem.color } itemActive={ colorItem.isSelected } onClick={ () => onClick(colorItem) } />
return <NitroCardGridItemView itemColor={ colorItem.color } itemActive={ colorItem.isSelected } { ...rest } />
}

View File

@ -1,7 +1,7 @@
import { DetailsHTMLAttributes } from 'react';
import { AvatarEditorGridColorItem } from '../../common/AvatarEditorGridColorItem';
export interface AvatarEditorPaletteSetItemProps
export interface AvatarEditorPaletteSetItemProps extends DetailsHTMLAttributes<HTMLDivElement>
{
colorItem: AvatarEditorGridColorItem;
onClick: (item: AvatarEditorGridColorItem) => void;
}

View File

@ -7,7 +7,7 @@ import { AvatarEditorPaletteSetViewProps } from './AvatarEditorPaletteSetView.ty
export const AvatarEditorPaletteSetView: FC<AvatarEditorPaletteSetViewProps> = props =>
{
const { model = null, category = null, paletteSet = [], paletteIndex = -1 } = props;
const { model = null, category = null, paletteSet = [], paletteIndex = -1, ...rest } = props;
const selectColor = useCallback((item: AvatarEditorGridColorItem) =>
{
@ -19,10 +19,10 @@ export const AvatarEditorPaletteSetView: FC<AvatarEditorPaletteSetViewProps> = p
}, [ model, category, paletteSet, paletteIndex ]);
return (
<NitroCardGridView columns={ 3 } theme={ NitroCardGridThemes.THEME_SHADOWED }>
<NitroCardGridView columns={ 4 } theme={ NitroCardGridThemes.THEME_SHADOWED } { ...rest }>
{ (paletteSet.length > 0) && paletteSet.map((item, index) =>
{
return <AvatarEditorPaletteSetItem key={ index } colorItem={ item } onClick={ selectColor } />;
return <AvatarEditorPaletteSetItem key={ index } colorItem={ item } onClick={ event => selectColor(item) } />;
}) }
</NitroCardGridView>
);

View File

@ -1,8 +1,9 @@
import { DetailsHTMLAttributes } from 'react';
import { AvatarEditorGridColorItem } from '../../common/AvatarEditorGridColorItem';
import { CategoryData } from '../../common/CategoryData';
import { IAvatarEditorCategoryModel } from '../../common/IAvatarEditorCategoryModel';
export interface AvatarEditorPaletteSetViewProps
export interface AvatarEditorPaletteSetViewProps extends DetailsHTMLAttributes<HTMLDivElement>
{
model: IAvatarEditorCategoryModel;
category: CategoryData;