{ /* left: model view or wardrobe */ }
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 = () =>