diff --git a/public/UITexts.example b/public/UITexts.example index 25c2cfa..b12e768 100644 --- a/public/UITexts.example +++ b/public/UITexts.example @@ -22,6 +22,21 @@ "widget.room.youtube.open_video": "Open de video", "wiredfurni.params.area_selection.selected": "Geselecteerd gebied: Lengte=%x%, Breedte=%y%, breedte=%w%, hoogte=%h%", "wiredfurni.params.sources.users.11": "L'utente cliccato", - "wiredfurni.params.setexecutions": "Quantità di esecuzioni: %amount%", - "wiredfurni.params.settimewindow": "Tempo massimo consentito: %timewindow% secondi" + "wiredfurni.params.setexecutions": "Quantità di esecuzioni: %amount%", + "wiredfurni.params.settimewindow": "Tempo massimo consentito: %timewindow% secondi", + "wiredfurni.params.eval_mode": "Condizioni che devono corrispondere:", + "wiredfurni.params.eval_mode.0": "Tutte", + "wiredfurni.params.eval_mode.1": "Almeno una", + "wiredfurni.params.eval_mode.2": "Non tutte", + "wiredfurni.params.eval_mode.3": "Nessuna", + "wiredfurni.params.eval_mode.cmp.0": "Meno di:", + "wiredfurni.params.eval_mode.cmp.1": "Esattamente:", + "wiredfurni.params.eval_mode.cmp.2": "Più di:", + "wiredfurni.error.condition_evaluation_furni": "Puoi selezionare solo la Condizione e i componenti aggiuntivi!", + "wiredfurni.params.texts.placeholder_name": "Nome del segnaposto:", + "wiredfurni.params.texts.placeholder_preview": "Usalo digitando %placeholder% nei testi dei Wired.", + "wiredfurni.params.texts.placeholder_type": "Tipo di segnaposto:", + "wiredfurni.params.texts.placeholder_type.1": "Singolo", + "wiredfurni.params.texts.placeholder_type.2": "Multiplo", + "wiredfurni.params.texts.select_delimiter": "Seleziona il delimitatore:" } diff --git a/src/api/wired/WiredActionLayoutCode.ts b/src/api/wired/WiredActionLayoutCode.ts index d3f1f5e..0839bb1 100644 --- a/src/api/wired/WiredActionLayoutCode.ts +++ b/src/api/wired/WiredActionLayoutCode.ts @@ -64,4 +64,6 @@ export class WiredActionLayoutCode public static RANDOM_EXTRA: number = 63; public static EXEC_IN_ORDER_EXTRA: number = 64; public static EXECUTION_LIMIT_EXTRA: number = 65; + public static OR_EVAL_EXTRA: number = 66; + public static TEXT_OUTPUT_USERNAME_EXTRA: number = 67; } diff --git a/src/assets/images/wardrobe/nft.png b/src/assets/images/wardrobe/nft.png new file mode 100644 index 0000000..ad535a4 Binary files /dev/null and b/src/assets/images/wardrobe/nft.png differ diff --git a/src/assets/images/wardrobe/pets.png b/src/assets/images/wardrobe/pets.png index 2c36bff..4c04121 100644 Binary files a/src/assets/images/wardrobe/pets.png and b/src/assets/images/wardrobe/pets.png differ diff --git a/src/assets/images/wiredtools/furni.png b/src/assets/images/wiredtools/furni.png new file mode 100644 index 0000000..4cae0aa Binary files /dev/null and b/src/assets/images/wiredtools/furni.png differ diff --git a/src/assets/images/wiredtools/global.png b/src/assets/images/wiredtools/global.png new file mode 100644 index 0000000..8126b1e Binary files /dev/null and b/src/assets/images/wiredtools/global.png differ diff --git a/src/assets/images/wiredtools/user.png b/src/assets/images/wiredtools/user.png new file mode 100644 index 0000000..eaed802 Binary files /dev/null and b/src/assets/images/wiredtools/user.png differ diff --git a/src/common/layout/LayoutAvatarImageView.tsx b/src/common/layout/LayoutAvatarImageView.tsx index 69d3e20..2b3b156 100644 --- a/src/common/layout/LayoutAvatarImageView.tsx +++ b/src/common/layout/LayoutAvatarImageView.tsx @@ -15,7 +15,7 @@ export interface LayoutAvatarImageViewProps extends BaseProps export const LayoutAvatarImageView: FC = props => { - const { figure = '', gender = 'M', headOnly = false, direction = 0, scale = 1, classNames = [], style = {}, ...rest } = props; + const { figure = '', gender = '', headOnly = false, direction = 0, scale = 1, classNames = [], style = {}, ...rest } = props; const [ avatarUrl, setAvatarUrl ] = useState(null); const [ isReady, setIsReady ] = useState(false); const isDisposed = useRef(false); diff --git a/src/components/avatar-editor/AvatarEditorFigurePreviewView.tsx b/src/components/avatar-editor/AvatarEditorFigurePreviewView.tsx index 25bb609..857dcaa 100644 --- a/src/components/avatar-editor/AvatarEditorFigurePreviewView.tsx +++ b/src/components/avatar-editor/AvatarEditorFigurePreviewView.tsx @@ -9,7 +9,7 @@ const DEFAULT_DIRECTION: number = 4; export const AvatarEditorFigurePreviewView: FC<{}> = props => { const [ direction, setDirection ] = useState(DEFAULT_DIRECTION); - const { getFigureString = null } = useAvatarEditor(); + const { getFigureString = null, gender = 'M' } = useAvatarEditor(); const rotateFigure = (newDirection: number) => { @@ -28,7 +28,7 @@ export const AvatarEditorFigurePreviewView: FC<{}> = props => return (
- +
diff --git a/src/components/avatar-editor/AvatarEditorNftView.tsx b/src/components/avatar-editor/AvatarEditorNftView.tsx new file mode 100644 index 0000000..405f5ec --- /dev/null +++ b/src/components/avatar-editor/AvatarEditorNftView.tsx @@ -0,0 +1,117 @@ +import { AvatarFigurePartType, FigureDataContainer } from '@nitrots/nitro-renderer'; +import { FC, useCallback, useEffect, useMemo, useState } from 'react'; +import { CreateLinkEvent, GetClubMemberLevel, IAvatarEditorCategory } from '../../api'; +import { LayoutAvatarImageView, LayoutCurrencyIcon } from '../../common'; +import { useAvatarEditor } from '../../hooks'; +import { AvatarEditorIcon } from './AvatarEditorIcon'; +import { AvatarEditorFigureSetView } from './figure-set'; +import { AvatarEditorAdvancedColorView, AvatarEditorPaletteSetView } from './palette-set'; + +export const AvatarEditorNftView: FC<{ + categories: IAvatarEditorCategory[]; +}> = props => +{ + const { categories = [] } = props; + const [ didChange, setDidChange ] = useState(false); + const [ activeSetType, setActiveSetType ] = useState(''); + const [ advancedColorMode, setAdvancedColorMode ] = useState(false); + const hasHC = GetClubMemberLevel() > 0; + const { maxPaletteCount = 1, selectedColorParts = null, getFirstSelectableColor = null, selectEditorColor = null, gender = null, setGender = null, getFigureString = '' } = useAvatarEditor(); + + const activeCategory = useMemo(() => + { + return categories.find(category => category.setType === activeSetType) ?? null; + }, [ categories, activeSetType ]); + + const selectSet = useCallback((setType: string) => + { + const selectedPalettes = selectedColorParts[setType]; + + if(!selectedPalettes || !selectedPalettes.length) selectEditorColor(setType, 0, getFirstSelectableColor(setType)); + + setActiveSetType(setType); + }, [ getFirstSelectableColor, selectEditorColor, selectedColorParts ]); + + useEffect(() => + { + if(!categories || !categories.length || !didChange) return; + + selectSet(categories[0]?.setType); + setDidChange(false); + }, [ categories, didChange, selectSet ]); + + useEffect(() => + { + setDidChange(true); + }, [ categories ]); + + if(!categories.length || !activeCategory) + { + return ( +
+
NFT
+
No NFT items available.
+
+ ); + } + + return ( +
+
+ { categories.map(category => +
selectSet(category.setType) }> + { (category.setType === AvatarFigurePartType.HEAD) + ? ( +
+ +
+ ) + : } +
+ ) } +
+ + { (activeSetType === AvatarFigurePartType.HEAD) && +
+
setGender(AvatarFigurePartType.MALE) }> + +
+
setGender(AvatarFigurePartType.FEMALE) }> + +
+
} + +
+ +
+ +
+ +
+ +
+ { (maxPaletteCount >= 1) && +
+ { advancedColorMode + ? + : } +
} + { (maxPaletteCount === 2) && +
+ { advancedColorMode + ? + : } +
} +
+
+ ); +}; diff --git a/src/components/avatar-editor/AvatarEditorView.tsx b/src/components/avatar-editor/AvatarEditorView.tsx index f080e21..6d23a3c 100644 --- a/src/components/avatar-editor/AvatarEditorView.tsx +++ b/src/components/avatar-editor/AvatarEditorView.tsx @@ -6,6 +6,7 @@ import { Button, ButtonGroup, NitroCardContentView, NitroCardHeaderView, NitroCa import { useAvatarEditor } from '../../hooks'; import { AvatarEditorFigurePreviewView } from './AvatarEditorFigurePreviewView'; import { AvatarEditorModelView } from './AvatarEditorModelView'; +import { AvatarEditorNftView } from './AvatarEditorNftView'; import { AvatarEditorPetView } from './AvatarEditorPetView'; import { AvatarEditorWardrobeView } from './AvatarEditorWardrobeView'; @@ -16,6 +17,7 @@ export const AvatarEditorView: FC<{}> = props => const isWardrobeOpen = (activeModelKey === AvatarEditorFigureCategory.WARDROBE); const isPetsOpen = (activeModelKey === AvatarEditorFigureCategory.PETS); + const isNftOpen = (activeModelKey === AvatarEditorFigureCategory.NFT); const processAction = (action: string) => { @@ -85,10 +87,12 @@ export const AvatarEditorView: FC<{}> = props => const isActive = (activeModelKey === modelKey); const isWardrobe = (modelKey === AvatarEditorFigureCategory.WARDROBE); const isPets = (modelKey === AvatarEditorFigureCategory.PETS); + const isNft = (modelKey === AvatarEditorFigureCategory.NFT); let tabClass = `tab ${ modelKey }`; if(isWardrobe) tabClass = 'tab-wardrobe'; else if(isPets) tabClass = 'tab-pets'; + else if(isNft) tabClass = 'tab-nft'; return ( setActiveModelKey(modelKey) }> @@ -101,12 +105,14 @@ export const AvatarEditorView: FC<{}> = props =>
{ /* left: model view or wardrobe */ }
- { (activeModelKey.length > 0 && !isWardrobeOpen && !isPetsOpen) && + { (activeModelKey.length > 0 && !isWardrobeOpen && !isPetsOpen && !isNftOpen) && } { isWardrobeOpen && } { isPetsOpen && } + { isNftOpen && + }
{ /* right: preview + actions */ }
diff --git a/src/components/avatar-editor/figure-set/AvatarEditorFigureSetItemView.tsx b/src/components/avatar-editor/figure-set/AvatarEditorFigureSetItemView.tsx index b021c5e..9201ce2 100644 --- a/src/components/avatar-editor/figure-set/AvatarEditorFigureSetItemView.tsx +++ b/src/components/avatar-editor/figure-set/AvatarEditorFigureSetItemView.tsx @@ -36,11 +36,17 @@ export const AvatarEditorFigureSetItemView: FC<{ if(setType === AvatarFigurePartType.HEAD) { - url = await AvatarEditorThumbnailsHelper.buildForFace(getFigureStringWithFace(partItem.id), partIsLocked); + url = await AvatarEditorThumbnailsHelper.buildForFace(getFigureStringWithFace(partItem.id), partIsLocked || isSellableNotOwned); } else { - url = await AvatarEditorThumbnailsHelper.build(setType, partItem, partItem.usesColor, selectedColorParts[setType] ?? null, partIsLocked || isSellableNotOwned); + url = await AvatarEditorThumbnailsHelper.build( + setType, + partItem, + partItem.usesColor, + selectedColorParts[setType] ?? null, + partIsLocked || isSellableNotOwned + ); } if(url && url.length) setAssetUrl(url); diff --git a/src/components/avatar-editor/index.ts b/src/components/avatar-editor/index.ts index 30769a7..5b21fd7 100644 --- a/src/components/avatar-editor/index.ts +++ b/src/components/avatar-editor/index.ts @@ -1,6 +1,7 @@ export * from './AvatarEditorFigurePreviewView'; export * from './AvatarEditorIcon'; export * from './AvatarEditorModelView'; +export * from './AvatarEditorNftView'; export * from './AvatarEditorPetView'; export * from './AvatarEditorView'; export * from './AvatarEditorWardrobeView'; diff --git a/src/components/wired/views/actions/WiredActionLayoutView.tsx b/src/components/wired/views/actions/WiredActionLayoutView.tsx index 051ae58..2ebefff 100644 --- a/src/components/wired/views/actions/WiredActionLayoutView.tsx +++ b/src/components/wired/views/actions/WiredActionLayoutView.tsx @@ -57,8 +57,10 @@ import { WiredExtraMoveCarryUsersView } from '../extras/WiredExtraMoveCarryUsers import { WiredExtraExecuteInOrderView } from '../extras/WiredExtraExecuteInOrderView'; import { WiredExtraExecutionLimitView } from '../extras/WiredExtraExecutionLimitView'; import { WiredExtraMoveNoAnimationView } from '../extras/WiredExtraMoveNoAnimationView'; +import { WiredExtraOrEvalView } from '../extras/WiredExtraOrEvalView'; import { WiredExtraMovePhysicsView } from '../extras/WiredExtraMovePhysicsView'; import { WiredExtraRandomView } from '../extras/WiredExtraRandomView'; +import { WiredExtraTextOutputUsernameView } from '../extras/WiredExtraTextOutputUsernameView'; import { WiredExtraUnseenView } from '../extras/WiredExtraUnseenView'; export const WiredActionLayoutView = (code: number) => @@ -189,6 +191,10 @@ export const WiredActionLayoutView = (code: number) => return ; case WiredActionLayoutCode.EXECUTION_LIMIT_EXTRA: return ; + case WiredActionLayoutCode.OR_EVAL_EXTRA: + return ; + case WiredActionLayoutCode.TEXT_OUTPUT_USERNAME_EXTRA: + return ; case WiredActionLayoutCode.SEND_SIGNAL: return ; } diff --git a/src/components/wired/views/extras/WiredExtraOrEvalView.tsx b/src/components/wired/views/extras/WiredExtraOrEvalView.tsx new file mode 100644 index 0000000..3e05bb8 --- /dev/null +++ b/src/components/wired/views/extras/WiredExtraOrEvalView.tsx @@ -0,0 +1,143 @@ +import { FC, useEffect, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredSourceOption, WiredSourcesSelector } from '../WiredSourcesSelector'; +import { WiredExtraBaseView } from './WiredExtraBaseView'; + +const MODE_ALL = 0; +const MODE_AT_LEAST_ONE = 1; +const MODE_NOT_ALL = 2; +const MODE_NONE = 3; +const MODE_LESS_THAN = 4; +const MODE_EXACTLY = 5; +const MODE_MORE_THAN = 6; +const MIN_COMPARE_VALUE = 0; +const MAX_COMPARE_VALUE = 100; +const DEFAULT_COMPARE_VALUE = 1; +const COMPARE_VALUE_PATTERN = /^\d*$/; +const CONDITION_EVALUATION_INTERACTION_TYPES = [ 'wf_cnd_*', 'wf_xtra_*' ]; +const CONDITION_EVALUATION_ERROR_KEY = 'wiredfurni.error.condition_evaluation_furni'; + +const FURNI_SOURCES: WiredSourceOption[] = [ + { value: 100, label: 'wiredfurni.params.sources.furni.100' }, + { value: 0, label: 'wiredfurni.params.sources.furni.0' }, + { value: 200, label: 'wiredfurni.params.sources.furni.200' }, + { value: 201, label: 'wiredfurni.params.sources.furni.201' } +]; + +const MODE_OPTIONS = [ MODE_ALL, MODE_AT_LEAST_ONE, MODE_NOT_ALL, MODE_NONE ]; +const COMPARISON_OPTIONS = [ MODE_LESS_THAN, MODE_EXACTLY, MODE_MORE_THAN ]; + +const normalizeEvaluationMode = (value: number) => ([ ...MODE_OPTIONS, ...COMPARISON_OPTIONS ].includes(value) ? value : MODE_ALL); +const normalizeFurniSource = (value: number) => (FURNI_SOURCES.some(option => option.value === value) ? value : 0); +const normalizeCompareValue = (value: number) => +{ + if(isNaN(value)) return DEFAULT_COMPARE_VALUE; + + return Math.max(MIN_COMPARE_VALUE, Math.min(MAX_COMPARE_VALUE, Math.floor(value))); +}; + +export const WiredExtraOrEvalView: FC<{}> = () => +{ + const { trigger = null, setIntParams = null, setStringParam = null, setAllowedInteractionTypes = null, setAllowedInteractionErrorKey = null } = useWired(); + const [ evaluationMode, setEvaluationMode ] = useState(MODE_ALL); + const [ furniSource, setFurniSource ] = useState(0); + const [ compareValue, setCompareValue ] = useState(DEFAULT_COMPARE_VALUE); + const [ compareValueInput, setCompareValueInput ] = useState(DEFAULT_COMPARE_VALUE.toString()); + + useEffect(() => + { + setAllowedInteractionTypes(CONDITION_EVALUATION_INTERACTION_TYPES); + setAllowedInteractionErrorKey(CONDITION_EVALUATION_ERROR_KEY); + + return () => + { + setAllowedInteractionTypes(null); + setAllowedInteractionErrorKey(null); + }; + }, [ setAllowedInteractionErrorKey, setAllowedInteractionTypes ]); + + useEffect(() => + { + if(!trigger) return; + + setEvaluationMode(normalizeEvaluationMode((trigger.intData.length > 0) ? trigger.intData[0] : MODE_ALL)); + setFurniSource(normalizeFurniSource((trigger.intData.length > 1) ? trigger.intData[1] : 0)); + const nextCompareValue = normalizeCompareValue((trigger.intData.length > 2) ? trigger.intData[2] : DEFAULT_COMPARE_VALUE); + setCompareValue(nextCompareValue); + setCompareValueInput(nextCompareValue.toString()); + }, [ trigger ]); + + const updateCompareValue = (value: number) => + { + const nextValue = normalizeCompareValue(value); + + setCompareValue(nextValue); + setCompareValueInput(nextValue.toString()); + }; + + const updateCompareValueInput = (value: string) => + { + if(!COMPARE_VALUE_PATTERN.test(value)) return; + + setCompareValueInput(value); + + if(!value.length) + { + setCompareValue(MIN_COMPARE_VALUE); + return; + } + + updateCompareValue(parseInt(value)); + }; + + const save = () => + { + setIntParams([ normalizeEvaluationMode(evaluationMode), normalizeFurniSource(furniSource), normalizeCompareValue(compareValue) ]); + setStringParam(''); + }; + + return ( + setFurniSource(normalizeFurniSource(value)) } /> }> +
+ { LocalizeText('wiredfurni.params.eval_mode') } + { MODE_OPTIONS.map(mode => + { + return ( + + ); + }) } + { COMPARISON_OPTIONS.map(mode => + { + const isSelected = (evaluationMode === mode); + + return ( + + ); + }) } +
+
+ ); +}; diff --git a/src/components/wired/views/extras/WiredExtraTextOutputUsernameView.tsx b/src/components/wired/views/extras/WiredExtraTextOutputUsernameView.tsx new file mode 100644 index 0000000..1aa3856 --- /dev/null +++ b/src/components/wired/views/extras/WiredExtraTextOutputUsernameView.tsx @@ -0,0 +1,123 @@ +import { FC, useEffect, useMemo, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { NitroInput } from '../../../../layout'; +import { WiredSourcesSelector, CLICKED_USER_SOURCE_VALUE } from '../WiredSourcesSelector'; +import { WiredExtraBaseView } from './WiredExtraBaseView'; + +const TYPE_SINGLE = 1; +const TYPE_MULTIPLE = 2; +const DEFAULT_PLACEHOLDER_NAME = ''; +const DEFAULT_DELIMITER = ', '; +const MAX_PLACEHOLDER_NAME_LENGTH = 32; +const MAX_DELIMITER_LENGTH = 16; +const PLACEHOLDER_WRAPPER_PATTERN = /^\$\((.*)\)$/; + +const normalizePlaceholderType = (value: number) => ((value === TYPE_MULTIPLE) ? TYPE_MULTIPLE : TYPE_SINGLE); +const normalizeUserSource = (value: number) => ((value === 0) || (value === 200) || (value === 201) || (value === CLICKED_USER_SOURCE_VALUE) ? value : 0); +const normalizePlaceholderName = (value: string) => +{ + let normalizedValue = (value ?? '').trim().replace(/[\t\r\n]/g, ''); + + if(PLACEHOLDER_WRAPPER_PATTERN.test(normalizedValue)) + { + normalizedValue = normalizedValue.substring(2, normalizedValue.length - 1).trim(); + } + + return normalizedValue.slice(0, MAX_PLACEHOLDER_NAME_LENGTH); +}; + +const normalizeDelimiter = (value: string) => +{ + if(value === undefined || value === null) return DEFAULT_DELIMITER; + + return value.replace(/[\t\r\n]/g, '').slice(0, MAX_DELIMITER_LENGTH); +}; + +const splitStringData = (value: string) => +{ + if(!value?.length) return [ DEFAULT_PLACEHOLDER_NAME, DEFAULT_DELIMITER ]; + + const parts = value.split('\t'); + + if(parts.length <= 1) return [ value, DEFAULT_DELIMITER ]; + + return [ parts[0], parts[1] ]; +}; + +const escapeHtml = (value: string) => value + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + +export const WiredExtraTextOutputUsernameView: FC<{}> = () => +{ + const { trigger = null, setIntParams = null, setStringParam = null } = useWired(); + const [ placeholderName, setPlaceholderName ] = useState(DEFAULT_PLACEHOLDER_NAME); + const [ placeholderType, setPlaceholderType ] = useState(TYPE_SINGLE); + const [ delimiter, setDelimiter ] = useState(DEFAULT_DELIMITER); + const [ userSource, setUserSource ] = useState(0); + + useEffect(() => + { + if(!trigger) return; + + const [ nextPlaceholderName, nextDelimiter ] = splitStringData(trigger.stringData); + + setPlaceholderName(normalizePlaceholderName(nextPlaceholderName)); + setDelimiter(normalizeDelimiter(nextDelimiter)); + setPlaceholderType(normalizePlaceholderType((trigger.intData.length > 0) ? trigger.intData[0] : TYPE_SINGLE)); + setUserSource(normalizeUserSource((trigger.intData.length > 1) ? trigger.intData[1] : 0)); + }, [ trigger ]); + + const previewToken = useMemo(() => + { + const effectiveName = normalizePlaceholderName(placeholderName) || 'placeholder'; + + return `$(${ effectiveName })`; + }, [ placeholderName ]); + + const previewHtml = useMemo(() => LocalizeText('wiredfurni.params.texts.placeholder_preview', [ 'placeholder' ], [ escapeHtml(previewToken) ]), [ previewToken ]); + + const save = () => + { + setIntParams([ normalizePlaceholderType(placeholderType), normalizeUserSource(userSource) ]); + setStringParam(`${ normalizePlaceholderName(placeholderName) }\t${ normalizeDelimiter(delimiter) }`); + }; + + return ( + setUserSource(normalizeUserSource(value)) } /> }> +
+
+ { LocalizeText('wiredfurni.params.texts.placeholder_name') } + setPlaceholderName(normalizePlaceholderName(event.target.value)) } /> +
+ +
+ { LocalizeText('wiredfurni.params.texts.placeholder_type') } + + +
+ { placeholderType === TYPE_MULTIPLE && +
+ { LocalizeText('wiredfurni.params.texts.select_delimiter') } + setDelimiter(normalizeDelimiter(event.target.value)) } /> +
} +
+
+ ); +}; diff --git a/src/css/index.css b/src/css/index.css index 4365664..6c62fc7 100644 --- a/src/css/index.css +++ b/src/css/index.css @@ -1180,6 +1180,15 @@ body { background-position: center; background-size: 22px 22px; } + + .tab-nft { + width: 34px; + height: 22px; + background-image: url('@/assets/images/wardrobe/nft.png'); + background-repeat: no-repeat; + background-position: center; + background-size: 22px 22px; + } } /* ── Avatar Editor misc ─────────────────────────────────────────────────── */ diff --git a/src/hooks/avatar-editor/useAvatarEditor.ts b/src/hooks/avatar-editor/useAvatarEditor.ts index 420b9f9..3046605 100644 --- a/src/hooks/avatar-editor/useAvatarEditor.ts +++ b/src/hooks/avatar-editor/useAvatarEditor.ts @@ -15,6 +15,7 @@ const useAvatarEditorState = () => const [ maxPaletteCount, setMaxPaletteCount ] = useState(1); const [ figureSetIds, setFigureSetIds ] = useState([]); const [ boundFurnitureNames, setBoundFurnitureNames ] = useState([]); + const [ figureSetNames, setFigureSetNames ] = useState>({}); const [ savedFigures, setSavedFigures ] = useState<[ IAvatarFigureContainer, string ][]>(null); const { selectedColors, gender, setGender, loadAvatarData, selectPart, selectColor, getFigureString, getFigureStringWithFace, selectedParts } = useFigureData(); @@ -196,12 +197,27 @@ const useAvatarEditorState = () => loadAvatarData(figureContainer.getFigureString(), gender); }, [ figureSetIds, gender, loadAvatarData, selectedColors, selectedParts ]); + const nftFigureSetIds = useMemo(() => + { + const nftSetIds = new Set(); + + for(const [ setId, furnitureName ] of Object.entries(figureSetNames)) + { + if(!furnitureName?.toLowerCase().includes('nft')) continue; + + nftSetIds.add(Number(setId)); + } + + return nftSetIds; + }, [ figureSetNames ]); + useMessageEvent(FigureSetIdsMessageEvent, event => { const parser = event.getParser(); setFigureSetIds(parser.figureSetIds); setBoundFurnitureNames(parser.boundsFurnitureNames); + setFigureSetNames(parser.figureSetNameMap); }); useMessageEvent(UserWardrobePageEvent, event => @@ -238,8 +254,10 @@ const useAvatarEditorState = () => if(!isVisible) return; const newAvatarModels: { [index: string]: IAvatarEditorCategory[] } = {}; + const buildModeDefault = 'default'; + const buildModeNft = 'nft'; - const buildCategory = (setType: string) => + const buildCategory = (setType: string, buildMode: string = buildModeDefault) => { const partItems: IAvatarEditorCategoryPartItem[] = []; const colorItems: IPartColor[][] = []; @@ -273,9 +291,14 @@ const useAvatarEditorState = () => if(!partSet || !partSet.isSelectable || ((partSet.gender !== gender) && (partSet.gender !== AvatarFigurePartType.UNISEX))) continue; + const isNftPartSet = nftFigureSetIds.has(partSet.id); + + if((buildMode === buildModeDefault) && isNftPartSet) continue; + if((buildMode === buildModeNft) && !isNftPartSet) continue; + const isSellableNotOwned = partSet.isSellable && figureSetIds.indexOf(partSet.id) === -1; - if(isSellableNotOwned && setType !== AvatarFigurePartType.PET) continue; + if(isSellableNotOwned && (buildMode !== buildModeNft) && setType !== AvatarFigurePartType.PET) continue; let maxPaletteCount = 0; @@ -291,16 +314,31 @@ const useAvatarEditorState = () => return { setType, partItems, colorItems }; }; - newAvatarModels[AvatarEditorFigureCategory.GENERIC] = [ AvatarFigurePartType.HEAD ].map(setType => buildCategory(setType)); - newAvatarModels[AvatarEditorFigureCategory.HEAD] = [ AvatarFigurePartType.HAIR, AvatarFigurePartType.HEAD_ACCESSORY, AvatarFigurePartType.HEAD_ACCESSORY_EXTRA, AvatarFigurePartType.EYE_ACCESSORY, AvatarFigurePartType.FACE_ACCESSORY ].map(setType => buildCategory(setType)); - newAvatarModels[AvatarEditorFigureCategory.TORSO] = [ AvatarFigurePartType.CHEST, AvatarFigurePartType.CHEST_PRINT, AvatarFigurePartType.COAT_CHEST, AvatarFigurePartType.CHEST_ACCESSORY ].map(setType => buildCategory(setType)); - newAvatarModels[AvatarEditorFigureCategory.LEGS] = [ AvatarFigurePartType.LEGS, AvatarFigurePartType.SHOES, AvatarFigurePartType.WAIST_ACCESSORY ].map(setType => buildCategory(setType)); + newAvatarModels[AvatarEditorFigureCategory.GENERIC] = [ AvatarFigurePartType.HEAD ].map(setType => buildCategory(setType, buildModeDefault)); + newAvatarModels[AvatarEditorFigureCategory.HEAD] = [ AvatarFigurePartType.HAIR, AvatarFigurePartType.HEAD_ACCESSORY, AvatarFigurePartType.HEAD_ACCESSORY_EXTRA, AvatarFigurePartType.EYE_ACCESSORY, AvatarFigurePartType.FACE_ACCESSORY ].map(setType => buildCategory(setType, buildModeDefault)); + newAvatarModels[AvatarEditorFigureCategory.TORSO] = [ AvatarFigurePartType.CHEST, AvatarFigurePartType.CHEST_PRINT, AvatarFigurePartType.COAT_CHEST, AvatarFigurePartType.CHEST_ACCESSORY ].map(setType => buildCategory(setType, buildModeDefault)); + newAvatarModels[AvatarEditorFigureCategory.LEGS] = [ AvatarFigurePartType.LEGS, AvatarFigurePartType.SHOES, AvatarFigurePartType.WAIST_ACCESSORY ].map(setType => buildCategory(setType, buildModeDefault)); newAvatarModels[AvatarEditorFigureCategory.PETS] = [ AvatarFigurePartType.PET ].map(setType => buildCategory(setType)).filter(Boolean); + newAvatarModels[AvatarEditorFigureCategory.NFT] = [ + AvatarFigurePartType.HEAD, + AvatarFigurePartType.HAIR, + AvatarFigurePartType.HEAD_ACCESSORY, + AvatarFigurePartType.HEAD_ACCESSORY_EXTRA, + AvatarFigurePartType.EYE_ACCESSORY, + AvatarFigurePartType.FACE_ACCESSORY, + AvatarFigurePartType.CHEST, + AvatarFigurePartType.CHEST_PRINT, + AvatarFigurePartType.COAT_CHEST, + AvatarFigurePartType.CHEST_ACCESSORY, + AvatarFigurePartType.LEGS, + AvatarFigurePartType.SHOES, + AvatarFigurePartType.WAIST_ACCESSORY + ].map(setType => buildCategory(setType, buildModeNft)).filter(Boolean); newAvatarModels[AvatarEditorFigureCategory.WARDROBE] = []; setAvatarModels(newAvatarModels); setActiveModelKey(AvatarEditorFigureCategory.GENERIC); - }, [ isVisible, gender, figureSetIds ]); + }, [ isVisible, gender, figureSetIds, nftFigureSetIds ]); useEffect(() => { diff --git a/src/hooks/wired/useWired.ts b/src/hooks/wired/useWired.ts index e6e3b53..e35e211 100644 --- a/src/hooks/wired/useWired.ts +++ b/src/hooks/wired/useWired.ts @@ -75,14 +75,37 @@ const useWiredState = () => return rawValue.toLowerCase(); }; + const getComparableInteractionNames = (furniData: any): string[] => + { + if(!furniData) return []; + + const values = [ + getInteractionTypeName(furniData), + (typeof (furniData as any).className === 'string') ? (furniData as any).className.toLowerCase() : null, + (typeof (furniData as any).fullName === 'string') ? (furniData as any).fullName.toLowerCase() : null, + (typeof (furniData as any).name === 'string') ? (furniData as any).name.toLowerCase() : null + ]; + + return values.filter((value, index, array): value is string => !!value && (array.indexOf(value) === index)); + }; + + const matchesAllowedPattern = (value: string, pattern: string) => + { + const normalizedPattern = pattern.toLowerCase(); + + if(normalizedPattern.endsWith('*')) return value.startsWith(normalizedPattern.slice(0, -1)); + + return (normalizedPattern === value); + }; + const isAllowedInteraction = (furniData: any): boolean => { if(!allowedInteractionTypes || !allowedInteractionTypes.length) return true; - const interactionType = getInteractionTypeName(furniData); - if(!interactionType) return true; + const comparableNames = getComparableInteractionNames(furniData); + if(!comparableNames.length) return true; - return allowedInteractionTypes.some(type => (type && type.toLowerCase() === interactionType)); + return comparableNames.some(value => allowedInteractionTypes.some(type => !!type && matchesAllowedPattern(value, type))); }; const handleDisallowedInteraction = () =>