From 4370b20af7045f3a39adf0aa654f1b8927b66488 Mon Sep 17 00:00:00 2001 From: Bill Date: Wed, 4 Aug 2021 02:57:00 -0400 Subject: [PATCH] Avatar randomizer --- src/utils/Randomizer.ts | 28 ++++ src/utils/index.ts | 1 + src/views/avatar-editor/AvatarEditorView.tsx | 14 +- .../common/AvatarEditorUtilities.ts | 4 +- src/views/avatar-editor/common/FigureData.ts | 24 ++-- .../avatar-editor/common/FigureGenerator.ts | 128 ++++++++++++++++++ 6 files changed, 184 insertions(+), 15 deletions(-) create mode 100644 src/utils/Randomizer.ts create mode 100644 src/views/avatar-editor/common/FigureGenerator.ts diff --git a/src/utils/Randomizer.ts b/src/utils/Randomizer.ts new file mode 100644 index 00000000..1f67a129 --- /dev/null +++ b/src/utils/Randomizer.ts @@ -0,0 +1,28 @@ +export class Randomizer +{ + public static getRandomNumber(count: number): number + { + return Math.floor(Math.random() * count); + } + + public static getRandomElement(elements: T[]): T + { + return elements[this.getRandomNumber(elements.length)]; + } + + public static getRandomElements(elements: T[], count: number): T[] + { + const result: T[] = new Array(count); + let len = elements.length; + const taken = new Array(len); + + while(count--) + { + var x = this.getRandomNumber(len); + result[count] = elements[x in taken ? taken[x] : x]; + taken[x] = --len in taken ? taken[len] : len; + } + + return result; + } +} diff --git a/src/utils/index.ts b/src/utils/index.ts index eb8e19e2..3125981d 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -3,3 +3,4 @@ export * from './LocalizeBadgeDescription'; export * from './LocalizeBageName'; export * from './LocalizeShortNumber'; export * from './LocalizeText'; +export * from './Randomizer'; diff --git a/src/views/avatar-editor/AvatarEditorView.tsx b/src/views/avatar-editor/AvatarEditorView.tsx index 35cadc42..df6c4fbb 100644 --- a/src/views/avatar-editor/AvatarEditorView.tsx +++ b/src/views/avatar-editor/AvatarEditorView.tsx @@ -1,6 +1,6 @@ import { AvatarDirectionAngle, AvatarEditorFigureCategory, FigureSetIdsMessageEvent, UserFigureComposer, UserWardrobePageComposer, UserWardrobePageEvent } from 'nitro-renderer'; import { FC, useCallback, useEffect, useState } from 'react'; -import { GetSessionDataManager } from '../../api'; +import { GetClubMemberLevel, GetSessionDataManager } from '../../api'; import { AvatarEditorEvent } from '../../events/avatar-editor'; import { CreateMessageHook, SendMessageHook } from '../../hooks'; import { useUiEvent } from '../../hooks/events/ui/ui-event'; @@ -10,6 +10,7 @@ import { AvatarEditorViewProps } from './AvatarEditorView.types'; import { AvatarEditorUtilities } from './common/AvatarEditorUtilities'; import { BodyModel } from './common/BodyModel'; import { FigureData } from './common/FigureData'; +import { generateRandomFigure } from './common/FigureGenerator'; import { HeadModel } from './common/HeadModel'; import { IAvatarEditorCategoryModel } from './common/IAvatarEditorCategoryModel'; import { LegModel } from './common/LegModel'; @@ -167,6 +168,14 @@ export const AvatarEditorView: FC = props => resetCategories(); }, [ lastFigure, lastGender, loadAvatarInEditor, resetCategories ]); + const randomizeFigure = useCallback(() => + { + const figure = generateRandomFigure(figureData, figureData.gender, GetClubMemberLevel(), figureSetIds, [ FigureData.FACE ]); + + loadAvatarInEditor(figure, figureData.gender, false); + resetCategories(); + }, [ figureData, figureSetIds, loadAvatarInEditor, resetCategories ]); + const rotateFigure = useCallback((direction: number) => { if(direction < AvatarDirectionAngle.MIN_DIRECTION) @@ -308,6 +317,9 @@ export const AvatarEditorView: FC = props => + diff --git a/src/views/avatar-editor/common/AvatarEditorUtilities.ts b/src/views/avatar-editor/common/AvatarEditorUtilities.ts index 2977a315..a89e44c1 100644 --- a/src/views/avatar-editor/common/AvatarEditorUtilities.ts +++ b/src/views/avatar-editor/common/AvatarEditorUtilities.ts @@ -72,7 +72,7 @@ export class AvatarEditorUtilities const clubItemsDimmed = this.clubItemsDimmed; const clubMemberLevel = GetClubMemberLevel(); - for(const partColor of palette.colors.values()) + for(const partColor of palette.colors.getValues()) { if(partColor.isSelectable && (clubItemsDimmed || (clubMemberLevel >= partColor.clubLevel))) { @@ -255,7 +255,7 @@ export class AvatarEditorUtilities if(!palette) return -1; - for(const color of palette.colors.values()) + for(const color of palette.colors.getValues()) { if(!color.isSelectable || (GetClubMemberLevel() < color.clubLevel)) continue; diff --git a/src/views/avatar-editor/common/FigureData.ts b/src/views/avatar-editor/common/FigureData.ts index 51744a4e..cb28a5ec 100644 --- a/src/views/avatar-editor/common/FigureData.ts +++ b/src/views/avatar-editor/common/FigureData.ts @@ -23,7 +23,7 @@ export class FigureData public static TROUSERS: string = 'lg'; public static SHOES: string = 'sh'; public static TROUSER_ACCESSORIES: string = 'wa'; - public static PREVIEW_AVATAR_DIRECTION: number = 4; + public static SET_TYPES = [ FigureData.FACE, FigureData.HAIR, FigureData.HAT, FigureData.HEAD_ACCESSORIES, FigureData.EYE_ACCESSORIES, FigureData.FACE_ACCESSORIES, FigureData.JACKET, FigureData.SHIRT, FigureData.CHEST_ACCESSORIES, FigureData.CHEST_PRINTS, FigureData.TROUSERS, FigureData.SHOES, FigureData.TROUSERS ]; private _data: Map; private _colors: Map; @@ -76,9 +76,9 @@ export class FigureData } } - public getPartSetId(partSetId: string): number + public getPartSetId(setType: string): number { - const existing = this._data.get(partSetId); + const existing = this._data.get(setType); if(existing !== undefined) return existing; @@ -134,15 +134,15 @@ export class FigureData return figureString; } - public savePartData(k: string, _arg_2: number, _arg_3: number[], _arg_4: boolean = false): void + public savePartData(setType: string, partId: number, colorIds: number[], update: boolean = false): void { - this.savePartSetId(k, _arg_2, _arg_4); - this.savePartSetColourId(k, _arg_3, _arg_4); + this.savePartSetId(setType, partId, update); + this.savePartSetColourId(setType, colorIds, update); } - private savePartSetId(k: string, _arg_2: number, _arg_3: boolean = true): void + private savePartSetId(setType: string, partId: number, update: boolean = true): void { - switch(k) + switch(setType) { case FigureData.FACE: case FigureData.HAIR: @@ -157,18 +157,18 @@ export class FigureData case FigureData.TROUSERS: case FigureData.SHOES: case FigureData.TROUSER_ACCESSORIES: - if(_arg_2 >= 0) + if(partId >= 0) { - this._data.set(k, _arg_2); + this._data.set(setType, partId); } else { - this._data.delete(k); + this._data.delete(setType); } break; } - if(_arg_3) this.updateView(); + if(update) this.updateView(); } public savePartSetColourId(setType: string, colorIds: number[], update: boolean = true): void diff --git a/src/views/avatar-editor/common/FigureGenerator.ts b/src/views/avatar-editor/common/FigureGenerator.ts new file mode 100644 index 00000000..90f245ca --- /dev/null +++ b/src/views/avatar-editor/common/FigureGenerator.ts @@ -0,0 +1,128 @@ +import { AvatarFigureContainer, IFigurePartSet, IPalette, IPartColor, SetType } from 'nitro-renderer'; +import { GetAvatarRenderManager } from '../../../api'; +import { Randomizer } from '../../../utils'; +import { FigureData } from './FigureData'; + +const RANDOM_TRIES: number = 20; + +function getTotalColors(partSet: IFigurePartSet): number +{ + const parts = partSet.parts; + + let totalColors = 0; + + for(const part of parts) totalColors = Math.max(totalColors, part.colorLayerIndex); + + return totalColors; +} + +function getRandomSetTypes(requiredSets: string[], options: string[]): string[] +{ + options = options.filter(option => (requiredSets.indexOf(option) === -1)); + + return [ ...requiredSets, ...Randomizer.getRandomElements(options, (Randomizer.getRandomNumber(options.length) + 1)) ]; +} + +function getRandomPartSet(setType: SetType, gender: string, clubLevel: number = 0, figureSetIds: number[] = []): IFigurePartSet +{ + if(!setType) return null; + + const options = setType.partSets.getValues(); + + let selectedSet: IFigurePartSet = null; + let randomTries = RANDOM_TRIES; + + while(!selectedSet) + { + if(!randomTries) return setType.getDefaultPartSet(gender); + + const randomSet = Randomizer.getRandomElement(options); + + if(!randomSet.isSelectable || ((randomSet.gender !== 'U') && (randomSet.gender !== gender)) || (randomSet.clubLevel > clubLevel) || (randomSet.isSellable && (figureSetIds.indexOf(randomSet.id) === -1))) + { + randomTries--; + + continue; + } + + return randomSet; + } + + return null; +} + +function getRandomColor(options: IPartColor[], clubLevel: number = 0): IPartColor +{ + const randomColor = Randomizer.getRandomElement(options); + + if(!randomColor.isSelectable || (randomColor.clubLevel > clubLevel)) return null; + + return randomColor; +} + +function getRandomColors(palette: IPalette, partSet: IFigurePartSet, clubLevel: number = 0): number[] +{ + if(!palette) return []; + + const options = palette.colors.getValues(); + + let totalColors = getTotalColors(partSet); + let selectedColors: number[] = []; + let randomTries = RANDOM_TRIES; + + while(totalColors) + { + if(!randomTries) break; + + const randomColor = getRandomColor(options, clubLevel); + + if(!randomColor) + { + randomTries--; + + continue; + } + + selectedColors.push(randomColor.id); + + totalColors--; + } + + return selectedColors; +} + +export function generateRandomFigure(figureData: FigureData, gender: string, clubLevel: number = 0, figureSetIds: number[] = [], ignoredSets: string[] = []): string +{ + const structure = GetAvatarRenderManager().structure; + const figureContainer = new AvatarFigureContainer(''); + const requiredSets = getRandomSetTypes(structure.getMandatorySetTypeIds(gender, clubLevel), FigureData.SET_TYPES); + + for(const setType of ignoredSets) + { + const partSetId = figureData.getPartSetId(setType); + const colors = figureData.getColorIds(setType); + + figureContainer.updatePart(setType, partSetId, colors); + } + + for(const type of requiredSets) + { + if(figureContainer.hasPartType(type)) continue; + + const setType = (structure.figureData.getSetType(type) as SetType); + const selectedSet = getRandomPartSet(setType, gender, clubLevel, figureSetIds); + + if(!selectedSet) continue; + + let selectedColors: number[] = []; + + if(selectedSet.isColorable) + { + selectedColors = getRandomColors(structure.figureData.getPalette(setType.paletteID), selectedSet, clubLevel); + } + + figureContainer.updatePart(setType.type, selectedSet.id, selectedColors); + } + + return figureContainer.getFigureString(); +}