5180 fichiers 38.5 Mo 0 téléchargements 303 vues mis à jour il y a 6 heures ui
nitro-v36-with-renderer-v36 / src / components / login / components / RegistrationAvatarWardrobe.tsx
RegistrationAvatarWardrobe.tsx 14.5 Ko Télécharger le fichier
import { AvatarDirectionAngle, GetAvatarRenderManager, IPartColor } from '@octane/renderer';
import { FC, useEffect, useMemo, useState } from 'react';
import { AvatarEditorThumbnailsHelper } from '../../../api';
import faceTabIcon from '../../../assets/images/wardrobe/hd.png';
import headTabIcon from '../../../assets/images/wardrobe/head.png';
import legsTabIcon from '../../../assets/images/wardrobe/legs.png';
import torsoTabIcon from '../../../assets/images/wardrobe/torso.png';
import { LayoutAvatarImageView } from '../../../common';
import { InfiniteGrid } from '../../../layout';
import { AvatarEditorIcon } from '../../avatar-editor/AvatarEditorIcon';
import { t } from '../utils/i18n';
import { TabBar, TabBarTab } from '../../../common/TabBar';
import { BaseButton } from '../../../common/BaseButton';
import { ToggleButton } from '../../../common/ToggleButton';
import { Hint } from '../../../common/Tooltip';
import { NativeTextTree } from '../../../common/native-text/NativeTextTree';

type GenderKey = 'M' | 'F';
type FigureSelection = Record<string, { partId: number; colors: number[] }>;
type PartOptions = Record<string, Record<GenderKey, number[]>>;
type PaletteOptions = Record<string, { id: number; hex: string }[]>;

interface WardrobeGroup {
    key: string;
    icon: string;
    labelKey: string;
    fallback: string;
    setTypes: string[];
}

const WARDROBE_GROUPS: WardrobeGroup[] = [
    { key: 'face', icon: faceTabIcon, labelKey: 'nitro.login.register.wardrobe.face', fallback: 'Face', setTypes: ['hd'] },
    { key: 'head', icon: headTabIcon, labelKey: 'nitro.login.register.wardrobe.head', fallback: 'Head', setTypes: ['hr', 'ha', 'he', 'ea', 'fa'] },
    { key: 'torso', icon: torsoTabIcon, labelKey: 'nitro.login.register.wardrobe.torso', fallback: 'Torso', setTypes: ['ch', 'cp', 'cc', 'ca'] },
    { key: 'legs', icon: legsTabIcon, labelKey: 'nitro.login.register.wardrobe.legs', fallback: 'Legs', setTypes: ['lg', 'sh', 'wa'] }
];

const REQUIRED_SET_TYPES = new Set(['hd', 'hr', 'ch', 'lg', 'sh']);

const SET_TYPE_LABELS: Record<string, { key: string; fallback: string }> = {
    hd: { key: 'nitro.login.register.avatar.face', fallback: 'Face' },
    hr: { key: 'nitro.login.register.avatar.hair', fallback: 'Hair' },
    ha: { key: 'nitro.login.register.avatar.hat', fallback: 'Hats' },
    he: { key: 'nitro.login.register.avatar.head_accessory', fallback: 'Head accessories' },
    ea: { key: 'nitro.login.register.avatar.eyewear', fallback: 'Eyewear' },
    fa: { key: 'nitro.login.register.avatar.face_accessory', fallback: 'Face accessories' },
    ch: { key: 'nitro.login.register.avatar.top', fallback: 'Tops' },
    cp: { key: 'nitro.login.register.avatar.print', fallback: 'Prints' },
    cc: { key: 'nitro.login.register.avatar.jacket', fallback: 'Jackets' },
    ca: { key: 'nitro.login.register.avatar.chest_accessory', fallback: 'Chest accessories' },
    lg: { key: 'nitro.login.register.avatar.bottom', fallback: 'Bottoms' },
    sh: { key: 'nitro.login.register.avatar.shoes', fallback: 'Shoes' },
    wa: { key: 'nitro.login.register.avatar.waist_accessory', fallback: 'Waist accessories' }
};

interface WardrobePartItemProps {
    setType: string;
    partId: number;
    colorIds: number[];
    selected: boolean;
    label: string;
    onSelect: () => void;
}

