diff --git a/public/UITexts.example b/public/UITexts.example index 378e8e8..75ec9d5 100644 --- a/public/UITexts.example +++ b/public/UITexts.example @@ -1,9 +1,7 @@ -{ +{ "friendlist.search": "Zoek vrienden", "widget.chooser.checkall": "Selecteer meubels", "widget.chooser.btn.pickall": "pak de geselecteerde items op!", - "wiredfurni.params.requireall.2": "Als een van de geselecteerde furni een avatar heeft", - "wiredfurni.params.requireall.3": "Als alle geselecteerde furni avatars op hen hebben", "widget.settings.general": "Standaard", "widget.settings.general.title": "Pas de standaard nitro settings aan", "widget.settings.volume": "Volume", @@ -20,25 +18,6 @@ "widget.room.chat.clear_history": "leeg geschiedenis", "widget.room.youtube.shared": "YouTube word gedeeld", "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.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:", "widget.memenu.dance1": "Ballo 1", "widget.memenu.dance2": "Ballo 2", "widget.memenu.dance3": "Ballo 3", diff --git a/public/renderer-config.json b/public/renderer-config.json index b0beddf..b6657aa 100644 --- a/public/renderer-config.json +++ b/public/renderer-config.json @@ -1,22 +1,22 @@ { "socket.url": "ws://192.168.1.52:2096", - "asset.url": "https://client.paxxo.online/nitro/bundled", - "image.library.url": "https://client.paxxo.online/c_images/", - "hof.furni.url": "https://client.paxxo.online/c_images/dcr/hof_furni", - "images.url": "https://client.paxxo.online/nitro/images", - "gamedata.url": "https://client.paxxo.online/nitro/gamedata", + "asset.url": "https://client.slogga.it/nitro/bundled", + "image.library.url": "https://client.slogga.it/c_images/", + "hof.furni.url": "https://client.slogga.it/c_images/dcr/hof_furni", + "images.url": "https://client.slogga.it/nitro/images", + "gamedata.url": "https://client.slogga.it/nitro/gamedata", "sounds.url": "${asset.url}/sounds/%sample%.mp3", "external.texts.url": [ "${gamedata.url}/ExternalTexts.json", "${gamedata.url}/UITexts.json" ], "external.samples.url": "${hof.furni.url}/mp3/sound_machine_sample_%sample%.mp3", - "furnidata.url": "${gamedata.url}/FurnitureData.json?v=1", - "productdata.url": "${gamedata.url}/ProductData.json?v=1", - "avatar.actions.url": "${gamedata.url}/HabboAvatarActions.json?v=1", - "avatar.figuredata.url": "${gamedata.url}/FigureData.json?v=1", - "avatar.figuremap.url": "${gamedata.url}/FigureMap.json?v=1", - "avatar.effectmap.url": "${gamedata.url}/EffectMap.json?v=1", + "furnidata.url": "${gamedata.url}/FurnitureData.json?v=2", + "productdata.url": "${gamedata.url}/ProductData.json?v=2", + "avatar.actions.url": "${gamedata.url}/HabboAvatarActions.json?v=2", + "avatar.figuredata.url": "${gamedata.url}/FigureData.json?v=2", + "avatar.figuremap.url": "${gamedata.url}/FigureMap.json?v=2", + "avatar.effectmap.url": "${gamedata.url}/EffectMap.json?v=2", "avatar.asset.url": "${asset.url}/figure/%libname%.nitro", "avatar.asset.effect.url": "${asset.url}/effect/%libname%.nitro", "furni.asset.url": "${asset.url}/furniture/%libname%.nitro", diff --git a/src/api/wired/WiredActionLayoutCode.ts b/src/api/wired/WiredActionLayoutCode.ts index 790f829..088818b 100644 --- a/src/api/wired/WiredActionLayoutCode.ts +++ b/src/api/wired/WiredActionLayoutCode.ts @@ -67,4 +67,21 @@ export class WiredActionLayoutCode public static OR_EVAL_EXTRA: number = 66; public static TEXT_OUTPUT_USERNAME_EXTRA: number = 67; public static TEXT_OUTPUT_FURNI_NAME_EXTRA: number = 68; + public static GIVE_VARIABLE: number = 69; + public static USER_VARIABLE_EXTRA: number = 70; + public static FURNI_VARIABLE_EXTRA: number = 71; + public static ROOM_VARIABLE_EXTRA: number = 72; + public static REMOVE_VARIABLE: number = 73; + public static CHANGE_VARIABLE_VALUE: number = 74; + public static FURNI_WITH_VARIABLE_SELECTOR: number = 75; + public static USERS_WITH_VARIABLE_SELECTOR: number = 76; + public static FILTER_USERS_BY_VARIABLE_EXTRA: number = 77; + public static FILTER_FURNI_BY_VARIABLE_EXTRA: number = 78; + public static VARIABLE_TEXT_CONNECTOR_EXTRA: number = 79; + public static TEXT_OUTPUT_VARIABLE_EXTRA: number = 80; + public static VARIABLE_REFERENCE_EXTRA: number = 81; + public static VARIABLE_LEVELUP_SYSTEM_EXTRA: number = 82; + public static VARIABLE_ECHO_EXTRA: number = 83; + public static CONTEXT_VARIABLE_EXTRA: number = 84; + public static TEXT_INPUT_VARIABLE_EXTRA: number = 85; } diff --git a/src/api/wired/WiredConditionLayoutCode.ts b/src/api/wired/WiredConditionLayoutCode.ts index f6fd56a..31a8a1c 100644 --- a/src/api/wired/WiredConditionLayoutCode.ts +++ b/src/api/wired/WiredConditionLayoutCode.ts @@ -40,4 +40,8 @@ export class WiredConditionlayout public static MATCH_DATE: number = 37; public static ACTOR_DIR: number = 38; public static SLC_QUANTITY: number = 39; + public static HAS_VAR: number = 40; + public static NEG_HAS_VAR: number = 41; + public static VAR_VAL_MATCH: number = 42; + public static VAR_AGE_MATCH: number = 43; } diff --git a/src/api/wired/WiredTriggerLayoutCode.ts b/src/api/wired/WiredTriggerLayoutCode.ts index 6f6fbe8..1742e5c 100644 --- a/src/api/wired/WiredTriggerLayoutCode.ts +++ b/src/api/wired/WiredTriggerLayoutCode.ts @@ -22,4 +22,5 @@ export class WiredTriggerLayout public static CLICK_USER: number = 20; public static USER_PERFORMS_ACTION: number = 21; public static CLOCK_COUNTER: number = 22; + public static VARIABLE_CHANGED: number = 23; } diff --git a/src/assets/images/wired/var/ar_picker_cancel_search.png b/src/assets/images/wired/var/ar_picker_cancel_search.png new file mode 100644 index 0000000..8c60f40 Binary files /dev/null and b/src/assets/images/wired/var/ar_picker_cancel_search.png differ diff --git a/src/assets/images/wired/var/highlight_var_tailwind.html b/src/assets/images/wired/var/highlight_var_tailwind.html new file mode 100644 index 0000000..26c6465 --- /dev/null +++ b/src/assets/images/wired/var/highlight_var_tailwind.html @@ -0,0 +1,37 @@ + + + + + + Tailwind Bubble Component + + + + +
+ +
+ 0 +
+ + + + + +
+ + + \ No newline at end of file diff --git a/src/assets/images/wired/var/icon_source_context_clean.png b/src/assets/images/wired/var/icon_source_context_clean.png new file mode 100644 index 0000000..939178d Binary files /dev/null and b/src/assets/images/wired/var/icon_source_context_clean.png differ diff --git a/src/assets/images/wired/var/icon_source_furni.png b/src/assets/images/wired/var/icon_source_furni.png new file mode 100644 index 0000000..ed7fe4d Binary files /dev/null and b/src/assets/images/wired/var/icon_source_furni.png differ diff --git a/src/assets/images/wired/var/icon_source_global.png b/src/assets/images/wired/var/icon_source_global.png new file mode 100644 index 0000000..164351e Binary files /dev/null and b/src/assets/images/wired/var/icon_source_global.png differ diff --git a/src/assets/images/wired/var/icon_source_global2.png b/src/assets/images/wired/var/icon_source_global2.png new file mode 100644 index 0000000..c72b340 Binary files /dev/null and b/src/assets/images/wired/var/icon_source_global2.png differ diff --git a/src/assets/images/wired/var/icon_source_user.png b/src/assets/images/wired/var/icon_source_user.png new file mode 100644 index 0000000..8bec80d Binary files /dev/null and b/src/assets/images/wired/var/icon_source_user.png differ diff --git a/src/assets/images/wired/var/var_picker_all.png b/src/assets/images/wired/var/var_picker_all.png new file mode 100644 index 0000000..0960399 Binary files /dev/null and b/src/assets/images/wired/var/var_picker_all.png differ diff --git a/src/assets/images/wired/var/var_picker_internal.png b/src/assets/images/wired/var/var_picker_internal.png new file mode 100644 index 0000000..5c147a4 Binary files /dev/null and b/src/assets/images/wired/var/var_picker_internal.png differ diff --git a/src/assets/images/wired/var/var_picker_recent.png b/src/assets/images/wired/var/var_picker_recent.png new file mode 100644 index 0000000..449335c Binary files /dev/null and b/src/assets/images/wired/var/var_picker_recent.png differ diff --git a/src/assets/images/wired/var/var_picker_search.png b/src/assets/images/wired/var/var_picker_search.png new file mode 100644 index 0000000..0f637ae Binary files /dev/null and b/src/assets/images/wired/var/var_picker_search.png differ diff --git a/src/assets/images/wired/var/var_picker_smart.png b/src/assets/images/wired/var/var_picker_smart.png new file mode 100644 index 0000000..e0fbd4a Binary files /dev/null and b/src/assets/images/wired/var/var_picker_smart.png differ diff --git a/src/assets/images/wired/var/var_picker_usermade.png b/src/assets/images/wired/var/var_picker_usermade.png new file mode 100644 index 0000000..36da8ca Binary files /dev/null and b/src/assets/images/wired/var/var_picker_usermade.png differ diff --git a/src/assets/images/wired/var/icon_source_context.png b/src/assets/images/wired/var/icon_source_context.png new file mode 100644 index 0000000..939178d Binary files /dev/null and b/src/assets/images/wired/var/icon_source_context.png differ diff --git a/src/assets/images/wiredtools/context.png b/src/assets/images/wiredtools/context.png new file mode 100644 index 0000000..dafcddd Binary files /dev/null and b/src/assets/images/wiredtools/context.png differ diff --git a/src/assets/images/wiredtools/wired_menu.png b/src/assets/images/wiredtools/wired_menu.png new file mode 100644 index 0000000..2f45ce0 Binary files /dev/null and b/src/assets/images/wiredtools/wired_menu.png differ diff --git a/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetFurniView.tsx b/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetFurniView.tsx index 48e9b84..1248d66 100644 --- a/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetFurniView.tsx +++ b/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetFurniView.tsx @@ -4,7 +4,7 @@ import { FaCrosshairs, FaRulerVertical, FaTimes } from 'react-icons/fa'; import { GrFormNextLink, GrRotateLeft, GrRotateRight } from 'react-icons/gr'; import { AvatarInfoFurni, GetGroupInformation, LocalizeText, SendMessageComposer } from '../../../../../api'; import { Button, Column, Flex, LayoutBadgeImageView, LayoutLimitedEditionCompactPlateView, LayoutRarityLevelView, LayoutRoomObjectImageView, Text, UserProfileIconView } from '../../../../../common'; -import { useMessageEvent, useNitroEvent, useRoom } from '../../../../../hooks'; +import { useMessageEvent, useNitroEvent, useRoom, useWiredTools } from '../../../../../hooks'; import { NitroInput } from '../../../../../layout'; interface InfoStandWidgetFurniViewProps @@ -21,6 +21,7 @@ export const InfoStandWidgetFurniView: FC = props { const { avatarInfo = null, onClose = null } = props; const { roomSession = null } = useRoom(); + const { openInspectionForFurni, showInspectButton } = useWiredTools(); const [ pickupMode, setPickupMode ] = useState(0); const [ canMove, setCanMove ] = useState(false); @@ -698,6 +699,10 @@ export const InfoStandWidgetFurniView: FC = props + { showInspectButton && + } { canMove && + { monitorStats.map(stat => (
{ stat.label }: @@ -1547,22 +3492,32 @@ export const WiredCreatorToolsView: FC<{}> = () => Monitor preview
-
+
Logs:
- + - { MONITOR_LOGS.map((log, index) => ( - - + { monitorLogs.map((log, index) => ( + openMonitorLogDetails(log.type, { + severity: log.category, + amount: log.amount, + latest: log.latest, + reason: log.latestReason, + sourceLabel: log.latestSourceLabel, + sourceId: log.latestSourceId + }) }> + @@ -1572,10 +3527,128 @@ export const WiredCreatorToolsView: FC<{}> = () =>
TypeCategorySeverity Amount Latest occurrence
{ log.type }
{ log.type } { log.category } { log.amount } { log.latest }
- - + +
+ { false && isMonitorHistoryOpen && +
+
+
+ Wired Monitor Logs + +
+
+
+ Severity: + { [ 'ALL', 'WARNING', 'ERROR' ].map(severity => ( + + )) } + Type: + +
+
+ + + + + + + + + + + + { !filteredMonitorHistoryRows.length && + + + } + { filteredMonitorHistoryRows.map((row, index) => ( + openMonitorLogDetails(row.type, { + severity: row.category, + occurredAt: row.occurredAt, + reason: row.reason, + sourceLabel: row.sourceLabel, + sourceId: row.sourceId + }) }> + + + + + + + )) } + +
TypeSeverityTriggerMotivationOccurred at
No log history for the current filters
{ row.type }{ row.category }{ formatMonitorSource(row.sourceLabel, row.sourceId) }{ row.reason }{ row.occurredAt }
+
+
+
+
} + { false && isMonitorInfoOpen && +
+
+
+ Wired Monitor Information + +
+
+ { monitorInfoSections.map(section => ( +
+ { section.title } + { section.lines.map((line, index) => ( + { line } + )) } +
+ )) } +
+
+
} + { false && !!selectedMonitorErrorInfo && +
+
+
+ Wired Error Information + +
+
+
+ { selectedMonitorErrorInfo.title } + + { selectedMonitorLogDetails?.severity ?? selectedMonitorErrorInfo.severity } + +
+ { !!selectedMonitorLogDetails && +
+ Trigger: { selectedMonitorDetailSource } + Motivation: { selectedMonitorLogDetails.reason } + { !!selectedMonitorLogDetails.amount && Amount: { selectedMonitorLogDetails.amount } } + { !!selectedMonitorLogDetails.latest && Latest occurrence: { selectedMonitorLogDetails.latest } } + { !!selectedMonitorLogDetails.occurredAt && Occurred at: { selectedMonitorLogDetails.occurredAt } } +
} + { selectedMonitorErrorInfo.description.map((paragraph, index) => ( + { paragraph } + )) } +
+
+
}
} { (activeTab === 'inspection') &&
@@ -1590,7 +3663,7 @@ export const WiredCreatorToolsView: FC<{}> = () => className={ `w-[42px] h-[38px] rounded border flex items-center justify-center shadow-[inset_0_1px_0_rgba(255,255,255,.7)] ${ (inspectionType === element.key) ? 'border-[#222] bg-[#d9d6cf]' : 'border-[#7f7f7f] bg-[#ece9e1]' }` } onClick={ () => setInspectionType(element.key) } title={ element.label }> - { + { )) }
@@ -1642,8 +3715,12 @@ export const WiredCreatorToolsView: FC<{}> = () => { displayedVariables.map((variable, index) => ( beginVariableEdit(variable) }> + className={ `${ (selectedInspectionVariableKey === variable.key) ? 'bg-[#d7dfea]' : ((index % 2 === 0) ? 'bg-white' : 'bg-[#f3f3f3]') } ${ variable.editable ? 'cursor-pointer hover:bg-[#e8eefc]' : 'cursor-pointer' }` } + onClick={ () => + { + setSelectedInspectionVariableKeys(prev => ({ ...prev, [inspectionType]: variable.key })); + beginVariableEdit(variable); + } }> { variable.key } { (editingVariable === variable.key) && @@ -1664,13 +3741,14 @@ export const WiredCreatorToolsView: FC<{}> = () => } } onKeyDown={ onVariableInputKeyDown } /> } { (editingVariable !== variable.key) && !variable.editable && { variable.value } } - { (editingVariable !== variable.key) && variable.editable && + { (editingVariable !== variable.key) && variable.editable && - +
+ { isInspectionGiveOpen && +
+ Variable: + + Value: + setInspectionGiveValue(event.target.value) } /> + +
} + +
} + { (activeTab === 'variables') && +
+
+
+ Variable type: +
+ { VARIABLES_ELEMENTS.map(element => ( + + )) } +
+
+
+ Variable picker: +
+
+ + + { variablePickerDefinitions.map((variable, index) => ( + setSelectedVariableKeys(prev => ({ ...prev, [variablesType]: variable.key })) }> + + + )) } + +
{ variable.key }
+
+
+
+
+ + +
+
+
+ { (variablesType === 'context') && +
+ Context variables live only during the current wired execution. This tab shows their definitions, text mappings and execution-scoped capabilities, but not live values from a running stack. +
} +
+ Properties: +
+
+ Property + Value +
+
+ + + { selectedVariableProperties.map((property, index) => ( + + + + + )) } + +
{ property.key }{ property.value }
+
+
+
+
+ Text values: +
+
+ Value + Text +
+ { !selectedVariableTextValues.length && +
+ Nothing to display +
} + { !!selectedVariableTextValues.length && +
+ + + { selectedVariableTextValues.map((entry, index) => ( + + + + + )) } + +
{ entry.value }{ entry.text }
+
} +
+
+
+
} + { (activeTab === 'settings') && } { (activeTab !== 'monitor') && (activeTab !== 'inspection') && + (activeTab !== 'variables') && + (activeTab !== 'settings') &&
{ currentTabLabel } @@ -1701,5 +3918,396 @@ export const WiredCreatorToolsView: FC<{}> = () =>
} + { isMonitorHistoryOpen && + + setIsMonitorHistoryOpen(false) } /> + +
+ Severity: + { [ 'ALL', 'WARNING', 'ERROR' ].map(severity => ( + + )) } + Type: + +
+
+ + + + + + + + + + + + { !filteredMonitorHistoryRows.length && + + + } + { filteredMonitorHistoryRows.map((row, index) => ( + openMonitorLogDetails(row.type, { + severity: row.category, + occurredAt: row.occurredAt, + reason: row.reason, + sourceLabel: row.sourceLabel, + sourceId: row.sourceId + }) }> + + + + + + + )) } + +
TypeSeverityTriggerMotivationOccurred at
No log history for the current filters
{ row.type }{ row.category }{ formatMonitorSource(row.sourceLabel, row.sourceId) }{ row.reason }{ row.occurredAt }
+
+
+
} + { isMonitorInfoOpen && + + setIsMonitorInfoOpen(false) } /> + + { monitorInfoSections.map(section => ( +
+ { section.title } + { section.lines.map((line, index) => ( + { line } + )) } +
+ )) } +
+
} + { isVariableManageOpen && !!selectedVariableDefinition && + + setIsVariableManageOpen(false) } /> + +
+
+ { variableManageDescription } + Variable name: { selectedVariableDefinition.key } +
+ +
+
+
+ { variableManageTypeLabel }: + +
+
+ Sort by: + +
+
+
+ + + + + + { !!selectedVariableDefinition.hasCreationTime && } + { !!selectedVariableDefinition.hasUpdateTime && } + + + + + + { !pagedVariableManageEntries.length && + + + } + { pagedVariableManageEntries.map((entry, index) => ( + + + + { !!selectedVariableDefinition.hasCreationTime && } + { !!selectedVariableDefinition.hasUpdateTime && } + + + + )) } + +
{ variableManageCategoryHeader }NameCreation timeLast update timeValueManage
No entries found for the current filters
{ entry.categoryLabel }{ entry.entityName }{ formatVariableTimestamp(entry.createdAt) }{ formatVariableTimestamp(entry.updatedAt) }{ (entry.value ?? variableManageNoValueLabel) } + +
+
+
+
+ + +
+
+ { `${ filteredVariableManageEntries.length } entr${ (filteredVariableManageEntries.length === 1) ? 'y' : 'ies' } found. Showing page` } + + { + const nextPage = Number(event.target.value || 1); + + setVariableManagePage(Math.min(variableManagePageCount, Math.max(1, nextPage))); + } } /> + { `of ${ variableManagePageCount }` } +
+
+ + +
+
+
+
} + { !!selectedManagedVariableEntry && !!selectedVariableDefinition && + + setSelectedManagedVariableEntry(null) } /> + +
+
+ { managedHolderWarningText } +
+ +
+
+ Holder info: +
+
+ { (variablesType === 'furni') && roomSession && + } + { (variablesType === 'user') && managedHolderUserData && + <> + { (managedHolderUserData.type === RoomObjectType.PET) + ? + : } + } + { (variablesType === 'global') && + Global placeholder } +
+
+ { managedHolderInfoLines.map((line, index) => ( + { line } + )) } +
+
+
+
+ { (variablesType === 'global') ? 'Room variables:' : 'Assigned variables:' } +
+ + + + + + + + + { !managedHolderVariableEntries.length && + + + } + { managedHolderVariableEntries.map((entry, index) => + { + const isSelected = (selectedManagedHolderVariableEntry?.variableItemId === entry.variableItemId); + const isEditing = (editingManagedHolderVariableId === entry.variableItemId); + + return ( + setSelectedManagedHolderVariableId(entry.variableItemId) }> + + + + ); + }) } + +
VariableValue
No variables currently assigned
+
+ { entry.name } + { entry.availability } +
+
+ { !entry.hasValue && / } + { !!entry.hasValue && !isEditing && + } + { !!entry.hasValue && isEditing && + commitManagedHolderValueEdit() } + onChange={ event => setEditingManagedHolderValue(event.target.value) } + onClick={ event => event.stopPropagation() } + onKeyDown={ (event: KeyboardEvent) => + { + if(event.key === 'Enter') commitManagedHolderValueEdit(); + + if(event.key === 'Escape') + { + setEditingManagedHolderVariableId(0); + setEditingManagedHolderValue(''); + } + } } /> } +
+
+
+
+ { isManagedGiveOpen && +
+ Variable: + + Value: + setManagedGiveValue(event.target.value) } /> + +
} + + +
+
+
} + { !!selectedMonitorErrorInfo && + + + { + setSelectedMonitorErrorType(null); + setSelectedMonitorLogDetails(null); + } } /> + +
+ { selectedMonitorErrorInfo.title } + + { selectedMonitorLogDetails?.severity ?? selectedMonitorErrorInfo.severity } + +
+ { !!selectedMonitorLogDetails && +
+ Trigger: { selectedMonitorDetailSource } + Motivation: { selectedMonitorLogDetails.reason } + { !!selectedMonitorLogDetails.amount && Amount: { selectedMonitorLogDetails.amount } } + { !!selectedMonitorLogDetails.latest && Latest occurrence: { selectedMonitorLogDetails.latest } } + { !!selectedMonitorLogDetails.occurredAt && Occurred at: { selectedMonitorLogDetails.occurredAt } } +
} + { selectedMonitorErrorInfo.description.map((paragraph, index) => ( + { paragraph } + )) } +
+
} + ); }; diff --git a/src/components/wired-tools/WiredToolsSettingsTabView.tsx b/src/components/wired-tools/WiredToolsSettingsTabView.tsx new file mode 100644 index 0000000..7858112 --- /dev/null +++ b/src/components/wired-tools/WiredToolsSettingsTabView.tsx @@ -0,0 +1,157 @@ +import { FC } from 'react'; +import { LocalizeText } from '../../api'; +import { Button, Text } from '../../common'; +import { useNotification, useRoom, useWiredTools } from '../../hooks'; + +const WIRED_ACCESS_EVERYONE = 1; +const WIRED_ACCESS_USERS_WITH_RIGHTS = 2; +const WIRED_ACCESS_GROUP_MEMBERS = 4; +const WIRED_ACCESS_GROUP_ADMINS = 8; + +interface RoomAccessOption +{ + bit: number; + label: string; +} + +const toggleMaskBit = (mask: number, bit: number): number => ((mask & bit) ? (mask & ~bit) : (mask | bit)); +const normalizeAccessMask = (mask: number): number => (((mask & WIRED_ACCESS_GROUP_MEMBERS) !== 0) ? (mask | WIRED_ACCESS_GROUP_ADMINS) : mask); + +const buildInspectOptions = (): RoomAccessOption[] => [ + { bit: WIRED_ACCESS_EVERYONE, label: 'Everyone' }, + { bit: WIRED_ACCESS_USERS_WITH_RIGHTS, label: 'Users with rights' }, + { bit: WIRED_ACCESS_GROUP_MEMBERS, label: 'Group members' }, + { bit: WIRED_ACCESS_GROUP_ADMINS, label: 'Group admins' } +]; + +const buildModifyOptions = (): RoomAccessOption[] => [ + { bit: WIRED_ACCESS_USERS_WITH_RIGHTS, label: 'Users with rights' }, + { bit: WIRED_ACCESS_GROUP_MEMBERS, label: 'Group members' }, + { bit: WIRED_ACCESS_GROUP_ADMINS, label: 'Group admins' } +]; + +export const WiredToolsSettingsTabView: FC<{}> = () => +{ + const { roomSession = null } = useRoom(); + const { showConfirm = null } = useNotification(); + const { accountPreferences, roomSettings, saveRoomSettings, updateAccountPreferences } = useWiredTools(); + + const canManageSettings = roomSettings.canManageSettings; + const canReloadRoom = !!roomSession?.isRoomOwner; + const inspectOptions = buildInspectOptions(); + const modifyOptions = buildModifyOptions(); + const serverTimeZone = roomSession?.hotelTimeZone || 'UTC'; + + const updateInspectMask = (bit: number) => + { + if(!canManageSettings) return; + + saveRoomSettings(normalizeAccessMask(toggleMaskBit(roomSettings.inspectMask, bit)), roomSettings.modifyMask); + }; + + const updateModifyMask = (bit: number) => + { + if(!canManageSettings) return; + + const nextModifyMask = toggleMaskBit(roomSettings.modifyMask, bit); + const enabledModifyBit = ((nextModifyMask & bit) !== 0); + const normalizedModifyMask = normalizeAccessMask(nextModifyMask); + const nextInspectMask = normalizeAccessMask(enabledModifyBit ? (roomSettings.inspectMask | bit) : roomSettings.inspectMask); + + saveRoomSettings(nextInspectMask, normalizedModifyMask); + }; + + const renderAccessOption = (option: RoomAccessOption, mask: number, onToggle: (bit: number) => void) => + { + const checked = ((mask & option.bit) !== 0); + const disabled = !roomSettings.isLoaded || !canManageSettings; + + return ( + + ); + }; + + return ( +
+ Room settings: +
+
+ Who can modify Wired: + { modifyOptions.map(option => renderAccessOption(option, roomSettings.modifyMask, updateModifyMask)) } +
+
+ Who can inspect Wired: + { inspectOptions.map(option => renderAccessOption(option, roomSettings.inspectMask, updateInspectMask)) } +
+
+ Timezone: + +
+
+ Room state: +
+ + +
+
+
+
+ Account preferences: +
+ General: + + + +
+
+
+ ); +}; diff --git a/src/components/wired/views/WiredBaseView.tsx b/src/components/wired/views/WiredBaseView.tsx index d55e5fc..681cb4b 100644 --- a/src/components/wired/views/WiredBaseView.tsx +++ b/src/components/wired/views/WiredBaseView.tsx @@ -2,7 +2,7 @@ import { GetRoomEngine, GetSessionDataManager } from '@nitrots/nitro-renderer'; import { CSSProperties, FC, PropsWithChildren, ReactNode, useEffect, useState } from 'react'; import { LocalizeText, WiredFurniType, WiredSelectionVisualizer } from '../../../api'; import { Button, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../common'; -import { useWired } from '../../../hooks'; +import { useWired, useWiredTools } from '../../../hooks'; import { WiredFurniSelectorView } from './WiredFurniSelectorView'; export interface WiredBaseViewProps @@ -25,6 +25,7 @@ export const WiredBaseView: FC> = props => const [ needsSave, setNeedsSave ] = useState(false); const [ showFooter, setShowFooter ] = useState(false); const { trigger = null, setTrigger = null, setIntParams = null, setStringParam = null, setFurniIds = null, setAllowsFurni = null, saveWired = null } = useWired(); + const { roomSettings } = useWiredTools(); const clearRoomAreaSelection = () => { @@ -41,6 +42,8 @@ export const WiredBaseView: FC> = props => const onSave = () => { + if(!roomSettings.canModify) return; + if(validate && !validate()) return; if(save) save(); @@ -82,24 +85,28 @@ export const WiredBaseView: FC> = props => setIntParams(trigger.intData); setStringParam(trigger.stringData); } + }, [ trigger, hasSpecialInput, setIntParams, setStringParam ]); - if(requiresFurni > WiredFurniType.STUFF_SELECTION_OPTION_NONE) + useEffect(() => + { + if(!trigger) return; + + setFurniIds(prevValue => { - setFurniIds(prevValue => + if(prevValue && prevValue.length) WiredSelectionVisualizer.clearSelectionShaderFromFurni(prevValue); + + if(requiresFurni <= WiredFurniType.STUFF_SELECTION_OPTION_NONE) return []; + + if(trigger.selectedItems && trigger.selectedItems.length) { - if(prevValue && prevValue.length) WiredSelectionVisualizer.clearSelectionShaderFromFurni(prevValue); + WiredSelectionVisualizer.applySelectionShaderToFurni(trigger.selectedItems); - if(trigger.selectedItems && trigger.selectedItems.length) - { - WiredSelectionVisualizer.applySelectionShaderToFurni(trigger.selectedItems); + return trigger.selectedItems; + } - return trigger.selectedItems; - } - - return []; - }); - } - }, [ trigger, hasSpecialInput, setIntParams, setStringParam, setFurniIds ]); + return []; + }); + }, [ trigger, requiresFurni, setFurniIds ]); useEffect(() => { @@ -171,7 +178,7 @@ export const WiredBaseView: FC> = props => }
- +
diff --git a/src/components/wired/views/WiredVariablePicker.tsx b/src/components/wired/views/WiredVariablePicker.tsx new file mode 100644 index 0000000..dff6eae --- /dev/null +++ b/src/components/wired/views/WiredVariablePicker.tsx @@ -0,0 +1,413 @@ +import { FC, useEffect, useLayoutEffect, useRef, useState } from 'react'; +import { createPortal } from 'react-dom'; +import { FaChevronRight } from 'react-icons/fa'; +import allIcon from '../../../assets/images/wired/var/var_picker_all.png'; +import internalIcon from '../../../assets/images/wired/var/var_picker_internal.png'; +import recentIcon from '../../../assets/images/wired/var/var_picker_recent.png'; +import searchClearIcon from '../../../assets/images/wired/var/ar_picker_cancel_search.png'; +import searchIcon from '../../../assets/images/wired/var/var_picker_search.png'; +import smartIcon from '../../../assets/images/wired/var/var_picker_smart.png'; +import userMadeIcon from '../../../assets/images/wired/var/var_picker_usermade.png'; +import { LocalizeText } from '../../../api'; +import { Text } from '../../../common'; +import { flattenWiredVariablePickerEntries, IWiredVariablePickerEntry } from './WiredVariablePickerData'; + +type WiredVariablePickerMode = 'all' | 'recent' | 'usermade' | 'smart' | 'internal' | 'search'; + +interface WiredVariablePickerProps +{ + emptyText?: string; + entries: IWiredVariablePickerEntry[]; + placeholder?: string; + recentScope: string; + selectedToken: string; + onSelect: (entry: IWiredVariablePickerEntry) => void; +} + +const RECENT_PICKER_LIMIT = 12; +const RECENT_STORAGE_PREFIX = 'nitro.wired.variable-picker.recent'; + +const PICKER_MODES: Array<{ icon: string; key: WiredVariablePickerMode; }> = [ + { key: 'all', icon: allIcon }, + { key: 'recent', icon: recentIcon }, + { key: 'usermade', icon: userMadeIcon }, + { key: 'smart', icon: smartIcon }, + { key: 'internal', icon: internalIcon }, + { key: 'search', icon: searchIcon } +]; + +const normalizeSearch = (value: string) => value.trim().toLocaleLowerCase(); + +const applyQuery = (entries: IWiredVariablePickerEntry[], query: string): IWiredVariablePickerEntry[] => +{ + if(!query) return entries; + + const nextEntries: IWiredVariablePickerEntry[] = []; + + for(const entry of entries) + { + const ownMatch = entry.searchableText.toLocaleLowerCase().includes(query); + const matchingChildren = entry.children?.length ? applyQuery(entry.children, query) : []; + + if(!ownMatch && !matchingChildren.length) continue; + + nextEntries.push(matchingChildren.length ? { ...entry, children: matchingChildren } : entry); + } + + return nextEntries; +}; + +const applyMode = (entries: IWiredVariablePickerEntry[], mode: WiredVariablePickerMode, recentTokens: string[]): IWiredVariablePickerEntry[] => +{ + const recentSet = new Set(recentTokens); + + const filterEntries = (items: IWiredVariablePickerEntry[]): IWiredVariablePickerEntry[] => + { + const filtered: IWiredVariablePickerEntry[] = []; + + for(const entry of items) + { + if(mode === 'smart') continue; + + const nextChildren = entry.children?.length ? filterEntries(entry.children) : []; + const childVisible = !!nextChildren.length; + const selfVisible = (() => + { + switch(mode) + { + case 'recent': return recentSet.has(entry.token); + case 'usermade': return entry.kind === 'custom'; + case 'internal': return entry.kind === 'internal'; + case 'search': + case 'all': + default: + return true; + } + })(); + + if(!selfVisible && !childVisible) continue; + + filtered.push(childVisible ? { ...entry, children: nextChildren } : entry); + } + + return filtered; + }; + + if(mode === 'recent') + { + const flatEntries = flattenWiredVariablePickerEntries(entries) + .filter(entry => recentSet.has(entry.token)) + .sort((left, right) => recentTokens.indexOf(left.token) - recentTokens.indexOf(right.token)) + .map(entry => ({ ...entry, label: entry.displayLabel })); + + return flatEntries.filter(entry => !entry.children?.length); + } + + return filterEntries(entries); +}; + +export const WiredVariablePicker: FC = props => +{ + const { entries = [], selectedToken = '', onSelect, recentScope, placeholder = LocalizeText('wiredfurni.variable_picker.search'), emptyText = 'Nothing to display' } = props; + const containerRef = useRef(null); + const panelRef = useRef(null); + const searchInputRef = useRef(null); + const submenuRef = useRef(null); + const storageKey = `${ RECENT_STORAGE_PREFIX }.${ recentScope }`; + const [ isOpen, setIsOpen ] = useState(false); + const [ mode, setMode ] = useState('all'); + const [ query, setQuery ] = useState(''); + const [ recentTokens, setRecentTokens ] = useState([]); + const [ activeParentToken, setActiveParentToken ] = useState(''); + const [ panelPosition, setPanelPosition ] = useState<{ left: number; top: number; width: number; } | null>(null); + const [ submenuPosition, setSubmenuPosition ] = useState<{ left: number; top: number; } | null>(null); + + const allEntries = flattenWiredVariablePickerEntries(entries); + const selectedEntry = allEntries.find(entry => (entry.token === selectedToken)) || null; + const modeEntries = applyMode(entries, mode, recentTokens); + const filteredEntries = applyQuery(modeEntries, normalizeSearch(query)); + const activeParent = filteredEntries.find(entry => (entry.token === activeParentToken) && entry.children?.length) || null; + const portalTarget = (typeof document !== 'undefined') ? (document.getElementById('draggable-windows-container') ?? document.body) : null; + + useEffect(() => + { + try + { + const rawValue = window.localStorage.getItem(storageKey); + + if(!rawValue) + { + setRecentTokens([]); + return; + } + + const parsedValue = JSON.parse(rawValue) as string[]; + + setRecentTokens(Array.isArray(parsedValue) ? parsedValue.filter(token => typeof token === 'string') : []); + } + catch + { + setRecentTokens([]); + } + }, [ storageKey ]); + + useEffect(() => + { + if(!isOpen) return; + + const handleClick = (event: MouseEvent) => + { + if(containerRef.current?.contains(event.target as Node)) return; + if(panelRef.current?.contains(event.target as Node)) return; + if(submenuRef.current?.contains(event.target as Node)) return; + + setIsOpen(false); + setActiveParentToken(''); + setPanelPosition(null); + setSubmenuPosition(null); + }; + + const handleEscape = (event: KeyboardEvent) => + { + if(event.key !== 'Escape') return; + + setIsOpen(false); + setActiveParentToken(''); + setPanelPosition(null); + setSubmenuPosition(null); + }; + + document.addEventListener('mousedown', handleClick); + document.addEventListener('keydown', handleEscape); + + return () => + { + document.removeEventListener('mousedown', handleClick); + document.removeEventListener('keydown', handleEscape); + }; + }, [ isOpen ]); + + useLayoutEffect(() => + { + if(!isOpen) + { + setPanelPosition(null); + return; + } + + const updatePanelPosition = () => + { + const triggerRect = containerRef.current?.getBoundingClientRect(); + + if(!triggerRect) + { + setPanelPosition(null); + return; + } + + const panelWidth = Math.max(202, Math.ceil(triggerRect.width)); + const viewportPadding = 8; + const left = Math.min(Math.max(viewportPadding, triggerRect.left), Math.max(viewportPadding, window.innerWidth - panelWidth - viewportPadding)); + + setPanelPosition({ + left, + top: triggerRect.bottom + 2, + width: panelWidth + }); + setActiveParentToken(''); + setSubmenuPosition(null); + }; + + updatePanelPosition(); + + window.addEventListener('resize', updatePanelPosition); + window.addEventListener('scroll', updatePanelPosition, true); + + return () => + { + window.removeEventListener('resize', updatePanelPosition); + window.removeEventListener('scroll', updatePanelPosition, true); + }; + }, [ isOpen ]); + + useEffect(() => + { + if(!isOpen) return; + if(mode !== 'search') return; + + searchInputRef.current?.focus(); + }, [ isOpen, mode ]); + + useEffect(() => + { + if(!activeParentToken) return; + if(filteredEntries.some(entry => entry.token === activeParentToken && entry.children?.length)) return; + + setActiveParentToken(''); + setSubmenuPosition(null); + }, [ activeParentToken, filteredEntries ]); + + const rememberSelection = (token: string) => + { + if(!token) return; + + const nextRecentTokens = [ token, ...recentTokens.filter(currentToken => (currentToken !== token)) ].slice(0, RECENT_PICKER_LIMIT); + + setRecentTokens(nextRecentTokens); + + try + { + window.localStorage.setItem(storageKey, JSON.stringify(nextRecentTokens)); + } + catch + { + } + }; + + const handleSelect = (entry: IWiredVariablePickerEntry) => + { + if(!entry.selectable) return; + + rememberSelection(entry.token); + onSelect(entry); + setIsOpen(false); + setActiveParentToken(''); + setSubmenuPosition(null); + }; + + const activateParent = (entry: IWiredVariablePickerEntry, element: HTMLButtonElement) => + { + if(!entry.children?.length) + { + setActiveParentToken(''); + setSubmenuPosition(null); + return; + } + + const rowRect = element.getBoundingClientRect(); + const panelRect = panelRef.current?.getBoundingClientRect(); + const submenuWidth = 140; + const submenuHeight = Math.min((entry.children.length * 20) + 22, 168); + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + const leftAnchor = panelRect ? panelRect.right : rowRect.right; + const rightSpace = viewportWidth - leftAnchor; + const canOpenRight = (rightSpace >= (submenuWidth + 8)); + const left = canOpenRight + ? (leftAnchor + 6) + : Math.max(8, (panelRect ? panelRect.left : rowRect.left) - submenuWidth - 6); + const top = Math.min(Math.max(8, rowRect.top), Math.max(8, viewportHeight - submenuHeight - 8)); + + setSubmenuPosition({ + left, + top + }); + setActiveParentToken(entry.token); + }; + + const renderEntry = (entry: IWiredVariablePickerEntry) => + { + const hasChildren = !!entry.children?.length; + + return ( + + ); + }; + + const renderPanel = () => + { + if(!panelPosition) return null; + + return ( +
+
+ { PICKER_MODES.map(button => ( + + )) } +
+ +
+ search + setQuery(event.target.value) } /> + { !!query.length && + } +
+ +
+ { filteredEntries.length + ? filteredEntries.map(renderEntry) + : { emptyText } } +
+
+ ); + }; + + return ( +
+ + + { isOpen && panelPosition && portalTarget && createPortal( +
+ { renderPanel() } + + { activeParent?.children?.length && submenuPosition && +
+ { activeParent.children.map(child => ( + + )) } +
} +
, + portalTarget + ) } +
+ ); +}; diff --git a/src/components/wired/views/WiredVariablePickerData.ts b/src/components/wired/views/WiredVariablePickerData.ts new file mode 100644 index 0000000..9216c25 --- /dev/null +++ b/src/components/wired/views/WiredVariablePickerData.ts @@ -0,0 +1,423 @@ +export type WiredVariablePickerTarget = 'user' | 'furni' | 'global' | 'context'; +export type WiredVariablePickerUsage = 'give' | 'remove' | 'change-destination' | 'change-reference' | 'condition' | 'filter-main' | 'echo'; + +export interface IWiredVariableDefinitionLike +{ + availability: number; + hasValue: boolean; + isReadOnly?: boolean; + itemId: number; + name: string; +} + +export interface IWiredVariablePickerEntry +{ + id: string; + token: string; + label: string; + displayLabel: string; + searchableText: string; + selectable: boolean; + hasValue: boolean; + kind: 'internal' | 'custom'; + target: WiredVariablePickerTarget; + children?: IWiredVariablePickerEntry[]; +} + +interface IInternalVariableMeta +{ + key: string; + canUseAsDestination: boolean; + canUseAsReference: boolean; +} + +const INTERNAL_VARIABLE_ALIASES: Record = { + '@position.x': '@position_x', + '@position.y': '@position_y', + '@effect': '@effect_id', + '@handitems': '@handitem_id', + '@is_mute': '@is_muted', + '@teams.red.score': '@team_red_score', + '@teams.green.score': '@team_green_score', + '@teams.blue.score': '@team_blue_score', + '@teams.yellow.score': '@team_yellow_score', + '@teams.red.size': '@team_red_size', + '@teams.green.size': '@team_green_size', + '@teams.blue.size': '@team_blue_size', + '@teams.yellow.size': '@team_yellow_size' +}; + +const CUSTOM_TOKEN_PREFIX = 'custom:'; +const INTERNAL_TOKEN_PREFIX = 'internal:'; +const GROUP_TOKEN_PREFIX = 'group:'; + +const createInternalMeta = (key: string, canUseAsDestination = false, canUseAsReference = false): IInternalVariableMeta => +({ + key, + canUseAsDestination, + canUseAsReference +}); + +export const normalizeInternalVariableKey = (key: string) => +{ + const normalizedKey = key?.trim(); + + if(!normalizedKey) return ''; + + return (INTERNAL_VARIABLE_ALIASES[normalizedKey] || normalizedKey); +}; + +const INTERNAL_VARIABLES: Record<'user' | 'furni' | 'global' | 'context', IInternalVariableMeta[]> = { + furni: [ + createInternalMeta('~teleport.target_id', false, true), + createInternalMeta('@id', false, true), + createInternalMeta('@class_id', false, true), + createInternalMeta('@height', false, true), + createInternalMeta('@state', true, true), + createInternalMeta('@position_x', true, true), + createInternalMeta('@position_y', true, true), + createInternalMeta('@rotation', true, true), + createInternalMeta('@altitude', true, true), + createInternalMeta('@is_invisible', false, true), + createInternalMeta('@type', false, true), + createInternalMeta('@is_stackable', false, true), + createInternalMeta('@can_stand_on', false, true), + createInternalMeta('@can_sit_on', false, true), + createInternalMeta('@can_lay_on', false, true), + createInternalMeta('@wallitem_offset', false, true), + createInternalMeta('@dimensions.x', false, true), + createInternalMeta('@dimensions.y', false, true), + createInternalMeta('@owner_id', false, true) + ], + user: [ + createInternalMeta('@index', false, true), + createInternalMeta('@type', false, true), + createInternalMeta('@gender', false, true), + createInternalMeta('@level', false, true), + createInternalMeta('@achievement_score', false, true), + createInternalMeta('@is_hc', false, true), + createInternalMeta('@has_rights', false, true), + createInternalMeta('@is_group_admin', false, true), + createInternalMeta('@is_owner', false, true), + createInternalMeta('@is_muted', false, true), + createInternalMeta('@is_trading', false, true), + createInternalMeta('@is_frozen', false, true), + createInternalMeta('@effect_id', false, true), + createInternalMeta('@team_score', false, true), + createInternalMeta('@team_color', false, true), + createInternalMeta('@team_type', false, true), + createInternalMeta('@sign', false, true), + createInternalMeta('@dance', false, true), + createInternalMeta('@is_idle', false, true), + createInternalMeta('@handitem_id', false, true), + createInternalMeta('@position_x', true, true), + createInternalMeta('@position_y', true, true), + createInternalMeta('@direction', true, true), + createInternalMeta('@altitude', false, true), + createInternalMeta('@favourite_group_id', false, true), + createInternalMeta('@room_entry.method', false, true), + createInternalMeta('@room_entry.teleport_id', false, true), + createInternalMeta('@user_id', false, true), + createInternalMeta('@bot_id', false, true), + createInternalMeta('@pet_id', false, true), + createInternalMeta('@pet_owner_id', false, true) + ], + global: [ + createInternalMeta('@furni_count', false, true), + createInternalMeta('@user_count', false, true), + createInternalMeta('@wired_timer', false, true), + createInternalMeta('@team_red_score', false, true), + createInternalMeta('@team_green_score', false, true), + createInternalMeta('@team_blue_score', false, true), + createInternalMeta('@team_yellow_score', false, true), + createInternalMeta('@team_red_size', false, true), + createInternalMeta('@team_green_size', false, true), + createInternalMeta('@team_blue_size', false, true), + createInternalMeta('@team_yellow_size', false, true), + createInternalMeta('@room_id', false, true), + createInternalMeta('@group_id', false, true), + createInternalMeta('@timezone_server', false, true), + createInternalMeta('@timezone_client', false, true), + createInternalMeta('@current_time', false, true), + createInternalMeta('@current_time.millisecond_of_second', false, true), + createInternalMeta('@current_time.seconds_of_minute', false, true), + createInternalMeta('@current_time.minute_of_hour', false, true), + createInternalMeta('@current_time.hour_of_day', false, true), + createInternalMeta('@current_time.day_of_week', false, true), + createInternalMeta('@current_time.day_of_month', false, true), + createInternalMeta('@current_time.day_of_year', false, true), + createInternalMeta('@current_time.week_of_year', false, true), + createInternalMeta('@current_time.month_of_year', false, true), + createInternalMeta('@current_time.year', false, true) + ], + context: [ + createInternalMeta('@selector_furni_count', false, true), + createInternalMeta('@selector_user_count', false, true), + createInternalMeta('@signal_furni_count', false, true), + createInternalMeta('@signal_user_count', false, true), + createInternalMeta('@antenna_id', false, true), + createInternalMeta('@chat_type', false, true), + createInternalMeta('@chat_style', false, true) + ] +}; + +const sortEntries = (left: IWiredVariablePickerEntry, right: IWiredVariablePickerEntry) => +{ + return left.displayLabel.localeCompare(right.displayLabel, undefined, { sensitivity: 'base' }); +}; + +const getNormalizedInternalTarget = (target: WiredVariablePickerTarget): 'user' | 'furni' | 'global' | 'context' => +{ + if(target === 'furni') return 'furni'; + if(target === 'user') return 'user'; + if(target === 'context') return 'context'; + + return 'global'; +}; + +const getInternalSelectable = (usage: WiredVariablePickerUsage, meta: IInternalVariableMeta) => +{ + switch(usage) + { + case 'condition': return true; + case 'filter-main': return meta.canUseAsReference; + case 'echo': return true; + case 'change-destination': return meta.canUseAsDestination; + case 'change-reference': return meta.canUseAsReference; + default: return false; + } +}; + +const getCustomSelectable = (usage: WiredVariablePickerUsage, definition: IWiredVariableDefinitionLike) => +{ + switch(usage) + { + case 'condition': + case 'filter-main': + return true; + case 'echo': + return definition.name.includes('.'); + case 'change-reference': + return !!definition.hasValue; + case 'change-destination': + return (!!definition.hasValue && !definition.isReadOnly); + default: + return !definition.isReadOnly; + } +}; + +const getRootKey = (key: string) => +{ + const separatorIndex = key.indexOf('.'); + + if(separatorIndex < 0) return null; + + return key.slice(0, separatorIndex); +}; + +const createInternalEntry = (target: WiredVariablePickerTarget, usage: WiredVariablePickerUsage, meta: IInternalVariableMeta): IWiredVariablePickerEntry => +({ + id: `${ INTERNAL_TOKEN_PREFIX }${ meta.key }`, + token: `${ INTERNAL_TOKEN_PREFIX }${ meta.key }`, + label: meta.key, + displayLabel: meta.key, + searchableText: meta.key, + selectable: getInternalSelectable(usage, meta), + hasValue: meta.canUseAsReference, + kind: 'internal', + target +}); + +const createCustomEntry = (target: WiredVariablePickerTarget, usage: WiredVariablePickerUsage, definition: IWiredVariableDefinitionLike): IWiredVariablePickerEntry => +({ + id: `${ CUSTOM_TOKEN_PREFIX }${ definition.itemId }`, + token: `${ CUSTOM_TOKEN_PREFIX }${ definition.itemId }`, + label: definition.name, + displayLabel: definition.name, + searchableText: definition.name, + selectable: getCustomSelectable(usage, definition), + hasValue: !!definition.hasValue, + kind: 'custom', + target +}); + +const groupEntries = (entries: IWiredVariablePickerEntry[]) => +{ + const groupedParents = new Map(); + + for(const entry of entries) + { + const displayLabel = entry.displayLabel?.trim(); + + if(!displayLabel?.length) + { + continue; + } + + const rootKey = getRootKey(displayLabel) || displayLabel; + let group = groupedParents.get(rootKey); + + if(!group) + { + group = { children: [] }; + groupedParents.set(rootKey, group); + } + + if(displayLabel === rootKey) + { + group.exact = { + ...entry, + label: displayLabel, + displayLabel, + searchableText: displayLabel + }; + continue; + } + + const childLabel = displayLabel.slice(rootKey.length + 1).trim(); + + if(!childLabel.length) continue; + + group.children.push({ + ...entry, + label: childLabel, + displayLabel, + searchableText: `${ displayLabel } ${ childLabel }` + }); + } + + const groupedEntries: IWiredVariablePickerEntry[] = []; + + for(const [ rootKey, group ] of groupedParents) + { + const sortedChildren = [ ...group.children ] + .sort(sortEntries) + .filter((child, index, collection) => collection.findIndex(entry => (entry.token === child.token)) === index); + const shouldGroup = !!sortedChildren.length && (sortedChildren.length > 1 || !!group.exact); + + if(!shouldGroup) + { + if(group.exact) groupedEntries.push(group.exact); + groupedEntries.push(...sortedChildren.map(child => ({ ...child, label: child.displayLabel }))); + continue; + } + + groupedEntries.push({ + ...(group.exact || { + id: `${ GROUP_TOKEN_PREFIX }${ rootKey }`, + token: `${ GROUP_TOKEN_PREFIX }${ rootKey }`, + label: rootKey, + displayLabel: rootKey, + searchableText: rootKey, + selectable: false, + hasValue: false, + kind: (sortedChildren[0]?.kind || 'custom'), + target: (sortedChildren[0]?.target || 'user') + }), + label: rootKey, + displayLabel: rootKey, + searchableText: `${ rootKey } ${ sortedChildren.map(child => child.displayLabel).join(' ') }`, + children: sortedChildren + }); + } + + return groupedEntries + .filter(entry => entry.displayLabel?.trim().length) + .sort(sortEntries) + .filter((entry, index, collection) => collection.findIndex(currentEntry => (currentEntry.token === entry.token)) === index); +}; + +export const createCustomVariableToken = (itemId: number) => (itemId > 0 ? `${ CUSTOM_TOKEN_PREFIX }${ itemId }` : ''); +export const createInternalVariableToken = (key: string) => +{ + const normalizedKey = normalizeInternalVariableKey(key); + + return normalizedKey ? `${ INTERNAL_TOKEN_PREFIX }${ normalizedKey }` : ''; +}; +export const isCustomVariableToken = (token: string) => !!token && token.startsWith(CUSTOM_TOKEN_PREFIX); +export const isInternalVariableToken = (token: string) => !!token && token.startsWith(INTERNAL_TOKEN_PREFIX); +export const getCustomVariableItemId = (token: string) => (isCustomVariableToken(token) ? parseInt(token.slice(CUSTOM_TOKEN_PREFIX.length), 10) || 0 : 0); +export const getInternalVariableKey = (token: string) => (isInternalVariableToken(token) ? normalizeInternalVariableKey(token.slice(INTERNAL_TOKEN_PREFIX.length)) : ''); + +export const normalizeVariableTokenFromWire = (value: string) => +{ + const normalizedValue = value?.trim(); + + if(!normalizedValue) return ''; + if(isCustomVariableToken(normalizedValue)) return normalizedValue; + if(isInternalVariableToken(normalizedValue)) return createInternalVariableToken(normalizedValue.slice(INTERNAL_TOKEN_PREFIX.length)); + + const parsedValue = parseInt(normalizedValue, 10); + + return (!Number.isNaN(parsedValue) && (parsedValue > 0)) ? createCustomVariableToken(parsedValue) : ''; +}; + +export const createFallbackVariableEntry = (target: WiredVariablePickerTarget, token: string): IWiredVariablePickerEntry | null => +{ + if(!token) return null; + + if(isCustomVariableToken(token)) + { + const itemId = getCustomVariableItemId(token); + + if(itemId <= 0) return null; + + return { + id: token, + token, + label: `#${ itemId }`, + displayLabel: `#${ itemId }`, + searchableText: `#${ itemId }`, + selectable: true, + hasValue: false, + kind: 'custom', + target + }; + } + + if(isInternalVariableToken(token)) + { + const key = getInternalVariableKey(token); + + if(!key) return null; + + return { + id: token, + token, + label: key, + displayLabel: key, + searchableText: key, + selectable: false, + hasValue: false, + kind: 'internal', + target + }; + } + + return null; +}; + +export const buildWiredVariablePickerEntries = (target: WiredVariablePickerTarget, usage: WiredVariablePickerUsage, customDefinitions: IWiredVariableDefinitionLike[]) => +{ + const internalTarget = getNormalizedInternalTarget(target); + const customEntries = groupEntries([ ...(customDefinitions || []) ] + .map(definition => createCustomEntry(target, usage, definition)) + .sort(sortEntries)); + const internalEntries = groupEntries(INTERNAL_VARIABLES[internalTarget].map(meta => createInternalEntry(target, usage, meta))); + + return [ ...customEntries, ...internalEntries ]; +}; + +export const flattenWiredVariablePickerEntries = (entries: IWiredVariablePickerEntry[]): IWiredVariablePickerEntry[] => +{ + const flattened: IWiredVariablePickerEntry[] = []; + + for(const entry of entries) + { + flattened.push(entry); + + if(entry.children?.length) flattened.push(...entry.children); + } + + return flattened; +}; diff --git a/src/components/wired/views/actions/WiredActionChangeVariableValueView.tsx b/src/components/wired/views/actions/WiredActionChangeVariableValueView.tsx new file mode 100644 index 0000000..e27b5ed --- /dev/null +++ b/src/components/wired/views/actions/WiredActionChangeVariableValueView.tsx @@ -0,0 +1,483 @@ +import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { GetWiredTimeLocale, LocalizeText, WiredFurniType, WiredSelectionVisualizer } from '../../../../api'; +import contextVariableIcon from '../../../../assets/images/wired/var/icon_source_context_clean.png'; +import furniVariableIcon from '../../../../assets/images/wired/var/icon_source_furni.png'; +import globalVariableIcon from '../../../../assets/images/wired/var/icon_source_global.png'; +import userVariableIcon from '../../../../assets/images/wired/var/icon_source_user.png'; +import { Slider, Text } from '../../../../common'; +import { useWired, useWiredTools } from '../../../../hooks'; +import { NitroInput } from '../../../../layout'; +import { WiredFurniSelectionSourceRow } from '../WiredFurniSelectionSourceRow'; +import { WiredVariablePicker } from '../WiredVariablePicker'; +import { buildWiredVariablePickerEntries, createFallbackVariableEntry, flattenWiredVariablePickerEntries, normalizeVariableTokenFromWire } from '../WiredVariablePickerData'; +import { CLICKED_USER_SOURCE, FURNI_SOURCES, sortWiredSourceOptions, USER_SOURCES, useAvailableUserSources, WiredSourceOption } from '../WiredSourcesSelector'; +import { WiredActionBaseView } from './WiredActionBaseView'; + +type VariableTargetType = 'user' | 'furni' | 'global' | 'context'; +type ReferenceMode = 'constant' | 'variable'; +type SelectionMode = 'destination' | 'reference'; + +interface IVariableDefinition +{ + availability: number; + hasValue: boolean; + isReadOnly?: boolean; + itemId: number; + name: string; +} + +const TARGET_USER = 0; +const TARGET_FURNI = 1; +const TARGET_CONTEXT = 2; +const TARGET_GLOBAL = 3; +const REFERENCE_CONSTANT = 0; +const REFERENCE_VARIABLE = 1; +const SOURCE_TRIGGER = 0; +const SOURCE_SELECTED = 100; +const SOURCE_SECONDARY_SELECTED = 101; + +const TARGET_BUTTONS: Array<{ key: VariableTargetType; icon: string; disabled?: boolean; }> = [ + { key: 'furni', icon: furniVariableIcon }, + { key: 'user', icon: userVariableIcon }, + { key: 'global', icon: globalVariableIcon }, + { key: 'context', icon: contextVariableIcon } +]; + +const OPERATION_OPTIONS = [ 0, 1, 2, 3, 4, 5, 6, 40, 41, 50, 60, 100, 101, 102, 103, 104, 105 ]; + +const SECONDARY_FURNI_SOURCES: WiredSourceOption[] = sortWiredSourceOptions([ + { value: SOURCE_TRIGGER, label: 'wiredfurni.params.sources.furni.0' }, + { value: SOURCE_SECONDARY_SELECTED, label: 'wiredfurni.params.sources.furni.101' }, + { value: 200, label: 'wiredfurni.params.sources.furni.200' }, + { value: 201, label: 'wiredfurni.params.sources.furni.201' } +], 'furni'); + +const GLOBAL_SOURCE_OPTIONS: WiredSourceOption[] = [ { value: SOURCE_TRIGGER, label: 'wiredfurni.params.sources.global' } ]; +const CONTEXT_SOURCE_OPTIONS: WiredSourceOption[] = [ { value: SOURCE_TRIGGER, label: 'Current execution' } ]; + +const parseIds = (value: string): number[] => +{ + if(!value?.length) return []; + + const ids = new Set(); + + for(const part of value.split(/[;,\t]/)) + { + const parsedValue = parseInt(part.trim(), 10); + + if(!Number.isNaN(parsedValue) && (parsedValue > 0)) ids.add(parsedValue); + } + + return [ ...ids ]; +}; + +const serializeIds = (ids: number[]) => (ids?.length ? ids.filter(id => (id > 0)).join(';') : ''); +const parseStringData = (value: string) => (value?.length ? value.split('\t', -1) : []); +const serializeStringData = (destinationVariableToken: string, referenceVariableToken: string, referenceFurniIds: number[]) => `${ destinationVariableToken || '' }\t${ referenceVariableToken || '' }\t${ serializeIds(referenceFurniIds) }`; + +const normalizeTargetType = (value: number): VariableTargetType => +{ + switch(value) + { + case TARGET_FURNI: return 'furni'; + case TARGET_GLOBAL: return 'global'; + case TARGET_CONTEXT: return 'context'; + default: return 'user'; + } +}; + +const getTargetValue = (value: VariableTargetType) => +{ + switch(value) + { + case 'furni': return TARGET_FURNI; + case 'global': return TARGET_GLOBAL; + case 'context': return TARGET_CONTEXT; + default: return TARGET_USER; + } +}; + +const resolveSourceOptions = (baseOptions: WiredSourceOption[], selectedValue: number, fallbackOptions: WiredSourceOption[]) => +{ + if(!baseOptions.length) return baseOptions; + if(baseOptions.some(option => (option.value === selectedValue))) return baseOptions; + + const fallbackOption = fallbackOptions.find(option => (option.value === selectedValue)); + + if(!fallbackOption) return baseOptions; + + return [ ...baseOptions, fallbackOption ]; +}; + +const getTargetDefinitions = (targetType: VariableTargetType, userDefinitions: IVariableDefinition[], furniDefinitions: IVariableDefinition[], roomDefinitions: IVariableDefinition[], contextDefinitions: IVariableDefinition[]) => +{ + switch(targetType) + { + case 'furni': return furniDefinitions; + case 'global': return roomDefinitions; + case 'context': return contextDefinitions; + default: return userDefinitions; + } +}; + +const isGlobalTarget = (targetType: VariableTargetType) => (targetType === 'global'); +const isFurniTarget = (targetType: VariableTargetType) => (targetType === 'furni'); +const isContextTarget = (targetType: VariableTargetType) => (targetType === 'context'); + +export const WiredActionChangeVariableValueView: FC<{}> = () => +{ + const { trigger = null, furniIds = [], actionDelay = 0, setActionDelay = null, setAllowsFurni = null, setFurniIds = null, setIntParams = null, setStringParam = null } = useWired(); + const { userVariableDefinitions = [], furniVariableDefinitions = [], roomVariableDefinitions = [], contextVariableDefinitions = [] } = useWiredTools(); + const [ destinationTargetType, setDestinationTargetType ] = useState('user'); + const [ destinationVariableToken, setDestinationVariableToken ] = useState(''); + const [ operation, setOperation ] = useState(0); + const [ referenceMode, setReferenceMode ] = useState('constant'); + const [ referenceConstantValueInput, setReferenceConstantValueInput ] = useState('0'); + const [ referenceTargetType, setReferenceTargetType ] = useState('user'); + const [ referenceVariableToken, setReferenceVariableToken ] = useState(''); + const [ destinationUserSource, setDestinationUserSource ] = useState(SOURCE_TRIGGER); + const [ destinationFurniSource, setDestinationFurniSource ] = useState(SOURCE_TRIGGER); + const [ referenceUserSource, setReferenceUserSource ] = useState(SOURCE_TRIGGER); + const [ referenceFurniSource, setReferenceFurniSource ] = useState(SOURCE_TRIGGER); + const [ destinationFurniIds, setDestinationFurniIds ] = useState([]); + const [ referenceFurniIds, setReferenceFurniIds ] = useState([]); + const [ selectionMode, setSelectionMode ] = useState('destination'); + const highlightedIds = useRef([]); + + const availableUserSources = useAvailableUserSources(trigger, USER_SOURCES); + const orderedUserSources = useMemo(() => sortWiredSourceOptions(availableUserSources, 'users'), [ availableUserSources ]); + const orderedFurniSources = useMemo(() => sortWiredSourceOptions(FURNI_SOURCES, 'furni'), []); + const userSourceFallbackOptions = useMemo(() => sortWiredSourceOptions([ ...USER_SOURCES, CLICKED_USER_SOURCE ], 'users'), []); + const destinationDefinitions = useMemo(() => getTargetDefinitions(destinationTargetType, userVariableDefinitions, furniVariableDefinitions, roomVariableDefinitions, contextVariableDefinitions), [ contextVariableDefinitions, destinationTargetType, furniVariableDefinitions, roomVariableDefinitions, userVariableDefinitions ]); + const referenceDefinitions = useMemo(() => getTargetDefinitions(referenceTargetType, userVariableDefinitions, furniVariableDefinitions, roomVariableDefinitions, contextVariableDefinitions), [ contextVariableDefinitions, furniVariableDefinitions, referenceTargetType, roomVariableDefinitions, userVariableDefinitions ]); + const destinationVariableEntries = useMemo(() => buildWiredVariablePickerEntries(destinationTargetType, 'change-destination', destinationDefinitions), [ destinationDefinitions, destinationTargetType ]); + const resolvedDestinationVariableEntries = useMemo(() => + { + if(!destinationVariableToken) return destinationVariableEntries; + if(flattenWiredVariablePickerEntries(destinationVariableEntries).some(entry => (entry.token === destinationVariableToken))) return destinationVariableEntries; + + const fallbackEntry = createFallbackVariableEntry(destinationTargetType, destinationVariableToken); + + return fallbackEntry ? [ fallbackEntry, ...destinationVariableEntries ] : destinationVariableEntries; + }, [ destinationTargetType, destinationVariableEntries, destinationVariableToken ]); + const referenceVariableEntries = useMemo(() => buildWiredVariablePickerEntries(referenceTargetType, 'change-reference', referenceDefinitions), [ referenceDefinitions, referenceTargetType ]); + const resolvedReferenceVariableEntries = useMemo(() => + { + if(!referenceVariableToken) return referenceVariableEntries; + if(flattenWiredVariablePickerEntries(referenceVariableEntries).some(entry => (entry.token === referenceVariableToken))) return referenceVariableEntries; + + const fallbackEntry = createFallbackVariableEntry(referenceTargetType, referenceVariableToken); + + return fallbackEntry ? [ fallbackEntry, ...referenceVariableEntries ] : referenceVariableEntries; + }, [ referenceTargetType, referenceVariableEntries, referenceVariableToken ]); + + const destinationSelectionEnabled = isFurniTarget(destinationTargetType) && (destinationFurniSource === SOURCE_SELECTED); + const referenceSelectionEnabled = (referenceMode === 'variable') && isFurniTarget(referenceTargetType) && (referenceFurniSource === SOURCE_SECONDARY_SELECTED); + const destinationSelectedSourceValue = isFurniTarget(destinationTargetType) ? destinationFurniSource : (isGlobalTarget(destinationTargetType) ? SOURCE_TRIGGER : destinationUserSource); + const referenceSelectedSourceValue = isFurniTarget(referenceTargetType) ? referenceFurniSource : (isGlobalTarget(referenceTargetType) ? SOURCE_TRIGGER : referenceUserSource); + + const destinationSourceOptions = useMemo(() => + { + if(isContextTarget(destinationTargetType)) return CONTEXT_SOURCE_OPTIONS; + if(isFurniTarget(destinationTargetType)) return resolveSourceOptions(orderedFurniSources, destinationSelectedSourceValue, orderedFurniSources); + if(isGlobalTarget(destinationTargetType)) return GLOBAL_SOURCE_OPTIONS; + + return resolveSourceOptions(orderedUserSources, destinationSelectedSourceValue, userSourceFallbackOptions); + }, [ destinationSelectedSourceValue, destinationTargetType, orderedFurniSources, orderedUserSources, userSourceFallbackOptions ]); + + const referenceSourceOptions = useMemo(() => + { + if(isContextTarget(referenceTargetType)) return CONTEXT_SOURCE_OPTIONS; + if(isFurniTarget(referenceTargetType)) return resolveSourceOptions(SECONDARY_FURNI_SOURCES, referenceSelectedSourceValue, SECONDARY_FURNI_SOURCES); + if(isGlobalTarget(referenceTargetType)) return GLOBAL_SOURCE_OPTIONS; + + return resolveSourceOptions(orderedUserSources, referenceSelectedSourceValue, userSourceFallbackOptions); + }, [ orderedUserSources, referenceSelectedSourceValue, referenceTargetType, userSourceFallbackOptions ]); + + const syncHighlights = useCallback((nextDestinationIds: number[], nextReferenceIds: number[]) => + { + if(highlightedIds.current.length) + { + WiredSelectionVisualizer.clearSelectionShaderFromFurni(highlightedIds.current); + WiredSelectionVisualizer.clearSecondarySelectionShaderFromFurni(highlightedIds.current); + } + + const secondarySet = new Set(nextReferenceIds); + const primaryOnlyIds = nextDestinationIds.filter(id => !secondarySet.has(id)); + + if(primaryOnlyIds.length) WiredSelectionVisualizer.applySelectionShaderToFurni(primaryOnlyIds); + if(nextReferenceIds.length) WiredSelectionVisualizer.applySecondarySelectionShaderToFurni(nextReferenceIds); + + highlightedIds.current = Array.from(new Set([ ...nextDestinationIds, ...nextReferenceIds ])); + }, []); + + const switchSelection = useCallback((mode: SelectionMode) => + { + if(mode === 'destination' && !destinationSelectionEnabled) return; + if(mode === 'reference' && !referenceSelectionEnabled) return; + + const nextDestinationIds = (selectionMode === 'destination') ? [ ...furniIds ] : [ ...destinationFurniIds ]; + const nextReferenceIds = (selectionMode === 'reference') ? [ ...furniIds ] : [ ...referenceFurniIds ]; + + setDestinationFurniIds(nextDestinationIds); + setReferenceFurniIds(nextReferenceIds); + setSelectionMode(mode); + setFurniIds([ ...(mode === 'destination' ? nextDestinationIds : nextReferenceIds) ]); + }, [ destinationFurniIds, destinationSelectionEnabled, furniIds, referenceFurniIds, referenceSelectionEnabled, selectionMode, setFurniIds ]); + + useEffect(() => + { + if(!trigger) return; + + const stringParts = parseStringData(trigger.stringData); + const nextDestinationTargetType = normalizeTargetType((trigger.intData.length > 0) ? trigger.intData[0] : TARGET_USER); + const nextReferenceTargetType = normalizeTargetType((trigger.intData.length > 4) ? trigger.intData[4] : TARGET_USER); + const nextDestinationFurniIds = [ ...(trigger.selectedItems ?? []) ]; + const nextReferenceFurniIds = parseIds((stringParts.length > 2) ? stringParts[2] : ''); + + setDestinationTargetType(nextDestinationTargetType); + setDestinationVariableToken(normalizeVariableTokenFromWire((stringParts.length > 0) ? stringParts[0] : '')); + setOperation((trigger.intData.length > 1) ? trigger.intData[1] : 0); + setReferenceMode(((trigger.intData.length > 2) ? trigger.intData[2] : REFERENCE_CONSTANT) === REFERENCE_VARIABLE ? 'variable' : 'constant'); + setReferenceConstantValueInput(((trigger.intData.length > 3) ? trigger.intData[3] : 0).toString()); + setReferenceTargetType(nextReferenceTargetType); + setReferenceVariableToken(normalizeVariableTokenFromWire((stringParts.length > 1) ? stringParts[1] : '')); + setDestinationUserSource((trigger.intData.length > 5) ? trigger.intData[5] : SOURCE_TRIGGER); + setDestinationFurniSource((trigger.intData.length > 6) ? trigger.intData[6] : (nextDestinationFurniIds.length ? SOURCE_SELECTED : SOURCE_TRIGGER)); + setReferenceUserSource((trigger.intData.length > 7) ? trigger.intData[7] : SOURCE_TRIGGER); + setReferenceFurniSource((trigger.intData.length > 8) ? trigger.intData[8] : (nextReferenceFurniIds.length ? SOURCE_SECONDARY_SELECTED : SOURCE_TRIGGER)); + setDestinationFurniIds(nextDestinationFurniIds); + setReferenceFurniIds(nextReferenceFurniIds); + setSelectionMode('destination'); + setFurniIds([ ...nextDestinationFurniIds ]); + }, [ setFurniIds, trigger ]); + + useEffect(() => + { + if(selectionMode === 'destination') setDestinationFurniIds([ ...furniIds ]); + else setReferenceFurniIds([ ...furniIds ]); + }, [ furniIds, selectionMode ]); + + useEffect(() => syncHighlights(destinationFurniIds, referenceFurniIds), [ destinationFurniIds, referenceFurniIds, syncHighlights ]); + + useEffect(() => + { + if(selectionMode === 'destination' && !destinationSelectionEnabled && referenceSelectionEnabled) + { + switchSelection('reference'); + return; + } + + if(selectionMode === 'reference' && !referenceSelectionEnabled && destinationSelectionEnabled) + { + switchSelection('destination'); + return; + } + + const canEditSelection = (selectionMode === 'destination') ? destinationSelectionEnabled : referenceSelectionEnabled; + + setAllowsFurni(canEditSelection ? WiredFurniType.STUFF_SELECTION_OPTION_BY_ID : WiredFurniType.STUFF_SELECTION_OPTION_NONE); + }, [ destinationSelectionEnabled, referenceSelectionEnabled, selectionMode, setAllowsFurni, switchSelection ]); + + useEffect(() => + { + return () => + { + if(!highlightedIds.current.length) return; + + WiredSelectionVisualizer.clearSelectionShaderFromFurni(highlightedIds.current); + WiredSelectionVisualizer.clearSecondarySelectionShaderFromFurni(highlightedIds.current); + highlightedIds.current = []; + }; + }, []); + + const save = () => + { + const nextDestinationFurniIds = (selectionMode === 'destination') ? [ ...furniIds ] : [ ...destinationFurniIds ]; + const nextReferenceFurniIds = (selectionMode === 'reference') ? [ ...furniIds ] : [ ...referenceFurniIds ]; + const parsedReferenceConstantValue = parseInt(referenceConstantValueInput.trim(), 10); + + setDestinationFurniIds(nextDestinationFurniIds); + setReferenceFurniIds(nextReferenceFurniIds); + setStringParam(serializeStringData(destinationVariableToken, referenceMode === 'variable' ? referenceVariableToken : '', nextReferenceFurniIds)); + setIntParams([ + getTargetValue(destinationTargetType), + operation, + referenceMode === 'variable' ? REFERENCE_VARIABLE : REFERENCE_CONSTANT, + Number.isFinite(parsedReferenceConstantValue) ? parsedReferenceConstantValue : 0, + getTargetValue(referenceTargetType), + destinationUserSource, + destinationFurniSource, + referenceUserSource, + referenceFurniSource + ]); + setFurniIds((isFurniTarget(destinationTargetType) && destinationFurniSource === SOURCE_SELECTED) ? [ ...nextDestinationFurniIds ] : []); + }; + + const validate = () => + { + if(!destinationVariableToken) return false; + if(referenceMode === 'variable' && !referenceVariableToken) return false; + + return true; + }; + + const selectionLimit = trigger?.maximumItemSelectionCount ?? 0; + + const handleDestinationTargetChange = (targetType: VariableTargetType) => + { + if(targetType === destinationTargetType) return; + + setDestinationTargetType(targetType); + setDestinationVariableToken(''); + }; + + const handleReferenceTargetChange = (targetType: VariableTargetType) => + { + if(targetType === referenceTargetType) return; + + setReferenceTargetType(targetType); + setReferenceVariableToken(''); + }; + + return ( + +
+
+ { LocalizeText('wiredfurni.params.variables.variable_selection') } +
+ { TARGET_BUTTONS.map(button => ( + + )) } +
+
+ + setDestinationVariableToken(entry.token) } /> + +
+ +
+
{ LocalizeText('wiredfurni.params.variables.operation') }
+ +
+ +
+ +
+
{ LocalizeText('wiredfurni.params.variables.reference_value') }
+ + +
+ + + { referenceMode === 'variable' && + setReferenceVariableToken(entry.token) } /> } +
+
+ +
+ +
+
{ LocalizeText('wiredfurni.params.delay', [ 'seconds' ], [ GetWiredTimeLocale(actionDelay) ]) }
+ setActionDelay(event) } /> +
+ +
+ +
+ + { + if(isFurniTarget(destinationTargetType)) + { + setDestinationFurniSource(value); + return; + } + + if(!isGlobalTarget(destinationTargetType) && !isContextTarget(destinationTargetType)) setDestinationUserSource(value); + } } + onSelectionActivate={ () => switchSelection('destination') } /> +
+ + { referenceMode === 'variable' && + <> +
+
+ + { + if(isFurniTarget(referenceTargetType)) + { + setReferenceFurniSource(value); + return; + } + + if(!isGlobalTarget(referenceTargetType) && !isContextTarget(referenceTargetType)) setReferenceUserSource(value); + } } + onSelectionActivate={ () => switchSelection('reference') } /> +
+ } +
+ + ); +}; diff --git a/src/components/wired/views/actions/WiredActionGiveVariableView.tsx b/src/components/wired/views/actions/WiredActionGiveVariableView.tsx new file mode 100644 index 0000000..f370c8b --- /dev/null +++ b/src/components/wired/views/actions/WiredActionGiveVariableView.tsx @@ -0,0 +1,283 @@ +import { FC, useEffect, useMemo, useState } from 'react'; +import { FaChevronLeft, FaChevronRight } from 'react-icons/fa'; +import { GetWiredTimeLocale, LocalizeText, WiredFurniType } from '../../../../api'; +import contextVariableIcon from '../../../../assets/images/wired/var/icon_source_context_clean.png'; +import furniVariableIcon from '../../../../assets/images/wired/var/icon_source_furni.png'; +import userVariableIcon from '../../../../assets/images/wired/var/icon_source_user.png'; +import { Button, Slider, Text } from '../../../../common'; +import { useWired, useWiredTools } from '../../../../hooks'; +import { NitroInput } from '../../../../layout'; +import { CLICKED_USER_SOURCE, FURNI_SOURCES, sortWiredSourceOptions, USER_SOURCES, useAvailableUserSources } from '../WiredSourcesSelector'; +import { WiredActionBaseView } from './WiredActionBaseView'; +import { WiredVariablePicker } from '../WiredVariablePicker'; +import { buildWiredVariablePickerEntries, createCustomVariableToken, createFallbackVariableEntry, flattenWiredVariablePickerEntries, getCustomVariableItemId, normalizeVariableTokenFromWire } from '../WiredVariablePickerData'; + +type VariableTargetType = 'user' | 'furni' | 'context'; + +const TARGET_USER = 0; +const TARGET_FURNI = 1; +const TARGET_CONTEXT = 2; +const SOURCE_SELECTED = 100; + +const TARGET_BUTTONS: Array<{ key: VariableTargetType; icon: string; }> = [ + { key: 'furni', icon: furniVariableIcon }, + { key: 'user', icon: userVariableIcon }, + { key: 'context', icon: contextVariableIcon } +]; + +const normalizeTargetType = (value: number): VariableTargetType => +{ + switch(value) + { + case TARGET_FURNI: return 'furni'; + case TARGET_CONTEXT: return 'context'; + default: return 'user'; + } +}; + +const getTargetValue = (value: VariableTargetType) => +{ + switch(value) + { + case 'furni': return TARGET_FURNI; + case 'context': return TARGET_CONTEXT; + default: return TARGET_USER; + } +}; + +export const WiredActionGiveVariableView: FC<{}> = () => +{ + const { trigger = null, furniIds = [], actionDelay = 0, setActionDelay = null, setIntParams = null, setFurniIds = null, setStringParam = null } = useWired(); + const { userVariableDefinitions = [], furniVariableDefinitions = [], contextVariableDefinitions = [] } = useWiredTools(); + const [ selectedTargetType, setSelectedTargetType ] = useState('user'); + const [ selectedVariableToken, setSelectedVariableToken ] = useState(''); + const [ overrideExisting, setOverrideExisting ] = useState(false); + const [ initialValueInput, setInitialValueInput ] = useState('0'); + const [ userSource, setUserSource ] = useState(0); + const [ furniSource, setFurniSource ] = useState(0); + + const targetDefinitions = useMemo(() => + { + if(selectedTargetType === 'furni') return furniVariableDefinitions; + if(selectedTargetType === 'context') return contextVariableDefinitions; + + return userVariableDefinitions; + }, [ contextVariableDefinitions, furniVariableDefinitions, selectedTargetType, userVariableDefinitions ]); + const variableEntries = useMemo(() => buildWiredVariablePickerEntries(selectedTargetType, 'give', targetDefinitions), [ selectedTargetType, targetDefinitions ]); + const resolvedVariableEntries = useMemo(() => + { + if(!selectedVariableToken) return variableEntries; + if(flattenWiredVariablePickerEntries(variableEntries).some(entry => (entry.token === selectedVariableToken))) return variableEntries; + + const fallbackEntry = createFallbackVariableEntry(selectedTargetType, selectedVariableToken); + + return fallbackEntry ? [ fallbackEntry, ...variableEntries ] : variableEntries; + }, [ selectedTargetType, selectedVariableToken, variableEntries ]); + const selectedVariableDefinition = useMemo(() => flattenWiredVariablePickerEntries(resolvedVariableEntries).find(entry => (entry.token === selectedVariableToken)) ?? null, [ resolvedVariableEntries, selectedVariableToken ]); + const availableUserSources = useAvailableUserSources(trigger, USER_SOURCES); + const orderedUserSources = useMemo(() => sortWiredSourceOptions(availableUserSources, 'users'), [ availableUserSources ]); + const orderedFurniSources = useMemo(() => sortWiredSourceOptions(FURNI_SOURCES, 'furni'), []); + const sourceOptions = ((selectedTargetType === 'user') + ? orderedUserSources + : ((selectedTargetType === 'furni') + ? orderedFurniSources + : [])); + const selectedSourceValue = ((selectedTargetType === 'user') ? userSource : furniSource); + const resolvedSourceOptions = useMemo(() => + { + if(selectedTargetType === 'context') return []; + if(sourceOptions.some(option => (option.value === selectedSourceValue))) return sourceOptions; + + const fallbackOptions = ((selectedTargetType === 'user') + ? sortWiredSourceOptions([ ...USER_SOURCES, CLICKED_USER_SOURCE ], 'users') + : orderedFurniSources); + const fallbackOption = fallbackOptions.find(option => (option.value === selectedSourceValue)); + + if(!fallbackOption) return sourceOptions; + + return [ ...sourceOptions, fallbackOption ]; + }, [ orderedFurniSources, selectedSourceValue, selectedTargetType, sourceOptions ]); + const selectedSourceIndex = resolvedSourceOptions.findIndex(option => (option.value === selectedSourceValue)); + const selectedSourceOption = (selectedSourceIndex >= 0) ? resolvedSourceOptions[selectedSourceIndex] : null; + + const handleTargetTypeChange = (value: VariableTargetType) => + { + if(value === selectedTargetType) return; + + setSelectedTargetType(value); + setSelectedVariableToken(''); + }; + + useEffect(() => + { + if(!trigger) return; + + const parsedVariableItemId = parseInt((trigger.stringData || '').trim(), 10); + const nextTargetType = normalizeTargetType((trigger.intData.length > 0) ? trigger.intData[0] : TARGET_USER); + + setSelectedTargetType(nextTargetType); + setSelectedVariableToken(normalizeVariableTokenFromWire((!Number.isNaN(parsedVariableItemId) && (parsedVariableItemId > 0)) + ? String(parsedVariableItemId) + : ((nextTargetType === 'user') && (trigger.selectedItems?.length ?? 0) > 0) + ? String(trigger.selectedItems[0]) + : '')); + setOverrideExisting((trigger.intData.length > 1) ? (trigger.intData[1] === 1) : false); + setInitialValueInput(((trigger.intData.length > 2) ? trigger.intData[2] : 0).toString()); + setUserSource((trigger.intData.length > 3) ? trigger.intData[3] : 0); + setFurniSource((trigger.intData.length > 4) ? trigger.intData[4] : ((trigger.selectedItems?.length ?? 0) > 0 ? SOURCE_SELECTED : 0)); + }, [ trigger ]); + + useEffect(() => + { + if(!selectedVariableDefinition) return; + if(selectedVariableDefinition.hasValue) return; + + setInitialValueInput('0'); + }, [ selectedVariableDefinition ]); + + const save = () => + { + const targetValue = getTargetValue(selectedTargetType); + const parsedInitialValue = parseInt(initialValueInput.trim(), 10); + const variableItemId = getCustomVariableItemId(selectedVariableToken); + + setStringParam(variableItemId ? String(variableItemId) : ''); + setIntParams([ targetValue, overrideExisting ? 1 : 0, Number.isFinite(parsedInitialValue) ? parsedInitialValue : 0, userSource, furniSource ]); + setFurniIds((selectedTargetType === 'furni' && furniSource === SOURCE_SELECTED) ? [ ...furniIds ] : []); + }; + + const validate = () => (getCustomVariableItemId(selectedVariableToken) > 0); + + const requiresFurni = (selectedTargetType === 'furni') + ? WiredFurniType.STUFF_SELECTION_OPTION_BY_ID_BY_TYPE_OR_FROM_CONTEXT + : WiredFurniType.STUFF_SELECTION_OPTION_NONE; + + const missingVariablesText = (() => + { + switch(selectedTargetType) + { + case 'furni': return 'No wf_var_furni variables found in this room.'; + case 'context': return 'No wf_var_context variables found in this room.'; + default: return 'No wf_var_user variables found in this room.'; + } + })(); + + const cycleSource = (direction: number) => + { + if(!resolvedSourceOptions.length) return; + + const currentIndex = (selectedSourceIndex >= 0) ? selectedSourceIndex : 0; + const nextIndex = (currentIndex + direction + resolvedSourceOptions.length) % resolvedSourceOptions.length; + const nextSourceValue = resolvedSourceOptions[nextIndex].value; + + if(selectedTargetType === 'user') + { + setUserSource(nextSourceValue); + + return; + } + + if(selectedTargetType === 'furni') + { + setFurniSource(nextSourceValue); + } + }; + + return ( + +
+
+ { LocalizeText('wiredfurni.params.variables.variable_selection') } +
+ { TARGET_BUTTONS.map(button => ( + + )) } +
+
+ + <> + setSelectedVariableToken(entry.token) } /> + + { !targetDefinitions.length && { missingVariablesText } } + + + +
+ +
+
{ LocalizeText('wiredfurni.params.variables.value_settings') }
+
+ { LocalizeText('wiredfurni.params.variables.value_settings.initial_value') } + setInitialValueInput(event.target.value) } /> +
+
+ +
+ +
+
{ LocalizeText('wiredfurni.params.delay', [ 'seconds' ], [ GetWiredTimeLocale(actionDelay) ]) }
+ setActionDelay(event) } /> +
+ + { selectedTargetType !== 'context' && + <> +
+ +
+
{ 'Destinazione variabile:' }
+
+ +
+ { selectedSourceOption ? LocalizeText(selectedSourceOption.label) : '-' } +
+ +
+
+ } + +
+ + ); +}; diff --git a/src/components/wired/views/actions/WiredActionLayoutView.tsx b/src/components/wired/views/actions/WiredActionLayoutView.tsx index 5e62ce4..2984a42 100644 --- a/src/components/wired/views/actions/WiredActionLayoutView.tsx +++ b/src/components/wired/views/actions/WiredActionLayoutView.tsx @@ -2,6 +2,9 @@ import { WiredActionLayoutCode } from '../../../../api'; import { WiredActionBotChangeFigureView } from './WiredActionBotChangeFigureView'; import { WiredActionAdjustClockView } from './WiredActionAdjustClockView'; import { WiredActionFreezeView } from './WiredActionFreezeView'; +import { WiredActionGiveVariableView } from './WiredActionGiveVariableView'; +import { WiredActionChangeVariableValueView } from './WiredActionChangeVariableValueView'; +import { WiredActionRemoveVariableView } from './WiredActionRemoveVariableView'; import { WiredActionControlClockView } from './WiredActionControlClockView'; import { WiredActionFurniToFurniView } from './WiredActionFurniToFurniView'; import { WiredActionSetAltitudeView } from './WiredActionSetAltitudeView'; @@ -13,6 +16,7 @@ import { WiredSelectorFurniAltitudeView } from '../selectors/WiredSelectorFurniA import { WiredSelectorFurniOnFurniView } from '../selectors/WiredSelectorFurniOnFurniView'; import { WiredSelectorFurniPicksView } from '../selectors/WiredSelectorFurniPicksView'; import { WiredSelectorFurniSignalView } from '../selectors/WiredSelectorFurniSignalView'; +import { WiredSelectorFurniWithVariableView } from '../selectors/WiredSelectorFurniWithVariableView'; import { WiredSelectorUsersAreaView } from '../selectors/WiredSelectorUsersAreaView'; import { WiredSelectorUsersByTypeView } from '../selectors/WiredSelectorUsersByTypeView'; import { WiredSelectorUsersByActionView } from '../selectors/WiredSelectorUsersByActionView'; @@ -23,6 +27,7 @@ import { WiredSelectorUsersHandItemView } from '../selectors/WiredSelectorUsersH import { WiredSelectorUsersNeighborhoodView } from '../selectors/WiredSelectorUsersNeighborhoodView'; import { WiredSelectorUsersSignalView } from '../selectors/WiredSelectorUsersSignalView'; import { WiredSelectorUsersTeamView } from '../selectors/WiredSelectorUsersTeamView'; +import { WiredSelectorUsersWithVariableView } from '../selectors/WiredSelectorUsersWithVariableView'; import { WiredActionBotFollowAvatarView } from './WiredActionBotFollowAvatarView'; import { WiredActionBotGiveHandItemView } from './WiredActionBotGiveHandItemView'; import { WiredActionBotMoveView } from './WiredActionBotMoveView'; @@ -51,7 +56,9 @@ import { WiredActionTeleportView } from './WiredActionTeleportView'; import { WiredActionToggleFurniStateView } from './WiredActionToggleFurniStateView'; import { WiredActionUnfreezeView } from './WiredActionUnfreezeView'; import { WiredExtraFilterFurniView } from '../extras/WiredExtraFilterFurniView'; +import { WiredExtraFilterFurniByVariableView } from '../extras/WiredExtraFilterFurniByVariableView'; import { WiredExtraFilterUserView } from '../extras/WiredExtraFilterUserView'; +import { WiredExtraFilterUsersByVariableView } from '../extras/WiredExtraFilterUsersByVariableView'; import { WiredExtraAnimationTimeView } from '../extras/WiredExtraAnimationTimeView'; import { WiredExtraMoveCarryUsersView } from '../extras/WiredExtraMoveCarryUsersView'; import { WiredExtraExecuteInOrderView } from '../extras/WiredExtraExecuteInOrderView'; @@ -62,7 +69,17 @@ import { WiredExtraMovePhysicsView } from '../extras/WiredExtraMovePhysicsView'; import { WiredExtraRandomView } from '../extras/WiredExtraRandomView'; import { WiredExtraTextOutputFurniNameView } from '../extras/WiredExtraTextOutputFurniNameView'; import { WiredExtraTextOutputUsernameView } from '../extras/WiredExtraTextOutputUsernameView'; +import { WiredExtraTextOutputVariableView } from '../extras/WiredExtraTextOutputVariableView'; +import { WiredExtraFurniVariableView } from '../extras/WiredExtraFurniVariableView'; +import { WiredExtraRoomVariableView } from '../extras/WiredExtraRoomVariableView'; +import { WiredExtraContextVariableView } from '../extras/WiredExtraContextVariableView'; +import { WiredExtraUserVariableView } from '../extras/WiredExtraUserVariableView'; import { WiredExtraUnseenView } from '../extras/WiredExtraUnseenView'; +import { WiredExtraTextInputVariableView } from '../extras/WiredExtraTextInputVariableView'; +import { WiredExtraVariableLevelUpSystemView } from '../extras/WiredExtraVariableLevelUpSystemView'; +import { WiredExtraVariableEchoView } from '../extras/WiredExtraVariableEchoView'; +import { WiredExtraVariableReferenceView } from '../extras/WiredExtraVariableReferenceView'; +import { WiredExtraVariableTextConnectorView } from '../extras/WiredExtraVariableTextConnectorView'; export const WiredActionLayoutView = (code: number) => { @@ -106,6 +123,12 @@ export const WiredActionLayoutView = (code: number) => return ; case WiredActionLayoutCode.GIVE_SCORE: return ; + case WiredActionLayoutCode.GIVE_VARIABLE: + return ; + case WiredActionLayoutCode.CHANGE_VARIABLE_VALUE: + return ; + case WiredActionLayoutCode.REMOVE_VARIABLE: + return ; case WiredActionLayoutCode.GIVE_SCORE_TO_PREDEFINED_TEAM: return ; case WiredActionLayoutCode.JOIN_TEAM: @@ -152,6 +175,8 @@ export const WiredActionLayoutView = (code: number) => return ; case WiredActionLayoutCode.FURNI_SIGNAL_SELECTOR: return ; + case WiredActionLayoutCode.FURNI_WITH_VARIABLE_SELECTOR: + return ; case WiredActionLayoutCode.USERS_AREA_SELECTOR: return ; case WiredActionLayoutCode.USERS_NEIGHBORHOOD_SELECTOR: @@ -172,10 +197,16 @@ export const WiredActionLayoutView = (code: number) => return ; case WiredActionLayoutCode.USERS_TEAM_SELECTOR: return ; + case WiredActionLayoutCode.USERS_WITH_VARIABLE_SELECTOR: + return ; case WiredActionLayoutCode.FILTER_FURNI_EXTRA: return ; case WiredActionLayoutCode.FILTER_USER_EXTRA: return ; + case WiredActionLayoutCode.FILTER_USERS_BY_VARIABLE_EXTRA: + return ; + case WiredActionLayoutCode.FILTER_FURNI_BY_VARIABLE_EXTRA: + return ; case WiredActionLayoutCode.MOVE_CARRY_USERS_EXTRA: return ; case WiredActionLayoutCode.MOVE_NO_ANIMATION_EXTRA: @@ -198,6 +229,26 @@ export const WiredActionLayoutView = (code: number) => return ; case WiredActionLayoutCode.TEXT_OUTPUT_FURNI_NAME_EXTRA: return ; + case WiredActionLayoutCode.VARIABLE_TEXT_CONNECTOR_EXTRA: + return ; + case WiredActionLayoutCode.TEXT_OUTPUT_VARIABLE_EXTRA: + return ; + case WiredActionLayoutCode.USER_VARIABLE_EXTRA: + return ; + case WiredActionLayoutCode.FURNI_VARIABLE_EXTRA: + return ; + case WiredActionLayoutCode.ROOM_VARIABLE_EXTRA: + return ; + case WiredActionLayoutCode.CONTEXT_VARIABLE_EXTRA: + return ; + case WiredActionLayoutCode.VARIABLE_REFERENCE_EXTRA: + return ; + case WiredActionLayoutCode.VARIABLE_LEVELUP_SYSTEM_EXTRA: + return ; + case WiredActionLayoutCode.VARIABLE_ECHO_EXTRA: + return ; + case WiredActionLayoutCode.TEXT_INPUT_VARIABLE_EXTRA: + return ; case WiredActionLayoutCode.SEND_SIGNAL: return ; } diff --git a/src/components/wired/views/actions/WiredActionRemoveVariableView.tsx b/src/components/wired/views/actions/WiredActionRemoveVariableView.tsx new file mode 100644 index 0000000..08f3399 --- /dev/null +++ b/src/components/wired/views/actions/WiredActionRemoveVariableView.tsx @@ -0,0 +1,249 @@ +import { FC, useEffect, useMemo, useState } from 'react'; +import { FaChevronLeft, FaChevronRight } from 'react-icons/fa'; +import { GetWiredTimeLocale, LocalizeText, WiredFurniType } from '../../../../api'; +import contextVariableIcon from '../../../../assets/images/wired/var/icon_source_context_clean.png'; +import furniVariableIcon from '../../../../assets/images/wired/var/icon_source_furni.png'; +import userVariableIcon from '../../../../assets/images/wired/var/icon_source_user.png'; +import { Button, Slider, Text } from '../../../../common'; +import { useWired, useWiredTools } from '../../../../hooks'; +import { CLICKED_USER_SOURCE, FURNI_SOURCES, sortWiredSourceOptions, USER_SOURCES, useAvailableUserSources } from '../WiredSourcesSelector'; +import { WiredActionBaseView } from './WiredActionBaseView'; +import { WiredVariablePicker } from '../WiredVariablePicker'; +import { buildWiredVariablePickerEntries, createFallbackVariableEntry, flattenWiredVariablePickerEntries, getCustomVariableItemId, normalizeVariableTokenFromWire } from '../WiredVariablePickerData'; + +type VariableTargetType = 'user' | 'furni' | 'context'; + +const TARGET_USER = 0; +const TARGET_FURNI = 1; +const TARGET_CONTEXT = 2; +const SOURCE_SELECTED = 100; + +const TARGET_BUTTONS: Array<{ key: VariableTargetType; icon: string; }> = [ + { key: 'furni', icon: furniVariableIcon }, + { key: 'user', icon: userVariableIcon }, + { key: 'context', icon: contextVariableIcon } +]; + +const normalizeTargetType = (value: number): VariableTargetType => +{ + switch(value) + { + case TARGET_FURNI: return 'furni'; + case TARGET_CONTEXT: return 'context'; + default: return 'user'; + } +}; + +const getTargetValue = (value: VariableTargetType) => +{ + switch(value) + { + case 'furni': return TARGET_FURNI; + case 'context': return TARGET_CONTEXT; + default: return TARGET_USER; + } +}; + +export const WiredActionRemoveVariableView: FC<{}> = () => +{ + const { trigger = null, furniIds = [], actionDelay = 0, setActionDelay = null, setIntParams = null, setFurniIds = null, setStringParam = null } = useWired(); + const { userVariableDefinitions = [], furniVariableDefinitions = [], contextVariableDefinitions = [] } = useWiredTools(); + const [ selectedTargetType, setSelectedTargetType ] = useState('user'); + const [ selectedVariableToken, setSelectedVariableToken ] = useState(''); + const [ userSource, setUserSource ] = useState(0); + const [ furniSource, setFurniSource ] = useState(0); + + const targetDefinitions = useMemo(() => + { + const definitions = (() => + { + switch(selectedTargetType) + { + case 'furni': return furniVariableDefinitions; + case 'context': return contextVariableDefinitions; + default: return userVariableDefinitions; + } + })(); + + return definitions; + }, [ contextVariableDefinitions, furniVariableDefinitions, selectedTargetType, userVariableDefinitions ]); + const variableEntries = useMemo(() => buildWiredVariablePickerEntries(selectedTargetType, 'remove', targetDefinitions), [ selectedTargetType, targetDefinitions ]); + const resolvedVariableEntries = useMemo(() => + { + if(!selectedVariableToken) return variableEntries; + if(flattenWiredVariablePickerEntries(variableEntries).some(entry => (entry.token === selectedVariableToken))) return variableEntries; + + const fallbackEntry = createFallbackVariableEntry(selectedTargetType, selectedVariableToken); + + return fallbackEntry ? [ fallbackEntry, ...variableEntries ] : variableEntries; + }, [ selectedTargetType, selectedVariableToken, variableEntries ]); + + const availableUserSources = useAvailableUserSources(trigger, USER_SOURCES); + const orderedUserSources = useMemo(() => sortWiredSourceOptions(availableUserSources, 'users'), [ availableUserSources ]); + const orderedFurniSources = useMemo(() => sortWiredSourceOptions(FURNI_SOURCES, 'furni'), []); + const sourceOptions = ((selectedTargetType === 'user') + ? orderedUserSources + : ((selectedTargetType === 'furni') + ? orderedFurniSources + : [])); + const selectedSourceValue = ((selectedTargetType === 'user') ? userSource : furniSource); + const resolvedSourceOptions = useMemo(() => + { + if(selectedTargetType === 'context') return []; + if(sourceOptions.some(option => (option.value === selectedSourceValue))) return sourceOptions; + + const fallbackOptions = ((selectedTargetType === 'user') + ? sortWiredSourceOptions([ ...USER_SOURCES, CLICKED_USER_SOURCE ], 'users') + : orderedFurniSources); + const fallbackOption = fallbackOptions.find(option => (option.value === selectedSourceValue)); + + if(!fallbackOption) return sourceOptions; + + return [ ...sourceOptions, fallbackOption ]; + }, [ orderedFurniSources, selectedSourceValue, selectedTargetType, sourceOptions ]); + const selectedSourceIndex = resolvedSourceOptions.findIndex(option => (option.value === selectedSourceValue)); + const selectedSourceOption = (selectedSourceIndex >= 0) ? resolvedSourceOptions[selectedSourceIndex] : null; + + const handleTargetTypeChange = (value: VariableTargetType) => + { + if(value === selectedTargetType) return; + + setSelectedTargetType(value); + setSelectedVariableToken(''); + }; + + useEffect(() => + { + if(!trigger) return; + + const parsedVariableItemId = parseInt((trigger.stringData || '').trim(), 10); + const nextTargetType = normalizeTargetType((trigger.intData.length > 0) ? trigger.intData[0] : TARGET_USER); + + setSelectedTargetType(nextTargetType); + setSelectedVariableToken(normalizeVariableTokenFromWire((!Number.isNaN(parsedVariableItemId) && (parsedVariableItemId > 0)) ? String(parsedVariableItemId) : '')); + setUserSource((trigger.intData.length > 1) ? trigger.intData[1] : 0); + setFurniSource((trigger.intData.length > 2) ? trigger.intData[2] : ((trigger.selectedItems?.length ?? 0) > 0 ? SOURCE_SELECTED : 0)); + }, [ trigger ]); + + const save = () => + { + const targetValue = getTargetValue(selectedTargetType); + const variableItemId = getCustomVariableItemId(selectedVariableToken); + + setStringParam(variableItemId ? String(variableItemId) : ''); + setIntParams([ targetValue, userSource, furniSource ]); + setFurniIds((selectedTargetType === 'furni' && furniSource === SOURCE_SELECTED) ? [ ...furniIds ] : []); + }; + + const validate = () => (getCustomVariableItemId(selectedVariableToken) > 0); + + const requiresFurni = (selectedTargetType === 'furni') + ? WiredFurniType.STUFF_SELECTION_OPTION_BY_ID_BY_TYPE_OR_FROM_CONTEXT + : WiredFurniType.STUFF_SELECTION_OPTION_NONE; + + const missingVariablesText = (() => + { + switch(selectedTargetType) + { + case 'furni': return 'No wf_var_furni variables found in this room.'; + case 'context': return 'No wf_var_context variables found in this room.'; + default: return 'No wf_var_user variables found in this room.'; + } + })(); + + const cycleSource = (direction: number) => + { + if(!resolvedSourceOptions.length) return; + + const currentIndex = (selectedSourceIndex >= 0) ? selectedSourceIndex : 0; + const nextIndex = (currentIndex + direction + resolvedSourceOptions.length) % resolvedSourceOptions.length; + const nextSourceValue = resolvedSourceOptions[nextIndex].value; + + if(selectedTargetType === 'user') + { + setUserSource(nextSourceValue); + return; + } + + if(selectedTargetType === 'furni') + { + setFurniSource(nextSourceValue); + } + }; + + return ( + +
+
+ { LocalizeText('wiredfurni.params.variables.variable_selection') } +
+ { TARGET_BUTTONS.map(button => ( + + )) } +
+
+ + setSelectedVariableToken(entry.token) } /> + + { !targetDefinitions.length && { missingVariablesText } } + +
+ +
+
{ LocalizeText('wiredfurni.params.delay', [ 'seconds' ], [ GetWiredTimeLocale(actionDelay) ]) }
+ setActionDelay(event) } /> +
+ + { selectedTargetType !== 'context' && + <> +
+ +
+
{ 'Fonte variabile:' }
+
+ +
+ { selectedSourceOption ? LocalizeText(selectedSourceOption.label) : '-' } +
+ +
+
+ } +
+ + ); +}; diff --git a/src/components/wired/views/actions/WiredActionSendSignalView.tsx b/src/components/wired/views/actions/WiredActionSendSignalView.tsx index c3f080a..f09264b 100644 --- a/src/components/wired/views/actions/WiredActionSendSignalView.tsx +++ b/src/components/wired/views/actions/WiredActionSendSignalView.tsx @@ -6,8 +6,6 @@ import { WiredFurniSelectionSourceRow } from '../WiredFurniSelectionSourceRow'; import { FURNI_SOURCES, USER_SOURCES } from '../WiredSourcesSelector'; import { WiredActionBaseView } from './WiredActionBaseView'; -const ANTENNA_INTERACTION_TYPES = [ 'antenna' ]; - const SOURCE_TRIGGER = 0; const SOURCE_SELECTED = 100; @@ -51,7 +49,7 @@ export const WiredActionSendSignalView: FC<{}> = () => const [ selectionMode, setSelectionMode ] = useState('antenna'); const highlightedIds = useRef([]); - const { trigger = null, furniIds = [], setFurniIds = null, setIntParams = null, setStringParam = null, setAllowedInteractionTypes = null } = useWired(); + const { trigger = null, furniIds = [], setFurniIds = null, setIntParams = null, setStringParam = null } = useWired(); useEffect(() => { @@ -72,14 +70,6 @@ export const WiredActionSendSignalView: FC<{}> = () => setSelectionMode('antenna'); }, [ trigger ]); - useEffect(() => - { - if(selectionMode === 'antenna') setAllowedInteractionTypes(ANTENNA_INTERACTION_TYPES); - else setAllowedInteractionTypes(null); - - return () => setAllowedInteractionTypes(null); - }, [ selectionMode, setAllowedInteractionTypes ]); - useEffect(() => { if(selectionMode === 'antenna') setAntennaIds(furniIds); diff --git a/src/components/wired/views/conditions/WiredConditionBaseView.tsx b/src/components/wired/views/conditions/WiredConditionBaseView.tsx index 4566a8b..d3fc7cf 100644 --- a/src/components/wired/views/conditions/WiredConditionBaseView.tsx +++ b/src/components/wired/views/conditions/WiredConditionBaseView.tsx @@ -1,4 +1,4 @@ -import { FC, PropsWithChildren, ReactNode } from 'react'; +import { CSSProperties, FC, PropsWithChildren, ReactNode } from 'react'; import { WiredFurniType } from '../../../../api'; import { WiredBaseView } from '../WiredBaseView'; @@ -7,6 +7,8 @@ export interface WiredConditionBaseViewProps hasSpecialInput: boolean; requiresFurni: number; save: () => void; + validate?: () => boolean; + cardStyle?: CSSProperties; footer?: ReactNode; footerCollapsible?: boolean; selectionPreview?: ReactNode; @@ -14,12 +16,12 @@ export interface WiredConditionBaseViewProps export const WiredConditionBaseView: FC> = props => { - const { requiresFurni = WiredFurniType.STUFF_SELECTION_OPTION_NONE, save = null, hasSpecialInput = false, children = null, footer = null, footerCollapsible = true, selectionPreview = null } = props; + const { requiresFurni = WiredFurniType.STUFF_SELECTION_OPTION_NONE, save = null, validate = null, cardStyle = undefined, hasSpecialInput = false, children = null, footer = null, footerCollapsible = true, selectionPreview = null } = props; const onSave = () => (save && save()); return ( - + { children } ); diff --git a/src/components/wired/views/conditions/WiredConditionHasVariableView.tsx b/src/components/wired/views/conditions/WiredConditionHasVariableView.tsx new file mode 100644 index 0000000..f9b485e --- /dev/null +++ b/src/components/wired/views/conditions/WiredConditionHasVariableView.tsx @@ -0,0 +1,241 @@ +import { FC, useEffect, useMemo, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import contextVariableIcon from '../../../../assets/images/wired/var/icon_source_context_clean.png'; +import furniVariableIcon from '../../../../assets/images/wired/var/icon_source_furni.png'; +import userVariableIcon from '../../../../assets/images/wired/var/icon_source_user.png'; +import { Text } from '../../../../common'; +import { useWired, useWiredTools } from '../../../../hooks'; +import { WiredFurniSelectionSourceRow } from '../WiredFurniSelectionSourceRow'; +import { WiredVariablePicker } from '../WiredVariablePicker'; +import { buildWiredVariablePickerEntries, createFallbackVariableEntry, flattenWiredVariablePickerEntries, normalizeVariableTokenFromWire } from '../WiredVariablePickerData'; +import { FURNI_SOURCES, sortWiredSourceOptions, USER_SOURCES, useAvailableUserSources } from '../WiredSourcesSelector'; +import { WiredConditionBaseView } from './WiredConditionBaseView'; + +interface WiredConditionHasVariableViewProps +{ + negative?: boolean; +} + +type ConditionVariableTargetType = 'user' | 'furni' | 'context'; + +interface IVariableDefinition +{ + availability: number; + hasValue: boolean; + itemId: number; + name: string; +} + +const TARGET_USER = 0; +const TARGET_FURNI = 1; +const TARGET_CONTEXT = 2; +const SOURCE_TRIGGER = 0; +const SOURCE_SELECTED = 100; +const QUANTIFIER_ALL = 0; +const QUANTIFIER_ANY = 1; + +const TARGET_BUTTONS: Array<{ key: ConditionVariableTargetType; icon: string; disabled?: boolean; }> = [ + { key: 'furni', icon: furniVariableIcon }, + { key: 'user', icon: userVariableIcon }, + { key: 'context', icon: contextVariableIcon } +]; +const CONTEXT_SOURCE_OPTIONS = [ { value: SOURCE_TRIGGER, label: 'Current execution' } ]; + +const getTargetValue = (value: ConditionVariableTargetType) => +{ + switch(value) + { + case 'furni': return TARGET_FURNI; + case 'context': return TARGET_CONTEXT; + default: return TARGET_USER; + } +}; + +const normalizeTargetType = (value: number): ConditionVariableTargetType => +{ + switch(value) + { + case TARGET_FURNI: return 'furni'; + case TARGET_CONTEXT: return 'context'; + default: return 'user'; + } +}; + +const getTargetDefinitions = (targetType: ConditionVariableTargetType, userDefinitions: IVariableDefinition[], furniDefinitions: IVariableDefinition[], contextDefinitions: IVariableDefinition[]) => +{ + switch(targetType) + { + case 'furni': return furniDefinitions; + case 'context': return contextDefinitions; + default: return userDefinitions; + } +}; + +const getSourceTitle = (targetType: ConditionVariableTargetType) => +{ + switch(targetType) + { + case 'furni': return LocalizeText('wiredfurni.params.sources.furni.title'); + case 'context': return LocalizeText('wiredfurni.params.sources.merged.title.variables'); + default: return LocalizeText('wiredfurni.params.sources.users.title'); + } +}; + +export const WiredConditionHasVariableView: FC = ({ negative = false }) => +{ + const { trigger = null, furniIds = [], setFurniIds = null, setIntParams = null, setStringParam = null } = useWired(); + const { userVariableDefinitions = [], furniVariableDefinitions = [], contextVariableDefinitions = [] } = useWiredTools(); + const [ targetType, setTargetType ] = useState('user'); + const [ variableToken, setVariableToken ] = useState(''); + const [ userSource, setUserSource ] = useState(SOURCE_TRIGGER); + const [ furniSource, setFurniSource ] = useState(SOURCE_TRIGGER); + const [ quantifier, setQuantifier ] = useState(QUANTIFIER_ALL); + + const availableUserSources = useAvailableUserSources(trigger, USER_SOURCES); + const orderedUserSources = useMemo(() => sortWiredSourceOptions(availableUserSources, 'users'), [ availableUserSources ]); + const orderedFurniSources = useMemo(() => sortWiredSourceOptions(FURNI_SOURCES, 'furni'), []); + const variableDefinitions = useMemo(() => getTargetDefinitions(targetType, userVariableDefinitions, furniVariableDefinitions, contextVariableDefinitions), [ contextVariableDefinitions, furniVariableDefinitions, targetType, userVariableDefinitions ]); + const variableEntries = useMemo(() => buildWiredVariablePickerEntries(targetType, 'condition', variableDefinitions), [ targetType, variableDefinitions ]); + const resolvedVariableEntries = useMemo(() => + { + if(!variableToken) return variableEntries; + if(flattenWiredVariablePickerEntries(variableEntries).some(entry => (entry.token === variableToken))) return variableEntries; + + const fallbackEntry = createFallbackVariableEntry(targetType, variableToken); + + return fallbackEntry ? [ fallbackEntry, ...variableEntries ] : variableEntries; + }, [ targetType, variableEntries, variableToken ]); + + const sourceOptions = useMemo(() => + { + switch(targetType) + { + case 'furni': return orderedFurniSources; + case 'context': return CONTEXT_SOURCE_OPTIONS; + default: return orderedUserSources; + } + }, [ orderedFurniSources, orderedUserSources, targetType ]); + + const sourceValue = (targetType === 'furni') ? furniSource : ((targetType === 'user') ? userSource : SOURCE_TRIGGER); + const requiresFurni = ((targetType === 'furni') && (furniSource === SOURCE_SELECTED)) ? WiredFurniType.STUFF_SELECTION_OPTION_BY_ID : WiredFurniType.STUFF_SELECTION_OPTION_NONE; + const selectionLimit = trigger?.maximumItemSelectionCount ?? 0; + + useEffect(() => + { + if(!trigger) return; + + const intData = trigger.intData || []; + const nextTargetType = normalizeTargetType((intData.length > 0) ? intData[0] : TARGET_USER); + const nextUserSource = (intData.length > 1) ? intData[1] : SOURCE_TRIGGER; + const nextFurniSource = (intData.length > 2) ? intData[2] : (((trigger.selectedItems?.length ?? 0) > 0) ? SOURCE_SELECTED : SOURCE_TRIGGER); + const nextQuantifier = ((intData.length > 3) && (intData[3] === QUANTIFIER_ANY)) ? QUANTIFIER_ANY : QUANTIFIER_ALL; + + setTargetType(nextTargetType); + setVariableToken(normalizeVariableTokenFromWire(trigger.stringData || '')); + setUserSource(nextUserSource); + setFurniSource(nextFurniSource); + setQuantifier(nextQuantifier); + }, [ trigger ]); + + useEffect(() => + { + if(targetType !== 'user') return; + if(orderedUserSources.some(option => (option.value === userSource))) return; + + setUserSource(SOURCE_TRIGGER); + }, [ orderedUserSources, targetType, userSource ]); + + const save = () => + { + setStringParam(variableToken); + setIntParams([ + getTargetValue(targetType), + userSource, + furniSource, + quantifier + ]); + + if(requiresFurni <= WiredFurniType.STUFF_SELECTION_OPTION_NONE) setFurniIds([]); + }; + + const validate = () => !!variableToken.length; + + const handleTargetChange = (nextTargetType: ConditionVariableTargetType) => + { + if(nextTargetType === targetType) return; + + setTargetType(nextTargetType); + setVariableToken(''); + }; + + return ( + +
+ { LocalizeText('wiredfurni.params.quantifier_selection') } + { [ QUANTIFIER_ALL, QUANTIFIER_ANY ].map(value => ( + + )) } +
+ WiredFurniType.STUFF_SELECTION_OPTION_NONE } + selectionCount={ furniIds.length } + selectionLimit={ selectionLimit } + selectionEnabledValues={ [ SOURCE_SELECTED ] } + showSelectionToggle={ false } + onChange={ value => + { + if(targetType === 'furni') + { + setFurniSource(value); + return; + } + + if(targetType === 'user') + { + setUserSource(value); + return; + } + } } /> +
+ ) }> +
+
+ { LocalizeText('wiredfurni.params.variables.variable_selection') } +
+ { TARGET_BUTTONS.map(button => ( + + )) } +
+
+ + setVariableToken(entry.token) } /> +
+ + ); +}; diff --git a/src/components/wired/views/conditions/WiredConditionLayoutView.tsx b/src/components/wired/views/conditions/WiredConditionLayoutView.tsx index 0839d67..fbfd96d 100644 --- a/src/components/wired/views/conditions/WiredConditionLayoutView.tsx +++ b/src/components/wired/views/conditions/WiredConditionLayoutView.tsx @@ -17,6 +17,9 @@ import { WiredConditionFurniHasFurniOnView } from './WiredConditionFurniHasFurni import { WiredConditionFurniHasNotFurniOnView } from './WiredConditionFurniHasNotFurniOnView'; import { WiredConditionFurniIsOfTypeView } from './WiredConditionFurniIsOfTypeView'; import { WiredConditionFurniMatchesSnapshotView } from './WiredConditionFurniMatchesSnapshotView'; +import { WiredConditionHasVariableView } from './WiredConditionHasVariableView'; +import { WiredConditionVariableAgeMatchView } from './WiredConditionVariableAgeMatchView'; +import { WiredConditionVariableValueMatchView } from './WiredConditionVariableValueMatchView'; import { WiredConditionTimeElapsedLessView } from './WiredConditionTimeElapsedLessView'; import { WiredConditionTimeElapsedMoreView } from './WiredConditionTimeElapsedMoreView'; import { WiredConditionTeamHasRankView } from './WiredConditionTeamHasRankView'; @@ -38,6 +41,14 @@ export const WiredConditionLayoutView = (code: number) => return ; case WiredConditionlayout.SLC_QUANTITY: return ; + case WiredConditionlayout.HAS_VAR: + return ; + case WiredConditionlayout.NEG_HAS_VAR: + return ; + case WiredConditionlayout.VAR_VAL_MATCH: + return ; + case WiredConditionlayout.VAR_AGE_MATCH: + return ; case WiredConditionlayout.TRIGGERER_MATCH: return ; case WiredConditionlayout.NOT_TRIGGERER_MATCH: diff --git a/src/components/wired/views/conditions/WiredConditionVariableAgeMatchView.tsx b/src/components/wired/views/conditions/WiredConditionVariableAgeMatchView.tsx new file mode 100644 index 0000000..92c2d53 --- /dev/null +++ b/src/components/wired/views/conditions/WiredConditionVariableAgeMatchView.tsx @@ -0,0 +1,335 @@ +import { FC, useEffect, useMemo, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import contextVariableIcon from '../../../../assets/images/wired/var/icon_source_context_clean.png'; +import furniVariableIcon from '../../../../assets/images/wired/var/icon_source_furni.png'; +import globalVariableIcon from '../../../../assets/images/wired/var/icon_source_global.png'; +import userVariableIcon from '../../../../assets/images/wired/var/icon_source_user.png'; +import { Text } from '../../../../common'; +import { useWired, useWiredTools } from '../../../../hooks'; +import { NitroInput } from '../../../../layout'; +import { WiredFurniSelectionSourceRow } from '../WiredFurniSelectionSourceRow'; +import { WiredVariablePicker } from '../WiredVariablePicker'; +import { createFallbackVariableEntry, flattenWiredVariablePickerEntries, IWiredVariablePickerEntry, normalizeVariableTokenFromWire } from '../WiredVariablePickerData'; +import { FURNI_SOURCES, sortWiredSourceOptions, USER_SOURCES, useAvailableUserSources, WiredSourceOption } from '../WiredSourcesSelector'; +import { WiredConditionBaseView } from './WiredConditionBaseView'; + +type VariableTargetType = 'user' | 'furni' | 'global' | 'context'; + +interface IVariableDefinition +{ + availability: number; + hasValue: boolean; + itemId: number; + name: string; +} + +const TARGET_USER = 0; +const TARGET_FURNI = 1; +const TARGET_CONTEXT = 2; +const TARGET_GLOBAL = 3; +const COMPARE_VALUE_CREATED = 0; +const COMPARE_VALUE_UPDATED = 1; +const COMPARISON_LOWER_THAN = 0; +const COMPARISON_HIGHER_THAN = 2; +const SOURCE_TRIGGER = 0; +const SOURCE_SELECTED = 100; +const QUANTIFIER_ALL = 0; +const QUANTIFIER_ANY = 1; + +const TARGET_BUTTONS: Array<{ key: VariableTargetType; icon: string; disabled?: boolean; }> = [ + { key: 'furni', icon: furniVariableIcon }, + { key: 'user', icon: userVariableIcon }, + { key: 'global', icon: globalVariableIcon }, + { key: 'context', icon: contextVariableIcon } +]; + +const GLOBAL_SOURCE_OPTIONS: WiredSourceOption[] = [ { value: SOURCE_TRIGGER, label: 'wiredfurni.params.sources.global' } ]; +const CONTEXT_SOURCE_OPTIONS: WiredSourceOption[] = [ { value: SOURCE_TRIGGER, label: 'Current execution' } ]; +const COMPARE_VALUE_OPTIONS = [ COMPARE_VALUE_CREATED, COMPARE_VALUE_UPDATED ]; +const COMPARISON_OPTIONS = [ COMPARISON_LOWER_THAN, COMPARISON_HIGHER_THAN ]; +const DURATION_UNITS = [ 0, 1, 2, 3, 4, 5, 6, 7 ]; + +const getTargetValue = (value: VariableTargetType) => +{ + switch(value) + { + case 'furni': return TARGET_FURNI; + case 'global': return TARGET_GLOBAL; + case 'context': return TARGET_CONTEXT; + default: return TARGET_USER; + } +}; + +const normalizeTargetType = (value: number): VariableTargetType => +{ + switch(value) + { + case TARGET_FURNI: return 'furni'; + case TARGET_GLOBAL: return 'global'; + case TARGET_CONTEXT: return 'context'; + default: return 'user'; + } +}; + +const getTargetDefinitions = (targetType: VariableTargetType, userDefinitions: IVariableDefinition[], furniDefinitions: IVariableDefinition[], roomDefinitions: IVariableDefinition[], contextDefinitions: IVariableDefinition[]) => +{ + switch(targetType) + { + case 'furni': return furniDefinitions; + case 'global': return roomDefinitions; + case 'context': return contextDefinitions; + default: return userDefinitions; + } +}; + +const buildCustomVariableEntries = (target: VariableTargetType, definitions: IVariableDefinition[]): IWiredVariablePickerEntry[] => +{ + return [ ...definitions ] + .sort((left, right) => left.name.localeCompare(right.name, undefined, { sensitivity: 'base' }) || (left.itemId - right.itemId)) + .map(definition => ({ + id: `custom:${ definition.itemId }`, + token: `custom:${ definition.itemId }`, + label: definition.name, + displayLabel: definition.name, + searchableText: definition.name, + selectable: true, + hasValue: !!definition.hasValue, + kind: 'custom', + target + })); +}; + +const getSourceTitle = () => LocalizeText('wiredfurni.params.sources.merged.title.variables'); + +export const WiredConditionVariableAgeMatchView: FC<{}> = () => +{ + const { trigger = null, furniIds = [], setFurniIds = null, setIntParams = null, setStringParam = null } = useWired(); + const { userVariableDefinitions = [], furniVariableDefinitions = [], roomVariableDefinitions = [], contextVariableDefinitions = [] } = useWiredTools(); + const [ targetType, setTargetType ] = useState('user'); + const [ variableToken, setVariableToken ] = useState(''); + const [ compareValue, setCompareValue ] = useState(COMPARE_VALUE_CREATED); + const [ comparison, setComparison ] = useState(COMPARISON_LOWER_THAN); + const [ durationInput, setDurationInput ] = useState('0'); + const [ durationUnit, setDurationUnit ] = useState(1); + const [ userSource, setUserSource ] = useState(SOURCE_TRIGGER); + const [ furniSource, setFurniSource ] = useState(SOURCE_TRIGGER); + const [ quantifier, setQuantifier ] = useState(QUANTIFIER_ALL); + + const availableUserSources = useAvailableUserSources(trigger, USER_SOURCES); + const orderedUserSources = useMemo(() => sortWiredSourceOptions(availableUserSources, 'users'), [ availableUserSources ]); + const orderedFurniSources = useMemo(() => sortWiredSourceOptions(FURNI_SOURCES, 'furni'), []); + const variableDefinitions = useMemo(() => getTargetDefinitions(targetType, userVariableDefinitions, furniVariableDefinitions, roomVariableDefinitions, contextVariableDefinitions), [ contextVariableDefinitions, furniVariableDefinitions, roomVariableDefinitions, targetType, userVariableDefinitions ]); + const variableEntries = useMemo(() => buildCustomVariableEntries(targetType, variableDefinitions), [ targetType, variableDefinitions ]); + const resolvedVariableEntries = useMemo(() => + { + if(!variableToken) return variableEntries; + if(flattenWiredVariablePickerEntries(variableEntries).some(entry => (entry.token === variableToken))) return variableEntries; + + const fallbackEntry = createFallbackVariableEntry(targetType, variableToken); + return fallbackEntry ? [ fallbackEntry, ...variableEntries ] : variableEntries; + }, [ targetType, variableEntries, variableToken ]); + const sourceOptions = useMemo(() => + { + switch(targetType) + { + case 'furni': return orderedFurniSources; + case 'global': return GLOBAL_SOURCE_OPTIONS; + case 'context': return CONTEXT_SOURCE_OPTIONS; + default: return orderedUserSources; + } + }, [ orderedFurniSources, orderedUserSources, targetType ]); + + const sourceValue = (targetType === 'furni') ? furniSource : ((targetType === 'user') ? userSource : SOURCE_TRIGGER); + const requiresFurni = ((targetType === 'furni') && (furniSource === SOURCE_SELECTED)) ? WiredFurniType.STUFF_SELECTION_OPTION_BY_ID : WiredFurniType.STUFF_SELECTION_OPTION_NONE; + const selectionLimit = trigger?.maximumItemSelectionCount ?? 0; + + useEffect(() => + { + if(!trigger) return; + + const intData = trigger.intData || []; + const nextTargetType = normalizeTargetType((intData.length > 0) ? intData[0] : TARGET_USER); + + setTargetType(nextTargetType); + setVariableToken(normalizeVariableTokenFromWire(trigger.stringData || '')); + setCompareValue(((intData.length > 1) && (intData[1] === COMPARE_VALUE_UPDATED)) ? COMPARE_VALUE_UPDATED : COMPARE_VALUE_CREATED); + setComparison(((intData.length > 2) && (intData[2] === COMPARISON_HIGHER_THAN)) ? COMPARISON_HIGHER_THAN : COMPARISON_LOWER_THAN); + setDurationInput(String((intData.length > 3) ? intData[3] : 0)); + setDurationUnit((intData.length > 4) ? intData[4] : 1); + setUserSource((intData.length > 5) ? intData[5] : SOURCE_TRIGGER); + setFurniSource((intData.length > 6) ? intData[6] : (((trigger.selectedItems?.length ?? 0) > 0) ? SOURCE_SELECTED : SOURCE_TRIGGER)); + setQuantifier(((intData.length > 7) && (intData[7] === QUANTIFIER_ANY)) ? QUANTIFIER_ANY : QUANTIFIER_ALL); + }, [ trigger ]); + + useEffect(() => + { + if(targetType !== 'user') return; + if(orderedUserSources.some(option => (option.value === userSource))) return; + + setUserSource(SOURCE_TRIGGER); + }, [ orderedUserSources, targetType, userSource ]); + + useEffect(() => + { + if(targetType !== 'global') return; + if(compareValue === COMPARE_VALUE_UPDATED) return; + + setCompareValue(COMPARE_VALUE_UPDATED); + }, [ targetType, compareValue ]); + + const save = () => + { + const parsedDuration = parseInt(durationInput.trim(), 10); + + setStringParam(variableToken); + setIntParams([ + getTargetValue(targetType), + compareValue, + comparison, + Number.isFinite(parsedDuration) ? Math.max(0, parsedDuration) : 0, + durationUnit, + userSource, + furniSource, + quantifier + ]); + + if(requiresFurni <= WiredFurniType.STUFF_SELECTION_OPTION_NONE) setFurniIds([]); + }; + + const validate = () => + { + if(!variableToken.length) return false; + if((targetType === 'global') && (compareValue !== COMPARE_VALUE_UPDATED)) return false; + + return true; + }; + + const handleTargetChange = (nextTargetType: VariableTargetType) => + { + if(nextTargetType === targetType) return; + + setTargetType(nextTargetType); + setVariableToken(''); + }; + + return ( + +
+ { LocalizeText('wiredfurni.params.quantifier_selection') } + { [ QUANTIFIER_ALL, QUANTIFIER_ANY ].map(value => ( + + )) } +
+ WiredFurniType.STUFF_SELECTION_OPTION_NONE } + selectionCount={ furniIds.length } + selectionLimit={ selectionLimit } + selectionEnabledValues={ [ SOURCE_SELECTED ] } + showSelectionToggle={ false } + onChange={ value => + { + if(targetType === 'furni') + { + setFurniSource(value); + return; + } + + if(targetType === 'user') + { + setUserSource(value); + } + } } /> +
+ ) }> +
+
+ { LocalizeText('wiredfurni.params.variables.variable_selection') } +
+ { TARGET_BUTTONS.map(button => ( + + )) } +
+
+ + setVariableToken(entry.token) } /> + +
+ +
+
{ LocalizeText('wiredfurni.params.variables.compare_value') }
+ { COMPARE_VALUE_OPTIONS.map(value => ( + + )) } +
+ +
+ +
+
{ LocalizeText('wiredfurni.params.choose_type') }
+ { COMPARISON_OPTIONS.map(value => ( + + )) } +
+ +
+ +
+
{ LocalizeText('wiredfurni.params.variables.time_selection') }
+
+ { LocalizeText('wiredfurni.params.variables.duration') } + setDurationInput(event.target.value) } /> + +
+
+
+ + ); +}; diff --git a/src/components/wired/views/conditions/WiredConditionVariableValueMatchView.tsx b/src/components/wired/views/conditions/WiredConditionVariableValueMatchView.tsx new file mode 100644 index 0000000..a731bba --- /dev/null +++ b/src/components/wired/views/conditions/WiredConditionVariableValueMatchView.tsx @@ -0,0 +1,500 @@ +import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { LocalizeText, WiredFurniType, WiredSelectionVisualizer } from '../../../../api'; +import contextVariableIcon from '../../../../assets/images/wired/var/icon_source_context_clean.png'; +import furniVariableIcon from '../../../../assets/images/wired/var/icon_source_furni.png'; +import globalVariableIcon from '../../../../assets/images/wired/var/icon_source_global.png'; +import userVariableIcon from '../../../../assets/images/wired/var/icon_source_user.png'; +import { Text } from '../../../../common'; +import { useWired, useWiredTools } from '../../../../hooks'; +import { NitroInput } from '../../../../layout'; +import { WiredFurniSelectionSourceRow } from '../WiredFurniSelectionSourceRow'; +import { WiredVariablePicker } from '../WiredVariablePicker'; +import { buildWiredVariablePickerEntries, createFallbackVariableEntry, flattenWiredVariablePickerEntries, normalizeVariableTokenFromWire } from '../WiredVariablePickerData'; +import { CLICKED_USER_SOURCE, FURNI_SOURCES, sortWiredSourceOptions, USER_SOURCES, useAvailableUserSources, WiredSourceOption } from '../WiredSourcesSelector'; +import { WiredConditionBaseView } from './WiredConditionBaseView'; + +type VariableTargetType = 'user' | 'furni' | 'global' | 'context'; +type ReferenceMode = 'constant' | 'variable'; +type SelectionMode = 'destination' | 'reference'; + +interface IVariableDefinition +{ + availability: number; + hasValue: boolean; + itemId: number; + name: string; +} + +const TARGET_USER = 0; +const TARGET_FURNI = 1; +const TARGET_CONTEXT = 2; +const TARGET_GLOBAL = 3; +const REFERENCE_CONSTANT = 0; +const REFERENCE_VARIABLE = 1; +const SOURCE_TRIGGER = 0; +const SOURCE_SELECTED = 100; +const SOURCE_SECONDARY_SELECTED = 101; +const QUANTIFIER_ALL = 0; +const QUANTIFIER_ANY = 1; + +const TARGET_BUTTONS: Array<{ key: VariableTargetType; icon: string; disabled?: boolean; }> = [ + { key: 'furni', icon: furniVariableIcon }, + { key: 'user', icon: userVariableIcon }, + { key: 'global', icon: globalVariableIcon }, + { key: 'context', icon: contextVariableIcon } +]; + +const COMPARISON_OPTIONS = [ + { value: 0, label: '>' }, + { value: 1, label: '≥' }, + { value: 2, label: '=' }, + { value: 3, label: '≤' }, + { value: 4, label: '<' }, + { value: 5, label: '≠' } +]; + +const SECONDARY_FURNI_SOURCES: WiredSourceOption[] = sortWiredSourceOptions([ + { value: SOURCE_TRIGGER, label: 'wiredfurni.params.sources.furni.0' }, + { value: SOURCE_SECONDARY_SELECTED, label: 'wiredfurni.params.sources.furni.101' }, + { value: 200, label: 'wiredfurni.params.sources.furni.200' }, + { value: 201, label: 'wiredfurni.params.sources.furni.201' } +], 'furni'); + +const GLOBAL_SOURCE_OPTIONS: WiredSourceOption[] = [ { value: SOURCE_TRIGGER, label: 'wiredfurni.params.sources.global' } ]; +const CONTEXT_SOURCE_OPTIONS: WiredSourceOption[] = [ { value: SOURCE_TRIGGER, label: 'Current execution' } ]; + +const parseIds = (value: string): number[] => +{ + if(!value?.length) return []; + + const ids = new Set(); + + for(const part of value.split(/[;,\t]/)) + { + const parsedValue = parseInt(part.trim(), 10); + + if(!Number.isNaN(parsedValue) && (parsedValue > 0)) ids.add(parsedValue); + } + + return [ ...ids ]; +}; + +const serializeIds = (ids: number[]) => (ids?.length ? ids.filter(id => (id > 0)).join(';') : ''); +const parseStringData = (value: string) => (value?.length ? value.split('\t', -1) : []); +const serializeStringData = (destinationVariableToken: string, referenceVariableToken: string, referenceFurniIds: number[]) => `${ destinationVariableToken || '' }\t${ referenceVariableToken || '' }\t${ serializeIds(referenceFurniIds) }`; + +const normalizeTargetType = (value: number): VariableTargetType => +{ + switch(value) + { + case TARGET_FURNI: return 'furni'; + case TARGET_GLOBAL: return 'global'; + case TARGET_CONTEXT: return 'context'; + default: return 'user'; + } +}; + +const getTargetValue = (value: VariableTargetType) => +{ + switch(value) + { + case 'furni': return TARGET_FURNI; + case 'global': return TARGET_GLOBAL; + case 'context': return TARGET_CONTEXT; + default: return TARGET_USER; + } +}; + +const resolveSourceOptions = (baseOptions: WiredSourceOption[], selectedValue: number, fallbackOptions: WiredSourceOption[]) => +{ + if(!baseOptions.length) return baseOptions; + if(baseOptions.some(option => (option.value === selectedValue))) return baseOptions; + + const fallbackOption = fallbackOptions.find(option => (option.value === selectedValue)); + + if(!fallbackOption) return baseOptions; + + return [ ...baseOptions, fallbackOption ]; +}; + +const getTargetDefinitions = (targetType: VariableTargetType, userDefinitions: IVariableDefinition[], furniDefinitions: IVariableDefinition[], roomDefinitions: IVariableDefinition[], contextDefinitions: IVariableDefinition[]) => +{ + switch(targetType) + { + case 'furni': return furniDefinitions; + case 'global': return roomDefinitions; + case 'context': return contextDefinitions; + default: return userDefinitions; + } +}; + +const isGlobalTarget = (targetType: VariableTargetType) => (targetType === 'global'); +const isFurniTarget = (targetType: VariableTargetType) => (targetType === 'furni'); +const isContextTarget = (targetType: VariableTargetType) => (targetType === 'context'); + +export const WiredConditionVariableValueMatchView: FC<{}> = () => +{ + const { trigger = null, furniIds = [], setAllowsFurni = null, setFurniIds = null, setIntParams = null, setStringParam = null } = useWired(); + const { userVariableDefinitions = [], furniVariableDefinitions = [], roomVariableDefinitions = [], contextVariableDefinitions = [] } = useWiredTools(); + const [ targetType, setTargetType ] = useState('user'); + const [ variableToken, setVariableToken ] = useState(''); + const [ comparison, setComparison ] = useState(2); + const [ referenceMode, setReferenceMode ] = useState('constant'); + const [ referenceConstantValueInput, setReferenceConstantValueInput ] = useState('0'); + const [ referenceTargetType, setReferenceTargetType ] = useState('user'); + const [ referenceVariableToken, setReferenceVariableToken ] = useState(''); + const [ userSource, setUserSource ] = useState(SOURCE_TRIGGER); + const [ furniSource, setFurniSource ] = useState(SOURCE_TRIGGER); + const [ referenceUserSource, setReferenceUserSource ] = useState(SOURCE_TRIGGER); + const [ referenceFurniSource, setReferenceFurniSource ] = useState(SOURCE_TRIGGER); + const [ quantifier, setQuantifier ] = useState(QUANTIFIER_ALL); + const [ destinationFurniIds, setDestinationFurniIds ] = useState([]); + const [ referenceFurniIds, setReferenceFurniIds ] = useState([]); + const [ selectionMode, setSelectionMode ] = useState('destination'); + const highlightedIds = useRef([]); + + const availableUserSources = useAvailableUserSources(trigger, USER_SOURCES); + const orderedUserSources = useMemo(() => sortWiredSourceOptions(availableUserSources, 'users'), [ availableUserSources ]); + const orderedFurniSources = useMemo(() => sortWiredSourceOptions(FURNI_SOURCES, 'furni'), []); + const userSourceFallbackOptions = useMemo(() => sortWiredSourceOptions([ ...USER_SOURCES, CLICKED_USER_SOURCE ], 'users'), []); + const targetDefinitions = useMemo(() => getTargetDefinitions(targetType, userVariableDefinitions, furniVariableDefinitions, roomVariableDefinitions, contextVariableDefinitions), [ contextVariableDefinitions, furniVariableDefinitions, roomVariableDefinitions, targetType, userVariableDefinitions ]); + const referenceDefinitions = useMemo(() => getTargetDefinitions(referenceTargetType, userVariableDefinitions, furniVariableDefinitions, roomVariableDefinitions, contextVariableDefinitions), [ contextVariableDefinitions, furniVariableDefinitions, referenceTargetType, roomVariableDefinitions, userVariableDefinitions ]); + const variableEntries = useMemo(() => buildWiredVariablePickerEntries(targetType, 'change-reference', targetDefinitions), [ targetDefinitions, targetType ]); + const referenceVariableEntries = useMemo(() => buildWiredVariablePickerEntries(referenceTargetType, 'change-reference', referenceDefinitions), [ referenceDefinitions, referenceTargetType ]); + const resolvedVariableEntries = useMemo(() => + { + if(!variableToken) return variableEntries; + if(flattenWiredVariablePickerEntries(variableEntries).some(entry => (entry.token === variableToken))) return variableEntries; + + const fallbackEntry = createFallbackVariableEntry(targetType, variableToken); + + return fallbackEntry ? [ fallbackEntry, ...variableEntries ] : variableEntries; + }, [ targetType, variableEntries, variableToken ]); + const resolvedReferenceVariableEntries = useMemo(() => + { + if(!referenceVariableToken) return referenceVariableEntries; + if(flattenWiredVariablePickerEntries(referenceVariableEntries).some(entry => (entry.token === referenceVariableToken))) return referenceVariableEntries; + + const fallbackEntry = createFallbackVariableEntry(referenceTargetType, referenceVariableToken); + + return fallbackEntry ? [ fallbackEntry, ...referenceVariableEntries ] : referenceVariableEntries; + }, [ referenceTargetType, referenceVariableEntries, referenceVariableToken ]); + + const destinationSelectionEnabled = isFurniTarget(targetType) && (furniSource === SOURCE_SELECTED); + const referenceSelectionEnabled = (referenceMode === 'variable') && isFurniTarget(referenceTargetType) && (referenceFurniSource === SOURCE_SECONDARY_SELECTED); + const destinationSelectedSourceValue = isFurniTarget(targetType) ? furniSource : ((isGlobalTarget(targetType) || isContextTarget(targetType)) ? SOURCE_TRIGGER : userSource); + const referenceSelectedSourceValue = isFurniTarget(referenceTargetType) ? referenceFurniSource : ((isGlobalTarget(referenceTargetType) || isContextTarget(referenceTargetType)) ? SOURCE_TRIGGER : referenceUserSource); + + const destinationSourceOptions = useMemo(() => + { + if(isFurniTarget(targetType)) return resolveSourceOptions(orderedFurniSources, destinationSelectedSourceValue, orderedFurniSources); + if(isGlobalTarget(targetType)) return GLOBAL_SOURCE_OPTIONS; + if(isContextTarget(targetType)) return CONTEXT_SOURCE_OPTIONS; + + return resolveSourceOptions(orderedUserSources, destinationSelectedSourceValue, userSourceFallbackOptions); + }, [ destinationSelectedSourceValue, orderedFurniSources, orderedUserSources, targetType, userSourceFallbackOptions ]); + + const referenceSourceOptions = useMemo(() => + { + if(isFurniTarget(referenceTargetType)) return resolveSourceOptions(SECONDARY_FURNI_SOURCES, referenceSelectedSourceValue, SECONDARY_FURNI_SOURCES); + if(isGlobalTarget(referenceTargetType)) return GLOBAL_SOURCE_OPTIONS; + if(isContextTarget(referenceTargetType)) return CONTEXT_SOURCE_OPTIONS; + + return resolveSourceOptions(orderedUserSources, referenceSelectedSourceValue, userSourceFallbackOptions); + }, [ orderedUserSources, referenceSelectedSourceValue, referenceTargetType, userSourceFallbackOptions ]); + + const syncHighlights = useCallback((nextDestinationIds: number[], nextReferenceIds: number[]) => + { + if(highlightedIds.current.length) + { + WiredSelectionVisualizer.clearSelectionShaderFromFurni(highlightedIds.current); + WiredSelectionVisualizer.clearSecondarySelectionShaderFromFurni(highlightedIds.current); + } + + const secondarySet = new Set(nextReferenceIds); + const primaryOnlyIds = nextDestinationIds.filter(id => !secondarySet.has(id)); + + if(primaryOnlyIds.length) WiredSelectionVisualizer.applySelectionShaderToFurni(primaryOnlyIds); + if(nextReferenceIds.length) WiredSelectionVisualizer.applySecondarySelectionShaderToFurni(nextReferenceIds); + + highlightedIds.current = Array.from(new Set([ ...nextDestinationIds, ...nextReferenceIds ])); + }, []); + + const switchSelection = useCallback((mode: SelectionMode) => + { + if(mode === 'destination' && !destinationSelectionEnabled) return; + if(mode === 'reference' && !referenceSelectionEnabled) return; + + const nextDestinationIds = (selectionMode === 'destination') ? [ ...furniIds ] : [ ...destinationFurniIds ]; + const nextReferenceIds = (selectionMode === 'reference') ? [ ...furniIds ] : [ ...referenceFurniIds ]; + + setDestinationFurniIds(nextDestinationIds); + setReferenceFurniIds(nextReferenceIds); + setSelectionMode(mode); + setFurniIds([ ...(mode === 'destination' ? nextDestinationIds : nextReferenceIds) ]); + }, [ destinationFurniIds, destinationSelectionEnabled, furniIds, referenceFurniIds, referenceSelectionEnabled, selectionMode, setFurniIds ]); + + useEffect(() => + { + if(!trigger) return; + + const stringParts = parseStringData(trigger.stringData); + const nextTargetType = normalizeTargetType((trigger.intData.length > 0) ? trigger.intData[0] : TARGET_USER); + const nextReferenceTargetType = normalizeTargetType((trigger.intData.length > 4) ? trigger.intData[4] : TARGET_USER); + const nextDestinationFurniIds = [ ...(trigger.selectedItems ?? []) ]; + const nextReferenceFurniIds = parseIds((stringParts.length > 2) ? stringParts[2] : ''); + + setTargetType(nextTargetType); + setVariableToken(normalizeVariableTokenFromWire((stringParts.length > 0) ? stringParts[0] : '')); + setComparison((trigger.intData.length > 1) ? trigger.intData[1] : 2); + setReferenceMode(((trigger.intData.length > 2) ? trigger.intData[2] : REFERENCE_CONSTANT) === REFERENCE_VARIABLE ? 'variable' : 'constant'); + setReferenceConstantValueInput(((trigger.intData.length > 3) ? trigger.intData[3] : 0).toString()); + setReferenceTargetType(nextReferenceTargetType); + setReferenceVariableToken(normalizeVariableTokenFromWire((stringParts.length > 1) ? stringParts[1] : '')); + setUserSource((trigger.intData.length > 5) ? trigger.intData[5] : SOURCE_TRIGGER); + setFurniSource((trigger.intData.length > 6) ? trigger.intData[6] : (nextDestinationFurniIds.length ? SOURCE_SELECTED : SOURCE_TRIGGER)); + setReferenceUserSource((trigger.intData.length > 7) ? trigger.intData[7] : SOURCE_TRIGGER); + setReferenceFurniSource((trigger.intData.length > 8) ? trigger.intData[8] : (nextReferenceFurniIds.length ? SOURCE_SECONDARY_SELECTED : SOURCE_TRIGGER)); + setQuantifier((trigger.intData.length > 9) ? ((trigger.intData[9] === QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL) : QUANTIFIER_ALL); + setDestinationFurniIds(nextDestinationFurniIds); + setReferenceFurniIds(nextReferenceFurniIds); + setSelectionMode('destination'); + setFurniIds([ ...nextDestinationFurniIds ]); + }, [ setFurniIds, trigger ]); + + useEffect(() => + { + if(selectionMode === 'destination') setDestinationFurniIds([ ...furniIds ]); + else setReferenceFurniIds([ ...furniIds ]); + }, [ furniIds, selectionMode ]); + + useEffect(() => syncHighlights(destinationFurniIds, referenceFurniIds), [ destinationFurniIds, referenceFurniIds, syncHighlights ]); + + useEffect(() => + { + if(selectionMode === 'destination' && !destinationSelectionEnabled && referenceSelectionEnabled) + { + switchSelection('reference'); + return; + } + + if(selectionMode === 'reference' && !referenceSelectionEnabled && destinationSelectionEnabled) + { + switchSelection('destination'); + return; + } + + const canEditSelection = (selectionMode === 'destination') ? destinationSelectionEnabled : referenceSelectionEnabled; + + setAllowsFurni(canEditSelection ? WiredFurniType.STUFF_SELECTION_OPTION_BY_ID : WiredFurniType.STUFF_SELECTION_OPTION_NONE); + }, [ destinationSelectionEnabled, referenceSelectionEnabled, selectionMode, setAllowsFurni, switchSelection ]); + + useEffect(() => + { + return () => + { + if(!highlightedIds.current.length) return; + + WiredSelectionVisualizer.clearSelectionShaderFromFurni(highlightedIds.current); + WiredSelectionVisualizer.clearSecondarySelectionShaderFromFurni(highlightedIds.current); + highlightedIds.current = []; + }; + }, []); + + const save = () => + { + const nextDestinationFurniIds = (selectionMode === 'destination') ? [ ...furniIds ] : [ ...destinationFurniIds ]; + const nextReferenceFurniIds = (selectionMode === 'reference') ? [ ...furniIds ] : [ ...referenceFurniIds ]; + const parsedReferenceConstantValue = parseInt(referenceConstantValueInput.trim(), 10); + + setDestinationFurniIds(nextDestinationFurniIds); + setReferenceFurniIds(nextReferenceFurniIds); + setStringParam(serializeStringData(variableToken, referenceMode === 'variable' ? referenceVariableToken : '', nextReferenceFurniIds)); + setIntParams([ + getTargetValue(targetType), + comparison, + referenceMode === 'variable' ? REFERENCE_VARIABLE : REFERENCE_CONSTANT, + Number.isFinite(parsedReferenceConstantValue) ? parsedReferenceConstantValue : 0, + getTargetValue(referenceTargetType), + userSource, + furniSource, + referenceUserSource, + referenceFurniSource, + quantifier + ]); + setFurniIds((isFurniTarget(targetType) && furniSource === SOURCE_SELECTED) ? [ ...nextDestinationFurniIds ] : []); + }; + + const validate = () => + { + if(!variableToken) return false; + if(referenceMode === 'variable' && !referenceVariableToken) return false; + + return true; + }; + + const selectionLimit = trigger?.maximumItemSelectionCount ?? 0; + + const handleTargetChange = (nextTargetType: VariableTargetType) => + { + if(nextTargetType === targetType) return; + + setTargetType(nextTargetType); + setVariableToken(''); + }; + + const handleReferenceTargetChange = (nextTargetType: VariableTargetType) => + { + if(nextTargetType === referenceTargetType) return; + + setReferenceTargetType(nextTargetType); + setReferenceVariableToken(''); + }; + + return ( + +
+ { LocalizeText('wiredfurni.params.quantifier_selection') } + { [ QUANTIFIER_ALL, QUANTIFIER_ANY ].map(value => ( + + )) } +
+ + + { + if(isFurniTarget(targetType)) + { + setFurniSource(value); + return; + } + + if(targetType === 'user') + { + setUserSource(value); + } + } } /> + + { referenceMode === 'variable' && + + { + if(isFurniTarget(referenceTargetType)) + { + setReferenceFurniSource(value); + return; + } + + if(referenceTargetType === 'user') + { + setReferenceUserSource(value); + } + } } /> } +
+ ) }> +
+
+ { LocalizeText('wiredfurni.params.variables.variable_selection') } +
+ { TARGET_BUTTONS.map(button => ( + + )) } +
+
+ + setVariableToken(entry.token) } /> + +
+ +
+
{ LocalizeText('wiredfurni.params.choose_type') }
+
+ { COMPARISON_OPTIONS.map(option => ( + + )) } +
+
+ +
+ +
+
{ LocalizeText('wiredfurni.params.variables.reference_value') }
+ + +
+ + + { referenceMode === 'variable' && + setReferenceVariableToken(entry.token) } /> } +
+
+
+ + ); +}; diff --git a/src/components/wired/views/extras/WiredExtraContextVariableView.tsx b/src/components/wired/views/extras/WiredExtraContextVariableView.tsx new file mode 100644 index 0000000..ec3c1e3 --- /dev/null +++ b/src/components/wired/views/extras/WiredExtraContextVariableView.tsx @@ -0,0 +1,64 @@ +import { FC, useEffect, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { NitroInput } from '../../../../layout'; +import { WiredExtraBaseView } from './WiredExtraBaseView'; + +const MAX_NAME_LENGTH = 40; + +const normalizeVariableName = (value: string) => +{ + let normalizedValue = (value ?? '').trim().replace(/[\t\r\n]/g, ''); + + if(normalizedValue.includes('=')) normalizedValue = normalizedValue.substring(0, normalizedValue.indexOf('=')).trim(); + + while(normalizedValue.startsWith('@') || normalizedValue.startsWith('~')) + { + normalizedValue = normalizedValue.substring(1).trim(); + } + + normalizedValue = normalizedValue.replace(/[^A-Za-z0-9_]/g, ''); + + return normalizedValue.slice(0, MAX_NAME_LENGTH); +}; + +export const WiredExtraContextVariableView: FC<{}> = () => +{ + const { trigger = null, setIntParams = null, setStringParam = null } = useWired(); + const [ variableName, setVariableName ] = useState(''); + const [ hasValue, setHasValue ] = useState(false); + + useEffect(() => + { + if(!trigger) return; + + setVariableName(normalizeVariableName(trigger.stringData)); + setHasValue((trigger.intData.length > 0) ? (trigger.intData[0] === 1) : false); + }, [ trigger ]); + + const save = () => + { + setStringParam(normalizeVariableName(variableName)); + setIntParams([ hasValue ? 1 : 0 ]); + }; + + return ( + +
+
+ { LocalizeText('wiredfurni.params.variables.variable_name') } + setVariableName(normalizeVariableName(event.target.value)) } /> +
+ +
+ { LocalizeText('wiredfurni.params.variables.settings') } + +
+
+
+ ); +}; diff --git a/src/components/wired/views/extras/WiredExtraFilterByVariableView.tsx b/src/components/wired/views/extras/WiredExtraFilterByVariableView.tsx new file mode 100644 index 0000000..a1f9494 --- /dev/null +++ b/src/components/wired/views/extras/WiredExtraFilterByVariableView.tsx @@ -0,0 +1,315 @@ +import { FC, useEffect, useMemo, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import contextVariableIcon from '../../../../assets/images/wired/var/icon_source_context_clean.png'; +import furniVariableIcon from '../../../../assets/images/wired/var/icon_source_furni.png'; +import globalVariableIcon from '../../../../assets/images/wired/var/icon_source_global.png'; +import userVariableIcon from '../../../../assets/images/wired/var/icon_source_user.png'; +import { Text } from '../../../../common'; +import { useWired, useWiredTools } from '../../../../hooks'; +import { NitroInput } from '../../../../layout'; +import { WiredFurniSelectionSourceRow } from '../WiredFurniSelectionSourceRow'; +import { WiredVariablePicker } from '../WiredVariablePicker'; +import { buildWiredVariablePickerEntries, createFallbackVariableEntry, flattenWiredVariablePickerEntries, normalizeVariableTokenFromWire, WiredVariablePickerTarget } from '../WiredVariablePickerData'; +import { CLICKED_USER_SOURCE, sortWiredSourceOptions, USER_SOURCES, useAvailableUserSources, WiredSourceOption } from '../WiredSourcesSelector'; +import { WiredExtraBaseView } from './WiredExtraBaseView'; + +type VariableTargetType = 'user' | 'furni' | 'global' | 'context'; +type AmountMode = 'constant' | 'variable'; + +interface IVariableDefinition +{ + availability: number; + hasValue: boolean; + itemId: number; + name: string; +} + +interface WiredExtraFilterByVariableViewProps +{ + target: 'user' | 'furni'; +} + +const TARGET_USER = 0; +const TARGET_FURNI = 1; +const TARGET_CONTEXT = 2; +const TARGET_GLOBAL = 3; +const AMOUNT_CONSTANT = 0; +const AMOUNT_VARIABLE = 1; +const SOURCE_TRIGGER = 0; +const SOURCE_SECONDARY_SELECTED = 101; + +const TARGET_BUTTONS: Array<{ key: VariableTargetType; icon: string; disabled?: boolean; }> = [ + { key: 'furni', icon: furniVariableIcon }, + { key: 'user', icon: userVariableIcon }, + { key: 'global', icon: globalVariableIcon }, + { key: 'context', icon: contextVariableIcon } +]; + +const SORT_OPTIONS = [ 0, 1, 2, 3, 4, 5 ]; + +const SECONDARY_FURNI_SOURCES: WiredSourceOption[] = sortWiredSourceOptions([ + { value: SOURCE_TRIGGER, label: 'wiredfurni.params.sources.furni.0' }, + { value: SOURCE_SECONDARY_SELECTED, label: 'wiredfurni.params.sources.furni.101' }, + { value: 200, label: 'wiredfurni.params.sources.furni.200' }, + { value: 201, label: 'wiredfurni.params.sources.furni.201' } +], 'furni'); + +const GLOBAL_SOURCE_OPTIONS: WiredSourceOption[] = [ { value: SOURCE_TRIGGER, label: 'wiredfurni.params.sources.global' } ]; +const CONTEXT_SOURCE_OPTIONS: WiredSourceOption[] = [ { value: SOURCE_TRIGGER, label: 'Current execution' } ]; + +const parseStringData = (value: string) => (value?.length ? value.split('\t', -1) : []); + +const normalizeTargetType = (value: number): VariableTargetType => +{ + switch(value) + { + case TARGET_FURNI: return 'furni'; + case TARGET_GLOBAL: return 'global'; + case TARGET_CONTEXT: return 'context'; + default: return 'user'; + } +}; + +const getTargetValue = (value: VariableTargetType) => +{ + switch(value) + { + case 'furni': return TARGET_FURNI; + case 'global': return TARGET_GLOBAL; + case 'context': return TARGET_CONTEXT; + default: return TARGET_USER; + } +}; + +const getReferenceDefinitions = (targetType: VariableTargetType, userDefinitions: IVariableDefinition[], furniDefinitions: IVariableDefinition[], roomDefinitions: IVariableDefinition[], contextDefinitions: IVariableDefinition[]) => +{ + switch(targetType) + { + case 'furni': return furniDefinitions; + case 'global': return roomDefinitions; + case 'context': return contextDefinitions; + default: return userDefinitions; + } +}; + +const resolveSourceOptions = (baseOptions: WiredSourceOption[], selectedValue: number, fallbackOptions: WiredSourceOption[]) => +{ + if(!baseOptions.length) return baseOptions; + if(baseOptions.some(option => (option.value === selectedValue))) return baseOptions; + + const fallbackOption = fallbackOptions.find(option => (option.value === selectedValue)); + + return fallbackOption ? [ ...baseOptions, fallbackOption ] : baseOptions; +}; + +export const WiredExtraFilterByVariableView: FC = ({ target }) => +{ + const { trigger = null, furniIds = [], setFurniIds = null, setIntParams = null, setStringParam = null } = useWired(); + const { userVariableDefinitions = [], furniVariableDefinitions = [], roomVariableDefinitions = [], contextVariableDefinitions = [] } = useWiredTools(); + const [ variableToken, setVariableToken ] = useState(''); + const [ sortBy, setSortBy ] = useState(0); + const [ amountMode, setAmountMode ] = useState('constant'); + const [ amountInput, setAmountInput ] = useState('1'); + const [ referenceTargetType, setReferenceTargetType ] = useState('user'); + const [ referenceVariableToken, setReferenceVariableToken ] = useState(''); + const [ referenceUserSource, setReferenceUserSource ] = useState(SOURCE_TRIGGER); + const [ referenceFurniSource, setReferenceFurniSource ] = useState(SOURCE_TRIGGER); + const [ referenceFurniIds, setReferenceFurniIds ] = useState([]); + + const availableUserSources = useAvailableUserSources(trigger, USER_SOURCES); + const orderedUserSources = useMemo(() => sortWiredSourceOptions(availableUserSources, 'users'), [ availableUserSources ]); + const userSourceFallbackOptions = useMemo(() => sortWiredSourceOptions([ ...USER_SOURCES, CLICKED_USER_SOURCE ], 'users'), []); + + const mainDefinitions = target === 'user' ? userVariableDefinitions : furniVariableDefinitions; + const mainEntries = useMemo(() => buildWiredVariablePickerEntries(target as WiredVariablePickerTarget, 'filter-main', mainDefinitions), [ mainDefinitions, target ]); + const resolvedMainEntries = useMemo(() => + { + if(!variableToken) return mainEntries; + if(flattenWiredVariablePickerEntries(mainEntries).some(entry => (entry.token === variableToken))) return mainEntries; + + const fallbackEntry = createFallbackVariableEntry(target as WiredVariablePickerTarget, variableToken); + return fallbackEntry ? [ fallbackEntry, ...mainEntries ] : mainEntries; + }, [ mainEntries, target, variableToken ]); + + const referenceDefinitions = useMemo(() => getReferenceDefinitions(referenceTargetType, userVariableDefinitions, furniVariableDefinitions, roomVariableDefinitions, contextVariableDefinitions), [ contextVariableDefinitions, furniVariableDefinitions, referenceTargetType, roomVariableDefinitions, userVariableDefinitions ]); + const referenceEntries = useMemo(() => buildWiredVariablePickerEntries(referenceTargetType, 'change-reference', referenceDefinitions), [ referenceDefinitions, referenceTargetType ]); + const resolvedReferenceEntries = useMemo(() => + { + if(!referenceVariableToken) return referenceEntries; + if(flattenWiredVariablePickerEntries(referenceEntries).some(entry => (entry.token === referenceVariableToken))) return referenceEntries; + + const fallbackEntry = createFallbackVariableEntry(referenceTargetType, referenceVariableToken); + return fallbackEntry ? [ fallbackEntry, ...referenceEntries ] : referenceEntries; + }, [ referenceEntries, referenceTargetType, referenceVariableToken ]); + + const referenceSelectionEnabled = amountMode === 'variable' && referenceTargetType === 'furni' && referenceFurniSource === SOURCE_SECONDARY_SELECTED; + const selectionLimit = trigger?.maximumItemSelectionCount ?? 0; + const requiresFurni = referenceSelectionEnabled ? WiredFurniType.STUFF_SELECTION_OPTION_BY_ID : WiredFurniType.STUFF_SELECTION_OPTION_NONE; + const selectedReferenceSourceValue = referenceTargetType === 'furni' ? referenceFurniSource : ((referenceTargetType === 'global' || referenceTargetType === 'context') ? SOURCE_TRIGGER : referenceUserSource); + + const referenceSourceOptions = useMemo(() => + { + if(referenceTargetType === 'furni') return resolveSourceOptions(SECONDARY_FURNI_SOURCES, selectedReferenceSourceValue, SECONDARY_FURNI_SOURCES); + if(referenceTargetType === 'global') return GLOBAL_SOURCE_OPTIONS; + if(referenceTargetType === 'context') return CONTEXT_SOURCE_OPTIONS; + + return resolveSourceOptions(orderedUserSources, selectedReferenceSourceValue, userSourceFallbackOptions); + }, [ orderedUserSources, referenceTargetType, selectedReferenceSourceValue, userSourceFallbackOptions ]); + + useEffect(() => + { + if(!trigger) return; + + const stringParts = parseStringData(trigger.stringData); + const nextReferenceFurniIds = [ ...(trigger.selectedItems ?? []) ]; + + setVariableToken(normalizeVariableTokenFromWire((stringParts.length > 0) ? stringParts[0] : '')); + setReferenceVariableToken(normalizeVariableTokenFromWire((stringParts.length > 1) ? stringParts[1] : '')); + setSortBy((trigger.intData.length > 0) ? trigger.intData[0] : 0); + setAmountMode(((trigger.intData.length > 1) ? trigger.intData[1] : AMOUNT_CONSTANT) === AMOUNT_VARIABLE ? 'variable' : 'constant'); + setAmountInput(((trigger.intData.length > 2) ? trigger.intData[2] : 1).toString()); + setReferenceTargetType(normalizeTargetType((trigger.intData.length > 3) ? trigger.intData[3] : TARGET_USER)); + setReferenceUserSource((trigger.intData.length > 4) ? trigger.intData[4] : SOURCE_TRIGGER); + setReferenceFurniSource((trigger.intData.length > 5) ? trigger.intData[5] : (nextReferenceFurniIds.length ? SOURCE_SECONDARY_SELECTED : SOURCE_TRIGGER)); + setReferenceFurniIds(nextReferenceFurniIds); + setFurniIds(nextReferenceFurniIds); + }, [ setFurniIds, trigger ]); + + useEffect(() => + { + if(referenceSelectionEnabled) setReferenceFurniIds([ ...furniIds ]); + }, [ furniIds, referenceSelectionEnabled ]); + + useEffect(() => + { + if(referenceTargetType !== 'user') return; + if(orderedUserSources.some(option => (option.value === referenceUserSource))) return; + + setReferenceUserSource(SOURCE_TRIGGER); + }, [ orderedUserSources, referenceTargetType, referenceUserSource ]); + + const save = () => + { + const parsedAmount = parseInt(amountInput.trim(), 10); + const nextReferenceFurniIds = referenceSelectionEnabled ? [ ...furniIds ] : [ ...referenceFurniIds ]; + + setReferenceFurniIds(nextReferenceFurniIds); + setStringParam(`${ variableToken || '' }\t${ amountMode === 'variable' ? referenceVariableToken : '' }`); + setIntParams([ + sortBy, + amountMode === 'variable' ? AMOUNT_VARIABLE : AMOUNT_CONSTANT, + Number.isFinite(parsedAmount) ? parsedAmount : 0, + getTargetValue(referenceTargetType), + referenceUserSource, + referenceFurniSource + ]); + setFurniIds(referenceSelectionEnabled ? nextReferenceFurniIds : []); + }; + + const validate = () => + { + if(!variableToken) return false; + if(amountMode === 'variable' && !referenceVariableToken) return false; + + return true; + }; + + const handleReferenceTargetChange = (targetType: VariableTargetType) => + { + if(targetType === referenceTargetType) return; + + if(referenceTargetType === 'furni') setFurniIds([]); + + setReferenceTargetType(targetType); + setReferenceVariableToken(''); + }; + + return ( + +
+
+ { LocalizeText('wiredfurni.params.variables.variable_selection') } + setVariableToken(entry.token) } /> +
+ +
+ +
+ { LocalizeText('wiredfurni.params.variables.sort_by') } + +
+ +
+ +
+
{ LocalizeText('wiredfurni.params.setfilter') }
+ + +
+ + + { amountMode === 'variable' && + setReferenceVariableToken(entry.token) } /> } +
+
+ + { amountMode === 'variable' && + <> +
+ + { + if(referenceTargetType === 'furni') + { + if(referenceFurniSource === SOURCE_SECONDARY_SELECTED) setReferenceFurniIds([ ...furniIds ]); + + setReferenceFurniSource(value); + setFurniIds(value === SOURCE_SECONDARY_SELECTED ? [ ...referenceFurniIds ] : []); + return; + } + + if(referenceTargetType === 'user') setReferenceUserSource(value); + } } /> + } +
+ + ); +}; diff --git a/src/components/wired/views/extras/WiredExtraFilterFurniByVariableView.tsx b/src/components/wired/views/extras/WiredExtraFilterFurniByVariableView.tsx new file mode 100644 index 0000000..6cdb589 --- /dev/null +++ b/src/components/wired/views/extras/WiredExtraFilterFurniByVariableView.tsx @@ -0,0 +1,7 @@ +import { FC } from 'react'; +import { WiredExtraFilterByVariableView } from './WiredExtraFilterByVariableView'; + +export const WiredExtraFilterFurniByVariableView: FC<{}> = () => +{ + return ; +}; diff --git a/src/components/wired/views/extras/WiredExtraFilterUsersByVariableView.tsx b/src/components/wired/views/extras/WiredExtraFilterUsersByVariableView.tsx new file mode 100644 index 0000000..6117473 --- /dev/null +++ b/src/components/wired/views/extras/WiredExtraFilterUsersByVariableView.tsx @@ -0,0 +1,7 @@ +import { FC } from 'react'; +import { WiredExtraFilterByVariableView } from './WiredExtraFilterByVariableView'; + +export const WiredExtraFilterUsersByVariableView: FC<{}> = () => +{ + return ; +}; diff --git a/src/components/wired/views/extras/WiredExtraFurniVariableView.tsx b/src/components/wired/views/extras/WiredExtraFurniVariableView.tsx new file mode 100644 index 0000000..4b693ff --- /dev/null +++ b/src/components/wired/views/extras/WiredExtraFurniVariableView.tsx @@ -0,0 +1,8 @@ +import { FC } from 'react'; +import { LocalizeText } from '../../../../api'; +import { WiredExtraVariableView } from './WiredExtraUserVariableView'; + +export const WiredExtraFurniVariableView: FC<{}> = () => +{ + return ; +}; diff --git a/src/components/wired/views/extras/WiredExtraRoomVariableView.tsx b/src/components/wired/views/extras/WiredExtraRoomVariableView.tsx new file mode 100644 index 0000000..873dd86 --- /dev/null +++ b/src/components/wired/views/extras/WiredExtraRoomVariableView.tsx @@ -0,0 +1,86 @@ +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 { WiredExtraBaseView } from './WiredExtraBaseView'; + +const AVAILABILITY_ROOM_ACTIVE = 1; +const AVAILABILITY_PERMANENT = 10; +const AVAILABILITY_SHARED = 11; +const MAX_NAME_LENGTH = 40; + +const normalizeVariableName = (value: string) => +{ + let normalizedValue = (value ?? '').trim().replace(/[\t\r\n]/g, ''); + + if(normalizedValue.includes('=')) normalizedValue = normalizedValue.substring(0, normalizedValue.indexOf('=')).trim(); + + while(normalizedValue.startsWith('@') || normalizedValue.startsWith('~')) + { + normalizedValue = normalizedValue.substring(1).trim(); + } + + normalizedValue = normalizedValue.replace(/[^A-Za-z0-9_]/g, ''); + + return normalizedValue.slice(0, MAX_NAME_LENGTH); +}; + +export const WiredExtraRoomVariableView: FC<{}> = () => +{ + const { trigger = null, setIntParams = null, setStringParam = null } = useWired(); + const [ variableName, setVariableName ] = useState(''); + const [ availability, setAvailability ] = useState(AVAILABILITY_ROOM_ACTIVE); + const [ currentValue, setCurrentValue ] = useState(0); + + const normalizedCurrentValue = useMemo(() => (Number.isFinite(currentValue) ? currentValue : 0), [ currentValue ]); + + useEffect(() => + { + if(!trigger) return; + + setVariableName(normalizeVariableName(trigger.stringData)); + const nextAvailability = (trigger.intData.length > 0) ? trigger.intData[0] : AVAILABILITY_ROOM_ACTIVE; + + setAvailability((nextAvailability === AVAILABILITY_PERMANENT || nextAvailability === AVAILABILITY_SHARED) ? nextAvailability : AVAILABILITY_ROOM_ACTIVE); + setCurrentValue((trigger.intData.length > 1) ? trigger.intData[1] : 0); + }, [ trigger ]); + + const save = () => + { + setStringParam(normalizeVariableName(variableName)); + setIntParams([ availability, normalizedCurrentValue ]); + }; + + return ( + +
+
+ { LocalizeText('wiredfurni.params.variables.variable_name') } + setVariableName(normalizeVariableName(event.target.value)) } /> +
+ +
+ { LocalizeText('wiredfurni.params.variables.availability') } + + + +
+ +
+ { LocalizeText('wiredfurni.params.variables.inspection') } + { LocalizeText('wiredfurni.params.variables.inspection.current_value', [ 'value' ], [ normalizedCurrentValue.toString() ]) } +
+
+
+ ); +}; diff --git a/src/components/wired/views/extras/WiredExtraTextInputVariableView.tsx b/src/components/wired/views/extras/WiredExtraTextInputVariableView.tsx new file mode 100644 index 0000000..41ac5e3 --- /dev/null +++ b/src/components/wired/views/extras/WiredExtraTextInputVariableView.tsx @@ -0,0 +1,155 @@ +import { FC, useEffect, useMemo, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Text } from '../../../../common'; +import { useWired, useWiredTools } from '../../../../hooks'; +import { NitroInput } from '../../../../layout'; +import { WiredVariablePicker } from '../WiredVariablePicker'; +import { buildWiredVariablePickerEntries, createFallbackVariableEntry, flattenWiredVariablePickerEntries, getCustomVariableItemId, isCustomVariableToken, normalizeVariableTokenFromWire } from '../WiredVariablePickerData'; +import { WiredExtraBaseView } from './WiredExtraBaseView'; +import { WiredPlaceholderPreview } from './WiredPlaceholderPreview'; + +interface IVariableDefinition +{ + hasValue: boolean; + isTextConnected: boolean; + itemId: number; + name: string; +} + +const DISPLAY_NUMERIC = 1; +const DISPLAY_TEXTUAL = 2; +const DEFAULT_CAPTURER_NAME = ''; +const MAX_CAPTURER_NAME_LENGTH = 32; +const PLACEHOLDER_WRAPPER_PATTERN = /^#(.*)#$/; + +const splitStringData = (value: string) => +{ + if(!value?.length) return [ '', DEFAULT_CAPTURER_NAME ]; + + const parts = value.split('\t'); + + if(parts.length === 1) return [ parts[0], DEFAULT_CAPTURER_NAME ]; + + return [ parts[0], parts[1] ]; +}; + +const normalizeDisplayType = (value: number) => ((value === DISPLAY_TEXTUAL) ? DISPLAY_TEXTUAL : DISPLAY_NUMERIC); +const normalizeCapturerName = (value: string) => +{ + let normalizedValue = (value ?? '').trim().replace(/[\t\r\n]/g, ''); + + if(PLACEHOLDER_WRAPPER_PATTERN.test(normalizedValue)) + { + normalizedValue = normalizedValue.substring(1, normalizedValue.length - 1).trim(); + } + + return normalizedValue.slice(0, MAX_CAPTURER_NAME_LENGTH); +}; + +const escapeHtml = (value: string) => value + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + +export const WiredExtraTextInputVariableView: FC<{}> = () => +{ + const { trigger = null, setIntParams = null, setStringParam = null } = useWired(); + const { contextVariableDefinitions = [] } = useWiredTools(); + const [ variableToken, setVariableToken ] = useState(''); + const [ capturerName, setCapturerName ] = useState(DEFAULT_CAPTURER_NAME); + const [ displayType, setDisplayType ] = useState(DISPLAY_NUMERIC); + + const targetDefinitions = useMemo(() => contextVariableDefinitions.filter(definition => !!definition.hasValue), [ contextVariableDefinitions ]); + const variableEntries = useMemo(() => buildWiredVariablePickerEntries('context', 'change-reference', targetDefinitions).filter(entry => (entry.kind === 'custom')), [ targetDefinitions ]); + const resolvedVariableEntries = useMemo(() => + { + if(!variableToken || !isCustomVariableToken(variableToken)) return variableEntries; + if(flattenWiredVariablePickerEntries(variableEntries).some(entry => (entry.token === variableToken))) return variableEntries; + + const fallbackEntry = createFallbackVariableEntry('context', variableToken); + + return (fallbackEntry && (fallbackEntry.kind === 'custom')) ? [ fallbackEntry, ...variableEntries ] : variableEntries; + }, [ variableEntries, variableToken ]); + + const selectedVariableDefinition = useMemo(() => + { + if(!isCustomVariableToken(variableToken)) return null; + + const itemId = getCustomVariableItemId(variableToken); + + return (targetDefinitions.find(definition => (definition.itemId === itemId)) ?? null) as IVariableDefinition | null; + }, [ targetDefinitions, variableToken ]); + + const canUseTextDisplay = !!selectedVariableDefinition?.isTextConnected; + + useEffect(() => + { + if(!trigger) return; + + const [ nextVariableToken, nextCapturerName ] = splitStringData(trigger.stringData); + + setVariableToken(normalizeVariableTokenFromWire(nextVariableToken)); + setCapturerName(normalizeCapturerName(nextCapturerName)); + setDisplayType(normalizeDisplayType((trigger.intData.length > 0) ? trigger.intData[0] : DISPLAY_NUMERIC)); + }, [ trigger ]); + + useEffect(() => + { + if(canUseTextDisplay || (displayType !== DISPLAY_TEXTUAL)) return; + + setDisplayType(DISPLAY_NUMERIC); + }, [ canUseTextDisplay, displayType ]); + + const previewToken = useMemo(() => + { + const effectiveName = normalizeCapturerName(capturerName) || 'capturer'; + + return `#${ effectiveName }#`; + }, [ capturerName ]); + + const previewHtml = useMemo(() => LocalizeText('wiredfurni.params.texts.placeholder_preview', [ 'placeholder' ], [ escapeHtml(previewToken) ]), [ previewToken ]); + + const save = () => + { + const variableItemId = getCustomVariableItemId(variableToken); + + setIntParams([ canUseTextDisplay ? normalizeDisplayType(displayType) : DISPLAY_NUMERIC ]); + setStringParam(`${ variableItemId ? String(variableItemId) : '' }\t${ normalizeCapturerName(capturerName) }`); + }; + + const validate = () => !!normalizeCapturerName(capturerName).length && (getCustomVariableItemId(variableToken) > 0); + + return ( + +
+
+ { LocalizeText('wiredfurni.params.texts.capturer_name') } + setCapturerName(normalizeCapturerName(event.target.value)) } /> +
+ + + +
+ { LocalizeText('wiredfurni.params.variables.variable_selection') } + setVariableToken(entry.token) } /> + { !targetDefinitions.length && { 'No wf_var_context variables with value found in this room.' } } +
+ +
+ { LocalizeText('wiredfurni.params.texts.variable_input_type') } + + + { LocalizeText('wiredfurni.params.texts.variable_display_type.2.info') } +
+
+
+ ); +}; diff --git a/src/components/wired/views/extras/WiredExtraTextOutputFurniNameView.tsx b/src/components/wired/views/extras/WiredExtraTextOutputFurniNameView.tsx index 95a5234..0e613be 100644 --- a/src/components/wired/views/extras/WiredExtraTextOutputFurniNameView.tsx +++ b/src/components/wired/views/extras/WiredExtraTextOutputFurniNameView.tsx @@ -5,6 +5,7 @@ import { useWired } from '../../../../hooks'; import { NitroInput } from '../../../../layout'; import { WiredSourcesSelector } from '../WiredSourcesSelector'; import { WiredExtraBaseView } from './WiredExtraBaseView'; +import { WiredPlaceholderPreview } from './WiredPlaceholderPreview'; const TYPE_SINGLE = 1; const TYPE_MULTIPLE = 2; @@ -100,7 +101,7 @@ export const WiredExtraTextOutputFurniNameView: FC<{}> = () => { LocalizeText('wiredfurni.params.texts.placeholder_name') } setPlaceholderName(normalizePlaceholderName(event.target.value)) } />
- +
{ LocalizeText('wiredfurni.params.texts.placeholder_type') }
- +
{ LocalizeText('wiredfurni.params.texts.placeholder_type') }