AvatarEditorView.tsx 15.4 Ko
Télécharger le fichier
import {
AddLinkEventTracker,
AvatarEditorFigureCategory,
AvatarFigurePartType,
GetSessionDataManager,
ILinkEventTracker,
RemoveLinkEventTracker,
SetClothingChangeDataMessageComposer,
UserFigureComposer
} from '@octane/renderer';
import { FC, useEffect, useState } from 'react';
import { FaDice, FaRedo, FaTrash } from 'react-icons/fa';
import { AVATAR_EDITOR_EFFECTS_TAB, AvatarEditorAction, GetConfigurationValue, LocalizeText, SendMessageComposer } from '../../api';
import mainEffectsSrc from '../../assets/images/avatareditor/air/main-effects.png';
import mainGenericSrc from '../../assets/images/avatareditor/air/main-generic.png';
import mainHeadSrc from '../../assets/images/avatareditor/air/main-head.png';
import mainHotLooksSrc from '../../assets/images/avatareditor/air/main-hotlooks.png';
import mainLegsSrc from '../../assets/images/avatareditor/air/main-legs.png';
import mainMiscSrc from '../../assets/images/avatareditor/air/main-misc.png';
import mainTorsoSrc from '../../assets/images/avatareditor/air/main-torso.png';
import wardrobeHangerSrc from '../../assets/images/avatareditor/wardrobe-hanger.png';
import mainNftSrc from '../../assets/images/wardrobe/nft.png';
import mainPetsSrc from '../../assets/images/wardrobe/pets.png';
import { OctaneCardContentView, OctaneCardHeaderView, OctaneCardTabsItemView, OctaneCardTabsView, OctaneCardView } from '../../common';
import { useAvatarEditor, useAvatarEffects, useHotLooks, useNftWardrobe } from '../../hooks';
import { AvatarEditorEffectsView } from './AvatarEditorEffectsView';
import { AvatarEditorFigurePreviewView } from './AvatarEditorFigurePreviewView';
import { AvatarEditorHotLooksView } from './AvatarEditorHotLooksView';
import { AvatarEditorModelView } from './AvatarEditorModelView';
import { AvatarEditorNftOutfitInfoView, AvatarEditorNftOutfitsView } from './AvatarEditorNftOutfitsView';
import { AvatarEditorPetView } from './AvatarEditorPetView';
import { AvatarEditorWardrobeView } from './AvatarEditorWardrobeView';
import { BaseButton } from '../../common/BaseButton';
import { ToggleButton } from '../../common/ToggleButton';
import { Hint } from '../../common/Tooltip';
import { NativeTextTree } from '../../common/native-text/NativeTextTree';
const MAIN_TAB_ICONS: Record<string, string> = {
[AvatarEditorFigureCategory.GENERIC]: mainGenericSrc,
[AvatarEditorFigureCategory.HEAD]: mainHeadSrc,
[AvatarEditorFigureCategory.TORSO]: mainTorsoSrc,
[AvatarEditorFigureCategory.LEGS]: mainLegsSrc,
[AvatarEditorFigureCategory.PETS]: mainPetsSrc,
[AvatarEditorFigureCategory.MISC]: mainMiscSrc,
[AvatarEditorFigureCategory.HOTLOOKS]: mainHotLooksSrc,
[AvatarEditorFigureCategory.NFT]: mainNftSrc,
[AVATAR_EDITOR_EFFECTS_TAB]: mainEffectsSrc
};
// AIR removes unavailable tabs from this sequence without reordering the
// survivors. Polaris-only categories are kept after the official tabs.
const MAIN_TAB_ORDER: string[] = [
AvatarEditorFigureCategory.GENERIC,
AvatarEditorFigureCategory.HEAD,
AvatarEditorFigureCategory.TORSO,
AvatarEditorFigureCategory.LEGS,
AvatarEditorFigureCategory.MISC,
AvatarEditorFigureCategory.HOTLOOKS,
AVATAR_EDITOR_EFFECTS_TAB,
AvatarEditorFigureCategory.NFT,
AvatarEditorFigureCategory.PETS
];
export const AvatarEditorView: FC<{}> = (props) => {
const [isVisible, setIsVisible] = useState(false);
const [isWardrobeOpen, setIsWardrobeOpen] = useState(false);
// Effect picked in the effects tab; null until the user picks one.
const [effectSelection, setEffectSelection] = useState<number>(null);
const { activeEffectType = 0, activateEffect = null } = useAvatarEffects();
const {
setIsVisible: setEditorVisibility,
clothingChangeData = null,
setClothingChangeData = null,
avatarModels,
activeModelKey,
setActiveModelKey,
loadAvatarData,
getFigureStringWithFace,
gender,
randomizeCurrentFigure = null,
getFigureString = null,
getValidFigureString = null
} = useAvatarEditor();
const isPetsOpen = activeModelKey === AvatarEditorFigureCategory.PETS;
const isNftOpen = activeModelKey === AvatarEditorFigureCategory.NFT;
// Official effects.in.avatar.editor; not for a clothing change booth.
const effectsEnabled = !clothingChangeData && GetConfigurationValue<boolean>('effects.in.avatar.editor', true);
const isEffectsOpen = effectsEnabled && activeModelKey === AVATAR_EDITOR_EFFECTS_TAB;
// AIR HotLooksModel: not part of a clothing change booth's restricted categories.
const hotLooksEnabled = !clothingChangeData;
const isHotLooksOpen = hotLooksEnabled && activeModelKey === AvatarEditorFigureCategory.HOTLOOKS;
const hotLooks = useHotLooks(isVisible && hotLooksEnabled);
const canUseWardrobe = !clothingChangeData && !isNftOpen;
// AIR avatar collectibles (NftAvatarsModel): the NFT tab always lists the user's collectible looks; it stays
// empty when the hotel doesn't run collectibles.
const nftWardrobe = useNftWardrobe(isVisible && !clothingChangeData, activeModelKey);
const selectMainTab = (modelKey: string) => {
nftWardrobe.onTabChange(modelKey);
setActiveModelKey(modelKey);
};
const orderedModelKeys = [
...Object.keys(avatarModels),
...(hotLooksEnabled ? [AvatarEditorFigureCategory.HOTLOOKS] : []),
...(effectsEnabled ? [AVATAR_EDITOR_EFFECTS_TAB] : [])
]
.filter((modelKey) => modelKey !== AvatarEditorFigureCategory.WARDROBE)
.sort((left, right) => {
const leftIndex = MAIN_TAB_ORDER.indexOf(left);
const rightIndex = MAIN_TAB_ORDER.indexOf(right);
if (leftIndex === -1) return rightIndex === -1 ? left.localeCompare(right) : 1;
if (rightIndex === -1) return -1;
return leftIndex - rightIndex;
});
const processAction = (action: string) => {
switch (action) {
case AvatarEditorAction.ACTION_RESET:
loadAvatarData(GetSessionDataManager().figure, GetSessionDataManager().gender);
return;
case AvatarEditorAction.ACTION_CLEAR:
loadAvatarData(getFigureStringWithFace(0, false), gender);
return;
case AvatarEditorAction.ACTION_RANDOMIZE:
randomizeCurrentFigure();
return;
case AvatarEditorAction.ACTION_SAVE:
if (clothingChangeData) {
SendMessageComposer(new SetClothingChangeDataMessageComposer(clothingChangeData.objectId, gender, getFigureString));
} else {
// A collectible look picked in the NFT tab is worn instead (AIR saveCurrentSelection).
if (!nftWardrobe.save()) SendMessageComposer(new UserFigureComposer(gender, getValidFigureString));
if (effectSelection !== null) activateEffect(effectSelection);
}
setIsVisible(false);
return;
}
};
useEffect(() => {
const linkTracker: ILinkEventTracker = {
linkReceived: (url: string) => {
const parts = url.split('/');
if (parts.length < 2) return;
switch (parts[1]) {
case 'show':
if (parts[2] && parts[3] && (parts[2] === AvatarFigurePartType.MALE || parts[2] === AvatarFigurePartType.FEMALE)) {
setClothingChangeData({ objectId: Number(parts[3]), gender: parts[2] });
setIsWardrobeOpen(false);
} else {
setClothingChangeData(null);
setIsWardrobeOpen(true);
}
setIsVisible(true);
return;
case 'hide':
setClothingChangeData(null);
setIsVisible(false);
return;
case 'toggle':
setClothingChangeData(null);
setIsVisible((prevValue) => {
if (!prevValue) setIsWardrobeOpen(true);
return !prevValue;
});
return;
}
},
eventUrlPrefix: 'avatar-editor/'
};
AddLinkEventTracker(linkTracker);
return () => RemoveLinkEventTracker(linkTracker);
}, [setClothingChangeData]);
useEffect(() => {
setEditorVisibility(isVisible);
if (!isVisible) {
setClothingChangeData(null);
setIsWardrobeOpen(false);
setEffectSelection(null);
}
}, [isVisible, setEditorVisibility, setClothingChangeData]);
useEffect(() => {
if (!canUseWardrobe) setIsWardrobeOpen(false);
}, [canUseWardrobe]);
if (!isVisible) return null;
return (
<NativeTextTree>
<OctaneCardView
className={`octane-avatar-editor${isWardrobeOpen ? ' is-wardrobe-open' : ''}`}
frameStyle={3}
isResizable={false}
uniqueKey="avatar-editor"
>
<OctaneCardHeaderView
headerText={LocalizeText(clothingChangeData ? 'widget.furni.clothingchange.editor.title' : 'avatareditor.title')}
onCloseClick={(event) => setIsVisible(false)}
/>
<OctaneCardContentView className="octane-avatar-editor-content">
<div className="octane-avatar-editor-stage">
<div className="octane-avatar-editor-nameplate">
<span>{GetSessionDataManager().userName}</span>
</div>
<div className="octane-avatar-editor-tab-row">
<OctaneCardTabsView classNames={['avatar-editor-tabs']}>
{orderedModelKeys.map((modelKey) => (
<OctaneCardTabsItemView
key={modelKey}
classNames={['octane-avatar-editor-main-tab', `is-${modelKey}`]}
isActive={activeModelKey === modelKey}
onClick={() => selectMainTab(modelKey)}
>
<img className="octane-avatar-editor-main-tab-icon" src={MAIN_TAB_ICONS[modelKey]} alt="" draggable={false} />
</OctaneCardTabsItemView>
))}
</OctaneCardTabsView>
</div>
{canUseWardrobe && (
<ToggleButton
type="button"
className={`octane-avatar-editor-wardrobe-toggle${isWardrobeOpen ? ' is-open' : ''}`} pressed={isWardrobeOpen}
aria-label={LocalizeText('avatareditor.wardrobe.title')}
onClick={() => setIsWardrobeOpen((open) => !open)}
>
<img alt="" draggable={false} src={wardrobeHangerSrc} />
</ToggleButton>
)}
<div className="octane-avatar-editor-main">
{activeModelKey.length > 0 && !isPetsOpen && !isNftOpen && !isEffectsOpen && !isHotLooksOpen && avatarModels[activeModelKey] && (
<AvatarEditorModelView categories={avatarModels[activeModelKey]} name={activeModelKey} />
)}
{isPetsOpen && <AvatarEditorPetView categories={avatarModels[activeModelKey]} />}
{isNftOpen && (
<AvatarEditorNftOutfitsView
outfits={nftWardrobe.outfits}
selectedOutfit={nftWardrobe.selectedOutfit}
wornOutfit={nftWardrobe.wornOutfit}
onSelect={nftWardrobe.selectOutfit}
/>
)}
{isHotLooksOpen && (
<AvatarEditorHotLooksView
hotLooks={hotLooks[gender === AvatarFigurePartType.FEMALE ? 'F' : 'M']}
onSelect={(look) => loadAvatarData(look.figure, look.gender)}
/>
)}
{isEffectsOpen && (
<AvatarEditorEffectsView selectedType={effectSelection ?? (activeEffectType > 0 ? activeEffectType : -1)} onSelect={setEffectSelection} />
)}
<AvatarEditorFigurePreviewView effectType={effectSelection ?? 0} />
{isNftOpen && <AvatarEditorNftOutfitInfoView outfit={nftWardrobe.selectedOutfit} />}
{!clothingChangeData && !isNftOpen && (
<div className="octane-avatar-editor-secondary-actions">
<Hint content="Reset avatar">
<BaseButton
type="button"
aria-label="Reset avatar"
onClick={() => processAction(AvatarEditorAction.ACTION_RESET)}
>
<FaRedo className="fa-icon" />
</BaseButton>
</Hint>
<Hint content="Clear avatar">
<BaseButton
type="button"
aria-label="Clear avatar"
onClick={() => processAction(AvatarEditorAction.ACTION_CLEAR)}
>
<FaTrash className="fa-icon" />
</BaseButton>
</Hint>
<Hint content="Randomize avatar">
<BaseButton
type="button"
aria-label="Randomize avatar"
onClick={() => processAction(AvatarEditorAction.ACTION_RANDOMIZE)}
>
<FaDice className="fa-icon" />
</BaseButton>
</Hint>
</div>
)}
<BaseButton type="button" className="octane-avatar-editor-save" onClick={() => processAction(AvatarEditorAction.ACTION_SAVE)}>
{LocalizeText('avatareditor.save')}
</BaseButton>
</div>
</div>
{isWardrobeOpen && canUseWardrobe && <AvatarEditorWardrobeView />}
</OctaneCardContentView>
</OctaneCardView>
</NativeTextTree>
);
};