const WardrobePartItem: FC<WardrobePartItemProps> = ({ setType, partId, colorIds, selected, label, onSelect }) => {
    const [assetUrl, setAssetUrl] = useState('');
    const colorKey = colorIds.join('-');

    useEffect(() => {
        if (partId < 0) {
            setAssetUrl('');
            return;
        }

        setAssetUrl('');
        let cancelled = false;

        const loadThumbnail = async () => {
            try {
                if (setType === 'hd') {
                    const colorSuffix = colorIds.length ? `-${colorIds.join('-')}` : '';
                    const url = await AvatarEditorThumbnailsHelper.buildForFace(`hd-${partId}${colorSuffix}`);
                    if (!cancelled && url) setAssetUrl(url);
                    return;
                }

                const setData = GetAvatarRenderManager().structureData.getSetType(setType);
                const partSet = setData?.partSets?.getValues().find((candidate) => candidate.id === partId);
                if (!setData || !partSet) return;

                const palette = GetAvatarRenderManager().structureData.getPalette(setData.paletteID);
                const partColors = colorIds
                    .map((colorId) => palette?.colors?.getValues().find((color) => color.id === colorId))
                    .filter((color): color is IPartColor => Boolean(color));
                let maxPaletteCount = 0;
                for (const part of partSet.parts) maxPaletteCount = Math.max(maxPaletteCount, part.colorLayerIndex);

                const url = await AvatarEditorThumbnailsHelper.build(
                    setType,
                    { id: partId, partSet, usesColor: partSet.isColorable, maxPaletteCount },
                    partSet.isColorable,
                    partColors
                );
                if (!cancelled && url) setAssetUrl(url);
            } catch {}
        };

        void loadThumbnail();
        return () => {
            cancelled = true;
        };
    }, [setType, partId, colorKey]);

    return (
        <NativeTextTree>
            <InfiniteGrid.Item
                itemActive={selected}
                itemImage={partId >= 0 ? assetUrl || undefined : undefined}
                className={`registration-wardrobe-part wardrobe-part-${setType} avatar-parts${selected ? ' part-selected' : ''}${partId >= 0 && !assetUrl ? ' wardrobe-part-loading' : ''}`}
                aria-label={partId < 0 ? t('nitro.login.register.wardrobe.none', 'None') : `${label} ${partId}`}
                onClick={onSelect}
            >
                {partId < 0 && <AvatarEditorIcon icon="clear" />}
            </InfiniteGrid.Item>
        </NativeTextTree>
    );
};

interface RegistrationAvatarWardrobeProps {
    gender: GenderKey;
    figure: string;
    selection: FigureSelection;
    partOptions: PartOptions;
    paletteOptions: PaletteOptions;
    onSelectPart: (setType: string, partId: number) => void;
    onSelectColor: (setType: string, colorId: number) => void;
}

