nitro/apps/frontend/src/components/avatar-editor/AvatarEditorView.tsx

392 lines
13 KiB
TypeScript

import {
AvatarEditorFigureCategory,
FigureSetIdsMessageEvent,
GetWardrobeMessageComposer,
IAvatarFigureContainer,
ILinkEventTracker,
SetClothingChangeDataMessageComposer,
UserFigureComposer,
UserWardrobePageEvent,
} from "@nitro/renderer";
import {FC, useCallback, useEffect, useMemo, useState} from "react";
import {FaDice, FaTrash, FaUndo} from "react-icons/fa";
import {
AddEventLinkTracker,
AvatarEditorAction,
AvatarEditorUtilities,
BodyModel,
FigureData,
GetAvatarRenderManager,
GetClubMemberLevel,
GetConfiguration,
GetSessionDataManager,
HeadModel,
IAvatarEditorCategoryModel,
LegModel,
LocalizeText,
RemoveLinkEventTracker,
SendMessageComposer,
TorsoModel,
generateRandomFigure,
} from "../../api";
import {
Button,
ButtonGroup,
Column,
Grid,
NitroCardContentView,
NitroCardHeaderView,
NitroCardTabsItemView,
NitroCardTabsView,
NitroCardView,
} from "../../common";
import {useMessageEvent} from "../../hooks";
import {AvatarEditorFigurePreviewView} from "./views/AvatarEditorFigurePreviewView";
import {AvatarEditorModelView} from "./views/AvatarEditorModelView";
import {AvatarEditorWardrobeView} from "./views/AvatarEditorWardrobeView";
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";
const DEFAULT_MALE_FOOTBALL_GATE: string = "ch-3109-92-1408.lg-3116-82-1408.sh-3115-1408-1408";
const DEFAULT_FEMALE_FOOTBALL_GATE: string = "ch-3112-1408-1408.lg-3116-71-1408.sh-3115-1408-1408";
export const AvatarEditorView: FC<{}> = props => {
const [isVisible, setIsVisible] = useState(false);
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 [figureSetIds, setFigureSetIds] = useState<number[]>([]);
const [boundFurnitureNames, setBoundFurnitureNames] = useState<string[]>([]);
const [savedFigures, setSavedFigures] = useState<[IAvatarFigureContainer, string][]>([]);
const [isWardrobeVisible, setIsWardrobeVisible] = useState(false);
const [lastFigure, setLastFigure] = useState<string>(null);
const [lastGender, setLastGender] = useState<string>(null);
const [needsReset, setNeedsReset] = useState(true);
const [isInitalized, setIsInitalized] = useState(false);
const [genderFootballGate, setGenderFootballGate] = useState<string>(null);
const [objectFootballGate, setObjectFootballGate] = useState<number>(null);
const maxWardrobeSlots = useMemo(() => GetConfiguration<number>("avatar.wardrobe.max.slots", 10), []);
const onClose = () => {
setGenderFootballGate(null);
setObjectFootballGate(null);
setIsVisible(false);
};
useMessageEvent<FigureSetIdsMessageEvent>(FigureSetIdsMessageEvent, event => {
const parser = event.getParser();
setFigureSetIds(parser.figureSetIds);
setBoundFurnitureNames(parser.boundsFurnitureNames);
});
useMessageEvent<UserWardrobePageEvent>(UserWardrobePageEvent, event => {
const parser = event.getParser();
const savedFigures: [IAvatarFigureContainer, string][] = [];
let i = 0;
while (i < maxWardrobeSlots) {
savedFigures.push([null, null]);
i++;
}
for (let [index, [look, gender]] of parser.looks.entries()) {
const container = GetAvatarRenderManager().createFigureContainer(look);
savedFigures[index - 1] = [container, gender];
}
setSavedFigures(savedFigures);
});
const selectCategory = useCallback(
(name: string) => {
if (!categories) return;
setActiveCategory(categories.get(name));
},
[categories]
);
const resetCategories = useCallback(() => {
const categories = new Map();
if (!genderFootballGate) {
categories.set(AvatarEditorFigureCategory.GENERIC, new BodyModel());
categories.set(AvatarEditorFigureCategory.HEAD, new HeadModel());
categories.set(AvatarEditorFigureCategory.TORSO, new TorsoModel());
categories.set(AvatarEditorFigureCategory.LEGS, new LegModel());
} else {
categories.set(AvatarEditorFigureCategory.TORSO, new TorsoModel());
categories.set(AvatarEditorFigureCategory.LEGS, new LegModel());
}
setCategories(categories);
}, [genderFootballGate]);
const setupFigures = useCallback(() => {
const figures: Map<string, FigureData> = new Map();
const maleFigure = new FigureData();
const femaleFigure = new FigureData();
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) => {
gender = AvatarEditorUtilities.getGender(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) {
setLastFigure(figureData.getFigureString());
setLastGender(figureData.gender);
}
},
[figures, figureData]
);
const processAction = useCallback(
(action: string) => {
switch (action) {
case AvatarEditorAction.ACTION_CLEAR:
loadAvatarInEditor(figureData.getFigureStringWithFace(0, false), figureData.gender, false);
resetCategories();
return;
case AvatarEditorAction.ACTION_RESET:
loadAvatarInEditor(lastFigure, lastGender);
resetCategories();
return;
case AvatarEditorAction.ACTION_RANDOMIZE:
const figure = generateRandomFigure(figureData, figureData.gender, GetClubMemberLevel(), figureSetIds, [FigureData.FACE]);
loadAvatarInEditor(figure, figureData.gender, false);
resetCategories();
return;
case AvatarEditorAction.ACTION_SAVE:
!genderFootballGate
? SendMessageComposer(new UserFigureComposer(figureData.gender, figureData.getFigureString()))
: SendMessageComposer(new SetClothingChangeDataMessageComposer(objectFootballGate, genderFootballGate, figureData.getFigureString()));
onClose();
return;
}
},
[loadAvatarInEditor, figureData, resetCategories, lastFigure, lastGender, figureSetIds, genderFootballGate, objectFootballGate]
);
const setGender = useCallback(
(gender: string) => {
gender = AvatarEditorUtilities.getGender(gender);
setFigureData(figures.get(gender));
},
[figures]
);
useEffect(() => {
const linkTracker: ILinkEventTracker = {
linkReceived: (url: string) => {
const parts = url.split("/");
setGenderFootballGate(parts[2] ? parts[2] : null);
setObjectFootballGate(parts[3] ? Number(parts[3]) : null);
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/",
};
AddEventLinkTracker(linkTracker);
return () => RemoveLinkEventTracker(linkTracker);
}, []);
useEffect(() => {
setSavedFigures(new Array(maxWardrobeSlots));
}, [maxWardrobeSlots]);
useEffect(() => {
if (!isWardrobeVisible) return;
setActiveCategory(null);
SendMessageComposer(new GetWardrobeMessageComposer());
}, [isWardrobeVisible]);
useEffect(() => {
if (!activeCategory) return;
setIsWardrobeVisible(false);
}, [activeCategory]);
useEffect(() => {
if (!categories) return;
selectCategory(!genderFootballGate ? AvatarEditorFigureCategory.GENERIC : AvatarEditorFigureCategory.TORSO);
}, [categories, genderFootballGate, selectCategory]);
useEffect(() => {
if (!figureData) return;
AvatarEditorUtilities.CURRENT_FIGURE = figureData;
resetCategories();
return () => (AvatarEditorUtilities.CURRENT_FIGURE = null);
}, [figureData, resetCategories]);
useEffect(() => {
AvatarEditorUtilities.FIGURE_SET_IDS = figureSetIds;
AvatarEditorUtilities.BOUND_FURNITURE_NAMES = boundFurnitureNames;
resetCategories();
return () => {
AvatarEditorUtilities.FIGURE_SET_IDS = null;
AvatarEditorUtilities.BOUND_FURNITURE_NAMES = null;
};
}, [figureSetIds, boundFurnitureNames, resetCategories]);
useEffect(() => {
if (!isVisible) return;
if (!figures) {
setupFigures();
setIsInitalized(true);
return;
}
}, [isVisible, figures, setupFigures]);
useEffect(() => {
if (!isVisible || !isInitalized || !needsReset) return;
loadAvatarInEditor(
!genderFootballGate ? GetSessionDataManager().figure : genderFootballGate === FigureData.MALE ? DEFAULT_MALE_FOOTBALL_GATE : DEFAULT_FEMALE_FOOTBALL_GATE,
!genderFootballGate ? GetSessionDataManager().gender : genderFootballGate
);
setNeedsReset(false);
}, [isVisible, isInitalized, needsReset, loadAvatarInEditor, genderFootballGate]);
useEffect(() =>
// This is so when you have the look editor open and you change the mode to Boy or Girl
{
if (!isVisible) return;
return () => {
setNeedsReset(true);
};
}, [isVisible, genderFootballGate]);
useEffect(() => {
if (isVisible) return;
return () => {
setNeedsReset(true);
};
}, [isVisible]);
if (!isVisible || !figureData) return null;
return (
<NitroCardView uniqueKey="avatar-editor" className="nitro-avatar-editor">
<NitroCardHeaderView
headerText={!genderFootballGate ? LocalizeText("avatareditor.title") : LocalizeText("widget.furni.clothingchange.editor.title")}
onCloseClick={onClose}
/>
<NitroCardTabsView>
{categories &&
categories.size > 0 &&
Array.from(categories.keys()).map(category => {
const isActive = activeCategory && activeCategory.name === category;
return (
<NitroCardTabsItemView key={category} isActive={isActive} onClick={event => selectCategory(category)}>
{LocalizeText(`avatareditor.category.${category}`)}
</NitroCardTabsItemView>
);
})}
{!genderFootballGate && (
<NitroCardTabsItemView isActive={isWardrobeVisible} onClick={event => setIsWardrobeVisible(true)}>
{LocalizeText("avatareditor.category.wardrobe")}
</NitroCardTabsItemView>
)}
</NitroCardTabsView>
<NitroCardContentView>
<Grid>
<Column size={9} overflow="hidden">
{activeCategory && !isWardrobeVisible && (
<AvatarEditorModelView
model={activeCategory}
gender={figureData.gender}
isFromFootballGate={!genderFootballGate ? false : true}
setGender={setGender}
/>
)}
{isWardrobeVisible && (
<AvatarEditorWardrobeView
figureData={figureData}
savedFigures={savedFigures}
setSavedFigures={setSavedFigures}
loadAvatarInEditor={loadAvatarInEditor}
/>
)}
</Column>
<Column size={3} overflow="hidden">
<AvatarEditorFigurePreviewView figureData={figureData} />
<Column grow gap={1}>
{!genderFootballGate && (
<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>
);
};