Avatar randomizer

This commit is contained in:
Bill 2021-08-04 02:57:00 -04:00
parent f88efac443
commit 4370b20af7
6 changed files with 184 additions and 15 deletions

28
src/utils/Randomizer.ts Normal file
View File

@ -0,0 +1,28 @@
export class Randomizer
{
public static getRandomNumber(count: number): number
{
return Math.floor(Math.random() * count);
}
public static getRandomElement<T>(elements: T[]): T
{
return elements[this.getRandomNumber(elements.length)];
}
public static getRandomElements<T>(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;
}
}

View File

@ -3,3 +3,4 @@ export * from './LocalizeBadgeDescription';
export * from './LocalizeBageName'; export * from './LocalizeBageName';
export * from './LocalizeShortNumber'; export * from './LocalizeShortNumber';
export * from './LocalizeText'; export * from './LocalizeText';
export * from './Randomizer';

View File

@ -1,6 +1,6 @@
import { AvatarDirectionAngle, AvatarEditorFigureCategory, FigureSetIdsMessageEvent, UserFigureComposer, UserWardrobePageComposer, UserWardrobePageEvent } from 'nitro-renderer'; import { AvatarDirectionAngle, AvatarEditorFigureCategory, FigureSetIdsMessageEvent, UserFigureComposer, UserWardrobePageComposer, UserWardrobePageEvent } from 'nitro-renderer';
import { FC, useCallback, useEffect, useState } from 'react'; import { FC, useCallback, useEffect, useState } from 'react';
import { GetSessionDataManager } from '../../api'; import { GetClubMemberLevel, GetSessionDataManager } from '../../api';
import { AvatarEditorEvent } from '../../events/avatar-editor'; import { AvatarEditorEvent } from '../../events/avatar-editor';
import { CreateMessageHook, SendMessageHook } from '../../hooks'; import { CreateMessageHook, SendMessageHook } from '../../hooks';
import { useUiEvent } from '../../hooks/events/ui/ui-event'; import { useUiEvent } from '../../hooks/events/ui/ui-event';
@ -10,6 +10,7 @@ import { AvatarEditorViewProps } from './AvatarEditorView.types';
import { AvatarEditorUtilities } from './common/AvatarEditorUtilities'; import { AvatarEditorUtilities } from './common/AvatarEditorUtilities';
import { BodyModel } from './common/BodyModel'; import { BodyModel } from './common/BodyModel';
import { FigureData } from './common/FigureData'; import { FigureData } from './common/FigureData';
import { generateRandomFigure } from './common/FigureGenerator';
import { HeadModel } from './common/HeadModel'; import { HeadModel } from './common/HeadModel';
import { IAvatarEditorCategoryModel } from './common/IAvatarEditorCategoryModel'; import { IAvatarEditorCategoryModel } from './common/IAvatarEditorCategoryModel';
import { LegModel } from './common/LegModel'; import { LegModel } from './common/LegModel';
@ -167,6 +168,14 @@ export const AvatarEditorView: FC<AvatarEditorViewProps> = props =>
resetCategories(); resetCategories();
}, [ lastFigure, lastGender, loadAvatarInEditor, 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) => const rotateFigure = useCallback((direction: number) =>
{ {
if(direction < AvatarDirectionAngle.MIN_DIRECTION) if(direction < AvatarDirectionAngle.MIN_DIRECTION)
@ -308,6 +317,9 @@ export const AvatarEditorView: FC<AvatarEditorViewProps> = props =>
<button type="button" className="btn btn-sm btn-secondary" onClick={ clearFigure }> <button type="button" className="btn btn-sm btn-secondary" onClick={ clearFigure }>
<i className="fas fa-trash" /> <i className="fas fa-trash" />
</button> </button>
<button type="button" className="btn btn-sm btn-secondary" onClick={ randomizeFigure }>
<i className="fas fa-dice" />
</button>
</div> </div>
<button type="button" className="btn btn-success btn-sm w-100" onClick={ saveFigure }>{ LocalizeText('avatareditor.save') }</button> <button type="button" className="btn btn-success btn-sm w-100" onClick={ saveFigure }>{ LocalizeText('avatareditor.save') }</button>
</div> </div>

View File

@ -72,7 +72,7 @@ export class AvatarEditorUtilities
const clubItemsDimmed = this.clubItemsDimmed; const clubItemsDimmed = this.clubItemsDimmed;
const clubMemberLevel = GetClubMemberLevel(); const clubMemberLevel = GetClubMemberLevel();
for(const partColor of palette.colors.values()) for(const partColor of palette.colors.getValues())
{ {
if(partColor.isSelectable && (clubItemsDimmed || (clubMemberLevel >= partColor.clubLevel))) if(partColor.isSelectable && (clubItemsDimmed || (clubMemberLevel >= partColor.clubLevel)))
{ {
@ -255,7 +255,7 @@ export class AvatarEditorUtilities
if(!palette) return -1; 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; if(!color.isSelectable || (GetClubMemberLevel() < color.clubLevel)) continue;

View File

@ -23,7 +23,7 @@ export class FigureData
public static TROUSERS: string = 'lg'; public static TROUSERS: string = 'lg';
public static SHOES: string = 'sh'; public static SHOES: string = 'sh';
public static TROUSER_ACCESSORIES: string = 'wa'; 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<string, number>; private _data: Map<string, number>;
private _colors: Map<string, number[]>; private _colors: Map<string, number[]>;
@ -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; if(existing !== undefined) return existing;
@ -134,15 +134,15 @@ export class FigureData
return figureString; 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.savePartSetId(setType, partId, update);
this.savePartSetColourId(k, _arg_3, _arg_4); 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.FACE:
case FigureData.HAIR: case FigureData.HAIR:
@ -157,18 +157,18 @@ export class FigureData
case FigureData.TROUSERS: case FigureData.TROUSERS:
case FigureData.SHOES: case FigureData.SHOES:
case FigureData.TROUSER_ACCESSORIES: case FigureData.TROUSER_ACCESSORIES:
if(_arg_2 >= 0) if(partId >= 0)
{ {
this._data.set(k, _arg_2); this._data.set(setType, partId);
} }
else else
{ {
this._data.delete(k); this._data.delete(setType);
} }
break; break;
} }
if(_arg_3) this.updateView(); if(update) this.updateView();
} }
public savePartSetColourId(setType: string, colorIds: number[], update: boolean = true): void public savePartSetColourId(setType: string, colorIds: number[], update: boolean = true): void

View File

@ -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();
}