export const RegistrationAvatarWardrobe: FC<RegistrationAvatarWardrobeProps> = ({
    gender,
    figure,
    selection,
    partOptions,
    paletteOptions,
    onSelectPart,
    onSelectColor
}) => {
    const [direction, setDirection] = useState(4);
    const [activeGroupKey, setActiveGroupKey] = useState(WARDROBE_GROUPS[0].key);
    const [activeSetType, setActiveSetType] = useState(WARDROBE_GROUPS[0].setTypes[0]);
    const activeGroup = WARDROBE_GROUPS.find((group) => group.key === activeGroupKey) ?? WARDROBE_GROUPS[0];
    const setTypeLabel = SET_TYPE_LABELS[activeSetType] ?? { key: activeSetType, fallback: activeSetType.toUpperCase() };
    const translatedSetTypeLabel = t(setTypeLabel.key, setTypeLabel.fallback);
    const selectedPart = selection[activeSetType]?.partId ?? -1;
    const selectedColor = selection[activeSetType]?.colors?.[0] ?? -1;
    const availableParts = partOptions[activeSetType]?.[gender] ?? [];
    const displayedParts = useMemo(() => (REQUIRED_SET_TYPES.has(activeSetType) ? availableParts : [-1, ...availableParts]), [activeSetType, availableParts]);
    const availableColors = paletteOptions[activeSetType] ?? [];

    const selectGroup = (group: WardrobeGroup) => {
        setActiveGroupKey(group.key);
        setActiveSetType(group.setTypes[0]);
    };

    const rotateAvatar = (delta: number) => {
        setDirection((currentDirection) => {
            const minimum = AvatarDirectionAngle.MIN_DIRECTION;
            const maximum = AvatarDirectionAngle.MAX_DIRECTION;
            const directionCount = maximum - minimum + 1;

            return ((((currentDirection - minimum + delta) % directionCount) + directionCount) % directionCount) + minimum;
        });
    };

    return (
        <NativeTextTree>
            <div className="registration-avatar-wardrobe">
                <TabBar className="registration-wardrobe-tabs" aria-label={t('nitro.login.register.wardrobe.categories', 'Clothing categories')} value={activeGroup.key}>
                    {WARDROBE_GROUPS.map((group) => {
                        const active = group.key === activeGroup.key;
                        const label = t(group.labelKey, group.fallback);
                        return (
                            <TabBarTab
                                key={group.key}
                                value={group.key}
                                className={active ? 'active' : ''}
                                aria-label={label}
                                title={label}
                                onClick={() => selectGroup(group)}
                            >
                                <img src={group.icon} alt="" draggable={false} />
                            </TabBarTab>
                        );
                    })}
                </TabBar>

                <div className="registration-wardrobe-main">
                    <div className="avatar-preview-panel registration-wardrobe-preview">
                        <div className="avatar-preview" aria-label={t('nitro.login.register.avatar.preview', 'Your Habbo preview')}>
                            <LayoutAvatarImageView direction={direction} figure={figure} gender={gender} scale={1.7} />
                            <div className="registration-avatar-shadow" />
                        </div>
                        <div className="registration-wardrobe-rotation">
                            <Hint content={t('nitro.login.register.wardrobe.rotate_left', 'Rotate left')}><BaseButton
                                type="button"
                                aria-label={t('nitro.login.register.wardrobe.rotate_left', 'Rotate left')}
                                onClick={() => rotateAvatar(1)}
                            >
                                <AvatarEditorIcon icon="arrow-left" />
                            </BaseButton></Hint>
                            <Hint content={t('nitro.login.register.wardrobe.rotate_right', 'Rotate right')}><BaseButton
                                type="button"
                                aria-label={t('nitro.login.register.wardrobe.rotate_right', 'Rotate right')}
                                onClick={() => rotateAvatar(-1)}
                            >
                                <AvatarEditorIcon icon="arrow-right" />
                            </BaseButton></Hint>
                        </div>
                    </div>

                    <div className="registration-wardrobe-editor">
                        <TabBar className="registration-wardrobe-set-tabs" aria-label={t(activeGroup.labelKey, activeGroup.fallback)} value={activeSetType}>
                            {activeGroup.setTypes.length > 1 &&
                                activeGroup.setTypes.map((setType) => {
                                    const labelInfo = SET_TYPE_LABELS[setType];
                                    const label = t(labelInfo.key, labelInfo.fallback);
                                    const active = activeSetType === setType;
                                    return (
                                        <TabBarTab
                                            key={setType}
                                            value={setType}
                                            className={active ? 'active' : ''}
                                            aria-label={label}
                                            title={label}
                                            onClick={() => setActiveSetType(setType)}
                                        >
                                            <AvatarEditorIcon icon={setType} selected={active} />
                                        </TabBarTab>
                                    );
                                })}
                        </TabBar>

                        <div className="registration-wardrobe-section-title">{translatedSetTypeLabel}</div>
                        <div className="registration-wardrobe-parts">
                            {displayedParts.length ? (
                                <InfiniteGrid<number>
                                    items={displayedParts}
                                    columnCount={7}
                                    itemMinWidth={48}
                                    estimateSize={52}
                                    rowGap={5}
                                    overscan={2}
                                    itemRender={(partId) => (
                                        <WardrobePartItem
                                            setType={activeSetType}
                                            partId={partId}
                                            colorIds={selection[activeSetType]?.colors ?? []}
                                            selected={partId === selectedPart}
                                            label={translatedSetTypeLabel}
                                            onSelect={() => onSelectPart(activeSetType, partId)}
                                        />
                                    )}
                                />
                            ) : (
                                <div className="registration-wardrobe-loading">{t('nitro.login.register.wardrobe.loading', 'Loading wardrobe…')}</div>
                            )}
                        </div>

                        <div className="registration-wardrobe-section-title">{t('nitro.login.register.wardrobe.colors', 'Colors')}</div>
                        <div className="registration-wardrobe-palette">
                            {availableColors.map((color) => (
                                <ToggleButton
                                    key={color.id}
                                    type="button"
                                    className={color.id === selectedColor ? 'active' : ''}
                                    style={{ backgroundColor: color.hex }}
                                    aria-label={`${t('nitro.login.register.wardrobe.color', 'Color')} ${color.id}`} pressed={color.id === selectedColor}
                                    disabled={selectedPart < 0}
                                    onClick={() => onSelectColor(activeSetType, color.id)}
                                />
                            ))}
                        </div>
                    </div>
                </div>
            </div>
        </NativeTextTree>
    );
};
15Sujets
1Dépôts
20Membres
2En ligne
Profile
Inscrit
Dernière visite
Messages
0
Votes positifs
0
Votes négatifs
0

Nous utilisons des cookies pour assurer le bon fonctionnement du site et améliorer votre expérience. en savoir plus

Autoriser