392 lines
13 KiB
TypeScript
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>
|
|
);
|
|
};
|