From 31d3dfea44a4de19a64dc083e8fca6ea67710872 Mon Sep 17 00:00:00 2001 From: Lorenzune Date: Mon, 16 Mar 2026 15:12:42 +0100 Subject: [PATCH 01/10] feat(wired): update source-aware furni and signal UI --- .../views/actions/WiredActionBotMoveView.tsx | 22 +- .../actions/WiredActionBotTeleportView.tsx | 22 +- .../WiredActionCallAnotherStackView.tsx | 22 +- .../views/actions/WiredActionChaseView.tsx | 22 +- .../views/actions/WiredActionFleeView.tsx | 22 +- .../WiredActionMoveAndRotateFurniView.tsx | 22 +- .../actions/WiredActionMoveFurniToView.tsx | 22 +- .../actions/WiredActionMoveFurniView.tsx | 22 +- .../actions/WiredActionSendSignalView.tsx | 320 ++++++++++-------- .../WiredActionSetFurniStateToView.tsx | 22 +- .../views/actions/WiredActionTeleportView.tsx | 22 +- .../WiredActionToggleFurniStateView.tsx | 22 +- .../WiredConditionActorIsOnFurniView.tsx | 22 +- .../WiredConditionFurniHasAvatarOnView.tsx | 22 +- .../WiredConditionFurniHasFurniOnView.tsx | 22 +- .../WiredConditionFurniHasNotFurniOnView.tsx | 22 +- .../WiredConditionFurniIsOfTypeView.tsx | 22 +- ...WiredConditionFurniMatchesSnapshotView.tsx | 22 +- .../WiredSelectorFurniByTypeView.tsx | 59 +--- ...iredTriggerExecutePeriodicallyLongView.tsx | 2 +- .../WiredTriggerExecutePeriodicallyView.tsx | 2 +- .../WiredTriggerReceiveSignalView.tsx | 13 +- src/hooks/wired/useWired.ts | 85 ++++- 23 files changed, 339 insertions(+), 516 deletions(-) diff --git a/src/components/wired/views/actions/WiredActionBotMoveView.tsx b/src/components/wired/views/actions/WiredActionBotMoveView.tsx index 14a3d47..d07bc7b 100644 --- a/src/components/wired/views/actions/WiredActionBotMoveView.tsx +++ b/src/components/wired/views/actions/WiredActionBotMoveView.tsx @@ -1,5 +1,5 @@ import { FC, useEffect, useState } from 'react'; -import { LocalizeText, WiredFurniType, WiredSelectionVisualizer } from '../../../../api'; +import { LocalizeText, WiredFurniType } from '../../../../api'; import { Text } from '../../../../common'; import { useWired } from '../../../../hooks'; import { NitroInput } from '../../../../layout'; @@ -9,7 +9,7 @@ import { WiredSourcesSelector } from '../WiredSourcesSelector'; export const WiredActionBotMoveView: FC<{}> = props => { const [ botName, setBotName ] = useState(''); - const { trigger = null, furniIds = [], setFurniIds = null, setStringParam = null, setIntParams = null } = useWired(); + const { trigger = null, setStringParam = null, setIntParams = null } = useWired(); const [ furniSource, setFurniSource ] = useState(() => { @@ -33,23 +33,9 @@ export const WiredActionBotMoveView: FC<{}> = props => else setFurniSource((trigger.selectedItems?.length ?? 0) > 0 ? 100 : 0); }, [ trigger ]); - const onChangeFurniSource = (next: number) => - { - if(furniIds.length && setFurniIds) - { - setFurniIds(prev => - { - if(prev && prev.length) WiredSelectionVisualizer.clearSelectionShaderFromFurni(prev); - return []; - }); - } + const onChangeFurniSource = (next: number) => setFurniSource(next); - setFurniSource(next); - }; - - const requiresFurni = (furniSource === 100) - ? WiredFurniType.STUFF_SELECTION_OPTION_BY_ID - : WiredFurniType.STUFF_SELECTION_OPTION_NONE; + const requiresFurni = WiredFurniType.STUFF_SELECTION_OPTION_BY_ID; return ( = props => { const [ botName, setBotName ] = useState(''); - const { trigger = null, furniIds = [], setFurniIds = null, setStringParam = null, setIntParams = null } = useWired(); + const { trigger = null, setStringParam = null, setIntParams = null } = useWired(); const [ furniSource, setFurniSource ] = useState(() => { @@ -33,23 +33,9 @@ export const WiredActionBotTeleportView: FC<{}> = props => else setFurniSource((trigger.selectedItems?.length ?? 0) > 0 ? 100 : 0); }, [ trigger ]); - const onChangeFurniSource = (next: number) => - { - if(furniIds.length && setFurniIds) - { - setFurniIds(prev => - { - if(prev && prev.length) WiredSelectionVisualizer.clearSelectionShaderFromFurni(prev); - return []; - }); - } + const onChangeFurniSource = (next: number) => setFurniSource(next); - setFurniSource(next); - }; - - const requiresFurni = (furniSource === 100) - ? WiredFurniType.STUFF_SELECTION_OPTION_BY_ID - : WiredFurniType.STUFF_SELECTION_OPTION_NONE; + const requiresFurni = WiredFurniType.STUFF_SELECTION_OPTION_BY_ID; return ( = props => { - const { trigger = null, furniIds = [], setFurniIds = null, setIntParams = null } = useWired(); + const { trigger = null, setIntParams = null } = useWired(); const [ furniSource, setFurniSource ] = useState(() => { if(trigger?.intData?.length >= 1) return trigger.intData[0]; @@ -21,25 +21,11 @@ export const WiredActionCallAnotherStackView: FC<{}> = props => else setFurniSource((trigger.selectedItems?.length ?? 0) > 0 ? 100 : 0); }, [ trigger ]); - const onChangeFurniSource = (next: number) => - { - if(furniIds.length && setFurniIds) - { - setFurniIds(prev => - { - if(prev && prev.length) WiredSelectionVisualizer.clearSelectionShaderFromFurni(prev); - return []; - }); - } - - setFurniSource(next); - }; + const onChangeFurniSource = (next: number) => setFurniSource(next); const save = () => setIntParams([ furniSource ]); - const requiresFurni = (furniSource === 100) - ? WiredFurniType.STUFF_SELECTION_OPTION_BY_ID_BY_TYPE_OR_FROM_CONTEXT - : WiredFurniType.STUFF_SELECTION_OPTION_NONE; + const requiresFurni = WiredFurniType.STUFF_SELECTION_OPTION_BY_ID_BY_TYPE_OR_FROM_CONTEXT; return ( = props => { - const { trigger = null, furniIds = [], setFurniIds = null, setIntParams = null } = useWired(); + const { trigger = null, setIntParams = null } = useWired(); const [ furniSource, setFurniSource ] = useState(() => { if(trigger?.intData?.length >= 1) return trigger.intData[0]; @@ -21,25 +21,11 @@ export const WiredActionChaseView: FC<{}> = props => else setFurniSource((trigger.selectedItems?.length ?? 0) > 0 ? 100 : 0); }, [ trigger ]); - const onChangeFurniSource = (next: number) => - { - if(furniIds.length && setFurniIds) - { - setFurniIds(prev => - { - if(prev && prev.length) WiredSelectionVisualizer.clearSelectionShaderFromFurni(prev); - return []; - }); - } - - setFurniSource(next); - }; + const onChangeFurniSource = (next: number) => setFurniSource(next); const save = () => setIntParams([ furniSource ]); - const requiresFurni = (furniSource === 100) - ? WiredFurniType.STUFF_SELECTION_OPTION_BY_ID_BY_TYPE_OR_FROM_CONTEXT - : WiredFurniType.STUFF_SELECTION_OPTION_NONE; + const requiresFurni = WiredFurniType.STUFF_SELECTION_OPTION_BY_ID_BY_TYPE_OR_FROM_CONTEXT; return ( = props => { - const { trigger = null, furniIds = [], setFurniIds = null, setIntParams = null } = useWired(); + const { trigger = null, setIntParams = null } = useWired(); const [ furniSource, setFurniSource ] = useState(() => { if(trigger?.intData?.length >= 1) return trigger.intData[0]; @@ -21,25 +21,11 @@ export const WiredActionFleeView: FC<{}> = props => else setFurniSource((trigger.selectedItems?.length ?? 0) > 0 ? 100 : 0); }, [ trigger ]); - const onChangeFurniSource = (next: number) => - { - if(furniIds.length && setFurniIds) - { - setFurniIds(prev => - { - if(prev && prev.length) WiredSelectionVisualizer.clearSelectionShaderFromFurni(prev); - return []; - }); - } - - setFurniSource(next); - }; + const onChangeFurniSource = (next: number) => setFurniSource(next); const save = () => setIntParams([ furniSource ]); - const requiresFurni = (furniSource === 100) - ? WiredFurniType.STUFF_SELECTION_OPTION_BY_ID_BY_TYPE_OR_FROM_CONTEXT - : WiredFurniType.STUFF_SELECTION_OPTION_NONE; + const requiresFurni = WiredFurniType.STUFF_SELECTION_OPTION_BY_ID_BY_TYPE_OR_FROM_CONTEXT; return ( = props => { const [ movement, setMovement ] = useState(-1); const [ rotation, setRotation ] = useState(-1); - const { trigger = null, furniIds = [], setFurniIds = null, setIntParams = null } = useWired(); + const { trigger = null, setIntParams = null } = useWired(); const [ furniSource, setFurniSource ] = useState(() => { if(trigger?.intData?.length > 2) return trigger.intData[2]; @@ -56,23 +56,9 @@ export const WiredActionMoveAndRotateFurniView: FC<{}> = props => else setFurniSource((trigger.selectedItems?.length ?? 0) > 0 ? 100 : 0); }, [ trigger ]); - const onChangeFurniSource = (next: number) => - { - if(furniIds.length && setFurniIds) - { - setFurniIds(prev => - { - if(prev && prev.length) WiredSelectionVisualizer.clearSelectionShaderFromFurni(prev); - return []; - }); - } + const onChangeFurniSource = (next: number) => setFurniSource(next); - setFurniSource(next); - }; - - const requiresFurni = (furniSource === 100) - ? WiredFurniType.STUFF_SELECTION_OPTION_BY_ID_BY_TYPE_OR_FROM_CONTEXT - : WiredFurniType.STUFF_SELECTION_OPTION_NONE; + const requiresFurni = WiredFurniType.STUFF_SELECTION_OPTION_BY_ID_BY_TYPE_OR_FROM_CONTEXT; return ( = props => { const [ spacing, setSpacing ] = useState(-1); const [ movement, setMovement ] = useState(-1); - const { trigger = null, furniIds = [], setFurniIds = null, setIntParams = null } = useWired(); + const { trigger = null, setIntParams = null } = useWired(); const [ furniSource, setFurniSource ] = useState(() => { if(trigger?.intData?.length > 2) return trigger.intData[2]; @@ -54,23 +54,9 @@ export const WiredActionMoveFurniToView: FC<{}> = props => else setFurniSource((trigger.selectedItems?.length ?? 0) > 0 ? 100 : 0); }, [ trigger ]); - const onChangeFurniSource = (next: number) => - { - if(furniIds.length && setFurniIds) - { - setFurniIds(prev => - { - if(prev && prev.length) WiredSelectionVisualizer.clearSelectionShaderFromFurni(prev); - return []; - }); - } + const onChangeFurniSource = (next: number) => setFurniSource(next); - setFurniSource(next); - }; - - const requiresFurni = (furniSource === 100) - ? WiredFurniType.STUFF_SELECTION_OPTION_BY_ID_OR_BY_TYPE - : WiredFurniType.STUFF_SELECTION_OPTION_NONE; + const requiresFurni = WiredFurniType.STUFF_SELECTION_OPTION_BY_ID_OR_BY_TYPE; return ( = props => { const [ movement, setMovement ] = useState(-1); const [ rotation, setRotation ] = useState(-1); - const { trigger = null, furniIds = [], setFurniIds = null, setIntParams = null } = useWired(); + const { trigger = null, setIntParams = null } = useWired(); const [ furniSource, setFurniSource ] = useState(() => { if(trigger?.intData?.length > 2) return trigger.intData[2]; @@ -68,23 +68,9 @@ export const WiredActionMoveFurniView: FC<{}> = props => else setFurniSource((trigger.selectedItems?.length ?? 0) > 0 ? 100 : 0); }, [ trigger ]); - const onChangeFurniSource = (next: number) => - { - if(furniIds.length && setFurniIds) - { - setFurniIds(prev => - { - if(prev && prev.length) WiredSelectionVisualizer.clearSelectionShaderFromFurni(prev); - return []; - }); - } + const onChangeFurniSource = (next: number) => setFurniSource(next); - setFurniSource(next); - }; - - const requiresFurni = (furniSource === 100) - ? WiredFurniType.STUFF_SELECTION_OPTION_BY_ID_BY_TYPE_OR_FROM_CONTEXT - : WiredFurniType.STUFF_SELECTION_OPTION_NONE; + const requiresFurni = WiredFurniType.STUFF_SELECTION_OPTION_BY_ID_BY_TYPE_OR_FROM_CONTEXT; return ( +{ + if(!data || !data.length) return []; + + const ids = new Set(); + + for(const part of data.split(/[;,\t]/)) + { + const trimmed = part.trim(); + if(!trimmed.length) continue; + + const value = parseInt(trimmed, 10); + if(!isNaN(value) && value > 0) ids.add(value); + } + + return Array.from(ids); +}; + +const serializeForwardIds = (ids: number[]): string => +{ + if(!ids || !ids.length) return ''; + + return ids.filter(id => (id > 0)).join(FORWARD_ITEM_DELIMITER); +}; export const WiredActionSendSignalView: FC<{}> = () => { - const [ antennaSource, setAntennaSource ] = useState(ANTENNA_PICKED); - const [ furniForward, setFurniForward ] = useState(FORWARD_NONE); - const [ userForward, setUserForward ] = useState(FORWARD_NONE); + const [ furniSource, setFurniSource ] = useState(SOURCE_TRIGGER); + const [ userSource, setUserSource ] = useState(SOURCE_TRIGGER); const [ signalPerFurni, setSignalPerFurni ] = useState(false); const [ signalPerUser, setSignalPerUser ] = useState(false); - const [ showAdvanced, setShowAdvanced ] = useState(false); + const [ antennaIds, setAntennaIds ] = useState([]); + const [ forwardFurniIds, setForwardFurniIds ] = useState([]); + const [ selectionMode, setSelectionMode ] = useState('antenna'); - const { trigger = null, setIntParams } = useWired(); + const { trigger = null, furniIds = [], setFurniIds = null, setIntParams = null, setStringParam = null, setAllowedInteractionTypes = null } = useWired(); useEffect(() => { if(!trigger) return; const p = trigger.intData; - if(p.length >= 1) setAntennaSource(p[0]); - if(p.length >= 2) setFurniForward(p[1]); - if(p.length >= 3) setUserForward(p[2]); - if(p.length >= 4) setSignalPerFurni(p[3] === 1); - if(p.length >= 5) setSignalPerUser(p[4] === 1); + if(p.length > 1) setFurniSource(p[1]); + else setFurniSource(SOURCE_TRIGGER); - if(p.length >= 1 && (p[0] !== ANTENNA_PICKED || p[1] !== FORWARD_NONE || - p[2] !== FORWARD_NONE || (p.length >= 4 && p[3] === 1) || (p.length >= 5 && p[4] === 1))) - { - setShowAdvanced(true); - } + if(p.length > 2) setUserSource(p[2]); + else setUserSource(SOURCE_TRIGGER); + + setSignalPerFurni(p.length > 3 && p[3] === 1); + setSignalPerUser(p.length > 4 && p[4] === 1); + + setAntennaIds(trigger.selectedItems ?? []); + setForwardFurniIds(parseForwardIds(trigger.stringData)); + 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); + else setForwardFurniIds(furniIds); + }, [ furniIds, selectionMode ]); + + const applySelection = useCallback((nextIds: number[]) => + { + if(!setFurniIds) return; + + setFurniIds(prev => + { + if(prev && prev.length) WiredSelectionVisualizer.clearSelectionShaderFromFurni(prev); + if(nextIds && nextIds.length) WiredSelectionVisualizer.applySelectionShaderToFurni(nextIds); + + return [ ...nextIds ]; + }); + }, [ setFurniIds ]); + + const switchSelection = useCallback((mode: SelectionMode) => + { + if(mode === selectionMode) return; + if(mode === 'furni' && furniSource !== SOURCE_SELECTED) return; + + const nextIds = (mode === 'antenna') ? antennaIds : forwardFurniIds; + applySelection(nextIds); + setSelectionMode(mode); + }, [ selectionMode, furniSource, antennaIds, forwardFurniIds, applySelection ]); + + const onChangeFurniSource = (next: number) => + { + if(forwardFurniIds.length) setForwardFurniIds([]); + + if(selectionMode === 'furni') + { + applySelection(antennaIds); + setSelectionMode('antenna'); + } + + setFurniSource(next); + }; + const save = useCallback(() => { + if(selectionMode === 'furni') + { + setSelectionMode('antenna'); + applySelection(antennaIds); + } + + const antennaSource = (antennaIds && antennaIds.length) ? antennaIds[0] : 0; + setIntParams([ antennaSource, - furniForward, - userForward, + furniSource, + userSource, signalPerFurni ? 1 : 0, signalPerUser ? 1 : 0, + 0, ]); - }, [ antennaSource, furniForward, userForward, signalPerFurni, signalPerUser, setIntParams ]); + + setStringParam(serializeForwardIds(forwardFurniIds)); + }, [ selectionMode, antennaIds, furniSource, userSource, signalPerFurni, signalPerUser, forwardFurniIds, setIntParams, setStringParam, applySelection, setSelectionMode ]); + + const selectionLimit = trigger?.maximumItemSelectionCount ?? 0; + const forwardSelectionEnabled = (furniSource === SOURCE_SELECTED); return ( -
- -
setShowAdvanced(!showAdvanced) }> - - { showAdvanced - ? LocalizeText('wiredfurni.params.hide_advanced') - : LocalizeText('wiredfurni.params.show_advanced') } - + cardStyle={ { width: '400px' } } + save={ save } + footer={ ( + + ) }> +
+
+ Antenne selezionate +
+ + { selectionLimit ? `${ antennaIds.length }/${ selectionLimit }` : antennaIds.length } +
+
+
+ Furni selezionati +
+ + { selectionLimit ? `${ forwardFurniIds.length }/${ selectionLimit }` : forwardFurniIds.length } +
- { showAdvanced && <> - - { /* --- Antennas --- */ } - { LocalizeText('wiredfurni.params.sources.furni.title.signal_antenna') } -
- setAntennaSource(antennaSource === ANTENNA_PICKED ? ANTENNA_TRIGGER : ANTENNA_PICKED) } /> - -
- - { /* --- Furni to forward --- */ } - { LocalizeText('wiredfurni.params.sources.furni.title.signal_forward') } -
-
- setFurniForward(e.target.checked ? FORWARD_TRIGGER : FORWARD_NONE) } /> - -
- { furniForward !== FORWARD_NONE && - - } -
- - { /* --- Users to forward --- */ } - { LocalizeText('wiredfurni.params.sources.users.title.signal_forward') } -
-
- setUserForward(e.target.checked ? FORWARD_TRIGGER : FORWARD_NONE) } /> - -
- { userForward !== FORWARD_NONE && - - } -
- - { LocalizeText('wiredfurni.params.signal.options') } -
- setSignalPerFurni(e.target.checked) } /> - -
-
- setSignalPerUser(e.target.checked) } /> - -
- } - + { LocalizeText('wiredfurni.params.signal.options') } +
+ setSignalPerFurni(e.target.checked) } /> + +
+
+ setSignalPerUser(e.target.checked) } /> + +
); diff --git a/src/components/wired/views/actions/WiredActionSetFurniStateToView.tsx b/src/components/wired/views/actions/WiredActionSetFurniStateToView.tsx index f7ee4c3..8f31754 100644 --- a/src/components/wired/views/actions/WiredActionSetFurniStateToView.tsx +++ b/src/components/wired/views/actions/WiredActionSetFurniStateToView.tsx @@ -1,5 +1,5 @@ import { FC, useEffect, useState } from 'react'; -import { LocalizeText, WiredFurniType, WiredSelectionVisualizer } from '../../../../api'; +import { LocalizeText, WiredFurniType } from '../../../../api'; import { Text } from '../../../../common'; import { useWired } from '../../../../hooks'; import { WiredActionBaseView } from './WiredActionBaseView'; @@ -10,7 +10,7 @@ export const WiredActionSetFurniStateToView: FC<{}> = props => const [ stateFlag, setStateFlag ] = useState(0); const [ directionFlag, setDirectionFlag ] = useState(0); const [ positionFlag, setPositionFlag ] = useState(0); - const { trigger = null, furniIds = [], setFurniIds = null, setIntParams = null } = useWired(); + const { trigger = null, setIntParams = null } = useWired(); const [ furniSource, setFurniSource ] = useState(() => { if(trigger?.intData?.length > 3) return trigger.intData[3]; @@ -29,23 +29,9 @@ export const WiredActionSetFurniStateToView: FC<{}> = props => else setFurniSource((trigger.selectedItems?.length ?? 0) > 0 ? 100 : 0); }, [ trigger ]); - const onChangeFurniSource = (next: number) => - { - if(furniIds.length && setFurniIds) - { - setFurniIds(prev => - { - if(prev && prev.length) WiredSelectionVisualizer.clearSelectionShaderFromFurni(prev); - return []; - }); - } + const onChangeFurniSource = (next: number) => setFurniSource(next); - setFurniSource(next); - }; - - const requiresFurni = (furniSource === 100) - ? WiredFurniType.STUFF_SELECTION_OPTION_BY_ID - : WiredFurniType.STUFF_SELECTION_OPTION_NONE; + const requiresFurni = WiredFurniType.STUFF_SELECTION_OPTION_BY_ID; return ( = props => { - const { trigger = null, furniIds = [], setFurniIds = null, setIntParams = null } = useWired(); + const { trigger = null, setIntParams = null } = useWired(); const [ furniSource, setFurniSource ] = useState(() => { @@ -31,25 +31,11 @@ export const WiredActionTeleportView: FC<{}> = props => else setUserSource(0); }, [ trigger ]); - const onChangeFurniSource = (next: number) => - { - if(furniIds.length && setFurniIds) - { - setFurniIds(prev => - { - if(prev && prev.length) WiredSelectionVisualizer.clearSelectionShaderFromFurni(prev); - return []; - }); - } - - setFurniSource(next); - }; + const onChangeFurniSource = (next: number) => setFurniSource(next); const save = () => setIntParams([ furniSource, userSource ]); - const requiresFurni = (furniSource === 100) - ? WiredFurniType.STUFF_SELECTION_OPTION_BY_ID_BY_TYPE_OR_FROM_CONTEXT - : WiredFurniType.STUFF_SELECTION_OPTION_NONE; + const requiresFurni = WiredFurniType.STUFF_SELECTION_OPTION_BY_ID_BY_TYPE_OR_FROM_CONTEXT; return ( = props => { - const { trigger = null, furniIds = [], setFurniIds = null, setIntParams = null } = useWired(); + const { trigger = null, setIntParams = null } = useWired(); const [ furniSource, setFurniSource ] = useState(() => { if(trigger?.intData?.length >= 1) return trigger.intData[0]; @@ -21,25 +21,11 @@ export const WiredActionToggleFurniStateView: FC<{}> = props => else setFurniSource((trigger.selectedItems?.length ?? 0) > 0 ? 100 : 0); }, [ trigger ]); - const onChangeFurniSource = (next: number) => - { - if(furniIds.length && setFurniIds) - { - setFurniIds(prev => - { - if(prev && prev.length) WiredSelectionVisualizer.clearSelectionShaderFromFurni(prev); - return []; - }); - } - - setFurniSource(next); - }; + const onChangeFurniSource = (next: number) => setFurniSource(next); const save = () => setIntParams([ furniSource ]); - const requiresFurni = (furniSource === 100) - ? WiredFurniType.STUFF_SELECTION_OPTION_BY_ID_BY_TYPE_OR_FROM_CONTEXT - : WiredFurniType.STUFF_SELECTION_OPTION_NONE; + const requiresFurni = WiredFurniType.STUFF_SELECTION_OPTION_BY_ID_BY_TYPE_OR_FROM_CONTEXT; return ( = props => { - const { trigger = null, furniIds = [], setFurniIds = null, setIntParams = null } = useWired(); + const { trigger = null, setIntParams = null } = useWired(); const [ furniSource, setFurniSource ] = useState(() => { if(trigger?.intData?.length > 0) return trigger.intData[0]; @@ -29,25 +29,11 @@ export const WiredConditionActorIsOnFurniView: FC<{}> = props => else setUserSource(0); }, [ trigger ]); - const onChangeFurniSource = (next: number) => - { - if(furniIds.length && setFurniIds) - { - setFurniIds(prev => - { - if(prev && prev.length) WiredSelectionVisualizer.clearSelectionShaderFromFurni(prev); - return []; - }); - } - - setFurniSource(next); - }; + const onChangeFurniSource = (next: number) => setFurniSource(next); const save = () => setIntParams([ furniSource, userSource ]); - const requiresFurni = (furniSource === 100) - ? WiredFurniType.STUFF_SELECTION_OPTION_BY_ID - : WiredFurniType.STUFF_SELECTION_OPTION_NONE; + const requiresFurni = WiredFurniType.STUFF_SELECTION_OPTION_BY_ID; return ( = props => { - const { trigger = null, furniIds = [], setFurniIds = null, setIntParams = null } = useWired(); + const { trigger = null, setIntParams = null } = useWired(); const [ furniSource, setFurniSource ] = useState(() => { if(trigger?.intData?.length >= 1) return trigger.intData[0]; @@ -20,25 +20,11 @@ export const WiredConditionFurniHasAvatarOnView: FC<{}> = props => else setFurniSource((trigger.selectedItems?.length ?? 0) > 0 ? 100 : 0); }, [ trigger ]); - const onChangeFurniSource = (next: number) => - { - if(furniIds.length && setFurniIds) - { - setFurniIds(prev => - { - if(prev && prev.length) WiredSelectionVisualizer.clearSelectionShaderFromFurni(prev); - return []; - }); - } - - setFurniSource(next); - }; + const onChangeFurniSource = (next: number) => setFurniSource(next); const save = () => setIntParams([ furniSource ]); - const requiresFurni = (furniSource === 100) - ? WiredFurniType.STUFF_SELECTION_OPTION_BY_ID - : WiredFurniType.STUFF_SELECTION_OPTION_NONE; + const requiresFurni = WiredFurniType.STUFF_SELECTION_OPTION_BY_ID; return ( = props => { const [ requireAll, setRequireAll ] = useState(-1); - const { trigger = null, furniIds = [], setFurniIds = null, setIntParams = null } = useWired(); + const { trigger = null, setIntParams = null } = useWired(); const [ furniSource, setFurniSource ] = useState(() => { if(trigger?.intData?.length > 1) return trigger.intData[1]; @@ -24,23 +24,9 @@ export const WiredConditionFurniHasFurniOnView: FC<{}> = props => else setFurniSource((trigger.selectedItems?.length ?? 0) > 0 ? 100 : 0); }, [ trigger ]); - const onChangeFurniSource = (next: number) => - { - if(furniIds.length && setFurniIds) - { - setFurniIds(prev => - { - if(prev && prev.length) WiredSelectionVisualizer.clearSelectionShaderFromFurni(prev); - return []; - }); - } + const onChangeFurniSource = (next: number) => setFurniSource(next); - setFurniSource(next); - }; - - const requiresFurni = (furniSource === 100) - ? WiredFurniType.STUFF_SELECTION_OPTION_BY_ID - : WiredFurniType.STUFF_SELECTION_OPTION_NONE; + const requiresFurni = WiredFurniType.STUFF_SELECTION_OPTION_BY_ID; return ( = props => { const [ requireAll, setRequireAll ] = useState(-1); - const { trigger = null, furniIds = [], setFurniIds = null, setIntParams = null } = useWired(); + const { trigger = null, setIntParams = null } = useWired(); const [ furniSource, setFurniSource ] = useState(() => { if(trigger?.intData?.length > 1) return trigger.intData[1]; @@ -24,23 +24,9 @@ export const WiredConditionFurniHasNotFurniOnView: FC<{}> = props => else setFurniSource((trigger.selectedItems?.length ?? 0) > 0 ? 100 : 0); }, [ trigger ]); - const onChangeFurniSource = (next: number) => - { - if(furniIds.length && setFurniIds) - { - setFurniIds(prev => - { - if(prev && prev.length) WiredSelectionVisualizer.clearSelectionShaderFromFurni(prev); - return []; - }); - } + const onChangeFurniSource = (next: number) => setFurniSource(next); - setFurniSource(next); - }; - - const requiresFurni = (furniSource === 100) - ? WiredFurniType.STUFF_SELECTION_OPTION_BY_ID - : WiredFurniType.STUFF_SELECTION_OPTION_NONE; + const requiresFurni = WiredFurniType.STUFF_SELECTION_OPTION_BY_ID; return ( = props => { - const { trigger = null, furniIds = [], setFurniIds = null, setIntParams = null } = useWired(); + const { trigger = null, setIntParams = null } = useWired(); const [ furniSource, setFurniSource ] = useState(() => { if(trigger?.intData?.length >= 1) return trigger.intData[0]; @@ -20,25 +20,11 @@ export const WiredConditionFurniIsOfTypeView: FC<{}> = props => else setFurniSource((trigger.selectedItems?.length ?? 0) > 0 ? 100 : 0); }, [ trigger ]); - const onChangeFurniSource = (next: number) => - { - if(furniIds.length && setFurniIds) - { - setFurniIds(prev => - { - if(prev && prev.length) WiredSelectionVisualizer.clearSelectionShaderFromFurni(prev); - return []; - }); - } - - setFurniSource(next); - }; + const onChangeFurniSource = (next: number) => setFurniSource(next); const save = () => setIntParams([ furniSource ]); - const requiresFurni = (furniSource === 100) - ? WiredFurniType.STUFF_SELECTION_OPTION_BY_ID_OR_BY_TYPE - : WiredFurniType.STUFF_SELECTION_OPTION_NONE; + const requiresFurni = WiredFurniType.STUFF_SELECTION_OPTION_BY_ID_OR_BY_TYPE; return ( = props => const [ stateFlag, setStateFlag ] = useState(0); const [ directionFlag, setDirectionFlag ] = useState(0); const [ positionFlag, setPositionFlag ] = useState(0); - const { trigger = null, furniIds = [], setFurniIds = null, setIntParams = null } = useWired(); + const { trigger = null, setIntParams = null } = useWired(); const [ furniSource, setFurniSource ] = useState(() => { if(trigger?.intData?.length > 3) return trigger.intData[3]; @@ -28,23 +28,9 @@ export const WiredConditionFurniMatchesSnapshotView: FC<{}> = props => else setFurniSource((trigger.selectedItems?.length ?? 0) > 0 ? 100 : 0); }, [ trigger ]); - const onChangeFurniSource = (next: number) => - { - if(furniIds.length && setFurniIds) - { - setFurniIds(prev => - { - if(prev && prev.length) WiredSelectionVisualizer.clearSelectionShaderFromFurni(prev); - return []; - }); - } + const onChangeFurniSource = (next: number) => setFurniSource(next); - setFurniSource(next); - }; - - const requiresFurni = (furniSource === 100) - ? WiredFurniType.STUFF_SELECTION_OPTION_BY_ID - : WiredFurniType.STUFF_SELECTION_OPTION_NONE; + const requiresFurni = WiredFurniType.STUFF_SELECTION_OPTION_BY_ID; return ( = () => { - const [ sourceType, setSourceType ] = useState(SOURCE_FURNI_PICKED); const [ matchState, setMatchState ] = useState(false); const [ filterExisting, setFilterExisting ] = useState(false); const [ invert, setInvert ] = useState(false); - const { trigger = null, furniIds = [], setIntParams, setSelectByType, setInvertSelection } = useWired(); + const { trigger = null, setIntParams, setSelectByType } = useWired(); useEffect(() => { if(!trigger) return; const p = trigger.intData; - if(p.length >= 1) setSourceType(p[0]); if(p.length >= 2) setMatchState(p[1] === 1); if(p.length >= 3) setFilterExisting(p[2] === 1); if(p.length >= 4) setInvert(p[3] === 1); @@ -37,38 +26,20 @@ export const WiredSelectorFurniByTypeView: FC<{}> = () => useEffect(() => { - setSelectByType(sourceType === SOURCE_FURNI_PICKED); - }, [ sourceType, setSelectByType ]); - - useEffect(() => - { - setInvertSelection(invert); - }, [ invert, setInvertSelection ]); + setSelectByType(true); + }, [ setSelectByType ]); const save = useCallback(() => { setIntParams([ - sourceType, + SOURCE_FURNI_PICKED, matchState ? 1 : 0, filterExisting ? 1 : 0, invert ? 1 : 0, ]); - }, [ sourceType, matchState, filterExisting, invert, setIntParams ]); + }, [ matchState, filterExisting, invert, setIntParams ]); - const sourceIndex = SOURCES.findIndex(s => s.value === sourceType); - - const prevSource = () => - setSourceType(SOURCES[(sourceIndex - 1 + SOURCES.length) % SOURCES.length].value); - - const nextSource = () => - setSourceType(SOURCES[(sourceIndex + 1) % SOURCES.length].value); - - const requiresFurni = sourceType === SOURCE_FURNI_PICKED - ? WiredFurniType.STUFF_SELECTION_OPTION_BY_ID - : WiredFurniType.STUFF_SELECTION_OPTION_NONE; - - const pickedCount = furniIds.length; - const pickedLimit = trigger?.maximumItemSelectionCount ?? 20; + const requiresFurni = WiredFurniType.STUFF_SELECTION_OPTION_BY_ID; return ( @@ -104,22 +75,6 @@ export const WiredSelectorFurniByTypeView: FC<{}> = () => onChange={ e => setInvert(e.target.checked) } /> { LocalizeText('wiredfurni.params.selector_option.1') } - -
- - { LocalizeText('wiredfurni.params.sources.furni.title') } - -
- -
- { LocalizeText(SOURCES[sourceIndex >= 0 ? sourceIndex : 0].label) } -
- -
); diff --git a/src/components/wired/views/triggers/WiredTriggerExecutePeriodicallyLongView.tsx b/src/components/wired/views/triggers/WiredTriggerExecutePeriodicallyLongView.tsx index 7b9a766..6cb4726 100644 --- a/src/components/wired/views/triggers/WiredTriggerExecutePeriodicallyLongView.tsx +++ b/src/components/wired/views/triggers/WiredTriggerExecutePeriodicallyLongView.tsx @@ -1,6 +1,6 @@ import { FC, useEffect, useState } from 'react'; import { FriendlyTime, LocalizeText, WiredFurniType } from '../../../../api'; -import { Text } from '../../../../common'; +import { Slider, Text } from '../../../../common'; import { useWired } from '../../../../hooks'; import { WiredTriggerBaseView } from './WiredTriggerBaseView'; diff --git a/src/components/wired/views/triggers/WiredTriggerExecutePeriodicallyView.tsx b/src/components/wired/views/triggers/WiredTriggerExecutePeriodicallyView.tsx index 7c3056a..ebed0ee 100644 --- a/src/components/wired/views/triggers/WiredTriggerExecutePeriodicallyView.tsx +++ b/src/components/wired/views/triggers/WiredTriggerExecutePeriodicallyView.tsx @@ -1,6 +1,6 @@ import { FC, useEffect, useState } from 'react'; import { GetWiredTimeLocale, LocalizeText, WiredFurniType } from '../../../../api'; -import { Text } from '../../../../common'; +import { Slider, Text } from '../../../../common'; import { useWired } from '../../../../hooks'; import { WiredTriggerBaseView } from './WiredTriggerBaseView'; diff --git a/src/components/wired/views/triggers/WiredTriggerReceiveSignalView.tsx b/src/components/wired/views/triggers/WiredTriggerReceiveSignalView.tsx index 036f81d..30bc9a6 100644 --- a/src/components/wired/views/triggers/WiredTriggerReceiveSignalView.tsx +++ b/src/components/wired/views/triggers/WiredTriggerReceiveSignalView.tsx @@ -4,12 +4,14 @@ import { Text } from '../../../../common'; import { useWired } from '../../../../hooks'; import { WiredTriggerBaseView } from './WiredTriggerBaseView'; +const ANTENNA_INTERACTION_TYPES = [ 'antenna' ]; + export const WiredTriggerReceiveSignalView: FC<{}> = () => { const [ senderCount, setSenderCount ] = useState(0); const [ maxSenders, setMaxSenders ] = useState(5); - const { trigger = null } = useWired(); + const { trigger = null, setAllowedInteractionTypes } = useWired(); useEffect(() => { @@ -20,8 +22,15 @@ export const WiredTriggerReceiveSignalView: FC<{}> = () => if(p.length >= 3) setMaxSenders(p[2]); }, [ trigger ]); + useEffect(() => + { + setAllowedInteractionTypes(ANTENNA_INTERACTION_TYPES); + + return () => setAllowedInteractionTypes(null); + }, [ setAllowedInteractionTypes ]); + return ( - +
{ LocalizeText('wiredfurni.params.signal.senders_connected') } { senderCount }/{ maxSenders } diff --git a/src/hooks/wired/useWired.ts b/src/hooks/wired/useWired.ts index b455766..b0c3f32 100644 --- a/src/hooks/wired/useWired.ts +++ b/src/hooks/wired/useWired.ts @@ -14,9 +14,9 @@ const useWiredState = () => const [ actionDelay, setActionDelay ] = useState(0); const [ allowsFurni, setAllowsFurni ] = useState(WiredFurniType.STUFF_SELECTION_OPTION_NONE); const [ selectByType, setSelectByType ] = useState(false); - const [ invertSelection, setInvertSelection ] = useState(false); const [ neighborhoodTiles, setNeighborhoodTiles ] = useState<{ x: number; y: number }[] | null>(null); const [ neighborhoodInvert, setNeighborhoodInvert ] = useState(false); + const [ allowedInteractionTypes, setAllowedInteractionTypes ] = useState(null); const { showConfirm = null, simpleAlert = null } = useNotification(); const saveWired = () => @@ -60,6 +60,30 @@ const useWiredState = () => if(objectId <= 0) return; + const getInteractionTypeName = (furniData: any): string => + { + if(!furniData) return null; + + const rawValue = (furniData as any).interactionType + ?? (furniData as any).interactionTypeName + ?? (furniData as any).interactionTypeId; + + if(rawValue === undefined || rawValue === null) return null; + if(typeof rawValue !== 'string') return null; + + return rawValue.toLowerCase(); + }; + + const isAllowedInteraction = (furniData: any): boolean => + { + if(!allowedInteractionTypes || !allowedInteractionTypes.length) return true; + + const interactionType = getInteractionTypeName(furniData); + if(!interactionType) return true; + + return allowedInteractionTypes.some(type => (type && type.toLowerCase() === interactionType)); + }; + if(selectByType && category === RoomObjectCategory.FLOOR) { const roomId = GetRoomSession().roomId; @@ -71,6 +95,21 @@ const useWiredState = () => const sourceFurniData = GetSessionDataManager().getFloorItemData(typeId); if(!sourceFurniData) return; + if(!isAllowedInteraction(sourceFurniData)) + { + setFurniIds(prevValue => + { + if(!prevValue.includes(objectId)) return prevValue; + + const remaining = prevValue.filter(id => id !== objectId); + + WiredSelectionVisualizer.hide(objectId); + + return remaining; + }); + + return; + } const matchFurniLine = sourceFurniData.furniLine; const matchName = sourceFurniData.name; @@ -84,6 +123,8 @@ const useWiredState = () => const fd = GetSessionDataManager().getFloorItemData(tId); if(!fd) return false; + if(!isAllowedInteraction(fd)) return false; + const furniLineMatch = matchFurniLine && matchFurniLine.length > 0 && fd.furniLine === matchFurniLine; return furniLineMatch || fd.name === matchName; }; @@ -102,10 +143,8 @@ const useWiredState = () => } // ── Select a new group ────────────────────────────────────── - if(prevValue && prevValue.length) WiredSelectionVisualizer.clearSelectionShaderFromFurni(prevValue); - const allFloorObjects = GetRoomEngine().getRoomObjects(roomId, RoomObjectCategory.FLOOR); - const newIds: number[] = []; + const newIds = [ ...prevValue ]; const limit = trigger.maximumItemSelectionCount; for(const obj of allFloorObjects) @@ -116,14 +155,16 @@ const useWiredState = () => const tId = obj.model.getValue(RoomObjectVariable.FURNITURE_TYPE_ID); const fd = GetSessionDataManager().getFloorItemData(tId); if(!fd) continue; + if(!isAllowedInteraction(fd)) continue; const furniLineMatch = matchFurniLine && matchFurniLine.length > 0 && fd.furniLine === matchFurniLine; const matches = furniLineMatch || fd.name === matchName; - if(invertSelection ? !matches : matches) newIds.push(obj.id); + if(matches && !newIds.includes(obj.id)) newIds.push(obj.id); } - WiredSelectionVisualizer.applySelectionShaderToFurni(newIds); + const addedIds = newIds.filter(id => !prevValue.includes(id)); + if(addedIds.length) WiredSelectionVisualizer.applySelectionShaderToFurni(addedIds); return newIds; }); @@ -131,6 +172,34 @@ const useWiredState = () => return; } + if(category === RoomObjectCategory.FLOOR && allowedInteractionTypes && allowedInteractionTypes.length) + { + const roomId = GetRoomSession().roomId; + const clickedObject = GetRoomEngine().getRoomObject(roomId, objectId, RoomObjectCategory.FLOOR); + + if(!clickedObject) return; + + const typeId = clickedObject.model.getValue(RoomObjectVariable.FURNITURE_TYPE_ID); + const sourceFurniData = GetSessionDataManager().getFloorItemData(typeId); + + if(!sourceFurniData) return; + if(!isAllowedInteraction(sourceFurniData)) + { + setFurniIds(prevValue => + { + if(!prevValue.includes(objectId)) return prevValue; + + const remaining = prevValue.filter(id => id !== objectId); + + WiredSelectionVisualizer.hide(objectId); + + return remaining; + }); + + return; + } + } + setFurniIds(prevValue => { const newFurniIds = [ ...prevValue ]; @@ -217,13 +286,13 @@ const useWiredState = () => }); setAllowsFurni(WiredFurniType.STUFF_SELECTION_OPTION_NONE); setSelectByType(false); - setInvertSelection(false); setNeighborhoodTiles(null); setNeighborhoodInvert(false); + setAllowedInteractionTypes(null); }; }, [ trigger ]); - return { trigger, setTrigger, intParams, setIntParams, stringParam, setStringParam, furniIds, setFurniIds, actionDelay, setActionDelay, setAllowsFurni, saveWired, selectObjectForWired, setSelectByType, setInvertSelection, setNeighborhoodTiles, setNeighborhoodInvert }; + return { trigger, setTrigger, intParams, setIntParams, stringParam, setStringParam, furniIds, setFurniIds, actionDelay, setActionDelay, setAllowsFurni, saveWired, selectObjectForWired, setSelectByType, setNeighborhoodTiles, setNeighborhoodInvert, setAllowedInteractionTypes }; }; export const useWired = () => useBetween(useWiredState); From 92f69442adf47b0fbf1e7b561f3f9965974b6da2 Mon Sep 17 00:00:00 2001 From: Lorenzune Date: Tue, 17 Mar 2026 01:34:33 +0100 Subject: [PATCH 02/10] feat(wired): add leave/click/action/short-period trigger views - add UI for wf_trg_leave_room, wf_trg_stuff_state, wf_trg_period_short, wf_trg_click_furni, wf_trg_click_tile, wf_trg_click_user and wf_trg_user_performs_action\n- add state snapshot mode options for wf_trg_stuff_state\n- add sign and dance filters for wf_trg_user_performs_action --- src/api/wired/WiredTriggerLayoutCode.ts | 6 + .../WiredTriggerAvatarLeaveRoomView.tsx | 39 +++++++ .../triggers/WiredTriggerClickFurniView.tsx | 8 ++ .../triggers/WiredTriggerClickTileView.tsx | 20 ++++ .../triggers/WiredTriggerClickUserView.tsx | 8 ++ ...redTriggerExecutePeriodicallyShortView.tsx | 32 ++++++ .../views/triggers/WiredTriggerLayoutView.tsx | 18 +++ .../triggers/WiredTriggerToggleFurniView.tsx | 34 +++++- .../WiredTriggerUserPerformsActionView.tsx | 103 ++++++++++++++++++ 9 files changed, 264 insertions(+), 4 deletions(-) create mode 100644 src/components/wired/views/triggers/WiredTriggerAvatarLeaveRoomView.tsx create mode 100644 src/components/wired/views/triggers/WiredTriggerClickFurniView.tsx create mode 100644 src/components/wired/views/triggers/WiredTriggerClickTileView.tsx create mode 100644 src/components/wired/views/triggers/WiredTriggerClickUserView.tsx create mode 100644 src/components/wired/views/triggers/WiredTriggerExecutePeriodicallyShortView.tsx create mode 100644 src/components/wired/views/triggers/WiredTriggerUserPerformsActionView.tsx diff --git a/src/api/wired/WiredTriggerLayoutCode.ts b/src/api/wired/WiredTriggerLayoutCode.ts index 683ff04..ac80e07 100644 --- a/src/api/wired/WiredTriggerLayoutCode.ts +++ b/src/api/wired/WiredTriggerLayoutCode.ts @@ -15,4 +15,10 @@ export class WiredTriggerLayout public static BOT_REACHED_STUFF: number = 13; public static BOT_REACHED_AVATAR: number = 14; public static RECEIVE_SIGNAL: number = 15; + public static AVATAR_LEAVES_ROOM: number = 16; + public static EXECUTE_PERIODICALLY_SHORT: number = 17; + public static CLICK_FURNI: number = 18; + public static CLICK_TILE: number = 19; + public static CLICK_USER: number = 20; + public static USER_PERFORMS_ACTION: number = 21; } diff --git a/src/components/wired/views/triggers/WiredTriggerAvatarLeaveRoomView.tsx b/src/components/wired/views/triggers/WiredTriggerAvatarLeaveRoomView.tsx new file mode 100644 index 0000000..36a773e --- /dev/null +++ b/src/components/wired/views/triggers/WiredTriggerAvatarLeaveRoomView.tsx @@ -0,0 +1,39 @@ +import { FC, useEffect, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { NitroInput } from '../../../../layout'; +import { WiredTriggerBaseView } from './WiredTriggerBaseView'; + +export const WiredTriggerAvatarLeaveRoomView: FC<{}> = props => +{ + const [ username, setUsername ] = useState(''); + const [ avatarMode, setAvatarMode ] = useState(0); + const { trigger = null, setStringParam = null } = useWired(); + + const save = () => setStringParam((avatarMode === 1) ? username : ''); + + useEffect(() => + { + setUsername(trigger.stringData); + setAvatarMode(trigger.stringData ? 1 : 0); + }, [ trigger ]); + + return ( + +
+ { LocalizeText('wiredfurni.params.picktriggerer') } +
+ setAvatarMode(0) } /> + { LocalizeText('wiredfurni.params.anyavatar') } +
+
+ setAvatarMode(1) } /> + { LocalizeText('wiredfurni.params.certainavatar') } +
+ { (avatarMode === 1) && + setUsername(event.target.value) } /> } +
+
+ ); +}; diff --git a/src/components/wired/views/triggers/WiredTriggerClickFurniView.tsx b/src/components/wired/views/triggers/WiredTriggerClickFurniView.tsx new file mode 100644 index 0000000..db8fbea --- /dev/null +++ b/src/components/wired/views/triggers/WiredTriggerClickFurniView.tsx @@ -0,0 +1,8 @@ +import { FC } from 'react'; +import { WiredFurniType } from '../../../../api'; +import { WiredTriggerBaseView } from './WiredTriggerBaseView'; + +export const WiredTriggerClickFurniView: FC<{}> = () => +{ + return ; +}; diff --git a/src/components/wired/views/triggers/WiredTriggerClickTileView.tsx b/src/components/wired/views/triggers/WiredTriggerClickTileView.tsx new file mode 100644 index 0000000..5a1fdc7 --- /dev/null +++ b/src/components/wired/views/triggers/WiredTriggerClickTileView.tsx @@ -0,0 +1,20 @@ +import { FC, useEffect } from 'react'; +import { WiredFurniType } from '../../../../api'; +import { useWired } from '../../../../hooks'; +import { WiredTriggerBaseView } from './WiredTriggerBaseView'; + +const CLICK_TILE_INTERACTION_TYPES = [ 'room_invisible_click_tile' ]; + +export const WiredTriggerClickTileView: FC<{}> = () => +{ + const { setAllowedInteractionTypes } = useWired(); + + useEffect(() => + { + setAllowedInteractionTypes(CLICK_TILE_INTERACTION_TYPES); + + return () => setAllowedInteractionTypes(null); + }, [ setAllowedInteractionTypes ]); + + return ; +}; diff --git a/src/components/wired/views/triggers/WiredTriggerClickUserView.tsx b/src/components/wired/views/triggers/WiredTriggerClickUserView.tsx new file mode 100644 index 0000000..f3f2222 --- /dev/null +++ b/src/components/wired/views/triggers/WiredTriggerClickUserView.tsx @@ -0,0 +1,8 @@ +import { FC } from 'react'; +import { WiredFurniType } from '../../../../api'; +import { WiredTriggerBaseView } from './WiredTriggerBaseView'; + +export const WiredTriggerClickUserView: FC<{}> = () => +{ + return ; +}; diff --git a/src/components/wired/views/triggers/WiredTriggerExecutePeriodicallyShortView.tsx b/src/components/wired/views/triggers/WiredTriggerExecutePeriodicallyShortView.tsx new file mode 100644 index 0000000..642b5cd --- /dev/null +++ b/src/components/wired/views/triggers/WiredTriggerExecutePeriodicallyShortView.tsx @@ -0,0 +1,32 @@ +import { FC, useEffect, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Slider, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredTriggerBaseView } from './WiredTriggerBaseView'; + +export const WiredTriggeExecutePeriodicallyShortView: FC<{}> = () => +{ + const [ time, setTime ] = useState(10); + const { trigger = null, setIntParams = null } = useWired(); + + const save = () => setIntParams([ time ]); + + useEffect(() => + { + setTime((trigger.intData.length > 0) ? trigger.intData[0] : 10); + }, [ trigger ]); + + return ( + +
+ { LocalizeText('wiredfurni.params.settime', [ 'seconds' ], [ ((time * 50) / 1000).toFixed(2) ]) } + { `${ time * 50 } ms` } + setTime(event) } /> +
+
+ ); +}; diff --git a/src/components/wired/views/triggers/WiredTriggerLayoutView.tsx b/src/components/wired/views/triggers/WiredTriggerLayoutView.tsx index 229464f..3b152fb 100644 --- a/src/components/wired/views/triggers/WiredTriggerLayoutView.tsx +++ b/src/components/wired/views/triggers/WiredTriggerLayoutView.tsx @@ -1,14 +1,20 @@ import { WiredTriggerLayout } from '../../../../api'; import { WiredTriggerAvatarEnterRoomView } from './WiredTriggerAvatarEnterRoomView'; +import { WiredTriggerAvatarLeaveRoomView } from './WiredTriggerAvatarLeaveRoomView'; import { WiredTriggerAvatarSaysSomethingView } from './WiredTriggerAvatarSaysSomethingView'; import { WiredTriggerAvatarWalksOffFurniView } from './WiredTriggerAvatarWalksOffFurniView'; import { WiredTriggerAvatarWalksOnFurniView } from './WiredTriggerAvatarWalksOnFurni'; import { WiredTriggerBotReachedAvatarView } from './WiredTriggerBotReachedAvatarView'; import { WiredTriggerBotReachedStuffView } from './WiredTriggerBotReachedStuffView'; +import { WiredTriggerClickFurniView } from './WiredTriggerClickFurniView'; +import { WiredTriggerClickTileView } from './WiredTriggerClickTileView'; +import { WiredTriggerClickUserView } from './WiredTriggerClickUserView'; import { WiredTriggerCollisionView } from './WiredTriggerCollisionView'; +import { WiredTriggerUserPerformsActionView } from './WiredTriggerUserPerformsActionView'; import { WiredTriggeExecuteOnceView } from './WiredTriggerExecuteOnceView'; import { WiredTriggeExecutePeriodicallyLongView } from './WiredTriggerExecutePeriodicallyLongView'; import { WiredTriggeExecutePeriodicallyView } from './WiredTriggerExecutePeriodicallyView'; +import { WiredTriggeExecutePeriodicallyShortView } from './WiredTriggerExecutePeriodicallyShortView'; import { WiredTriggerGameEndsView } from './WiredTriggerGameEndsView'; import { WiredTriggerGameStartsView } from './WiredTriggerGameStartsView'; import { WiredTriggeScoreAchievedView } from './WiredTriggerScoreAchievedView'; @@ -21,6 +27,8 @@ export const WiredTriggerLayoutView = (code: number) => { case WiredTriggerLayout.AVATAR_ENTERS_ROOM: return ; + case WiredTriggerLayout.AVATAR_LEAVES_ROOM: + return ; case WiredTriggerLayout.AVATAR_SAYS_SOMETHING: return ; case WiredTriggerLayout.AVATAR_WALKS_OFF_FURNI: @@ -31,12 +39,22 @@ export const WiredTriggerLayoutView = (code: number) => return ; case WiredTriggerLayout.BOT_REACHED_STUFF: return ; + case WiredTriggerLayout.CLICK_FURNI: + return ; + case WiredTriggerLayout.CLICK_TILE: + return ; + case WiredTriggerLayout.CLICK_USER: + return ; + case WiredTriggerLayout.USER_PERFORMS_ACTION: + return ; case WiredTriggerLayout.COLLISION: return ; case WiredTriggerLayout.EXECUTE_ONCE: return ; case WiredTriggerLayout.EXECUTE_PERIODICALLY: return ; + case WiredTriggerLayout.EXECUTE_PERIODICALLY_SHORT: + return ; case WiredTriggerLayout.EXECUTE_PERIODICALLY_LONG: return ; case WiredTriggerLayout.GAME_ENDS: diff --git a/src/components/wired/views/triggers/WiredTriggerToggleFurniView.tsx b/src/components/wired/views/triggers/WiredTriggerToggleFurniView.tsx index 4748481..01cb8a4 100644 --- a/src/components/wired/views/triggers/WiredTriggerToggleFurniView.tsx +++ b/src/components/wired/views/triggers/WiredTriggerToggleFurniView.tsx @@ -1,8 +1,34 @@ -import { FC } from 'react'; -import { WiredFurniType } from '../../../../api'; +import { FC, useEffect, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; import { WiredTriggerBaseView } from './WiredTriggerBaseView'; -export const WiredTriggerToggleFurniView: FC<{}> = props => +export const WiredTriggerToggleFurniView: FC<{}> = () => { - return ; + const [ triggerMode, setTriggerMode ] = useState(0); + const { trigger = null, setIntParams = null } = useWired(); + + const save = () => setIntParams([ triggerMode ]); + + useEffect(() => + { + setTriggerMode((trigger?.intData?.length > 0) ? trigger.intData[0] : 0); + }, [ trigger ]); + + return ( + +
+ { LocalizeText('wiredfurni.params.condition.state') } +
+ setTriggerMode(1) } /> + { LocalizeText('wiredfurni.params.state_trigger.1') } +
+
+ setTriggerMode(0) } /> + { LocalizeText('wiredfurni.params.state_trigger.0') } +
+
+
+ ); }; diff --git a/src/components/wired/views/triggers/WiredTriggerUserPerformsActionView.tsx b/src/components/wired/views/triggers/WiredTriggerUserPerformsActionView.tsx new file mode 100644 index 0000000..5e68651 --- /dev/null +++ b/src/components/wired/views/triggers/WiredTriggerUserPerformsActionView.tsx @@ -0,0 +1,103 @@ +import { FC, useEffect, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredTriggerBaseView } from './WiredTriggerBaseView'; + +const ACTION_WAVE = 1; +const ACTION_BLOW_KISS = 2; +const ACTION_LAUGH = 3; +const ACTION_AWAKE = 4; +const ACTION_RELAX = 5; +const ACTION_SIT = 6; +const ACTION_STAND = 7; +const ACTION_LAY = 8; +const ACTION_SIGN = 9; +const ACTION_DANCE = 10; +const ACTION_THUMB_UP = 11; + +const ACTION_OPTIONS = [ + { value: ACTION_WAVE, label: 'widget.memenu.wave' }, + { value: ACTION_BLOW_KISS, label: 'widget.memenu.blow' }, + { value: ACTION_LAUGH, label: 'widget.memenu.laugh' }, + { value: ACTION_THUMB_UP, label: 'widget.memenu.thumb' }, + { value: ACTION_AWAKE, label: 'wiredfurni.params.action.4' }, + { value: ACTION_RELAX, label: 'avatar.widget.random_walk' }, + { value: ACTION_SIT, label: 'widget.memenu.sit' }, + { value: ACTION_STAND, label: 'widget.memenu.stand' }, + { value: ACTION_LAY, label: 'wiredfurni.params.action.8' }, + { value: ACTION_SIGN, label: 'widget.memenu.sign' }, + { value: ACTION_DANCE, label: 'widget.memenu.dance' } +]; + +const SIGN_OPTIONS = Array.from({ length: 18 }, (_, value) => ({ + value, + label: `wiredfurni.params.action.sign.${ value }` +})); + +const DANCE_OPTIONS = [ + { value: 1, label: 'widget.memenu.dance1' }, + { value: 2, label: 'widget.memenu.dance2' }, + { value: 3, label: 'widget.memenu.dance3' }, + { value: 4, label: 'widget.memenu.dance4' } +]; + +export const WiredTriggerUserPerformsActionView: FC<{}> = () => +{ + const [ selectedAction, setSelectedAction ] = useState(ACTION_WAVE); + const [ signFilterEnabled, setSignFilterEnabled ] = useState(false); + const [ signId, setSignId ] = useState(0); + const [ danceFilterEnabled, setDanceFilterEnabled ] = useState(false); + const [ danceId, setDanceId ] = useState(1); + const { trigger = null, setIntParams = null } = useWired(); + + const save = () => setIntParams([ + selectedAction, + signFilterEnabled ? 1 : 0, + signId, + danceFilterEnabled ? 1 : 0, + danceId + ]); + + useEffect(() => + { + setSelectedAction((trigger?.intData?.length > 0) ? trigger.intData[0] : ACTION_WAVE); + setSignFilterEnabled((trigger?.intData?.length > 1) ? (trigger.intData[1] === 1) : false); + setSignId((trigger?.intData?.length > 2) ? trigger.intData[2] : 0); + setDanceFilterEnabled((trigger?.intData?.length > 3) ? (trigger.intData[3] === 1) : false); + setDanceId((trigger?.intData?.length > 4) ? trigger.intData[4] : 1); + }, [ trigger ]); + + return ( + +
+ Action + +
+ { (selectedAction === ACTION_SIGN) && +
+
+ setSignFilterEnabled(event.target.checked) } /> + { LocalizeText('wiredfurni.params.sign_filter') } +
+ { signFilterEnabled && + } +
} + { (selectedAction === ACTION_DANCE) && +
+
+ setDanceFilterEnabled(event.target.checked) } /> + { LocalizeText('wiredfurni.params.dance_filter') } +
+ { danceFilterEnabled && + } +
} +
+ ); +}; From 5f2e9af7fb1c2d92f71c98a43f1cda258dde6988 Mon Sep 17 00:00:00 2001 From: Lorenzune Date: Tue, 17 Mar 2026 03:28:00 +0100 Subject: [PATCH 03/10] feat(wired-ui): add freeze and furni movement action views - add UI support for FREEZE, UNFREEZE, FURNI_TO_USER, USER_TO_FURNI and FURNI_TO_FURNI - add secondary furni source 101 and dual furni-source labels for furni-to-furni targeting - extend source selectors for custom source sets and titles - add green primary and blue secondary wired highlights - clear wired highlights globally on close, reopen and save to avoid stuck selections --- src/api/wired/WiredActionLayoutCode.ts | 5 + src/api/wired/WiredSelectionVisualizer.ts | 71 +++++- src/components/wired/views/WiredBaseView.tsx | 8 +- .../wired/views/WiredSourcesSelector.tsx | 52 ++-- .../views/actions/WiredActionFreezeView.tsx | 54 +++++ .../actions/WiredActionFurniToFurniView.tsx | 222 ++++++++++++++++++ .../views/actions/WiredActionLayoutView.tsx | 13 + .../views/actions/WiredActionUnfreezeView.tsx | 26 ++ src/hooks/wired/useWired.ts | 2 + 9 files changed, 426 insertions(+), 27 deletions(-) create mode 100644 src/components/wired/views/actions/WiredActionFreezeView.tsx create mode 100644 src/components/wired/views/actions/WiredActionFurniToFurniView.tsx create mode 100644 src/components/wired/views/actions/WiredActionUnfreezeView.tsx diff --git a/src/api/wired/WiredActionLayoutCode.ts b/src/api/wired/WiredActionLayoutCode.ts index 3240a00..c9d4324 100644 --- a/src/api/wired/WiredActionLayoutCode.ts +++ b/src/api/wired/WiredActionLayoutCode.ts @@ -32,4 +32,9 @@ export class WiredActionLayoutCode public static USERS_AREA_SELECTOR: number = 31; public static USERS_NEIGHBORHOOD_SELECTOR: number = 32; public static SEND_SIGNAL: number = 33; + public static FREEZE: number = 34; + public static UNFREEZE: number = 35; + public static FURNI_TO_USER: number = 36; + public static USER_TO_FURNI: number = 37; + public static FURNI_TO_FURNI: number = 38; } diff --git a/src/api/wired/WiredSelectionVisualizer.ts b/src/api/wired/WiredSelectionVisualizer.ts index 18edbf7..06aeb57 100644 --- a/src/api/wired/WiredSelectionVisualizer.ts +++ b/src/api/wired/WiredSelectionVisualizer.ts @@ -3,25 +3,42 @@ import { GetRoomEngine, IRoomObject, IRoomObjectSpriteVisualization, RoomObjectC export class WiredSelectionVisualizer { private static _selectionShader: WiredFilter = new WiredFilter({ - lineColor: [ 1, 1, 1 ], - color: [ 0.6, 0.6, 0.6 ] + lineColor: [ 0.45, 0.95, 0.55 ], + color: [ 0.18, 0.78, 0.30 ] + }); + private static _secondarySelectionShader: WiredFilter = new WiredFilter({ + lineColor: [ 0.45, 0.78, 1 ], + color: [ 0.20, 0.52, 0.95 ] }); public static show(furniId: number): void { - WiredSelectionVisualizer.applySelectionShader(WiredSelectionVisualizer.getRoomObject(furniId)); + WiredSelectionVisualizer.applySelectionShader(WiredSelectionVisualizer.getRoomObject(furniId), WiredSelectionVisualizer._selectionShader); } public static hide(furniId: number): void { - WiredSelectionVisualizer.clearSelectionShader(WiredSelectionVisualizer.getRoomObject(furniId)); + const roomObject = WiredSelectionVisualizer.getRoomObject(furniId); + + WiredSelectionVisualizer.clearSelectionShader(roomObject, WiredSelectionVisualizer._selectionShader); + WiredSelectionVisualizer.clearSelectionShader(roomObject, WiredSelectionVisualizer._secondarySelectionShader); + } + + public static showSecondary(furniId: number): void + { + WiredSelectionVisualizer.applySelectionShader(WiredSelectionVisualizer.getRoomObject(furniId), WiredSelectionVisualizer._secondarySelectionShader); + } + + public static hideSecondary(furniId: number): void + { + WiredSelectionVisualizer.clearSelectionShader(WiredSelectionVisualizer.getRoomObject(furniId), WiredSelectionVisualizer._secondarySelectionShader); } public static clearSelectionShaderFromFurni(furniIds: number[]): void { for(const furniId of furniIds) { - WiredSelectionVisualizer.clearSelectionShader(WiredSelectionVisualizer.getRoomObject(furniId)); + WiredSelectionVisualizer.clearSelectionShader(WiredSelectionVisualizer.getRoomObject(furniId), WiredSelectionVisualizer._selectionShader); } } @@ -29,7 +46,39 @@ export class WiredSelectionVisualizer { for(const furniId of furniIds) { - WiredSelectionVisualizer.applySelectionShader(WiredSelectionVisualizer.getRoomObject(furniId)); + WiredSelectionVisualizer.applySelectionShader(WiredSelectionVisualizer.getRoomObject(furniId), WiredSelectionVisualizer._selectionShader); + } + } + + public static clearSecondarySelectionShaderFromFurni(furniIds: number[]): void + { + for(const furniId of furniIds) + { + WiredSelectionVisualizer.clearSelectionShader(WiredSelectionVisualizer.getRoomObject(furniId), WiredSelectionVisualizer._secondarySelectionShader); + } + } + + public static applySecondarySelectionShaderToFurni(furniIds: number[]): void + { + for(const furniId of furniIds) + { + WiredSelectionVisualizer.applySelectionShader(WiredSelectionVisualizer.getRoomObject(furniId), WiredSelectionVisualizer._secondarySelectionShader); + } + } + + public static clearAllSelectionShaders(): void + { + const roomEngine = GetRoomEngine(); + const roomId = roomEngine.activeRoomId; + + if(roomId < 0) return; + + const roomObjects = roomEngine.getRoomObjects(roomId, RoomObjectCategory.FLOOR); + + for(const roomObject of roomObjects) + { + WiredSelectionVisualizer.clearSelectionShader(roomObject, WiredSelectionVisualizer._selectionShader); + WiredSelectionVisualizer.clearSelectionShader(roomObject, WiredSelectionVisualizer._secondarySelectionShader); } } @@ -40,7 +89,7 @@ export class WiredSelectionVisualizer return roomEngine.getRoomObject(roomEngine.activeRoomId, objectId, RoomObjectCategory.FLOOR); } - private static applySelectionShader(roomObject: IRoomObject): void + private static applySelectionShader(roomObject: IRoomObject, filter: WiredFilter): void { if(!roomObject) return; @@ -54,13 +103,15 @@ export class WiredSelectionVisualizer if(!sprite.filters) sprite.filters = []; - sprite.filters.push(WiredSelectionVisualizer._selectionShader); + if(sprite.filters.includes(filter)) continue; + + sprite.filters.push(filter); sprite.increaseUpdateCounter(); } } - private static clearSelectionShader(roomObject: IRoomObject): void + private static clearSelectionShader(roomObject: IRoomObject, filter: WiredFilter): void { if(!roomObject) return; @@ -72,7 +123,7 @@ export class WiredSelectionVisualizer { if(!sprite.filters) continue; - const index = sprite.filters.indexOf(WiredSelectionVisualizer._selectionShader); + const index = sprite.filters.indexOf(filter); if(index >= 0) { diff --git a/src/components/wired/views/WiredBaseView.tsx b/src/components/wired/views/WiredBaseView.tsx index 4c4f0db..f9477af 100644 --- a/src/components/wired/views/WiredBaseView.tsx +++ b/src/components/wired/views/WiredBaseView.tsx @@ -24,7 +24,11 @@ export const WiredBaseView: FC> = props => const [ needsSave, setNeedsSave ] = useState(false); const { trigger = null, setTrigger = null, setIntParams = null, setStringParam = null, setFurniIds = null, setAllowsFurni = null, saveWired = null } = useWired(); - const onClose = () => setTrigger(null); + const onClose = () => + { + WiredSelectionVisualizer.clearAllSelectionShaders(); + setTrigger(null); + }; const onSave = () => { @@ -48,6 +52,8 @@ export const WiredBaseView: FC> = props => { if(!trigger) return; + WiredSelectionVisualizer.clearAllSelectionShaders(); + const spriteId = (trigger.spriteId || -1); const furniData = GetSessionDataManager().getFloorItemData(spriteId); diff --git a/src/components/wired/views/WiredSourcesSelector.tsx b/src/components/wired/views/WiredSourcesSelector.tsx index db072a6..2d5ebc1 100644 --- a/src/components/wired/views/WiredSourcesSelector.tsx +++ b/src/components/wired/views/WiredSourcesSelector.tsx @@ -16,45 +16,66 @@ export const USER_SOURCES = [ { value: 201, label: 'wiredfurni.params.sources.users.201' } ]; +export interface WiredSourceOption +{ + value: number; + label: string; +} + interface WiredSourcesSelectorProps { showFurni?: boolean; showUsers?: boolean; furniSource?: number; userSource?: number; + furniTitle?: string; + usersTitle?: string; + furniSources?: WiredSourceOption[]; + userSources?: WiredSourceOption[]; onChangeFurni?: (source: number) => void; onChangeUsers?: (source: number) => void; } export const WiredSourcesSelector: FC = props => { - const { showFurni = false, showUsers = false, furniSource = 0, userSource = 0, onChangeFurni = null, onChangeUsers = null } = props; + const { + showFurni = false, + showUsers = false, + furniSource = 0, + userSource = 0, + furniTitle = 'wiredfurni.params.sources.furni.title', + usersTitle = 'wiredfurni.params.sources.users.title', + furniSources = FURNI_SOURCES, + userSources = USER_SOURCES, + onChangeFurni = null, + onChangeUsers = null + } = props; - const furniIndex = Math.max(0, FURNI_SOURCES.findIndex(s => s.value === furniSource)); - const userIndex = Math.max(0, USER_SOURCES.findIndex(s => s.value === userSource)); + const furniIndex = Math.max(0, furniSources.findIndex(s => s.value === furniSource)); + const userIndex = Math.max(0, userSources.findIndex(s => s.value === userSource)); const prevFurni = () => { - const next = (furniIndex - 1 + FURNI_SOURCES.length) % FURNI_SOURCES.length; - onChangeFurni && onChangeFurni(FURNI_SOURCES[next].value); + const next = (furniIndex - 1 + furniSources.length) % furniSources.length; + onChangeFurni && onChangeFurni(furniSources[next].value); }; const nextFurni = () => { - const next = (furniIndex + 1) % FURNI_SOURCES.length; - onChangeFurni && onChangeFurni(FURNI_SOURCES[next].value); + const next = (furniIndex + 1) % furniSources.length; + onChangeFurni && onChangeFurni(furniSources[next].value); }; const prevUsers = () => { - const next = (userIndex - 1 + USER_SOURCES.length) % USER_SOURCES.length; - onChangeUsers && onChangeUsers(USER_SOURCES[next].value); + const next = (userIndex - 1 + userSources.length) % userSources.length; + onChangeUsers && onChangeUsers(userSources[next].value); }; const nextUsers = () => { - const next = (userIndex + 1) % USER_SOURCES.length; - onChangeUsers && onChangeUsers(USER_SOURCES[next].value); + const next = (userIndex + 1) % userSources.length; + onChangeUsers && onChangeUsers(userSources[next].value); }; if(!showFurni && !showUsers) return null; @@ -63,11 +84,11 @@ export const WiredSourcesSelector: FC = props =>
{ showFurni && <> - { LocalizeText('wiredfurni.params.sources.furni.title') } + { LocalizeText(furniTitle) }
- { LocalizeText(FURNI_SOURCES[furniIndex].label) } + { LocalizeText(furniSources[furniIndex].label) }
@@ -77,11 +98,11 @@ export const WiredSourcesSelector: FC = props => { showUsers && <> - { LocalizeText('wiredfurni.params.sources.users.title') } + { LocalizeText(usersTitle) }
- { LocalizeText(USER_SOURCES[userIndex].label) } + { LocalizeText(userSources[userIndex].label) }
@@ -89,4 +110,3 @@ export const WiredSourcesSelector: FC = props =>
); }; - diff --git a/src/components/wired/views/actions/WiredActionFreezeView.tsx b/src/components/wired/views/actions/WiredActionFreezeView.tsx new file mode 100644 index 0000000..716acaa --- /dev/null +++ b/src/components/wired/views/actions/WiredActionFreezeView.tsx @@ -0,0 +1,54 @@ +import { FC, useEffect, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredActionBaseView } from './WiredActionBaseView'; +import { WiredSourcesSelector } from '../WiredSourcesSelector'; + +const EFFECT_OPTIONS = [ + { value: 218, label: 'fx_218' }, + { value: 12, label: 'fx_12' }, + { value: 11, label: 'fx_11' }, + { value: 53, label: 'fx_53' }, + { value: 163, label: 'fx_163' } +]; + +export const WiredActionFreezeView: FC<{}> = () => +{ + const [ effectId, setEffectId ] = useState(218); + const [ cancelOnTeleport, setCancelOnTeleport ] = useState(false); + const [ userSource, setUserSource ] = useState(0); + const { trigger = null, setIntParams = null } = useWired(); + + const save = () => setIntParams([ + effectId, + cancelOnTeleport ? 1 : 0, + userSource + ]); + + useEffect(() => + { + setEffectId((trigger?.intData?.length > 0) ? trigger.intData[0] : 218); + setCancelOnTeleport((trigger?.intData?.length > 1) ? (trigger.intData[1] === 1) : false); + setUserSource((trigger?.intData?.length > 2) ? trigger.intData[2] : 0); + }, [ trigger ]); + + return ( + }> +
+ Effect + +
+
+ setCancelOnTeleport(event.target.checked) } /> + { LocalizeText('wiredfurni.params.freeze.cancel_on_teleport') } +
+
+ ); +}; diff --git a/src/components/wired/views/actions/WiredActionFurniToFurniView.tsx b/src/components/wired/views/actions/WiredActionFurniToFurniView.tsx new file mode 100644 index 0000000..5f7cefc --- /dev/null +++ b/src/components/wired/views/actions/WiredActionFurniToFurniView.tsx @@ -0,0 +1,222 @@ +import { FC, useCallback, useEffect, useRef, useState } from 'react'; +import { LocalizeText, WiredFurniType, WiredSelectionVisualizer } from '../../../../api'; +import { Button, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredSourcesSelector, FURNI_SOURCES, WiredSourceOption } from '../WiredSourcesSelector'; +import { WiredActionBaseView } from './WiredActionBaseView'; + +const SOURCE_TRIGGER = 0; +const SOURCE_SELECTED = 100; +const SOURCE_SECONDARY_SELECTED = 101; +const FURNI_DELIMITER = ';'; + +const TARGET_FURNI_SOURCES: WiredSourceOption[] = [ + { value: 0, 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' } +]; + +type SelectionMode = 'move' | 'target'; + +const parseIds = (data: string): number[] => +{ + if(!data || !data.length) return []; + + const ids = new Set(); + + for(const part of data.split(/[;,\t]/)) + { + const trimmed = part.trim(); + if(!trimmed.length) continue; + + const value = parseInt(trimmed, 10); + if(!isNaN(value) && value > 0) ids.add(value); + } + + return Array.from(ids); +}; + +const serializeIds = (ids: number[]): string => +{ + if(!ids || !ids.length) return ''; + + return ids.filter(id => (id > 0)).join(FURNI_DELIMITER); +}; + +export const WiredActionFurniToFurniView: FC<{}> = () => +{ + const [ moveSource, setMoveSource ] = useState(SOURCE_TRIGGER); + const [ targetSource, setTargetSource ] = useState(SOURCE_TRIGGER); + const [ moveFurniIds, setMoveFurniIds ] = useState([]); + const [ targetFurniIds, setTargetFurniIds ] = useState([]); + const [ selectionMode, setSelectionMode ] = useState('move'); + + const highlightedIds = useRef([]); + + const { trigger = null, furniIds = [], setFurniIds, setIntParams, setStringParam, setAllowsFurni } = useWired(); + + const syncHighlights = useCallback((nextMoveIds: number[], nextTargetIds: number[]) => + { + if(highlightedIds.current.length) + { + WiredSelectionVisualizer.clearSelectionShaderFromFurni(highlightedIds.current); + WiredSelectionVisualizer.clearSecondarySelectionShaderFromFurni(highlightedIds.current); + } + + const targetSet = new Set(nextTargetIds); + const moveOnlyIds = nextMoveIds.filter(id => !targetSet.has(id)); + + if(moveOnlyIds.length) WiredSelectionVisualizer.applySelectionShaderToFurni(moveOnlyIds); + if(nextTargetIds.length) WiredSelectionVisualizer.applySecondarySelectionShaderToFurni(nextTargetIds); + + highlightedIds.current = Array.from(new Set([ ...nextMoveIds, ...nextTargetIds ])); + }, []); + + const switchSelection = useCallback((mode: SelectionMode) => + { + const canEditMove = (moveSource === SOURCE_SELECTED); + const canEditTarget = (targetSource === SOURCE_SECONDARY_SELECTED); + + if(mode === 'move' && !canEditMove) return; + if(mode === 'target' && !canEditTarget) return; + + setSelectionMode(mode); + setFurniIds([ ...(mode === 'move' ? moveFurniIds : targetFurniIds) ]); + }, [ moveSource, targetSource, moveFurniIds, targetFurniIds, setFurniIds ]); + + useEffect(() => + { + if(!trigger) return; + + const nextMoveIds = trigger.selectedItems ?? []; + const nextTargetIds = parseIds(trigger.stringData); + const nextMoveSource = (trigger.intData.length >= 1) + ? trigger.intData[0] + : (nextMoveIds.length ? SOURCE_SELECTED : SOURCE_TRIGGER); + const nextTargetSourceRaw = (trigger.intData.length >= 2) + ? trigger.intData[1] + : (nextTargetIds.length ? SOURCE_SECONDARY_SELECTED : SOURCE_TRIGGER); + const nextTargetSource = (nextTargetSourceRaw === SOURCE_SELECTED) ? SOURCE_SECONDARY_SELECTED : nextTargetSourceRaw; + + setMoveSource(nextMoveSource); + setTargetSource(nextTargetSource); + setMoveFurniIds(nextMoveIds); + setTargetFurniIds(nextTargetIds); + setSelectionMode('move'); + setFurniIds([ ...nextMoveIds ]); + }, [ trigger, setFurniIds ]); + + useEffect(() => + { + if(selectionMode === 'move') setMoveFurniIds(furniIds); + else setTargetFurniIds(furniIds); + }, [ furniIds, selectionMode ]); + + useEffect(() => + { + syncHighlights(moveFurniIds, targetFurniIds); + }, [ moveFurniIds, targetFurniIds, syncHighlights ]); + + useEffect(() => + { + const canEditMove = (moveSource === SOURCE_SELECTED); + const canEditTarget = (targetSource === SOURCE_SECONDARY_SELECTED); + + if(selectionMode === 'move' && !canEditMove && canEditTarget) + { + switchSelection('target'); + return; + } + + if(selectionMode === 'target' && !canEditTarget && canEditMove) + { + switchSelection('move'); + return; + } + + const canEditCurrent = ((selectionMode === 'move') ? canEditMove : canEditTarget); + setAllowsFurni(canEditCurrent ? WiredFurniType.STUFF_SELECTION_OPTION_BY_ID : WiredFurniType.STUFF_SELECTION_OPTION_NONE); + }, [ selectionMode, moveSource, targetSource, switchSelection, setAllowsFurni ]); + + useEffect(() => + { + return () => + { + if(!highlightedIds.current.length) return; + + WiredSelectionVisualizer.clearSelectionShaderFromFurni(highlightedIds.current); + WiredSelectionVisualizer.clearSecondarySelectionShaderFromFurni(highlightedIds.current); + highlightedIds.current = []; + }; + }, []); + + const save = useCallback(() => + { + if(selectionMode === 'target') + { + setSelectionMode('move'); + setFurniIds([ ...moveFurniIds ]); + } + + setIntParams([ + moveSource, + targetSource + ]); + + setStringParam(serializeIds(targetFurniIds)); + }, [ selectionMode, moveFurniIds, moveSource, targetSource, targetFurniIds, setFurniIds, setIntParams, setStringParam ]); + + const selectionLimit = trigger?.maximumItemSelectionCount ?? 0; + + return ( + + +
+ setTargetSource((value === SOURCE_SELECTED) ? SOURCE_SECONDARY_SELECTED : value) } /> +
+ }> +
+
+ { LocalizeText('wiredfurni.params.sources.furni.title.mv.0') } +
+ + { selectionLimit ? `${ moveFurniIds.length }/${ selectionLimit }` : moveFurniIds.length } +
+
+
+ { LocalizeText('wiredfurni.params.sources.furni.title.mv.1') } +
+ + { selectionLimit ? `${ targetFurniIds.length }/${ selectionLimit }` : targetFurniIds.length } +
+
+
+ + ); +}; diff --git a/src/components/wired/views/actions/WiredActionLayoutView.tsx b/src/components/wired/views/actions/WiredActionLayoutView.tsx index 23c16d1..23d853a 100644 --- a/src/components/wired/views/actions/WiredActionLayoutView.tsx +++ b/src/components/wired/views/actions/WiredActionLayoutView.tsx @@ -1,5 +1,7 @@ import { WiredActionLayoutCode } from '../../../../api'; import { WiredActionBotChangeFigureView } from './WiredActionBotChangeFigureView'; +import { WiredActionFreezeView } from './WiredActionFreezeView'; +import { WiredActionFurniToFurniView } from './WiredActionFurniToFurniView'; import { WiredActionSendSignalView } from './WiredActionSendSignalView'; import { WiredActionFurniAreaView } from '../selectors/WiredActionFurniAreaView'; import { WiredSelectorFurniNeighborhoodView } from '../selectors/WiredSelectorFurniNeighborhoodView'; @@ -30,6 +32,7 @@ import { WiredActionResetView } from './WiredActionResetView'; import { WiredActionSetFurniStateToView } from './WiredActionSetFurniStateToView'; import { WiredActionTeleportView } from './WiredActionTeleportView'; import { WiredActionToggleFurniStateView } from './WiredActionToggleFurniStateView'; +import { WiredActionUnfreezeView } from './WiredActionUnfreezeView'; export const WiredActionLayoutView = (code: number) => { @@ -57,6 +60,12 @@ export const WiredActionLayoutView = (code: number) => return ; case WiredActionLayoutCode.FLEE: return ; + case WiredActionLayoutCode.FREEZE: + return ; + case WiredActionLayoutCode.FURNI_TO_USER: + return ; + case WiredActionLayoutCode.FURNI_TO_FURNI: + return ; case WiredActionLayoutCode.GIVE_REWARD: return ; case WiredActionLayoutCode.GIVE_SCORE: @@ -85,6 +94,10 @@ export const WiredActionLayoutView = (code: number) => return ; case WiredActionLayoutCode.TOGGLE_FURNI_STATE: return ; + case WiredActionLayoutCode.UNFREEZE: + return ; + case WiredActionLayoutCode.USER_TO_FURNI: + return ; case WiredActionLayoutCode.FURNI_AREA_SELECTOR: return ; case WiredActionLayoutCode.FURNI_NEIGHBORHOOD_SELECTOR: diff --git a/src/components/wired/views/actions/WiredActionUnfreezeView.tsx b/src/components/wired/views/actions/WiredActionUnfreezeView.tsx new file mode 100644 index 0000000..98b31a6 --- /dev/null +++ b/src/components/wired/views/actions/WiredActionUnfreezeView.tsx @@ -0,0 +1,26 @@ +import { FC, useEffect, useState } from 'react'; +import { WiredFurniType } from '../../../../api'; +import { useWired } from '../../../../hooks'; +import { WiredActionBaseView } from './WiredActionBaseView'; +import { WiredSourcesSelector } from '../WiredSourcesSelector'; + +export const WiredActionUnfreezeView: FC<{}> = () => +{ + const [ userSource, setUserSource ] = useState(0); + const { trigger = null, setIntParams = null } = useWired(); + + const save = () => setIntParams([ userSource ]); + + useEffect(() => + { + setUserSource((trigger?.intData?.length > 0) ? trigger.intData[0] : 0); + }, [ trigger ]); + + return ( + } /> + ); +}; diff --git a/src/hooks/wired/useWired.ts b/src/hooks/wired/useWired.ts index b0c3f32..19e6ed8 100644 --- a/src/hooks/wired/useWired.ts +++ b/src/hooks/wired/useWired.ts @@ -235,6 +235,7 @@ const useWiredState = () => { const parser = event.getParser(); + WiredSelectionVisualizer.clearAllSelectionShaders(); setTrigger(null); }); @@ -275,6 +276,7 @@ const useWiredState = () => return () => { + WiredSelectionVisualizer.clearAllSelectionShaders(); setIntParams([]); setStringParam(''); setActionDelay(0); From e35d06b248f0f0a6b8c1c347f252e2184bd8e3be Mon Sep 17 00:00:00 2001 From: Lorenzune Date: Wed, 18 Mar 2026 14:38:21 +0100 Subject: [PATCH 04/10] feat(wired-ui): add altitude and relative move actions --- src/api/wired/WiredActionLayoutCode.ts | 2 + src/common/Slider.tsx | 22 ++- .../views/actions/WiredActionLayoutView.tsx | 6 + .../actions/WiredActionRelativeMoveView.tsx | 120 +++++++++++++ .../actions/WiredActionSetAltitudeView.tsx | 162 ++++++++++++++++++ 5 files changed, 308 insertions(+), 4 deletions(-) create mode 100644 src/components/wired/views/actions/WiredActionRelativeMoveView.tsx create mode 100644 src/components/wired/views/actions/WiredActionSetAltitudeView.tsx diff --git a/src/api/wired/WiredActionLayoutCode.ts b/src/api/wired/WiredActionLayoutCode.ts index c9d4324..25de806 100644 --- a/src/api/wired/WiredActionLayoutCode.ts +++ b/src/api/wired/WiredActionLayoutCode.ts @@ -37,4 +37,6 @@ export class WiredActionLayoutCode public static FURNI_TO_USER: number = 36; public static USER_TO_FURNI: number = 37; public static FURNI_TO_FURNI: number = 38; + public static SET_ALTITUDE: number = 39; + public static RELATIVE_MOVE: number = 40; } diff --git a/src/common/Slider.tsx b/src/common/Slider.tsx index 50cba28..206c0a5 100644 --- a/src/common/Slider.tsx +++ b/src/common/Slider.tsx @@ -11,11 +11,25 @@ export interface SliderProps extends ReactSliderProps export const Slider: FC = props => { - const { disabledButton, max, min, value, onChange, ...rest } = props; + const { disabledButton, max, min, step, value, onChange, ...rest } = props; + const currentValue = Array.isArray(value) ? value[0] : ((typeof value === 'number') ? value : 0); + const minimum = (typeof min === 'number') ? min : 0; + const maximum = (typeof max === 'number') ? max : 0; + const buttonStep = ((typeof step === 'number') && (step > 0)) ? step : 1; + + const roundToStep = (nextValue: number) => + { + if(typeof buttonStep !== 'number') return nextValue; + + const decimalStep = buttonStep.toString(); + const precision = decimalStep.includes('.') ? (decimalStep.length - decimalStep.indexOf('.') - 1) : 0; + + return parseFloat(nextValue.toFixed(precision)); + }; return - { !disabledButton && } - - { !disabledButton && } + { !disabledButton && } + + { !disabledButton && } ; } diff --git a/src/components/wired/views/actions/WiredActionLayoutView.tsx b/src/components/wired/views/actions/WiredActionLayoutView.tsx index 23d853a..f515b63 100644 --- a/src/components/wired/views/actions/WiredActionLayoutView.tsx +++ b/src/components/wired/views/actions/WiredActionLayoutView.tsx @@ -2,6 +2,7 @@ import { WiredActionLayoutCode } from '../../../../api'; import { WiredActionBotChangeFigureView } from './WiredActionBotChangeFigureView'; import { WiredActionFreezeView } from './WiredActionFreezeView'; import { WiredActionFurniToFurniView } from './WiredActionFurniToFurniView'; +import { WiredActionSetAltitudeView } from './WiredActionSetAltitudeView'; import { WiredActionSendSignalView } from './WiredActionSendSignalView'; import { WiredActionFurniAreaView } from '../selectors/WiredActionFurniAreaView'; import { WiredSelectorFurniNeighborhoodView } from '../selectors/WiredSelectorFurniNeighborhoodView'; @@ -28,6 +29,7 @@ import { WiredActionMoveAndRotateFurniView } from './WiredActionMoveAndRotateFur import { WiredActionMoveFurniToView } from './WiredActionMoveFurniToView'; import { WiredActionMoveFurniView } from './WiredActionMoveFurniView'; import { WiredActionMuteUserView } from './WiredActionMuteUserView'; +import { WiredActionRelativeMoveView } from './WiredActionRelativeMoveView'; import { WiredActionResetView } from './WiredActionResetView'; import { WiredActionSetFurniStateToView } from './WiredActionSetFurniStateToView'; import { WiredActionTeleportView } from './WiredActionTeleportView'; @@ -66,6 +68,8 @@ export const WiredActionLayoutView = (code: number) => return ; case WiredActionLayoutCode.FURNI_TO_FURNI: return ; + case WiredActionLayoutCode.SET_ALTITUDE: + return ; case WiredActionLayoutCode.GIVE_REWARD: return ; case WiredActionLayoutCode.GIVE_SCORE: @@ -86,6 +90,8 @@ export const WiredActionLayoutView = (code: number) => return ; case WiredActionLayoutCode.MUTE_USER: return ; + case WiredActionLayoutCode.RELATIVE_MOVE: + return ; case WiredActionLayoutCode.RESET: return ; case WiredActionLayoutCode.SET_FURNI_STATE: diff --git a/src/components/wired/views/actions/WiredActionRelativeMoveView.tsx b/src/components/wired/views/actions/WiredActionRelativeMoveView.tsx new file mode 100644 index 0000000..c6f33a1 --- /dev/null +++ b/src/components/wired/views/actions/WiredActionRelativeMoveView.tsx @@ -0,0 +1,120 @@ +import { FC, useEffect, useState } from 'react'; +import { FaArrowDown, FaArrowLeft, FaArrowRight, FaArrowUp } from 'react-icons/fa'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Slider, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredSourcesSelector } from '../WiredSourcesSelector'; +import { WiredActionBaseView } from './WiredActionBaseView'; + +const MAX_DISTANCE = 20; + +const HORIZONTAL_OPTIONS = [ + { value: 0, icon: }, + { value: 1, icon: } +]; + +const VERTICAL_OPTIONS = [ + { value: 0, icon: }, + { value: 1, icon: } +]; + +const normalizeDirection = (value: number, fallback = 1) => +{ + if(value === 0 || value === 1) return value; + + return fallback; +}; + +const normalizeDistance = (value: number) => +{ + if(isNaN(value)) return 0; + + return Math.max(0, Math.min(MAX_DISTANCE, value)); +}; + +export const WiredActionRelativeMoveView: FC<{}> = () => +{ + const { trigger = null, setIntParams = null } = useWired(); + + const [horizontalDirection, setHorizontalDirection] = useState(1); + const [horizontalDistance, setHorizontalDistance] = useState(0); + const [verticalDirection, setVerticalDirection] = useState(1); + const [verticalDistance, setVerticalDistance] = useState(0); + const [ furniSource, setFurniSource ] = useState(() => + { + if(trigger?.intData?.length > 4) return trigger.intData[4]; + return (trigger?.selectedItems?.length ?? 0) > 0 ? 100 : 0; + }); + + useEffect(() => + { + if(!trigger) return; + + setHorizontalDirection((trigger.intData.length > 0) ? normalizeDirection(trigger.intData[0], 1) : 1); + setHorizontalDistance((trigger.intData.length > 1) ? normalizeDistance(trigger.intData[1]) : 0); + setVerticalDirection((trigger.intData.length > 2) ? normalizeDirection(trigger.intData[2], 1) : 1); + setVerticalDistance((trigger.intData.length > 3) ? normalizeDistance(trigger.intData[3]) : 0); + + if(trigger.intData.length > 4) setFurniSource(trigger.intData[4]); + else setFurniSource((trigger.selectedItems?.length ?? 0) > 0 ? 100 : 0); + }, [ trigger ]); + + const save = () => setIntParams([ + horizontalDirection, + horizontalDistance, + verticalDirection, + verticalDistance, + furniSource + ]); + + return ( + }> +
+ { LocalizeText('wiredfurni.params.movement.horizontal.selection') } +
+ { HORIZONTAL_OPTIONS.map(option => + { + return ( + + ); + }) } +
+ { LocalizeText('wiredfurni.params.movement.horizontal.distance', [ 'distance' ], [ horizontalDistance.toString() ]) } + setHorizontalDistance(value as number) } /> +
+
+ { LocalizeText('wiredfurni.params.movement.vertical.selection') } +
+ { VERTICAL_OPTIONS.map(option => + { + return ( + + ); + }) } +
+ { LocalizeText('wiredfurni.params.movement.vertical.distance', [ 'distance' ], [ verticalDistance.toString() ]) } + setVerticalDistance(value as number) } /> +
+
+ ); +}; diff --git a/src/components/wired/views/actions/WiredActionSetAltitudeView.tsx b/src/components/wired/views/actions/WiredActionSetAltitudeView.tsx new file mode 100644 index 0000000..3e4017b --- /dev/null +++ b/src/components/wired/views/actions/WiredActionSetAltitudeView.tsx @@ -0,0 +1,162 @@ +import { FC, useEffect, useMemo, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Slider, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredSourcesSelector } from '../WiredSourcesSelector'; +import { WiredActionBaseView } from './WiredActionBaseView'; + +const MIN_ALTITUDE = 0; +const MAX_ALTITUDE = 40; +const ALTITUDE_STEP = 0.01; +const ALTITUDE_PATTERN = /^\d*(\.\d{0,2})?$/; + +const clampAltitude = (value: number) => +{ + if(isNaN(value)) return MIN_ALTITUDE; + + const clamped = Math.min(MAX_ALTITUDE, Math.max(MIN_ALTITUDE, value)); + + return parseFloat(clamped.toFixed(2)); +}; + +const formatAltitude = (value: number) => +{ + const normalized = clampAltitude(value); + const text = normalized.toFixed(2); + + return text.replace(/\.00$/, '').replace(/(\.\d)0$/, '$1'); +}; + +const parseAltitude = (value: string) => +{ + if(!value || !value.trim().length) return 0; + + const parsed = parseFloat(value); + + if(isNaN(parsed)) return 0; + + return clampAltitude(parsed); +}; + +const OPERATOR_OPTIONS = [ + { value: 0, label: 'wiredfurni.params.operator.0' }, + { value: 1, label: 'wiredfurni.params.operator.1' }, + { value: 2, label: 'wiredfurni.params.operator.2' } +]; + +const normalizeOperator = (value: number) => +{ + if(value < 0 || value > 2) return 2; + + return value; +}; + +export const WiredActionSetAltitudeView: FC<{}> = () => +{ + const { trigger = null, setIntParams = null, setStringParam = null } = useWired(); + + const [ operator, setOperator ] = useState(2); + const [ furniSource, setFurniSource ] = useState(() => + { + if(trigger?.intData?.length > 1) return trigger.intData[1]; + return (trigger?.selectedItems?.length ?? 0) > 0 ? 100 : 0; + }); + const [ altitude, setAltitude ] = useState(0); + const [ altitudeInput, setAltitudeInput ] = useState('0'); + + const normalizedAltitudeText = useMemo(() => formatAltitude(altitude), [ altitude ]); + + useEffect(() => + { + if(!trigger) return; + + setOperator((trigger.intData.length > 0) ? normalizeOperator(trigger.intData[0]) : 2); + setFurniSource((trigger.intData.length > 1) ? trigger.intData[1] : ((trigger.selectedItems?.length ?? 0) > 0 ? 100 : 0)); + + const nextAltitude = parseAltitude(trigger.stringData); + setAltitude(nextAltitude); + setAltitudeInput(formatAltitude(nextAltitude)); + }, [ trigger ]); + + const updateAltitude = (value: number) => + { + const nextValue = clampAltitude(value); + + setAltitude(nextValue); + setAltitudeInput(formatAltitude(nextValue)); + }; + + const updateAltitudeInput = (value: string) => + { + if(!ALTITUDE_PATTERN.test(value)) return; + + setAltitudeInput(value); + + if(!value.length) + { + setAltitude(0); + return; + } + + const parsedValue = parseFloat(value); + + if(isNaN(parsedValue)) return; + + if(parsedValue > MAX_ALTITUDE) + { + updateAltitude(MAX_ALTITUDE); + return; + } + + setAltitude(clampAltitude(parsedValue)); + }; + + const save = () => + { + setIntParams([ + operator, + furniSource + ]); + + setStringParam(normalizedAltitudeText); + }; + + return ( + }> +
+ { OPERATOR_OPTIONS.map(option => + { + return ( +
+ setOperator(option.value) } /> + { LocalizeText(option.label) } +
+ ); + }) } +
+
+ { LocalizeText('wiredfurni.params.setaltitude') } + setAltitudeInput(formatAltitude(altitude)) } + onChange={ event => updateAltitudeInput(event.target.value) } /> +
+
+ updateAltitude(event as number) } /> + { normalizedAltitudeText } +
+
+ ); +}; From 97aae7170814081c0cb61e4b392d81f13e97b4d2 Mon Sep 17 00:00:00 2001 From: Lorenzune Date: Wed, 18 Mar 2026 17:01:10 +0100 Subject: [PATCH 05/10] fix(wired-ui): clarify reward fields and mute alerts --- .../actions/WiredActionGiveRewardView.tsx | 226 +++++++++++++++--- src/hooks/notification/useNotification.ts | 24 +- src/hooks/rooms/widgets/useChatWidget.ts | 7 +- 3 files changed, 224 insertions(+), 33 deletions(-) diff --git a/src/components/wired/views/actions/WiredActionGiveRewardView.tsx b/src/components/wired/views/actions/WiredActionGiveRewardView.tsx index 91c1468..3fb239a 100644 --- a/src/components/wired/views/actions/WiredActionGiveRewardView.tsx +++ b/src/components/wired/views/actions/WiredActionGiveRewardView.tsx @@ -7,6 +7,131 @@ import { NitroInput } from '../../../../layout'; import { WiredActionBaseView } from './WiredActionBaseView'; import { WiredSourcesSelector } from '../WiredSourcesSelector'; +type RewardType = 'badge' | 'credits' | 'pixels' | 'diamonds' | 'points' | 'furni' | 'respect'; + +interface RewardEntry +{ + rewardType: RewardType; + rewardValue: string; + probability: number; + pointsType: number; +} + +const DEFAULT_PROBABILITY = 100; +const DEFAULT_POINTS_TYPE = 5; + +const REWARD_TYPES: { value: RewardType, label: string }[] = [ + { value: 'badge', label: 'Badge' }, + { value: 'credits', label: 'Credits' }, + { value: 'pixels', label: 'Pixels / Duckets' }, + { value: 'diamonds', label: 'Diamonds' }, + { value: 'points', label: 'Extra Currency' }, + { value: 'furni', label: 'Furni' }, + { value: 'respect', label: 'Respect' } +]; + +const SELECTABLE_REWARD_TYPES = REWARD_TYPES.filter(entry => (entry.value !== 'respect')); + +const createReward = (): RewardEntry => +({ + rewardType: 'furni', + rewardValue: '', + probability: DEFAULT_PROBABILITY, + pointsType: DEFAULT_POINTS_TYPE +}); + +const getRewardValuePlaceholder = (rewardType: RewardType) => +{ + switch(rewardType) + { + case 'badge': + return 'Badge code'; + case 'credits': + return 'Credits amount'; + case 'pixels': + return 'Pixels amount'; + case 'diamonds': + return 'Diamonds amount'; + case 'points': + return 'Amount'; + case 'furni': + return 'Furni base item id'; + case 'respect': + return 'Respect amount'; + } +}; + +const getExtraFieldLabel = (rewardType: RewardType) => +{ + switch(rewardType) + { + case 'points': + return 'Currency Type'; + case 'badge': + return 'Code'; + default: + return 'Info'; + } +}; + +const getExtraFieldPlaceholder = (rewardType: RewardType) => +{ + switch(rewardType) + { + case 'points': + return 'Type id (e.g. 105)'; + case 'badge': + return 'Badge'; + default: + return ''; + } +}; + +const parseRewardEntry = (rawType: string, rawCode: string, rawProbability: string): RewardEntry => +{ + const probability = Number(rawProbability); + const parsedProbability = Number.isFinite(probability) ? probability : DEFAULT_PROBABILITY; + + if(rawType === '0') + { + return { rewardType: 'badge', rewardValue: rawCode, probability: parsedProbability, pointsType: DEFAULT_POINTS_TYPE }; + } + + const separatorIndex = rawCode.indexOf('#'); + + if(separatorIndex === -1) + { + return { rewardType: 'furni', rewardValue: rawCode, probability: parsedProbability, pointsType: DEFAULT_POINTS_TYPE }; + } + + const rewardType = rawCode.slice(0, separatorIndex); + const rewardValue = rawCode.slice(separatorIndex + 1); + + if(rewardType.startsWith('points')) + { + const pointsType = Number(rewardType.slice('points'.length)); + + return { + rewardType: 'points', + rewardValue, + probability: parsedProbability, + pointsType: Number.isFinite(pointsType) ? pointsType : DEFAULT_POINTS_TYPE + }; + } + + if(REWARD_TYPES.some(entry => (entry.value === rewardType))) + { + return { rewardType: rewardType as RewardType, rewardValue, probability: parsedProbability, pointsType: DEFAULT_POINTS_TYPE }; + } + + if(rewardType === 'cata') + { + return { rewardType: 'furni', rewardValue, probability: parsedProbability, pointsType: DEFAULT_POINTS_TYPE }; + } + + return { rewardType: 'furni', rewardValue: rawCode, probability: parsedProbability, pointsType: DEFAULT_POINTS_TYPE }; +}; + export const WiredActionGiveRewardView: FC<{}> = props => { const [ limitEnabled, setLimitEnabled ] = useState(false); @@ -14,7 +139,7 @@ export const WiredActionGiveRewardView: FC<{}> = props => const [ uniqueRewards, setUniqueRewards ] = useState(false); const [ rewardsLimit, setRewardsLimit ] = useState(1); const [ limitationInterval, setLimitationInterval ] = useState(1); - const [ rewards, setRewards ] = useState<{ isBadge: boolean, itemCode: string, probability: number }[]>([]); + const [ rewards, setRewards ] = useState([]); const { trigger = null, setIntParams = null, setStringParam = null } = useWired(); const [ userSource, setUserSource ] = useState(() => { @@ -22,7 +147,8 @@ export const WiredActionGiveRewardView: FC<{}> = props => return 0; }); - const addReward = () => setRewards(rewards => [ ...rewards, { isBadge: false, itemCode: '', probability: null } ]); + const addReward = () => setRewards(rewards => [ ...rewards, createReward() ]); + const hasCustomCurrencyReward = rewards.some(reward => (reward.rewardType === 'points')); const removeReward = (index: number) => { @@ -36,18 +162,9 @@ export const WiredActionGiveRewardView: FC<{}> = props => }); }; - const updateReward = (index: number, isBadge: boolean, itemCode: string, probability: number) => + const updateReward = (index: number, updater: (reward: RewardEntry) => RewardEntry) => { - const rewardsClone = Array.from(rewards); - const reward = rewardsClone[index]; - - if(!reward) return; - - reward.isBadge = isBadge; - reward.itemCode = itemCode; - reward.probability = probability; - - setRewards(rewardsClone); + setRewards(prevValue => prevValue.map((reward, rewardIndex) => ((rewardIndex === index) ? updater(reward) : reward))); }; const save = () => @@ -56,9 +173,20 @@ export const WiredActionGiveRewardView: FC<{}> = props => for(const reward of rewards) { - if(!reward.itemCode) continue; + const rewardValue = reward.rewardValue.trim(); - const rewardsString = [ reward.isBadge ? '0' : '1', reward.itemCode, reward.probability.toString() ]; + if(!rewardValue) continue; + + const probability = Math.max(0, Number.isFinite(reward.probability) ? reward.probability : DEFAULT_PROBABILITY); + const rewardCode = (() => + { + if(reward.rewardType === 'badge') return rewardValue; + if(reward.rewardType === 'points') return `points${ Math.max(0, reward.pointsType) }#${ rewardValue }`; + + return `${ reward.rewardType }#${ rewardValue }`; + })(); + + const rewardsString = [ reward.rewardType === 'badge' ? '0' : '1', rewardCode, (uniqueRewards ? DEFAULT_PROBABILITY : probability).toString() ]; stringRewards.push(rewardsString.join(',')); } @@ -71,9 +199,9 @@ export const WiredActionGiveRewardView: FC<{}> = props => useEffect(() => { - const readRewards: { isBadge: boolean, itemCode: string, probability: number }[] = []; + const readRewards: RewardEntry[] = []; - if(trigger.stringData.length > 0 && trigger.stringData.includes(';')) + if(trigger.stringData.length > 0) { const splittedRewards = trigger.stringData.split(';'); @@ -83,11 +211,11 @@ export const WiredActionGiveRewardView: FC<{}> = props => if(reward.length !== 3) continue; - readRewards.push({ isBadge: reward[0] === '0', itemCode: reward[1], probability: Number(reward[2]) }); + readRewards.push(parseRewardEntry(reward[0], reward[1], reward[2])); } } - if(readRewards.length === 0) readRewards.push({ isBadge: false, itemCode: '', probability: null }); + if(readRewards.length === 0) readRewards.push(createReward()); setRewardTime((trigger.intData.length > 0) ? trigger.intData[0] : 0); setUniqueRewards((trigger.intData.length > 1) ? (trigger.intData[1] === 1) : false); @@ -147,24 +275,64 @@ export const WiredActionGiveRewardView: FC<{}> = props =>
+
+ Type + Amount / Value + { uniqueRewards ? 'Mode' : 'Chance %' } + { hasCustomCurrencyReward ? 'Currency Type' : 'Extra / Info' } + Action +
{ rewards && rewards.map((reward, index) => { + const rewardTypeOptions = (reward.rewardType === 'respect') + ? REWARD_TYPES + : SELECTABLE_REWARD_TYPES; + return ( -
-
- updateReward(index, e.target.checked, reward.itemCode, reward.probability) } /> - Badge? +
+ + updateReward(index, prevValue => ({ ...prevValue, rewardValue: event.target.value })) } /> + { uniqueRewards + ?
+ Unique +
+ : updateReward(index, prevValue => ({ ...prevValue, probability: Number(event.target.value) })) } /> } + { (reward.rewardType === 'points') + ? + updateReward(index, prevValue => ({ ...prevValue, pointsType: Number(event.target.value) })) } /> + :
+ { getExtraFieldLabel(reward.rewardType) } +
} +
+ { (index > 0) && + }
- updateReward(index, reward.isBadge, e.target.value, reward.probability) } /> - updateReward(index, reward.isBadge, reward.itemCode, Number(e.target.value)) } /> - { (index > 0) && - }
); }) }
+ + Extra Currency uses Amount as the quantity and Currency Type as the purse type id. Example: amount 200 + type 105. + ); }; diff --git a/src/hooks/notification/useNotification.ts b/src/hooks/notification/useNotification.ts index 4cd3f68..41e85d1 100644 --- a/src/hooks/notification/useNotification.ts +++ b/src/hooks/notification/useNotification.ts @@ -1,4 +1,4 @@ -import { AchievementNotificationMessageEvent, ActivityPointNotificationMessageEvent, ClubGiftNotificationEvent, ClubGiftSelectedEvent, ConnectionErrorEvent, GetLocalizationManager, GetRoomEngine, GetSessionDataManager, HabboBroadcastMessageEvent, HotelClosedAndOpensEvent, HotelClosesAndWillOpenAtEvent, HotelWillCloseInMinutesEvent, InfoFeedEnableMessageEvent, MaintenanceStatusMessageEvent, ModeratorCautionEvent, ModeratorMessageEvent, MOTDNotificationEvent, NotificationDialogMessageEvent, PetLevelNotificationEvent, PetReceivedMessageEvent, RespectReceivedEvent, RoomEnterEffect, RoomEnterEvent, SimpleAlertMessageEvent, UserBannedMessageEvent, Vector3d } from '@nitrots/nitro-renderer'; +import { AchievementNotificationMessageEvent, ActivityPointNotificationMessageEvent, ClubGiftNotificationEvent, ClubGiftSelectedEvent, ConnectionErrorEvent, GetLocalizationManager, GetRoomEngine, GetSessionDataManager, HabboBroadcastMessageEvent, HotelClosedAndOpensEvent, HotelClosesAndWillOpenAtEvent, HotelWillCloseInMinutesEvent, InfoFeedEnableMessageEvent, MaintenanceStatusMessageEvent, ModeratorCautionEvent, ModeratorMessageEvent, MOTDNotificationEvent, NotificationDialogMessageEvent, PetLevelNotificationEvent, PetReceivedMessageEvent, RespectReceivedEvent, RoomEnterEffect, RoomEnterEvent, SimpleAlertMessageEvent, UserBannedMessageEvent, Vector3d, WiredRewardResultMessageEvent } from '@nitrots/nitro-renderer'; import { useCallback, useState } from 'react'; import { useBetween } from 'use-between'; import { GetConfigurationValue, LocalizeBadgeName, LocalizeText, NotificationAlertItem, NotificationAlertType, NotificationBubbleItem, NotificationBubbleType, NotificationConfirmItem, PlaySound, ProductImageUtility, TradingNotificationType } from '../../api'; @@ -397,6 +397,28 @@ const useNotificationState = () => simpleAlert(LocalizeText(parser.alertMessage), NotificationAlertType.DEFAULT, null, null, LocalizeText(parser.titleMessage ? parser.titleMessage : 'notifications.broadcast.title')); }); + useMessageEvent(WiredRewardResultMessageEvent, event => + { + const parser = event.getParser(); + + switch(parser.reason) + { + case WiredRewardResultMessageEvent.PRODUCT_DONATED_CODE: + case WiredRewardResultMessageEvent.BADGE_DONATED_CODE: + simpleAlert(LocalizeText('wiredfurni.rewardsuccess.body'), NotificationAlertType.DEFAULT, null, null, LocalizeText('wiredfurni.rewardsuccess.title')); + return; + case 0: + case 1: + case 2: + case 3: + case 4: + case 5: + case 8: + simpleAlert(LocalizeText(`wiredfurni.rewardfailed.reason.${ parser.reason }`), NotificationAlertType.DEFAULT, null, null, LocalizeText('wiredfurni.rewardfailed.title')); + return; + } + }); + const onRoomEnterEvent = useCallback(() => { if(modDisclaimerShown) return; diff --git a/src/hooks/rooms/widgets/useChatWidget.ts b/src/hooks/rooms/widgets/useChatWidget.ts index d430a38..648a102 100644 --- a/src/hooks/rooms/widgets/useChatWidget.ts +++ b/src/hooks/rooms/widgets/useChatWidget.ts @@ -123,9 +123,10 @@ const useChatWidgetState = () => text = LocalizeText('widget.chatbubble.handitem', ['username', 'handitem'], [username, LocalizeText(('handitem' + event.extraParam))]); break; case RoomSessionChatEvent.CHAT_TYPE_MUTE_REMAINING: { - const hours = ((event.extraParam > 0) ? Math.floor((event.extraParam / 3600)) : 0).toString(); - const minutes = ((event.extraParam > 0) ? Math.floor((event.extraParam % 3600) / 60) : 0).toString(); - const seconds = (event.extraParam % 60).toString(); + const remainingSeconds = Math.max(0, event.extraParam); + const hours = Math.floor(remainingSeconds / 3600).toString(); + const minutes = Math.floor((remainingSeconds % 3600) / 60).toString(); + const seconds = (remainingSeconds % 60).toString(); text = LocalizeText('widget.chatbubble.mutetime', ['hours', 'minutes', 'seconds'], [hours, minutes, seconds]); break; From 146f8e6b0d636ebea7490ed7449a4e2d50286c9c Mon Sep 17 00:00:00 2001 From: Lorenzune Date: Thu, 19 Mar 2026 00:01:59 +0100 Subject: [PATCH 06/10] feat(wired-ui): add upcounter clock controls and checks --- src/api/wired/WiredActionLayoutCode.ts | 2 + src/api/wired/WiredConditionLayoutCode.ts | 1 + src/api/wired/WiredTriggerLayoutCode.ts | 1 + .../actions/WiredActionAdjustClockView.tsx | 123 ++++++++++++++++++ .../actions/WiredActionControlClockView.tsx | 82 ++++++++++++ .../views/actions/WiredActionLayoutView.tsx | 6 + .../WiredConditionCounterTimeMatchesView.tsx | 123 ++++++++++++++++++ .../conditions/WiredConditionLayoutView.tsx | 3 + .../views/triggers/WiredTriggerBaseView.tsx | 7 +- .../triggers/WiredTriggerClockCounterView.tsx | 101 ++++++++++++++ .../views/triggers/WiredTriggerLayoutView.tsx | 3 + src/hooks/wired/useWired.ts | 15 ++- 12 files changed, 462 insertions(+), 5 deletions(-) create mode 100644 src/components/wired/views/actions/WiredActionAdjustClockView.tsx create mode 100644 src/components/wired/views/actions/WiredActionControlClockView.tsx create mode 100644 src/components/wired/views/conditions/WiredConditionCounterTimeMatchesView.tsx create mode 100644 src/components/wired/views/triggers/WiredTriggerClockCounterView.tsx diff --git a/src/api/wired/WiredActionLayoutCode.ts b/src/api/wired/WiredActionLayoutCode.ts index 25de806..6f98d74 100644 --- a/src/api/wired/WiredActionLayoutCode.ts +++ b/src/api/wired/WiredActionLayoutCode.ts @@ -39,4 +39,6 @@ export class WiredActionLayoutCode public static FURNI_TO_FURNI: number = 38; public static SET_ALTITUDE: number = 39; public static RELATIVE_MOVE: number = 40; + public static CONTROL_CLOCK: number = 41; + public static ADJUST_CLOCK: number = 42; } diff --git a/src/api/wired/WiredConditionLayoutCode.ts b/src/api/wired/WiredConditionLayoutCode.ts index 58cae5d..d63b3e1 100644 --- a/src/api/wired/WiredConditionLayoutCode.ts +++ b/src/api/wired/WiredConditionLayoutCode.ts @@ -26,4 +26,5 @@ export class WiredConditionlayout public static NOT_ACTOR_WEARING_EFFECT: number = 23; public static DATE_RANGE_ACTIVE: number = 24; public static ACTOR_HAS_HANDITEM: number = 25; + public static COUNTER_TIME_MATCHES: number = 27; } diff --git a/src/api/wired/WiredTriggerLayoutCode.ts b/src/api/wired/WiredTriggerLayoutCode.ts index ac80e07..6f6fbe8 100644 --- a/src/api/wired/WiredTriggerLayoutCode.ts +++ b/src/api/wired/WiredTriggerLayoutCode.ts @@ -21,4 +21,5 @@ export class WiredTriggerLayout public static CLICK_TILE: number = 19; public static CLICK_USER: number = 20; public static USER_PERFORMS_ACTION: number = 21; + public static CLOCK_COUNTER: number = 22; } diff --git a/src/components/wired/views/actions/WiredActionAdjustClockView.tsx b/src/components/wired/views/actions/WiredActionAdjustClockView.tsx new file mode 100644 index 0000000..8ae55ec --- /dev/null +++ b/src/components/wired/views/actions/WiredActionAdjustClockView.tsx @@ -0,0 +1,123 @@ +import { FC, useEffect, useMemo, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Slider, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredSourcesSelector } from '../WiredSourcesSelector'; +import { WiredActionBaseView } from './WiredActionBaseView'; + +const COUNTER_INTERACTION_TYPES = [ 'game_upcounter' ]; +const MINUTES_MIN = 0; +const MINUTES_MAX = 99; +const HALF_SECONDS_MIN = 0; +const HALF_SECONDS_MAX = 119; + +const OPERATOR_OPTIONS = [ + { value: 0, label: 'wiredfurni.params.operator.0' }, + { value: 1, label: 'wiredfurni.params.operator.1' }, + { value: 2, label: 'wiredfurni.params.operator.2' } +]; + +const normalizeOperator = (value: number) => +{ + if(value < 0 || value > 2) return 2; + + return value; +}; + +const normalizeMinutes = (value: number) => Math.max(MINUTES_MIN, Math.min(MINUTES_MAX, value)); +const normalizeHalfSeconds = (value: number) => Math.max(HALF_SECONDS_MIN, Math.min(HALF_SECONDS_MAX, value)); + +const formatSeconds = (halfSeconds: number) => +{ + const value = normalizeHalfSeconds(halfSeconds) / 2; + const text = value.toFixed(1); + + return text.endsWith('.0') ? text.slice(0, -2) : text; +}; + +export const WiredActionAdjustClockView: FC<{}> = () => +{ + const { trigger = null, setIntParams = null, setAllowedInteractionTypes = null, setAllowedInteractionErrorKey = null } = useWired(); + const [ operator, setOperator ] = useState(2); + const [ furniSource, setFurniSource ] = useState(() => + { + if(trigger?.intData?.length > 1) return trigger.intData[1]; + return (trigger?.selectedItems?.length ?? 0) > 0 ? 100 : 0; + }); + const [ minutes, setMinutes ] = useState(0); + const [ halfSeconds, setHalfSeconds ] = useState(0); + + const secondsLabel = useMemo(() => formatSeconds(halfSeconds), [ halfSeconds ]); + + useEffect(() => + { + if(!trigger) return; + + setOperator((trigger.intData.length > 0) ? normalizeOperator(trigger.intData[0]) : 2); + setFurniSource((trigger.intData.length > 1) ? trigger.intData[1] : ((trigger.selectedItems?.length ?? 0) > 0 ? 100 : 0)); + setMinutes((trigger.intData.length > 2) ? normalizeMinutes(trigger.intData[2]) : 0); + setHalfSeconds((trigger.intData.length > 3) ? normalizeHalfSeconds(trigger.intData[3]) : 0); + }, [ trigger ]); + + useEffect(() => + { + setAllowedInteractionTypes(COUNTER_INTERACTION_TYPES); + setAllowedInteractionErrorKey('wiredfurni.error.require_counter_furni'); + + return () => + { + setAllowedInteractionTypes(null); + setAllowedInteractionErrorKey(null); + }; + }, [ setAllowedInteractionErrorKey, setAllowedInteractionTypes ]); + + const save = () => + { + setIntParams([ + operator, + furniSource, + normalizeMinutes(minutes), + normalizeHalfSeconds(halfSeconds) + ]); + }; + + return ( + }> +
+ { OPERATOR_OPTIONS.map(option => + { + return ( +
+ setOperator(option.value) } /> + { LocalizeText(option.label) } +
+ ); + }) } +
+
+ { LocalizeText('wiredfurni.params.time.minute_selection') } + setMinutes(normalizeMinutes(event as number)) } /> + { minutes } +
+
+ { LocalizeText('wiredfurni.params.time.second_selection') } + setHalfSeconds(normalizeHalfSeconds(event as number)) } /> + { secondsLabel } +
+
+ ); +}; diff --git a/src/components/wired/views/actions/WiredActionControlClockView.tsx b/src/components/wired/views/actions/WiredActionControlClockView.tsx new file mode 100644 index 0000000..cba4691 --- /dev/null +++ b/src/components/wired/views/actions/WiredActionControlClockView.tsx @@ -0,0 +1,82 @@ +import { FC, useEffect, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredSourcesSelector } from '../WiredSourcesSelector'; +import { WiredActionBaseView } from './WiredActionBaseView'; + +const COUNTER_INTERACTION_TYPES = [ 'game_upcounter' ]; + +const CONTROL_OPTIONS = [ + { value: 0, label: 'wiredfurni.params.clock_control.0' }, + { value: 1, label: 'wiredfurni.params.clock_control.1' }, + { value: 2, label: 'wiredfurni.params.clock_control.2' }, + { value: 3, label: 'wiredfurni.params.clock_control.3' }, + { value: 4, label: 'wiredfurni.params.clock_control.4' } +]; + +const normalizeControl = (value: number) => +{ + if(value < 0 || value > 4) return 0; + + return value; +}; + +export const WiredActionControlClockView: FC<{}> = () => +{ + const { trigger = null, setIntParams = null, setAllowedInteractionTypes = null, setAllowedInteractionErrorKey = null } = useWired(); + const [ control, setControl ] = useState(0); + const [ furniSource, setFurniSource ] = useState(() => + { + if(trigger?.intData?.length > 1) return trigger.intData[1]; + return (trigger?.selectedItems?.length ?? 0) > 0 ? 100 : 0; + }); + + const save = () => + { + setIntParams([ + control, + furniSource + ]); + }; + + useEffect(() => + { + if(!trigger) return; + + setControl((trigger.intData.length > 0) ? normalizeControl(trigger.intData[0]) : 0); + setFurniSource((trigger.intData.length > 1) ? trigger.intData[1] : ((trigger.selectedItems?.length ?? 0) > 0 ? 100 : 0)); + }, [ trigger ]); + + useEffect(() => + { + setAllowedInteractionTypes(COUNTER_INTERACTION_TYPES); + setAllowedInteractionErrorKey('wiredfurni.error.require_counter_furni'); + + return () => + { + setAllowedInteractionTypes(null); + setAllowedInteractionErrorKey(null); + }; + }, [ setAllowedInteractionErrorKey, setAllowedInteractionTypes ]); + + return ( + }> +
+ { CONTROL_OPTIONS.map(option => + { + return ( +
+ setControl(option.value) } /> + { LocalizeText(option.label) } +
+ ); + }) } +
+
+ ); +}; diff --git a/src/components/wired/views/actions/WiredActionLayoutView.tsx b/src/components/wired/views/actions/WiredActionLayoutView.tsx index f515b63..c445dbb 100644 --- a/src/components/wired/views/actions/WiredActionLayoutView.tsx +++ b/src/components/wired/views/actions/WiredActionLayoutView.tsx @@ -1,6 +1,8 @@ import { WiredActionLayoutCode } from '../../../../api'; import { WiredActionBotChangeFigureView } from './WiredActionBotChangeFigureView'; +import { WiredActionAdjustClockView } from './WiredActionAdjustClockView'; import { WiredActionFreezeView } from './WiredActionFreezeView'; +import { WiredActionControlClockView } from './WiredActionControlClockView'; import { WiredActionFurniToFurniView } from './WiredActionFurniToFurniView'; import { WiredActionSetAltitudeView } from './WiredActionSetAltitudeView'; import { WiredActionSendSignalView } from './WiredActionSendSignalView'; @@ -42,6 +44,8 @@ export const WiredActionLayoutView = (code: number) => { case WiredActionLayoutCode.BOT_CHANGE_FIGURE: return ; + case WiredActionLayoutCode.ADJUST_CLOCK: + return ; case WiredActionLayoutCode.BOT_FOLLOW_AVATAR: return ; case WiredActionLayoutCode.BOT_GIVE_HAND_ITEM: @@ -64,6 +68,8 @@ export const WiredActionLayoutView = (code: number) => return ; case WiredActionLayoutCode.FREEZE: return ; + case WiredActionLayoutCode.CONTROL_CLOCK: + return ; case WiredActionLayoutCode.FURNI_TO_USER: return ; case WiredActionLayoutCode.FURNI_TO_FURNI: diff --git a/src/components/wired/views/conditions/WiredConditionCounterTimeMatchesView.tsx b/src/components/wired/views/conditions/WiredConditionCounterTimeMatchesView.tsx new file mode 100644 index 0000000..fb68c4e --- /dev/null +++ b/src/components/wired/views/conditions/WiredConditionCounterTimeMatchesView.tsx @@ -0,0 +1,123 @@ +import { FC, useEffect, useMemo, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Slider, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredSourcesSelector } from '../WiredSourcesSelector'; +import { WiredConditionBaseView } from './WiredConditionBaseView'; + +const COUNTER_INTERACTION_TYPES = [ 'game_upcounter' ]; +const MINUTES_MIN = 0; +const MINUTES_MAX = 99; +const HALF_SECONDS_MIN = 0; +const HALF_SECONDS_MAX = 119; + +const COMPARISON_OPTIONS = [ + { value: 0, label: 'wiredfurni.params.comparison.0' }, + { value: 1, label: 'wiredfurni.params.comparison.1' }, + { value: 2, label: 'wiredfurni.params.comparison.2' } +]; + +const normalizeComparison = (value: number) => +{ + if(value < 0 || value > 2) return 1; + + return value; +}; + +const normalizeMinutes = (value: number) => Math.max(MINUTES_MIN, Math.min(MINUTES_MAX, value)); +const normalizeHalfSeconds = (value: number) => Math.max(HALF_SECONDS_MIN, Math.min(HALF_SECONDS_MAX, value)); + +const formatSeconds = (halfSeconds: number) => +{ + const value = normalizeHalfSeconds(halfSeconds) / 2; + const text = value.toFixed(1); + + return text.endsWith('.0') ? text.slice(0, -2) : text; +}; + +export const WiredConditionCounterTimeMatchesView: FC<{}> = () => +{ + const { trigger = null, setIntParams = null, setAllowedInteractionTypes = null, setAllowedInteractionErrorKey = null } = useWired(); + const [ comparison, setComparison ] = useState(1); + const [ furniSource, setFurniSource ] = useState(() => + { + if(trigger?.intData?.length > 3) return trigger.intData[3]; + return (trigger?.selectedItems?.length ?? 0) > 0 ? 100 : 0; + }); + const [ minutes, setMinutes ] = useState(0); + const [ halfSeconds, setHalfSeconds ] = useState(0); + + const secondsLabel = useMemo(() => formatSeconds(halfSeconds), [ halfSeconds ]); + + const save = () => + { + setIntParams([ + normalizeComparison(comparison), + normalizeMinutes(minutes), + normalizeHalfSeconds(halfSeconds), + furniSource + ]); + }; + + useEffect(() => + { + if(!trigger) return; + + setComparison((trigger.intData.length > 0) ? normalizeComparison(trigger.intData[0]) : 1); + setMinutes((trigger.intData.length > 1) ? normalizeMinutes(trigger.intData[1]) : 0); + setHalfSeconds((trigger.intData.length > 2) ? normalizeHalfSeconds(trigger.intData[2]) : 0); + setFurniSource((trigger.intData.length > 3) ? trigger.intData[3] : ((trigger.selectedItems?.length ?? 0) > 0 ? 100 : 0)); + }, [ trigger ]); + + useEffect(() => + { + setAllowedInteractionTypes(COUNTER_INTERACTION_TYPES); + setAllowedInteractionErrorKey('wiredfurni.error.require_counter_furni'); + + return () => + { + setAllowedInteractionTypes(null); + setAllowedInteractionErrorKey(null); + }; + }, [ setAllowedInteractionErrorKey, setAllowedInteractionTypes ]); + + return ( + }> +
+ { COMPARISON_OPTIONS.map(option => + { + return ( +
+ setComparison(option.value) } /> + { LocalizeText(option.label) } +
+ ); + }) } +
+
+ { LocalizeText('wiredfurni.params.clock_minutes_elapsed', [ 'minutes' ], [ minutes.toString() ]) } + setMinutes(normalizeMinutes(event as number)) } /> + { minutes } +
+
+ { LocalizeText('wiredfurni.params.clock_seconds_elapsed', [ 'seconds' ], [ secondsLabel ]) } + setHalfSeconds(normalizeHalfSeconds(event as number)) } /> + { secondsLabel } +
+
+ ); +}; diff --git a/src/components/wired/views/conditions/WiredConditionLayoutView.tsx b/src/components/wired/views/conditions/WiredConditionLayoutView.tsx index a1a88c2..11c2a30 100644 --- a/src/components/wired/views/conditions/WiredConditionLayoutView.tsx +++ b/src/components/wired/views/conditions/WiredConditionLayoutView.tsx @@ -5,6 +5,7 @@ import { WiredConditionActorIsOnFurniView } from './WiredConditionActorIsOnFurni import { WiredConditionActorIsTeamMemberView } from './WiredConditionActorIsTeamMemberView'; import { WiredConditionActorIsWearingBadgeView } from './WiredConditionActorIsWearingBadgeView'; import { WiredConditionActorIsWearingEffectView } from './WiredConditionActorIsWearingEffectView'; +import { WiredConditionCounterTimeMatchesView } from './WiredConditionCounterTimeMatchesView'; import { WiredConditionDateRangeView } from './WiredConditionDateRangeView'; import { WiredConditionFurniHasAvatarOnView } from './WiredConditionFurniHasAvatarOnView'; import { WiredConditionFurniHasFurniOnView } from './WiredConditionFurniHasFurniOnView'; @@ -58,6 +59,8 @@ export const WiredConditionLayoutView = (code: number) => case WiredConditionlayout.USER_COUNT_IN: case WiredConditionlayout.NOT_USER_COUNT_IN: return ; + case WiredConditionlayout.COUNTER_TIME_MATCHES: + return ; } return null; diff --git a/src/components/wired/views/triggers/WiredTriggerBaseView.tsx b/src/components/wired/views/triggers/WiredTriggerBaseView.tsx index 7590d9a..2c51980 100644 --- a/src/components/wired/views/triggers/WiredTriggerBaseView.tsx +++ b/src/components/wired/views/triggers/WiredTriggerBaseView.tsx @@ -1,4 +1,4 @@ -import { FC, PropsWithChildren } from 'react'; +import { FC, PropsWithChildren, ReactNode } from 'react'; import { WiredFurniType } from '../../../../api'; import { WiredBaseView } from '../WiredBaseView'; @@ -7,16 +7,17 @@ export interface WiredTriggerBaseViewProps hasSpecialInput: boolean; requiresFurni: number; save: () => void; + footer?: ReactNode; } export const WiredTriggerBaseView: FC> = props => { - const { requiresFurni = WiredFurniType.STUFF_SELECTION_OPTION_NONE, save = null, hasSpecialInput = false, children = null } = props; + const { requiresFurni = WiredFurniType.STUFF_SELECTION_OPTION_NONE, save = null, hasSpecialInput = false, children = null, footer = null } = props; const onSave = () => (save && save()); return ( - + { children } ); diff --git a/src/components/wired/views/triggers/WiredTriggerClockCounterView.tsx b/src/components/wired/views/triggers/WiredTriggerClockCounterView.tsx new file mode 100644 index 0000000..5b6240c --- /dev/null +++ b/src/components/wired/views/triggers/WiredTriggerClockCounterView.tsx @@ -0,0 +1,101 @@ +import { FC, useEffect, useMemo, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Slider, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredSourcesSelector, WiredSourceOption } from '../WiredSourcesSelector'; +import { WiredTriggerBaseView } from './WiredTriggerBaseView'; + +const COUNTER_INTERACTION_TYPES = [ 'game_upcounter' ]; +const MINUTES_MIN = 0; +const MINUTES_MAX = 99; +const HALF_SECONDS_MIN = 0; +const HALF_SECONDS_MAX = 119; +const TRIGGER_FURNI_SOURCES: WiredSourceOption[] = [ + { value: 0, label: 'wiredfurni.params.sources.furni.0' }, + { value: 100, label: 'wiredfurni.params.sources.furni.100' } +]; + +const normalizeMinutes = (value: number) => Math.max(MINUTES_MIN, Math.min(MINUTES_MAX, value)); +const normalizeHalfSeconds = (value: number) => Math.max(HALF_SECONDS_MIN, Math.min(HALF_SECONDS_MAX, value)); +const normalizeFurniSource = (value: number) => (value === 100 ? 100 : 0); + +const formatSeconds = (halfSeconds: number) => +{ + const value = normalizeHalfSeconds(halfSeconds) / 2; + const text = value.toFixed(1); + + return text.endsWith('.0') ? text.slice(0, -2) : text; +}; + +export const WiredTriggerClockCounterView: FC<{}> = () => +{ + const { trigger = null, setIntParams = null, setAllowedInteractionTypes = null, setAllowedInteractionErrorKey = null } = useWired(); + const [ furniSource, setFurniSource ] = useState(() => + { + if(trigger?.intData?.length > 2) return normalizeFurniSource(trigger.intData[2]); + return (trigger?.selectedItems?.length ?? 0) > 0 ? 100 : 0; + }); + const [ minutes, setMinutes ] = useState(0); + const [ halfSeconds, setHalfSeconds ] = useState(0); + + const secondsLabel = useMemo(() => formatSeconds(halfSeconds), [ halfSeconds ]); + + const save = () => + { + setIntParams([ + normalizeMinutes(minutes), + normalizeHalfSeconds(halfSeconds), + normalizeFurniSource(furniSource) + ]); + }; + + useEffect(() => + { + if(!trigger) return; + + setMinutes((trigger.intData.length > 0) ? normalizeMinutes(trigger.intData[0]) : 0); + setHalfSeconds((trigger.intData.length > 1) ? normalizeHalfSeconds(trigger.intData[1]) : 0); + setFurniSource((trigger.intData.length > 2) ? normalizeFurniSource(trigger.intData[2]) : ((trigger.selectedItems?.length ?? 0) > 0 ? 100 : 0)); + }, [ trigger ]); + + useEffect(() => + { + setAllowedInteractionTypes(COUNTER_INTERACTION_TYPES); + setAllowedInteractionErrorKey('wiredfurni.error.require_counter_furni'); + + return () => + { + setAllowedInteractionTypes(null); + setAllowedInteractionErrorKey(null); + }; + }, [ setAllowedInteractionErrorKey, setAllowedInteractionTypes ]); + + return ( + }> +
+ { LocalizeText('wiredfurni.params.clock_minutes_elapsed', [ 'minutes' ], [ minutes.toString() ]) } + setMinutes(normalizeMinutes(event as number)) } /> + { minutes } +
+
+ { LocalizeText('wiredfurni.params.clock_seconds_elapsed', [ 'seconds' ], [ secondsLabel ]) } + setHalfSeconds(normalizeHalfSeconds(event as number)) } /> + { secondsLabel } +
+
+ ); +}; diff --git a/src/components/wired/views/triggers/WiredTriggerLayoutView.tsx b/src/components/wired/views/triggers/WiredTriggerLayoutView.tsx index 3b152fb..409bdf0 100644 --- a/src/components/wired/views/triggers/WiredTriggerLayoutView.tsx +++ b/src/components/wired/views/triggers/WiredTriggerLayoutView.tsx @@ -9,6 +9,7 @@ import { WiredTriggerBotReachedStuffView } from './WiredTriggerBotReachedStuffVi import { WiredTriggerClickFurniView } from './WiredTriggerClickFurniView'; import { WiredTriggerClickTileView } from './WiredTriggerClickTileView'; import { WiredTriggerClickUserView } from './WiredTriggerClickUserView'; +import { WiredTriggerClockCounterView } from './WiredTriggerClockCounterView'; import { WiredTriggerCollisionView } from './WiredTriggerCollisionView'; import { WiredTriggerUserPerformsActionView } from './WiredTriggerUserPerformsActionView'; import { WiredTriggeExecuteOnceView } from './WiredTriggerExecuteOnceView'; @@ -45,6 +46,8 @@ export const WiredTriggerLayoutView = (code: number) => return ; case WiredTriggerLayout.CLICK_USER: return ; + case WiredTriggerLayout.CLOCK_COUNTER: + return ; case WiredTriggerLayout.USER_PERFORMS_ACTION: return ; case WiredTriggerLayout.COLLISION: diff --git a/src/hooks/wired/useWired.ts b/src/hooks/wired/useWired.ts index 19e6ed8..884137f 100644 --- a/src/hooks/wired/useWired.ts +++ b/src/hooks/wired/useWired.ts @@ -17,6 +17,7 @@ const useWiredState = () => const [ neighborhoodTiles, setNeighborhoodTiles ] = useState<{ x: number; y: number }[] | null>(null); const [ neighborhoodInvert, setNeighborhoodInvert ] = useState(false); const [ allowedInteractionTypes, setAllowedInteractionTypes ] = useState(null); + const [ allowedInteractionErrorKey, setAllowedInteractionErrorKey ] = useState(null); const { showConfirm = null, simpleAlert = null } = useNotification(); const saveWired = () => @@ -84,6 +85,11 @@ const useWiredState = () => return allowedInteractionTypes.some(type => (type && type.toLowerCase() === interactionType)); }; + const handleDisallowedInteraction = () => + { + if(allowedInteractionErrorKey) simpleAlert(LocalizeText(allowedInteractionErrorKey), null, null, null, LocalizeText('wiredfurni.title')); + }; + if(selectByType && category === RoomObjectCategory.FLOOR) { const roomId = GetRoomSession().roomId; @@ -97,6 +103,7 @@ const useWiredState = () => if(!sourceFurniData) return; if(!isAllowedInteraction(sourceFurniData)) { + handleDisallowedInteraction(); setFurniIds(prevValue => { if(!prevValue.includes(objectId)) return prevValue; @@ -185,6 +192,7 @@ const useWiredState = () => if(!sourceFurniData) return; if(!isAllowedInteraction(sourceFurniData)) { + handleDisallowedInteraction(); setFurniIds(prevValue => { if(!prevValue.includes(objectId)) return prevValue; @@ -245,7 +253,9 @@ const useWiredState = () => if(parser.info && parser.info.length) { - simpleAlert(parser.info, null, null, null, LocalizeText('wiredfurni.title')); + const message = (/^[a-z0-9_.]+$/i.test(parser.info) ? LocalizeText(parser.info) : parser.info); + + simpleAlert(message, null, null, null, LocalizeText('wiredfurni.title')); } }); @@ -291,10 +301,11 @@ const useWiredState = () => setNeighborhoodTiles(null); setNeighborhoodInvert(false); setAllowedInteractionTypes(null); + setAllowedInteractionErrorKey(null); }; }, [ trigger ]); - return { trigger, setTrigger, intParams, setIntParams, stringParam, setStringParam, furniIds, setFurniIds, actionDelay, setActionDelay, setAllowsFurni, saveWired, selectObjectForWired, setSelectByType, setNeighborhoodTiles, setNeighborhoodInvert, setAllowedInteractionTypes }; + return { trigger, setTrigger, intParams, setIntParams, stringParam, setStringParam, furniIds, setFurniIds, actionDelay, setActionDelay, setAllowsFurni, saveWired, selectObjectForWired, setSelectByType, setNeighborhoodTiles, setNeighborhoodInvert, setAllowedInteractionTypes, setAllowedInteractionErrorKey }; }; export const useWired = () => useBetween(useWiredState); From 4a0661181cba333de953be343f16704644b6d693 Mon Sep 17 00:00:00 2001 From: Lorenzune Date: Thu, 19 Mar 2026 14:27:33 +0100 Subject: [PATCH 07/10] feat(wired-ui): add advanced condition editors --- public/UITexts.example | 76 ++++++- src/api/wired/WiredConditionLayoutCode.ts | 10 + .../WiredConditionHasAltitudeView.tsx | 185 +++++++++++++++++ .../conditions/WiredConditionLayoutView.tsx | 25 +++ .../WiredConditionMatchDateView.tsx | 195 ++++++++++++++++++ .../WiredConditionMatchTimeView.tsx | 163 +++++++++++++++ .../WiredConditionTeamHasRankView.tsx | 102 +++++++++ .../WiredConditionTeamHasScoreView.tsx | 158 ++++++++++++++ .../WiredConditionTriggererMatchView.tsx | 130 ++++++++++++ .../WiredConditionUserPerformsActionView.tsx | 151 ++++++++++++++ 10 files changed, 1187 insertions(+), 8 deletions(-) create mode 100644 src/components/wired/views/conditions/WiredConditionHasAltitudeView.tsx create mode 100644 src/components/wired/views/conditions/WiredConditionMatchDateView.tsx create mode 100644 src/components/wired/views/conditions/WiredConditionMatchTimeView.tsx create mode 100644 src/components/wired/views/conditions/WiredConditionTeamHasRankView.tsx create mode 100644 src/components/wired/views/conditions/WiredConditionTeamHasScoreView.tsx create mode 100644 src/components/wired/views/conditions/WiredConditionTriggererMatchView.tsx create mode 100644 src/components/wired/views/conditions/WiredConditionUserPerformsActionView.tsx diff --git a/public/UITexts.example b/public/UITexts.example index f4c6168..980458f 100644 --- a/public/UITexts.example +++ b/public/UITexts.example @@ -13,12 +13,72 @@ "widget.settings.interface.fps.warning": "Het zetten van FPS naar unlimited kan prestatie problemen veroorzaken!", "widget.settings.interface.secondary": "Verander de window header kleur", "widget.settings.interface.reset": "Reset header kleur naar default", - "widget.room.chat.hide_pets": "Verberg dieren", - "widget.room.chat.hide_avatars": "Verberg avatars", - "widget.room.chat.hide_balloon": "Verberg Spreekballon", - "widget.room.chat.show_balloon": "Spreekballon", - "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%" + "widget.room.chat.hide_pets": "Verberg dieren", + "widget.room.chat.hide_avatars": "Verberg avatars", + "widget.room.chat.hide_balloon": "Verberg Spreekballon", + "widget.room.chat.show_balloon": "Spreekballon", + "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.collapse": "Nascondi le impostazioni avanzate", + "wiredfurni.params.sources.expand": "Mostra le impostazioni avanzate", + "wiredfurni.params.quantifier_selection": "Abbina condizione se:", + "wiredfurni.params.quantifier.users.0": "Tutti gli utenti corrispondono", + "wiredfurni.params.quantifier.users.1": "Uno qualsiasi degli utenti corrisponde", + "wiredfurni.params.quantifier.users.neg.0": "Uno qualsiasi degli utenti non corrisponde", + "wiredfurni.params.quantifier.users.neg.1": "Nessuno degli utenti corrisponde", + "wiredfurni.params.quantifier.furni.0": "Tutti i Furni corrispondono", + "wiredfurni.params.quantifier.furni.1": "Uno qualsiasi dei Furni corrisponde", + "wiredfurni.params.usertype.1": "Habbo", + "wiredfurni.params.usertype.2": "Cucciolo", + "wiredfurni.params.usertype.4": "Bot", + "wiredfurni.params.sources.users.title.match.0": "Gli utenti da abbinare:", + "wiredfurni.params.sources.users.title.match.1": "Utenti da comparare con:", + "wiredfurni.params.sources.users.101": "Usa l'utente specificato dal nome", + "wiredfurni.params.comparison.0": "Più basso di", + "wiredfurni.params.comparison.1": "È uguale a", + "wiredfurni.params.comparison.2": "Più alto di", + "wiredfurni.params.team": "Scegli una squadra", + "wiredfurni.params.team.1": "Rossa", + "wiredfurni.params.team.2": "Verde", + "wiredfurni.params.team.3": "Blu", + "wiredfurni.params.team.4": "Gialla", + "wiredfurni.params.team.triggerer": "Squadra dell'innescatore", + "wiredfurni.params.comparison_selection": "Scegli tipo:", + "wiredfurni.params.setscore2": "La squadra deve segnare:", + "wiredfurni.params.placement_selection": "Posizione:", + "wiredfurni.params.placement.1": "1.", + "wiredfurni.params.placement.2": "2.", + "wiredfurni.params.placement.3": "3.", + "wiredfurni.params.placement.4": "4.", + "wiredfurni.params.time.hour_selection": "Ore:", + "wiredfurni.params.time.minute_selection": "Minuti:", + "wiredfurni.params.time.second_selection": "Secondi:", + "wiredfurni.params.time.skip": "Non usare il filtro", + "wiredfurni.params.time.exact": "Esatto", + "wiredfurni.params.time.range": "Range", + "wiredfurni.params.time.weekday_selection": "Giorno della settimana:", + "wiredfurni.params.time.weekday.1": "Lunedì", + "wiredfurni.params.time.weekday.2": "Martedì", + "wiredfurni.params.time.weekday.3": "Mercoledì", + "wiredfurni.params.time.weekday.4": "Giovedì", + "wiredfurni.params.time.weekday.5": "Venerdì", + "wiredfurni.params.time.weekday.6": "Sabato", + "wiredfurni.params.time.weekday.7": "Domenica", + "wiredfurni.params.time.day_selection": "Giorno:", + "wiredfurni.params.time.month_selection": "Mese:", + "wiredfurni.params.time.month.10": "Ott.", + "wiredfurni.params.time.month.11": "Nov.", + "wiredfurni.params.time.month.12": "Dic.", + "wiredfurni.params.time.month.1": "Gen.", + "wiredfurni.params.time.month.2": "Feb.", + "wiredfurni.params.time.month.3": "Mar.", + "wiredfurni.params.time.month.4": "Apr.", + "wiredfurni.params.time.month.5": "Mag.", + "wiredfurni.params.time.month.6": "Giu.", + "wiredfurni.params.time.month.7": "Lug.", + "wiredfurni.params.time.month.8": "Ago.", + "wiredfurni.params.time.month.9": "Set.", + "wiredfurni.params.time.year_selection": "Anno:" } diff --git a/src/api/wired/WiredConditionLayoutCode.ts b/src/api/wired/WiredConditionLayoutCode.ts index d63b3e1..b344f8b 100644 --- a/src/api/wired/WiredConditionLayoutCode.ts +++ b/src/api/wired/WiredConditionLayoutCode.ts @@ -27,4 +27,14 @@ export class WiredConditionlayout public static DATE_RANGE_ACTIVE: number = 24; public static ACTOR_HAS_HANDITEM: number = 25; public static COUNTER_TIME_MATCHES: number = 27; + public static USER_PERFORMS_ACTION: number = 28; + public static HAS_ALTITUDE: number = 29; + public static NOT_USER_PERFORMS_ACTION: number = 30; + public static NOT_ACTOR_HAS_HANDITEM: number = 31; + public static TRIGGERER_MATCH: number = 32; + public static NOT_TRIGGERER_MATCH: number = 33; + public static TEAM_HAS_SCORE: number = 34; + public static TEAM_HAS_RANK: number = 35; + public static MATCH_TIME: number = 36; + public static MATCH_DATE: number = 37; } diff --git a/src/components/wired/views/conditions/WiredConditionHasAltitudeView.tsx b/src/components/wired/views/conditions/WiredConditionHasAltitudeView.tsx new file mode 100644 index 0000000..91a6a82 --- /dev/null +++ b/src/components/wired/views/conditions/WiredConditionHasAltitudeView.tsx @@ -0,0 +1,185 @@ +import { FC, useEffect, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Slider, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredSourcesSelector } from '../WiredSourcesSelector'; +import { WiredConditionBaseView } from './WiredConditionBaseView'; + +const COUNTER_INTERACTION_TYPES = [ 'game_upcounter' ]; +const MIN_ALTITUDE = 0; +const MAX_ALTITUDE = 40; +const ALTITUDE_STEP = 0.01; +const ALTITUDE_PATTERN = /^\d*(\.\d{0,2})?$/; + +const clampAltitude = (value: number) => +{ + if(isNaN(value)) return MIN_ALTITUDE; + + const clamped = Math.min(MAX_ALTITUDE, Math.max(MIN_ALTITUDE, value)); + + return parseFloat(clamped.toFixed(2)); +}; + +const formatAltitude = (value: number) => +{ + const normalized = clampAltitude(value); + const text = normalized.toFixed(2); + + return text.replace(/\.00$/, '').replace(/(\.\d)0$/, '$1'); +}; + +const parseAltitude = (value: string) => +{ + if(!value || !value.trim().length) return 0; + + const parsed = parseFloat(value); + + if(isNaN(parsed)) return 0; + + return clampAltitude(parsed); +}; + +export const WiredConditionHasAltitudeView: FC<{}> = () => +{ + const { trigger = null, setIntParams = null, setStringParam = null, setAllowedInteractionTypes = null, setAllowedInteractionErrorKey = null } = useWired(); + const [ comparison, setComparison ] = useState(1); + const [ furniSource, setFurniSource ] = useState(() => + { + if(trigger?.intData?.length > 1) return trigger.intData[1]; + return (trigger?.selectedItems?.length ?? 0) > 0 ? 100 : 0; + }); + const [ quantifier, setQuantifier ] = useState(0); + const [ showAdvanced, setShowAdvanced ] = useState(false); + const [ altitude, setAltitude ] = useState(0); + const [ altitudeInput, setAltitudeInput ] = useState('0'); + + useEffect(() => + { + setAllowedInteractionTypes(COUNTER_INTERACTION_TYPES); + setAllowedInteractionErrorKey('wiredfurni.error.require_counter_furni'); + + return () => + { + setAllowedInteractionTypes(null); + setAllowedInteractionErrorKey(null); + }; + }, [ setAllowedInteractionErrorKey, setAllowedInteractionTypes ]); + + useEffect(() => + { + if(!trigger) return; + + setComparison((trigger.intData.length > 0) ? trigger.intData[0] : 1); + setFurniSource((trigger.intData.length > 1) ? trigger.intData[1] : ((trigger.selectedItems?.length ?? 0) > 0 ? 100 : 0)); + setQuantifier((trigger.intData.length > 2) ? trigger.intData[2] : 0); + setShowAdvanced((trigger.intData.length > 1) ? (trigger.intData[1] !== 0 || trigger.intData[2] !== 0) : false); + + const nextAltitude = parseAltitude(trigger.stringData); + setAltitude(nextAltitude); + setAltitudeInput(formatAltitude(nextAltitude)); + }, [ trigger ]); + + const updateAltitude = (value: number) => + { + const nextValue = clampAltitude(value); + + setAltitude(nextValue); + setAltitudeInput(formatAltitude(nextValue)); + }; + + const updateAltitudeInput = (value: string) => + { + if(!ALTITUDE_PATTERN.test(value)) return; + + setAltitudeInput(value); + + if(!value.length) + { + setAltitude(0); + return; + } + + const parsedValue = parseFloat(value); + + if(isNaN(parsedValue)) return; + + if(parsedValue > MAX_ALTITUDE) + { + updateAltitude(MAX_ALTITUDE); + return; + } + + setAltitude(clampAltitude(parsedValue)); + }; + + const save = () => + { + setIntParams([ + comparison, + furniSource, + quantifier + ]); + setStringParam(formatAltitude(altitude)); + }; + + return ( + + + { showAdvanced && + <> +
+ { LocalizeText('wiredfurni.params.quantifier_selection') } + { [ 0, 1 ].map(value => + { + return ( +
+ setQuantifier(value) } /> + { LocalizeText(`wiredfurni.params.quantifier.furni.${ value }`) } +
+ ); + }) } +
+ + } +
+ }> +
+ { [ 0, 1, 2 ].map(value => + { + return ( +
+ setComparison(value) } /> + { LocalizeText(`wiredfurni.params.comparison.${ value }`) } +
+ ); + }) } +
+
+ { LocalizeText('wiredfurni.params.setaltitude') } + setAltitudeInput(formatAltitude(altitude)) } + onChange={ event => updateAltitudeInput(event.target.value) } /> +
+
+ updateAltitude(event as number) } /> + { formatAltitude(altitude) } +
+ + ); +}; diff --git a/src/components/wired/views/conditions/WiredConditionLayoutView.tsx b/src/components/wired/views/conditions/WiredConditionLayoutView.tsx index 11c2a30..888df16 100644 --- a/src/components/wired/views/conditions/WiredConditionLayoutView.tsx +++ b/src/components/wired/views/conditions/WiredConditionLayoutView.tsx @@ -7,6 +7,9 @@ import { WiredConditionActorIsWearingBadgeView } from './WiredConditionActorIsWe import { WiredConditionActorIsWearingEffectView } from './WiredConditionActorIsWearingEffectView'; import { WiredConditionCounterTimeMatchesView } from './WiredConditionCounterTimeMatchesView'; import { WiredConditionDateRangeView } from './WiredConditionDateRangeView'; +import { WiredConditionMatchDateView } from './WiredConditionMatchDateView'; +import { WiredConditionMatchTimeView } from './WiredConditionMatchTimeView'; +import { WiredConditionHasAltitudeView } from './WiredConditionHasAltitudeView'; import { WiredConditionFurniHasAvatarOnView } from './WiredConditionFurniHasAvatarOnView'; import { WiredConditionFurniHasFurniOnView } from './WiredConditionFurniHasFurniOnView'; import { WiredConditionFurniHasNotFurniOnView } from './WiredConditionFurniHasNotFurniOnView'; @@ -14,6 +17,10 @@ import { WiredConditionFurniIsOfTypeView } from './WiredConditionFurniIsOfTypeVi import { WiredConditionFurniMatchesSnapshotView } from './WiredConditionFurniMatchesSnapshotView'; import { WiredConditionTimeElapsedLessView } from './WiredConditionTimeElapsedLessView'; import { WiredConditionTimeElapsedMoreView } from './WiredConditionTimeElapsedMoreView'; +import { WiredConditionTeamHasRankView } from './WiredConditionTeamHasRankView'; +import { WiredConditionTeamHasScoreView } from './WiredConditionTeamHasScoreView'; +import { WiredConditionTriggererMatchView } from './WiredConditionTriggererMatchView'; +import { WiredConditionUserPerformsActionView } from './WiredConditionUserPerformsActionView'; import { WiredConditionUserCountInRoomView } from './WiredConditionUserCountInRoomView'; export const WiredConditionLayoutView = (code: number) => @@ -21,7 +28,11 @@ export const WiredConditionLayoutView = (code: number) => switch(code) { case WiredConditionlayout.ACTOR_HAS_HANDITEM: + case WiredConditionlayout.NOT_ACTOR_HAS_HANDITEM: return ; + case WiredConditionlayout.TRIGGERER_MATCH: + case WiredConditionlayout.NOT_TRIGGERER_MATCH: + return ; case WiredConditionlayout.ACTOR_IS_GROUP_MEMBER: case WiredConditionlayout.NOT_ACTOR_IN_GROUP: return ; @@ -39,6 +50,10 @@ export const WiredConditionLayoutView = (code: number) => return ; case WiredConditionlayout.DATE_RANGE_ACTIVE: return ; + case WiredConditionlayout.MATCH_TIME: + return ; + case WiredConditionlayout.MATCH_DATE: + return ; case WiredConditionlayout.FURNIS_HAVE_AVATARS: case WiredConditionlayout.FURNI_NOT_HAVE_HABBO: return ; @@ -61,6 +76,16 @@ export const WiredConditionLayoutView = (code: number) => return ; case WiredConditionlayout.COUNTER_TIME_MATCHES: return ; + case WiredConditionlayout.USER_PERFORMS_ACTION: + return ; + case WiredConditionlayout.NOT_USER_PERFORMS_ACTION: + return ; + case WiredConditionlayout.HAS_ALTITUDE: + return ; + case WiredConditionlayout.TEAM_HAS_SCORE: + return ; + case WiredConditionlayout.TEAM_HAS_RANK: + return ; } return null; diff --git a/src/components/wired/views/conditions/WiredConditionMatchDateView.tsx b/src/components/wired/views/conditions/WiredConditionMatchDateView.tsx new file mode 100644 index 0000000..f1b9ccc --- /dev/null +++ b/src/components/wired/views/conditions/WiredConditionMatchDateView.tsx @@ -0,0 +1,195 @@ +import { ChangeEvent, FC, useEffect, useMemo, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredConditionBaseView } from './WiredConditionBaseView'; + +const MODE_SKIP = 0; +const MODE_EXACT = 1; +const MODE_RANGE = 2; +const MODE_OPTIONS = [ MODE_SKIP, MODE_EXACT, MODE_RANGE ]; +const WEEKDAY_OPTIONS = [ 1, 2, 3, 4, 5, 6, 7 ]; +const MONTH_OPTIONS = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ]; + +const createMask = (values: number[]) => values.reduce((mask, value) => (mask | (1 << value)), 0); +const ALL_WEEKDAYS_MASK = createMask(WEEKDAY_OPTIONS); +const ALL_MONTHS_MASK = createMask(MONTH_OPTIONS); + +const clampValue = (value: number, min: number, max: number) => +{ + if(isNaN(value)) return min; + + return Math.max(min, Math.min(max, Math.floor(value))); +}; + +const parseInputValue = (event: ChangeEvent, min: number, max: number) => +{ + return clampValue(parseInt(event.target.value || min.toString(), 10), min, max); +}; + +const toggleMaskValue = (mask: number, value: number, enabled: boolean) => +{ + if(enabled) return (mask | (1 << value)); + + return (mask & ~(1 << value)); +}; + +const InlineNumberInput: FC<{ value: number; min: number; max: number; onChange: (value: number) => void }> = props => +{ + const { value = 0, min = 0, max = 0, onChange = null } = props; + + return ( + onChange(parseInputValue(event, min, max)) } /> + ); +}; + +interface MatchDateSectionProps +{ + sectionId: string; + titleKey: string; + mode: number; + fromValue: number; + toValue: number; + min: number; + max: number; + onModeChange: (value: number) => void; + onFromChange: (value: number) => void; + onToChange: (value: number) => void; +} + +const MatchDateSection: FC = props => +{ + const { sectionId = '', titleKey = '', mode = MODE_SKIP, fromValue = 0, toValue = 0, min = 0, max = 0, onModeChange = null, onFromChange = null, onToChange = null } = props; + + return ( +
+ { LocalizeText(titleKey) } +
+ onModeChange(MODE_SKIP) } /> + { LocalizeText('wiredfurni.params.time.skip') } +
+
+ onModeChange(MODE_EXACT) } /> + { LocalizeText('wiredfurni.params.time.exact') } + +
+
+ onModeChange(MODE_RANGE) } /> + { LocalizeText('wiredfurni.params.time.range') } + + - + +
+
+ ); +}; + +export const WiredConditionMatchDateView: FC<{}> = () => +{ + const { trigger = null, setIntParams = null } = useWired(); + const currentYear = useMemo(() => new Date().getFullYear(), []); + const [ weekdayMask, setWeekdayMask ] = useState(ALL_WEEKDAYS_MASK); + const [ dayMode, setDayMode ] = useState(MODE_SKIP); + const [ dayFrom, setDayFrom ] = useState(1); + const [ dayTo, setDayTo ] = useState(31); + const [ monthMask, setMonthMask ] = useState(ALL_MONTHS_MASK); + const [ yearMode, setYearMode ] = useState(MODE_SKIP); + const [ yearFrom, setYearFrom ] = useState(currentYear); + const [ yearTo, setYearTo ] = useState(currentYear); + + useEffect(() => + { + if(!trigger) return; + + setWeekdayMask((trigger.intData[0] && (trigger.intData[0] > 0)) ? trigger.intData[0] : ALL_WEEKDAYS_MASK); + setDayMode(MODE_OPTIONS.includes(trigger.intData[1]) ? trigger.intData[1] : MODE_SKIP); + setDayFrom(clampValue(trigger.intData[2] ?? 1, 1, 31)); + setDayTo(clampValue(trigger.intData[3] ?? 31, 1, 31)); + setMonthMask((trigger.intData[4] && (trigger.intData[4] > 0)) ? trigger.intData[4] : ALL_MONTHS_MASK); + setYearMode(MODE_OPTIONS.includes(trigger.intData[5]) ? trigger.intData[5] : MODE_SKIP); + setYearFrom(clampValue(trigger.intData[6] ?? currentYear, 1, 9999)); + setYearTo(clampValue(trigger.intData[7] ?? currentYear, 1, 9999)); + }, [ currentYear, trigger ]); + + const save = () => + { + setIntParams([ + weekdayMask || ALL_WEEKDAYS_MASK, + dayMode, + clampValue(dayFrom, 1, 31), + clampValue(dayTo, 1, 31), + monthMask || ALL_MONTHS_MASK, + yearMode, + clampValue(yearFrom, 1, 9999), + clampValue(yearTo, 1, 9999) + ]); + }; + + return ( + +
+
+ { LocalizeText('wiredfurni.params.time.weekday_selection') } +
+ { WEEKDAY_OPTIONS.map(value => + { + const checked = ((weekdayMask & (1 << value)) !== 0); + + return ( + + ); + }) } +
+
+ setDayFrom(clampValue(value, 1, 31)) } + onModeChange={ setDayMode } + onToChange={ value => setDayTo(clampValue(value, 1, 31)) } /> +
+ { LocalizeText('wiredfurni.params.time.month_selection') } +
+ { MONTH_OPTIONS.map(value => + { + const checked = ((monthMask & (1 << value)) !== 0); + + return ( + + ); + }) } +
+
+ setYearFrom(clampValue(value, 1, 9999)) } + onModeChange={ setYearMode } + onToChange={ value => setYearTo(clampValue(value, 1, 9999)) } /> +
+
+ ); +}; diff --git a/src/components/wired/views/conditions/WiredConditionMatchTimeView.tsx b/src/components/wired/views/conditions/WiredConditionMatchTimeView.tsx new file mode 100644 index 0000000..426ab99 --- /dev/null +++ b/src/components/wired/views/conditions/WiredConditionMatchTimeView.tsx @@ -0,0 +1,163 @@ +import { ChangeEvent, FC, useEffect, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredConditionBaseView } from './WiredConditionBaseView'; + +const MODE_SKIP = 0; +const MODE_EXACT = 1; +const MODE_RANGE = 2; +const MODE_OPTIONS = [ MODE_SKIP, MODE_EXACT, MODE_RANGE ]; + +const clampValue = (value: number, min: number, max: number) => +{ + if(isNaN(value)) return min; + + return Math.max(min, Math.min(max, Math.floor(value))); +}; + +interface TimeFilterSectionProps +{ + sectionId: string; + titleKey: string; + min: number; + max: number; + mode: number; + fromValue: number; + toValue: number; + onModeChange: (value: number) => void; + onFromChange: (value: number) => void; + onToChange: (value: number) => void; +} + +const parseInputValue = (event: ChangeEvent, min: number, max: number) => +{ + return clampValue(parseInt(event.target.value || min.toString(), 10), min, max); +}; + +const InlineNumberInput: FC<{ value: number; min: number; max: number; onChange: (value: number) => void }> = props => +{ + const { value = 0, min = 0, max = 0, onChange = null } = props; + + return ( + onChange(parseInputValue(event, min, max)) } /> + ); +}; + +const TimeFilterSection: FC = props => +{ + const { sectionId = '', titleKey = '', min = 0, max = 0, mode = MODE_SKIP, fromValue = 0, toValue = 0, onModeChange = null, onFromChange = null, onToChange = null } = props; + + return ( +
+ { LocalizeText(titleKey) } +
+ onModeChange(MODE_SKIP) } /> + { LocalizeText('wiredfurni.params.time.skip') } +
+
+ onModeChange(MODE_EXACT) } /> + { LocalizeText('wiredfurni.params.time.exact') } + +
+
+ onModeChange(MODE_RANGE) } /> + { LocalizeText('wiredfurni.params.time.range') } + + - + +
+
+ ); +}; + +export const WiredConditionMatchTimeView: FC<{}> = () => +{ + const { trigger = null, setIntParams = null } = useWired(); + const [ hourMode, setHourMode ] = useState(MODE_SKIP); + const [ hourFrom, setHourFrom ] = useState(0); + const [ hourTo, setHourTo ] = useState(0); + const [ minuteMode, setMinuteMode ] = useState(MODE_SKIP); + const [ minuteFrom, setMinuteFrom ] = useState(0); + const [ minuteTo, setMinuteTo ] = useState(0); + const [ secondMode, setSecondMode ] = useState(MODE_SKIP); + const [ secondFrom, setSecondFrom ] = useState(0); + const [ secondTo, setSecondTo ] = useState(0); + + useEffect(() => + { + if(!trigger) return; + + setHourMode(MODE_OPTIONS.includes(trigger.intData[0]) ? trigger.intData[0] : MODE_SKIP); + setHourFrom(clampValue(trigger.intData[1] ?? 0, 0, 23)); + setHourTo(clampValue(trigger.intData[2] ?? 0, 0, 23)); + setMinuteMode(MODE_OPTIONS.includes(trigger.intData[3]) ? trigger.intData[3] : MODE_SKIP); + setMinuteFrom(clampValue(trigger.intData[4] ?? 0, 0, 59)); + setMinuteTo(clampValue(trigger.intData[5] ?? 0, 0, 59)); + setSecondMode(MODE_OPTIONS.includes(trigger.intData[6]) ? trigger.intData[6] : MODE_SKIP); + setSecondFrom(clampValue(trigger.intData[7] ?? 0, 0, 59)); + setSecondTo(clampValue(trigger.intData[8] ?? 0, 0, 59)); + }, [ trigger ]); + + const save = () => + { + setIntParams([ + hourMode, + clampValue(hourFrom, 0, 23), + clampValue(hourTo, 0, 23), + minuteMode, + clampValue(minuteFrom, 0, 59), + clampValue(minuteTo, 0, 59), + secondMode, + clampValue(secondFrom, 0, 59), + clampValue(secondTo, 0, 59) + ]); + }; + + return ( + +
+ setHourFrom(clampValue(value, 0, 23)) } + onModeChange={ setHourMode } + onToChange={ value => setHourTo(clampValue(value, 0, 23)) } /> + setMinuteFrom(clampValue(value, 0, 59)) } + onModeChange={ setMinuteMode } + onToChange={ value => setMinuteTo(clampValue(value, 0, 59)) } /> + setSecondFrom(clampValue(value, 0, 59)) } + onModeChange={ setSecondMode } + onToChange={ value => setSecondTo(clampValue(value, 0, 59)) } /> +
+
+ ); +}; diff --git a/src/components/wired/views/conditions/WiredConditionTeamHasRankView.tsx b/src/components/wired/views/conditions/WiredConditionTeamHasRankView.tsx new file mode 100644 index 0000000..1df5347 --- /dev/null +++ b/src/components/wired/views/conditions/WiredConditionTeamHasRankView.tsx @@ -0,0 +1,102 @@ +import { FC, useEffect, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredSourcesSelector } from '../WiredSourcesSelector'; +import { WiredConditionBaseView } from './WiredConditionBaseView'; + +const TEAM_OPTIONS = [ 0, 1, 2, 3, 4 ]; +const PLACEMENT_OPTIONS = [ 1, 2, 3, 4 ]; + +export const WiredConditionTeamHasRankView: FC<{}> = () => +{ + const { trigger = null, setIntParams = null } = useWired(); + const [ team, setTeam ] = useState(1); + const [ placement, setPlacement ] = useState(1); + const [ userSource, setUserSource ] = useState(0); + const [ quantifier, setQuantifier ] = useState(0); + const [ showAdvanced, setShowAdvanced ] = useState(false); + + useEffect(() => + { + if(!trigger) return; + + const nextTeam = (trigger.intData.length > 0) ? trigger.intData[0] : 1; + const nextPlacement = (trigger.intData.length > 1) ? trigger.intData[1] : 1; + const nextUserSource = (trigger.intData.length > 2) ? trigger.intData[2] : 0; + const nextQuantifier = (trigger.intData.length > 3) ? trigger.intData[3] : 0; + + setTeam(TEAM_OPTIONS.includes(nextTeam) ? nextTeam : 1); + setPlacement(PLACEMENT_OPTIONS.includes(nextPlacement) ? nextPlacement : 1); + setUserSource(nextUserSource); + setQuantifier((nextQuantifier === 1) ? 1 : 0); + setShowAdvanced(nextUserSource !== 0 || nextQuantifier !== 0); + }, [ trigger ]); + + const save = () => + { + setIntParams([ + team, + placement, + userSource, + quantifier + ]); + }; + + return ( + + + { showAdvanced && + <> +
+ { LocalizeText('wiredfurni.params.quantifier_selection') } + { [ 0, 1 ].map(value => + { + return ( +
+ setQuantifier(value) } /> + { LocalizeText(`wiredfurni.params.quantifier.users.${ value }`) } +
+ ); + }) } +
+ + } +
+ }> +
+ { LocalizeText('wiredfurni.params.team') } + { TEAM_OPTIONS.map(value => + { + const labelKey = (value === 0) ? 'wiredfurni.params.team.triggerer' : `wiredfurni.params.team.${ value }`; + + return ( +
+ setTeam(value) } /> + { LocalizeText(labelKey) } +
+ ); + }) } +
+
+ { LocalizeText('wiredfurni.params.placement_selection') } + { PLACEMENT_OPTIONS.map(value => + { + return ( +
+ setPlacement(value) } /> + { LocalizeText(`wiredfurni.params.placement.${ value }`) } +
+ ); + }) } +
+ + ); +}; diff --git a/src/components/wired/views/conditions/WiredConditionTeamHasScoreView.tsx b/src/components/wired/views/conditions/WiredConditionTeamHasScoreView.tsx new file mode 100644 index 0000000..a2317aa --- /dev/null +++ b/src/components/wired/views/conditions/WiredConditionTeamHasScoreView.tsx @@ -0,0 +1,158 @@ +import { FC, useEffect, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Slider, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredSourcesSelector } from '../WiredSourcesSelector'; +import { WiredConditionBaseView } from './WiredConditionBaseView'; + +const TEAM_OPTIONS = [ 1, 2, 3, 4 ]; +const COMPARISON_OPTIONS = [ 0, 1, 2 ]; +const MIN_SCORE = 0; +const MAX_SCORE = 999; +const SCORE_PATTERN = /^\d*$/; + +const clampScore = (value: number) => +{ + if(isNaN(value)) return MIN_SCORE; + + return Math.max(MIN_SCORE, Math.min(MAX_SCORE, Math.floor(value))); +}; + +export const WiredConditionTeamHasScoreView: FC<{}> = () => +{ + const { trigger = null, setIntParams = null } = useWired(); + const [ team, setTeam ] = useState(1); + const [ comparison, setComparison ] = useState(1); + const [ score, setScore ] = useState(0); + const [ scoreInput, setScoreInput ] = useState('0'); + const [ userSource, setUserSource ] = useState(0); + const [ quantifier, setQuantifier ] = useState(0); + const [ showAdvanced, setShowAdvanced ] = useState(false); + + useEffect(() => + { + if(!trigger) return; + + const nextTeam = (trigger.intData.length > 0) ? trigger.intData[0] : 1; + const nextComparison = (trigger.intData.length > 1) ? trigger.intData[1] : 1; + const nextScore = clampScore((trigger.intData.length > 2) ? trigger.intData[2] : 0); + const nextUserSource = (trigger.intData.length > 3) ? trigger.intData[3] : 0; + const nextQuantifier = (trigger.intData.length > 4) ? trigger.intData[4] : 0; + + setTeam(TEAM_OPTIONS.includes(nextTeam) ? nextTeam : 1); + setComparison(COMPARISON_OPTIONS.includes(nextComparison) ? nextComparison : 1); + setScore(nextScore); + setScoreInput(nextScore.toString()); + setUserSource(nextUserSource); + setQuantifier((nextQuantifier === 1) ? 1 : 0); + setShowAdvanced(nextUserSource !== 0 || nextQuantifier !== 0); + }, [ trigger ]); + + const updateScore = (value: number) => + { + const nextValue = clampScore(value); + + setScore(nextValue); + setScoreInput(nextValue.toString()); + }; + + const updateScoreInput = (value: string) => + { + if(!SCORE_PATTERN.test(value)) return; + + setScoreInput(value); + + if(!value.length) + { + setScore(0); + return; + } + + updateScore(parseInt(value)); + }; + + const save = () => + { + setIntParams([ + team, + comparison, + clampScore(score), + userSource, + quantifier + ]); + }; + + return ( + + + { showAdvanced && + <> +
+ { LocalizeText('wiredfurni.params.quantifier_selection') } + { [ 0, 1 ].map(value => + { + return ( +
+ setQuantifier(value) } /> + { LocalizeText(`wiredfurni.params.quantifier.users.${ value }`) } +
+ ); + }) } +
+ + } + + }> +
+ { LocalizeText('wiredfurni.params.team') } + { TEAM_OPTIONS.map(value => + { + return ( +
+ setTeam(value) } /> + { LocalizeText(`wiredfurni.params.team.${ value }`) } +
+ ); + }) } +
+
+ { LocalizeText('wiredfurni.params.comparison_selection') } + { COMPARISON_OPTIONS.map(value => + { + return ( +
+ setComparison(value) } /> + { LocalizeText(`wiredfurni.params.comparison.${ value }`) } +
+ ); + }) } +
+
+ { LocalizeText('wiredfurni.params.setscore2') } + setScoreInput(clampScore(score).toString()) } + onChange={ event => updateScoreInput(event.target.value) } /> +
+
+ updateScore(event as number) } /> + { score } +
+
+ ); +}; diff --git a/src/components/wired/views/conditions/WiredConditionTriggererMatchView.tsx b/src/components/wired/views/conditions/WiredConditionTriggererMatchView.tsx new file mode 100644 index 0000000..d66403b --- /dev/null +++ b/src/components/wired/views/conditions/WiredConditionTriggererMatchView.tsx @@ -0,0 +1,130 @@ +import { FC, useEffect, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { NitroInput } from '../../../../layout'; +import { WiredSourceOption, WiredSourcesSelector } from '../WiredSourcesSelector'; +import { WiredConditionBaseView } from './WiredConditionBaseView'; + +const ENTITY_HABBO = 1; +const ENTITY_PET = 2; +const ENTITY_BOT = 4; +const AVATAR_MODE_ANY = 0; +const AVATAR_MODE_CERTAIN = 1; +const SOURCE_SPECIFIED_USERNAME = 101; + +const MATCH_USER_SOURCES: WiredSourceOption[] = [ + { value: 0, label: 'wiredfurni.params.sources.users.0' }, + { value: 200, label: 'wiredfurni.params.sources.users.200' }, + { value: 201, label: 'wiredfurni.params.sources.users.201' } +]; + +const COMPARE_USER_SOURCES: WiredSourceOption[] = [ + ...MATCH_USER_SOURCES, + { value: SOURCE_SPECIFIED_USERNAME, label: 'wiredfurni.params.sources.users.101' } +]; + +export const WiredConditionTriggererMatchView: FC<{}> = () => +{ + const [ entityType, setEntityType ] = useState(ENTITY_HABBO); + const [ avatarMode, setAvatarMode ] = useState(AVATAR_MODE_ANY); + const [ username, setUsername ] = useState(''); + const [ matchUserSource, setMatchUserSource ] = useState(0); + const [ compareUserSource, setCompareUserSource ] = useState(0); + const [ quantifier, setQuantifier ] = useState(0); + const [ showAdvanced, setShowAdvanced ] = useState(false); + const { trigger = null, setIntParams = null, setStringParam = null } = useWired(); + + const needsUsername = (avatarMode === AVATAR_MODE_CERTAIN) || (compareUserSource === SOURCE_SPECIFIED_USERNAME); + + const save = () => + { + setIntParams([ + entityType, + avatarMode, + matchUserSource, + compareUserSource, + quantifier + ]); + setStringParam(username); + }; + + useEffect(() => + { + if(!trigger) return; + + setEntityType((trigger.intData.length > 0) ? trigger.intData[0] : ENTITY_HABBO); + setAvatarMode((trigger.intData.length > 1) ? trigger.intData[1] : AVATAR_MODE_ANY); + setMatchUserSource((trigger.intData.length > 2) ? trigger.intData[2] : 0); + setCompareUserSource((trigger.intData.length > 3) ? trigger.intData[3] : 0); + setQuantifier((trigger.intData.length > 4) ? trigger.intData[4] : 0); + setUsername(trigger.stringData || ''); + setShowAdvanced((trigger.intData.length > 2) ? (trigger.intData[2] !== 0 || trigger.intData[3] !== 0 || trigger.intData[4] !== 0) : false); + }, [ trigger ]); + + return ( + + + { showAdvanced && + <> +
+ { LocalizeText('wiredfurni.params.quantifier_selection') } + { [ 0, 1 ].map(value => + { + return ( +
+ setQuantifier(value) } /> + { LocalizeText(`wiredfurni.params.quantifier.users.${ value }`) } +
+ ); + }) } +
+ + + } + + }> +
+ { [ ENTITY_HABBO, ENTITY_PET, ENTITY_BOT ].map(value => + { + return ( +
+ setEntityType(value) } /> + { LocalizeText(`wiredfurni.params.usertype.${ value }`) } +
+ ); + }) } +
+
+ { LocalizeText('wiredfurni.params.picktriggerer') } +
+ setAvatarMode(AVATAR_MODE_ANY) } /> + { LocalizeText('wiredfurni.params.anyavatar') } +
+
+ setAvatarMode(AVATAR_MODE_CERTAIN) } /> + { LocalizeText('wiredfurni.params.certainavatar') } +
+ { needsUsername && + setUsername(event.target.value) } /> } +
+
+ ); +}; diff --git a/src/components/wired/views/conditions/WiredConditionUserPerformsActionView.tsx b/src/components/wired/views/conditions/WiredConditionUserPerformsActionView.tsx new file mode 100644 index 0000000..a26c859 --- /dev/null +++ b/src/components/wired/views/conditions/WiredConditionUserPerformsActionView.tsx @@ -0,0 +1,151 @@ +import { FC, useEffect, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredSourceOption, WiredSourcesSelector } from '../WiredSourcesSelector'; +import { WiredConditionBaseView } from './WiredConditionBaseView'; + +const ACTION_WAVE = 1; +const ACTION_BLOW_KISS = 2; +const ACTION_LAUGH = 3; +const ACTION_AWAKE = 4; +const ACTION_RELAX = 5; +const ACTION_SIT = 6; +const ACTION_STAND = 7; +const ACTION_LAY = 8; +const ACTION_SIGN = 9; +const ACTION_DANCE = 10; +const ACTION_THUMB_UP = 11; + +const ACTION_OPTIONS = [ + { value: ACTION_WAVE, label: 'widget.memenu.wave' }, + { value: ACTION_BLOW_KISS, label: 'widget.memenu.blow' }, + { value: ACTION_LAUGH, label: 'widget.memenu.laugh' }, + { value: ACTION_THUMB_UP, label: 'widget.memenu.thumb' }, + { value: ACTION_AWAKE, label: 'wiredfurni.params.action.4' }, + { value: ACTION_RELAX, label: 'avatar.widget.random_walk' }, + { value: ACTION_SIT, label: 'widget.memenu.sit' }, + { value: ACTION_STAND, label: 'widget.memenu.stand' }, + { value: ACTION_LAY, label: 'wiredfurni.params.action.8' }, + { value: ACTION_SIGN, label: 'widget.memenu.sign' }, + { value: ACTION_DANCE, label: 'widget.memenu.dance' } +]; + +const SIGN_OPTIONS = Array.from({ length: 18 }, (_, value) => ({ + value, + label: `wiredfurni.params.action.sign.${ value }` +})); + +const DANCE_OPTIONS = [ + { value: 1, label: 'widget.memenu.dance1' }, + { value: 2, label: 'widget.memenu.dance2' }, + { value: 3, label: 'widget.memenu.dance3' }, + { value: 4, label: 'widget.memenu.dance4' } +]; + +const USER_ACTION_SOURCES: WiredSourceOption[] = [ + { value: 0, label: 'wiredfurni.params.sources.users.0' }, + { value: 200, label: 'wiredfurni.params.sources.users.200' }, + { value: 201, label: 'wiredfurni.params.sources.users.201' } +]; + +interface WiredConditionUserPerformsActionViewProps +{ + negative?: boolean; +} + +export const WiredConditionUserPerformsActionView: FC = props => +{ + const { negative = false } = props; + const [ selectedAction, setSelectedAction ] = useState(ACTION_WAVE); + const [ signFilterEnabled, setSignFilterEnabled ] = useState(false); + const [ signId, setSignId ] = useState(0); + const [ danceFilterEnabled, setDanceFilterEnabled ] = useState(false); + const [ danceId, setDanceId ] = useState(1); + const [ userSource, setUserSource ] = useState(0); + const [ quantifier, setQuantifier ] = useState(0); + const [ showAdvanced, setShowAdvanced ] = useState(false); + const { trigger = null, setIntParams = null } = useWired(); + const quantifierKeyPrefix = negative ? 'wiredfurni.params.quantifier.users.neg' : 'wiredfurni.params.quantifier.users'; + + const save = () => setIntParams([ + selectedAction, + signFilterEnabled ? 1 : 0, + signId, + danceFilterEnabled ? 1 : 0, + danceId, + userSource, + quantifier + ]); + + useEffect(() => + { + setSelectedAction((trigger?.intData?.length > 0) ? trigger.intData[0] : ACTION_WAVE); + setSignFilterEnabled((trigger?.intData?.length > 1) ? (trigger.intData[1] === 1) : false); + setSignId((trigger?.intData?.length > 2) ? trigger.intData[2] : 0); + setDanceFilterEnabled((trigger?.intData?.length > 3) ? (trigger.intData[3] === 1) : false); + setDanceId((trigger?.intData?.length > 4) ? trigger.intData[4] : 1); + setUserSource((trigger?.intData?.length > 5) ? trigger.intData[5] : 0); + setQuantifier((trigger?.intData?.length > 6) ? trigger.intData[6] : 0); + setShowAdvanced((trigger?.intData?.length > 5) ? (trigger.intData[5] !== 0 || trigger.intData[6] !== 0) : false); + }, [ trigger ]); + + return ( + + + { showAdvanced && + <> +
+ { LocalizeText('wiredfurni.params.quantifier_selection') } + { [ 0, 1 ].map(value => + { + return ( +
+ setQuantifier(value) } /> + { LocalizeText(`${ quantifierKeyPrefix }.${ value }`) } +
+ ); + }) } +
+ + } + + }> +
+ Action + +
+ { (selectedAction === ACTION_SIGN) && +
+
+ setSignFilterEnabled(event.target.checked) } /> + { LocalizeText('wiredfurni.params.sign_filter') } +
+ { signFilterEnabled && + } +
} + { (selectedAction === ACTION_DANCE) && +
+
+ setDanceFilterEnabled(event.target.checked) } /> + { LocalizeText('wiredfurni.params.dance_filter') } +
+ { danceFilterEnabled && + } +
} +
+ ); +}; From cb0a9242b5749fd6aec338f296f85349abec50e2 Mon Sep 17 00:00:00 2001 From: Lorenzune Date: Thu, 19 Mar 2026 15:22:49 +0100 Subject: [PATCH 08/10] feat(ui): add wired creator tools shell --- src/components/MainView.tsx | 2 + .../wired-tools/WiredCreatorToolsView.tsx | 176 ++++++++++++++++++ 2 files changed, 178 insertions(+) create mode 100644 src/components/wired-tools/WiredCreatorToolsView.tsx diff --git a/src/components/MainView.tsx b/src/components/MainView.tsx index 4b3217f..d80e00e 100644 --- a/src/components/MainView.tsx +++ b/src/components/MainView.tsx @@ -27,6 +27,7 @@ import { ToolbarView } from './toolbar/ToolbarView'; import { UserProfileView } from './user-profile/UserProfileView'; import { UserSettingsView } from './user-settings/UserSettingsView'; import { WiredView } from './wired/WiredView'; +import { WiredCreatorToolsView } from './wired-tools/WiredCreatorToolsView'; import { YoutubeTvView } from './youtube-tv/YoutubeTvView'; export const MainView: FC<{}> = props => @@ -94,6 +95,7 @@ export const MainView: FC<{}> = props => + diff --git a/src/components/wired-tools/WiredCreatorToolsView.tsx b/src/components/wired-tools/WiredCreatorToolsView.tsx new file mode 100644 index 0000000..7d5d4ed --- /dev/null +++ b/src/components/wired-tools/WiredCreatorToolsView.tsx @@ -0,0 +1,176 @@ +import { AddLinkEventTracker, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer'; +import { FC, useEffect, useMemo, useState } from 'react'; +import { Button, DraggableWindowPosition, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView, Text } from '../../common'; + +type WiredToolsTab = 'monitor' | 'variables' | 'inspection' | 'chests' | 'settings'; + +interface MonitorStat +{ + label: string; + value: string; +} + +interface MonitorLog +{ + type: string; + category: string; + amount: string; + latest: string; +} + +const TABS: Array<{ key: WiredToolsTab; label: string; }> = [ + { key: 'monitor', label: 'Monitor' }, + { key: 'variables', label: 'Variables' }, + { key: 'inspection', label: 'Inspection' }, + { key: 'chests', label: 'Chests' }, + { key: 'settings', label: 'Settings' } +]; + +const MONITOR_STATS: MonitorStat[] = [ + { label: 'Wired usage', value: '0/10000' }, + { label: 'Is heavy', value: 'No' }, + { label: 'Floor furni', value: '0/4000' }, + { label: 'Wall furni', value: '0/4000' }, + { label: 'Permanent furni vars', value: '0/60' } +]; + +const MONITOR_LOGS: MonitorLog[] = [ + { type: 'EXECUTION_CAP', category: 'ERROR', amount: '0', latest: '/' }, + { type: 'DELAYED_EVENTS_CAP', category: 'ERROR', amount: '0', latest: '/' }, + { type: 'EXECUTOR_OVERLOAD', category: 'ERROR', amount: '0', latest: '/' }, + { type: 'MARKED_AS_HEAVY', category: 'WARNING', amount: '0', latest: '/' }, + { type: 'KILLED', category: 'ERROR', amount: '0', latest: '/' }, + { type: 'RECURSION_TIMEOUT', category: 'ERROR', amount: '0', latest: '/' } +]; + +export const WiredCreatorToolsView: FC<{}> = props => +{ + const [ isVisible, setIsVisible ] = useState(false); + const [ activeTab, setActiveTab ] = useState('monitor'); + + useEffect(() => + { + const linkTracker: ILinkEventTracker = { + linkReceived: (url: string) => + { + const parts = url.split('/'); + + if(parts.length < 2) return; + + switch(parts[1]) + { + case 'show': + setIsVisible(true); + return; + case 'hide': + setIsVisible(false); + return; + case 'toggle': + setIsVisible(prevValue => !prevValue); + return; + case 'tab': + if(parts.length > 2) + { + const tab = parts[2] as WiredToolsTab; + + if(TABS.some(entry => entry.key === tab)) setActiveTab(tab); + } + setIsVisible(true); + return; + } + }, + eventUrlPrefix: 'wired-tools/' + }; + + AddLinkEventTracker(linkTracker); + + return () => RemoveLinkEventTracker(linkTracker); + }, []); + + const currentTabLabel = useMemo(() => TABS.find(tab => tab.key === activeTab)?.label ?? 'Monitor', [ activeTab ]); + + if(!isVisible) return null; + + return ( + + setIsVisible(false) } /> + + { TABS.map(tab => ( + setActiveTab(tab.key) }> + { tab.label } + + )) } + + +
+
+ { currentTabLabel } +
+ { (activeTab === 'monitor') && +
+
+ This is the initial shell for the Wired Creator Tools. We can now build the real functionality tab by tab. +
+
+
+ Statistics: + { MONITOR_STATS.map(stat => ( +
+ { stat.label }: + { stat.value } +
+ )) } +
+
+
+ Monitor Preview +
+ Live statistics, executor health and diagnostics can be connected here next. +
+
+
+
+
+ Logs: +
+ + + + + + + + + + + { MONITOR_LOGS.map((log, index) => ( + + + + + + + )) } + +
TypeCategoryAmountLatest occurrence
{ log.type }{ log.category }{ log.amount }{ log.latest }
+
+
+ + +
+
+
} + { (activeTab !== 'monitor') && +
+
+ { currentTabLabel } +
+ This tab is now ready to be wired into the new `:wired` tools flow. +
+
+
} +
+
+
+ ); +}; From 27cb71f0cc50f6a8565380bf301eb7c144b04e28 Mon Sep 17 00:00:00 2001 From: Lorenzune Date: Sat, 21 Mar 2026 14:27:57 +0100 Subject: [PATCH 09/10] feat(wired-ui): expand advanced wired editors --- public/UITexts.example | 62 +---- src/api/wired/WiredActionLayoutCode.ts | 15 ++ src/api/wired/WiredConditionLayoutCode.ts | 3 + src/assets/images/wired/icon_wired_dir_e.png | Bin 0 -> 153 bytes .../icon_wired_dir_horizontal_random.png | Bin 0 -> 161 bytes src/assets/images/wired/icon_wired_dir_n.png | Bin 0 -> 153 bytes src/assets/images/wired/icon_wired_dir_ne.png | Bin 0 -> 244 bytes src/assets/images/wired/icon_wired_dir_nw.png | Bin 0 -> 231 bytes .../images/wired/icon_wired_dir_random.png | Bin 0 -> 156 bytes src/assets/images/wired/icon_wired_dir_s.png | Bin 0 -> 153 bytes src/assets/images/wired/icon_wired_dir_se.png | Bin 0 -> 232 bytes src/assets/images/wired/icon_wired_dir_sw.png | Bin 0 -> 235 bytes .../wired/icon_wired_dir_vertical_random.png | Bin 0 -> 164 bytes src/assets/images/wired/icon_wired_dir_w.png | Bin 0 -> 154 bytes src/common/card/NitroCardView.tsx | 7 +- .../draggable-window/DraggableWindow.tsx | 4 +- .../avatar-info/AvatarInfoWidgetView.tsx | 56 ++++- .../infostand/InfoStandWidgetUserView.tsx | 8 +- src/components/wired/views/WiredBaseView.tsx | 24 +- .../wired/views/WiredDirectionIcon.tsx | 53 ++++ .../wired/views/WiredHandItemField.tsx | 59 +++++ .../wired/views/WiredSourcesSelector.tsx | 7 + .../WiredActionBotChangeFigureView.tsx | 28 ++- .../WiredActionBotFollowAvatarView.tsx | 30 ++- .../WiredActionBotGiveHandItemView.tsx | 64 +++-- .../views/actions/WiredActionBotMoveView.tsx | 31 ++- .../WiredActionBotTalkToAvatarView.tsx | 30 ++- .../views/actions/WiredActionBotTalkView.tsx | 23 +- .../actions/WiredActionBotTeleportView.tsx | 31 ++- ...redActionGiveScoreToPredefinedTeamView.tsx | 23 +- .../actions/WiredActionGiveScoreView.tsx | 21 +- .../views/actions/WiredActionJoinTeamView.tsx | 36 ++- .../views/actions/WiredActionLayoutView.tsx | 45 ++++ .../WiredActionMoveAndRotateFurniView.tsx | 55 ++-- .../actions/WiredActionMoveFurniView.tsx | 82 ++++-- .../actions/WiredActionMoveRotateUserView.tsx | 121 +++++++++ .../actions/WiredActionSendSignalView.tsx | 60 +++-- .../WiredActionSetFurniStateToView.tsx | 12 +- .../views/actions/WiredActionTeleportView.tsx | 62 ++++- .../WiredActionToggleFurniStateView.tsx | 40 ++- .../conditions/WiredConditionActorDirView.tsx | 100 ++++++++ .../WiredConditionActorHasHandItem.tsx | 27 +- .../WiredConditionActorIsGroupMemberView.tsx | 88 ++++++- .../WiredConditionActorIsOnFurniView.tsx | 45 +++- .../WiredConditionActorIsTeamMemberView.tsx | 20 +- .../WiredConditionActorIsWearingBadgeView.tsx | 20 +- ...WiredConditionActorIsWearingEffectView.tsx | 20 +- .../WiredConditionCounterTimeMatchesView.tsx | 20 +- .../WiredConditionFurniHasAvatarOnView.tsx | 35 ++- .../WiredConditionFurniIsOfTypeView.tsx | 238 ++++++++++++++++-- ...WiredConditionFurniMatchesSnapshotView.tsx | 39 ++- .../conditions/WiredConditionLayoutView.tsx | 39 ++- .../WiredConditionMovementValidationView.tsx | 22 ++ .../WiredConditionSelectionQuantityView.tsx | 178 +++++++++++++ .../WiredConditionTriggererMatchView.tsx | 10 +- .../wired/views/extras/WiredExtraBaseView.tsx | 32 +++ .../extras/WiredExtraFilterFurniView.tsx | 48 ++++ .../views/extras/WiredExtraFilterUserView.tsx | 48 ++++ .../selectors/WiredActionFurniAreaView.tsx | 6 +- .../views/selectors/WiredSelectorBaseView.tsx | 25 ++ .../WiredSelectorFurniAltitudeView.tsx | 165 ++++++++++++ .../WiredSelectorFurniByTypeView.tsx | 6 +- .../WiredSelectorFurniNeighborhoodView.tsx | 221 ++++++++++------ .../WiredSelectorFurniOnFurniView.tsx | 82 ++++++ .../selectors/WiredSelectorFurniPicksView.tsx | 55 ++++ .../WiredSelectorFurniSignalView.tsx | 55 ++++ .../selectors/WiredSelectorUsersAreaView.tsx | 32 +-- .../WiredSelectorUsersByActionView.tsx | 142 +++++++++++ .../WiredSelectorUsersByNameView.tsx | 69 +++++ .../WiredSelectorUsersByTypeView.tsx | 73 ++++++ .../selectors/WiredSelectorUsersGroupView.tsx | 103 ++++++++ .../WiredSelectorUsersHandItemView.tsx | 64 +++++ .../WiredSelectorUsersNeighborhoodView.tsx | 231 ++++++++++++++--- .../WiredSelectorUsersOnFurniView.tsx | 64 +++++ .../WiredSelectorUsersSignalView.tsx | 55 ++++ .../selectors/WiredSelectorUsersTeamView.tsx | 73 ++++++ .../WiredTriggerAvatarSaysSomethingView.tsx | 46 +++- .../WiredTriggerAvatarWalksOffFurniView.tsx | 31 ++- .../WiredTriggerAvatarWalksOnFurni.tsx | 31 ++- .../WiredTriggerBotReachedAvatarView.tsx | 40 ++- .../WiredTriggerBotReachedStuffView.tsx | 52 +++- .../triggers/WiredTriggerClickFurniView.tsx | 29 ++- .../triggers/WiredTriggerClickTileView.tsx | 36 ++- .../triggers/WiredTriggerClickUserView.tsx | 36 ++- .../triggers/WiredTriggerClockCounterView.tsx | 10 +- .../WiredTriggerReceiveSignalView.tsx | 32 ++- .../WiredTriggerScoreAchievedView.tsx | 14 +- .../triggers/WiredTriggerToggleFurniView.tsx | 18 +- .../rooms/widgets/useAvatarInfoWidget.ts | 42 +++- src/hooks/wired/useWired.ts | 8 +- 90 files changed, 3529 insertions(+), 538 deletions(-) create mode 100644 src/assets/images/wired/icon_wired_dir_e.png create mode 100644 src/assets/images/wired/icon_wired_dir_horizontal_random.png create mode 100644 src/assets/images/wired/icon_wired_dir_n.png create mode 100644 src/assets/images/wired/icon_wired_dir_ne.png create mode 100644 src/assets/images/wired/icon_wired_dir_nw.png create mode 100644 src/assets/images/wired/icon_wired_dir_random.png create mode 100644 src/assets/images/wired/icon_wired_dir_s.png create mode 100644 src/assets/images/wired/icon_wired_dir_se.png create mode 100644 src/assets/images/wired/icon_wired_dir_sw.png create mode 100644 src/assets/images/wired/icon_wired_dir_vertical_random.png create mode 100644 src/assets/images/wired/icon_wired_dir_w.png create mode 100644 src/components/wired/views/WiredDirectionIcon.tsx create mode 100644 src/components/wired/views/WiredHandItemField.tsx create mode 100644 src/components/wired/views/actions/WiredActionMoveRotateUserView.tsx create mode 100644 src/components/wired/views/conditions/WiredConditionActorDirView.tsx create mode 100644 src/components/wired/views/conditions/WiredConditionMovementValidationView.tsx create mode 100644 src/components/wired/views/conditions/WiredConditionSelectionQuantityView.tsx create mode 100644 src/components/wired/views/extras/WiredExtraBaseView.tsx create mode 100644 src/components/wired/views/extras/WiredExtraFilterFurniView.tsx create mode 100644 src/components/wired/views/extras/WiredExtraFilterUserView.tsx create mode 100644 src/components/wired/views/selectors/WiredSelectorBaseView.tsx create mode 100644 src/components/wired/views/selectors/WiredSelectorFurniAltitudeView.tsx create mode 100644 src/components/wired/views/selectors/WiredSelectorFurniOnFurniView.tsx create mode 100644 src/components/wired/views/selectors/WiredSelectorFurniPicksView.tsx create mode 100644 src/components/wired/views/selectors/WiredSelectorFurniSignalView.tsx create mode 100644 src/components/wired/views/selectors/WiredSelectorUsersByActionView.tsx create mode 100644 src/components/wired/views/selectors/WiredSelectorUsersByNameView.tsx create mode 100644 src/components/wired/views/selectors/WiredSelectorUsersByTypeView.tsx create mode 100644 src/components/wired/views/selectors/WiredSelectorUsersGroupView.tsx create mode 100644 src/components/wired/views/selectors/WiredSelectorUsersHandItemView.tsx create mode 100644 src/components/wired/views/selectors/WiredSelectorUsersOnFurniView.tsx create mode 100644 src/components/wired/views/selectors/WiredSelectorUsersSignalView.tsx create mode 100644 src/components/wired/views/selectors/WiredSelectorUsersTeamView.tsx diff --git a/public/UITexts.example b/public/UITexts.example index 980458f..fe20439 100644 --- a/public/UITexts.example +++ b/public/UITexts.example @@ -20,65 +20,5 @@ "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.collapse": "Nascondi le impostazioni avanzate", - "wiredfurni.params.sources.expand": "Mostra le impostazioni avanzate", - "wiredfurni.params.quantifier_selection": "Abbina condizione se:", - "wiredfurni.params.quantifier.users.0": "Tutti gli utenti corrispondono", - "wiredfurni.params.quantifier.users.1": "Uno qualsiasi degli utenti corrisponde", - "wiredfurni.params.quantifier.users.neg.0": "Uno qualsiasi degli utenti non corrisponde", - "wiredfurni.params.quantifier.users.neg.1": "Nessuno degli utenti corrisponde", - "wiredfurni.params.quantifier.furni.0": "Tutti i Furni corrispondono", - "wiredfurni.params.quantifier.furni.1": "Uno qualsiasi dei Furni corrisponde", - "wiredfurni.params.usertype.1": "Habbo", - "wiredfurni.params.usertype.2": "Cucciolo", - "wiredfurni.params.usertype.4": "Bot", - "wiredfurni.params.sources.users.title.match.0": "Gli utenti da abbinare:", - "wiredfurni.params.sources.users.title.match.1": "Utenti da comparare con:", - "wiredfurni.params.sources.users.101": "Usa l'utente specificato dal nome", - "wiredfurni.params.comparison.0": "Più basso di", - "wiredfurni.params.comparison.1": "È uguale a", - "wiredfurni.params.comparison.2": "Più alto di", - "wiredfurni.params.team": "Scegli una squadra", - "wiredfurni.params.team.1": "Rossa", - "wiredfurni.params.team.2": "Verde", - "wiredfurni.params.team.3": "Blu", - "wiredfurni.params.team.4": "Gialla", - "wiredfurni.params.team.triggerer": "Squadra dell'innescatore", - "wiredfurni.params.comparison_selection": "Scegli tipo:", - "wiredfurni.params.setscore2": "La squadra deve segnare:", - "wiredfurni.params.placement_selection": "Posizione:", - "wiredfurni.params.placement.1": "1.", - "wiredfurni.params.placement.2": "2.", - "wiredfurni.params.placement.3": "3.", - "wiredfurni.params.placement.4": "4.", - "wiredfurni.params.time.hour_selection": "Ore:", - "wiredfurni.params.time.minute_selection": "Minuti:", - "wiredfurni.params.time.second_selection": "Secondi:", - "wiredfurni.params.time.skip": "Non usare il filtro", - "wiredfurni.params.time.exact": "Esatto", - "wiredfurni.params.time.range": "Range", - "wiredfurni.params.time.weekday_selection": "Giorno della settimana:", - "wiredfurni.params.time.weekday.1": "Lunedì", - "wiredfurni.params.time.weekday.2": "Martedì", - "wiredfurni.params.time.weekday.3": "Mercoledì", - "wiredfurni.params.time.weekday.4": "Giovedì", - "wiredfurni.params.time.weekday.5": "Venerdì", - "wiredfurni.params.time.weekday.6": "Sabato", - "wiredfurni.params.time.weekday.7": "Domenica", - "wiredfurni.params.time.day_selection": "Giorno:", - "wiredfurni.params.time.month_selection": "Mese:", - "wiredfurni.params.time.month.10": "Ott.", - "wiredfurni.params.time.month.11": "Nov.", - "wiredfurni.params.time.month.12": "Dic.", - "wiredfurni.params.time.month.1": "Gen.", - "wiredfurni.params.time.month.2": "Feb.", - "wiredfurni.params.time.month.3": "Mar.", - "wiredfurni.params.time.month.4": "Apr.", - "wiredfurni.params.time.month.5": "Mag.", - "wiredfurni.params.time.month.6": "Giu.", - "wiredfurni.params.time.month.7": "Lug.", - "wiredfurni.params.time.month.8": "Ago.", - "wiredfurni.params.time.month.9": "Set.", - "wiredfurni.params.time.year_selection": "Anno:" + "wiredfurni.params.area_selection.selected": "Geselecteerd gebied: Lengte=%x%, Breedte=%y%, breedte=%w%, hoogte=%h%" } diff --git a/src/api/wired/WiredActionLayoutCode.ts b/src/api/wired/WiredActionLayoutCode.ts index 6f98d74..94c8f94 100644 --- a/src/api/wired/WiredActionLayoutCode.ts +++ b/src/api/wired/WiredActionLayoutCode.ts @@ -41,4 +41,19 @@ export class WiredActionLayoutCode public static RELATIVE_MOVE: number = 40; public static CONTROL_CLOCK: number = 41; public static ADJUST_CLOCK: number = 42; + public static MOVE_ROTATE_USER: number = 43; + public static FURNI_ALTITUDE_SELECTOR: number = 44; + public static FURNI_ON_FURNI_SELECTOR: number = 45; + public static FURNI_PICKS_SELECTOR: number = 46; + public static FURNI_SIGNAL_SELECTOR: number = 47; + public static USERS_SIGNAL_SELECTOR: number = 48; + public static USERS_BY_TYPE_SELECTOR: number = 49; + public static USERS_TEAM_SELECTOR: number = 50; + public static USERS_BY_ACTION_SELECTOR: number = 51; + public static USERS_BY_NAME_SELECTOR: number = 52; + public static USERS_ON_FURNI_SELECTOR: number = 53; + public static USERS_GROUP_SELECTOR: number = 54; + public static USERS_HANDITEM_SELECTOR: number = 55; + public static FILTER_FURNI_EXTRA: number = 56; + public static FILTER_USER_EXTRA: number = 57; } diff --git a/src/api/wired/WiredConditionLayoutCode.ts b/src/api/wired/WiredConditionLayoutCode.ts index b344f8b..f6fd56a 100644 --- a/src/api/wired/WiredConditionLayoutCode.ts +++ b/src/api/wired/WiredConditionLayoutCode.ts @@ -26,6 +26,7 @@ export class WiredConditionlayout public static NOT_ACTOR_WEARING_EFFECT: number = 23; public static DATE_RANGE_ACTIVE: number = 24; public static ACTOR_HAS_HANDITEM: number = 25; + public static MOVEMENT_VALIDATION: number = 26; public static COUNTER_TIME_MATCHES: number = 27; public static USER_PERFORMS_ACTION: number = 28; public static HAS_ALTITUDE: number = 29; @@ -37,4 +38,6 @@ export class WiredConditionlayout public static TEAM_HAS_RANK: number = 35; public static MATCH_TIME: number = 36; public static MATCH_DATE: number = 37; + public static ACTOR_DIR: number = 38; + public static SLC_QUANTITY: number = 39; } diff --git a/src/assets/images/wired/icon_wired_dir_e.png b/src/assets/images/wired/icon_wired_dir_e.png new file mode 100644 index 0000000000000000000000000000000000000000..dfcfff293966e3ef05c298f0a35156c28179b2b1 GIT binary patch literal 153 zcmeAS@N?(olHy`uVBq!ia0vp^QXn=HGmt#>eOeum;tcQ!agB?M^Yioj|NsBuz!o8( zAY)08UoeBivm0qZj-98AV@O4uYtKer216dEyMO9TMBe|6nV~5xeOeum;tcQ!agB?M^Yioj|NsBuz!o8( zAY)08UoeBivm0qZj+>{8V@O4u?U{vw4Gugm7xORqtg_t9@n>d;Bj=1M4GN(~o109n z&(}1xiXXacIbCyq$(h`+?LWP`Z(J1$P^6NN3~uJlC$O=R$N^>bP0 Hl+XkKVOcpd literal 0 HcmV?d00001 diff --git a/src/assets/images/wired/icon_wired_dir_n.png b/src/assets/images/wired/icon_wired_dir_n.png new file mode 100644 index 0000000000000000000000000000000000000000..7d8aa0e7eb919d87670b812bff08228785f04ea1 GIT binary patch literal 153 zcmeAS@N?(olHy`uVBq!ia0vp^QXn=HGmt#>eOeum;tcQ!agB?M^Yioj|NsBuz!o8( zAY)08UoeBivm0qZj-98AV@O4uYmXrxg8~oJ?iaszT{!rE;|H&m$4)ruy%qF3cJe^3 yf_~H8)}&U>c zv7h@-A}f&3S>O>_%)r2R7=#&*=dVZs3U+$BIEGmGFTEVe*PtNc99Yn}AuO!J$8=f8 z`WJsEt|)MFnO|IHeN^Ih%alp^=0{RE7?wAwm9U>$lzc_|{IL^k{e^Q^9mo>*k9_Zc zLGQrYXYm)}8y&U>c zv7h@-A}f&3S>O>_%)r2R7=#&*=dVZs3f6eKIEGmG@4Xnv)u15IlDK!lysN@dQNe~g zU$Z>?zih$$?v2-^_Dv~%$L^)MF~8%a0D~XvaslSwM|0oD+&$|cEXA0?qR3t2DA!=a zVOZhFwctoUgG8{`eufjX%I>9W%@zrldr!}&a4?26E*3DG XeZSyxWqY9o&>ak(u6{1-oD!MeOeum;tcQ!asB`Qzn`BUki5QCw-hMI zSQ6wH%;50sMjDXg=;`7ZQW0m`bDFaufQLo=;J>1o3ezUk={Op!_v+m+o#*#Sub?wG zubr)GEBLo(MPk`E#kW_UdraU>e0!(%fA#s_`sZHCOsTinbq{D5gQu&X%Q~loCIH(( BJ$nEE literal 0 HcmV?d00001 diff --git a/src/assets/images/wired/icon_wired_dir_s.png b/src/assets/images/wired/icon_wired_dir_s.png new file mode 100644 index 0000000000000000000000000000000000000000..5b0e4e7d7c53cd8d686cb65fb7ae0b64144a49c6 GIT binary patch literal 153 zcmeAS@N?(olHy`uVBq!ia0vp^QXn=HGmt#>eOeum;tcQ!agB?M^Yioj|NsBuz!o8( zAY)08UoeBivm0qZj-98AV@O4u>zRdu2MlbP0l+XkKrOY-Z literal 0 HcmV?d00001 diff --git a/src/assets/images/wired/icon_wired_dir_se.png b/src/assets/images/wired/icon_wired_dir_se.png new file mode 100644 index 0000000000000000000000000000000000000000..3bab96801be356530f861688b8fb915ac04f59b2 GIT binary patch literal 232 zcmeAS@N?(olHy`uVBq!ia0vp^QXn=58<5;Sv+gaBVk{1FcVbv~PUa<$!;&U>c zv7h@-A}f&3S>O>_%)r2R7=#&*=dVZs3f6kMIEGmG@4e{A*Py`T61YQQ&Q#$=tjA~E zx^$7Xo}IHc^TgioUzSF_uDB3&`f*cF8^enTgBG0!GJ4;yXNooIJ;_zzY>D{5x|ZQf zO90Eg32bW;edK0%Yd&MES6p(iRyy~;@?EYPC&7joj>rWLIt@-7mC|kuixgQVu%>Cs YZc!+>Y!L7!0O$|~Pgg&ebxsLQ0Q!+m8vp&U>c zv7h@-A}f&3S>O>_%)r2R7=#&*=dVZs3O0DUIEGmGpS^OC?|=YL>%&hCHmlW5vO_j0 zU!4{EX1}1(-$!W+uX$NNo%7R4Q}simvAGJvGge=YMza~$%g(J%Y1CT6I757nf__8f zgNA!dGuYauT>9+UEpU9{>PNbdjQ9IIJHAhzy|!BBob-eeuB`9s9=BN6zW@I@{Q%3I dN?pN=te>W4Dc^Y1xDx0V22WQ%mvv4FO#oY@SL6Ty literal 0 HcmV?d00001 diff --git a/src/assets/images/wired/icon_wired_dir_vertical_random.png b/src/assets/images/wired/icon_wired_dir_vertical_random.png new file mode 100644 index 0000000000000000000000000000000000000000..e9b8993ed3d5b7783d47198aa7a77405eded55d9 GIT binary patch literal 164 zcmeAS@N?(olHy`uVBq!ia0vp^QXn=HGmt#>eOeum;tcQ!agB?M^Yioj|NsBuz!o8( zAY)08UoeBivm0qZj;E)KV@O4u>6wkZ4Guic7fmADGS~eS`Eosl%Wc6)4v`cuy=muq zm#W{~X5>>C;%<0Fzv)ND2g#cZ%MU0T`3CeqyOPU0z3TtV`*mjy3R|)mIK2iM%HZkh K=d#Wzp$P!{>^ao{ literal 0 HcmV?d00001 diff --git a/src/assets/images/wired/icon_wired_dir_w.png b/src/assets/images/wired/icon_wired_dir_w.png new file mode 100644 index 0000000000000000000000000000000000000000..26b787a8df3266c119126191aab31bde48bd8818 GIT binary patch literal 154 zcmeAS@N?(olHy`uVBq!ia0vp^QXn=HGmt#>eOeum;tcQ!agB?M^Yioj|NsBuz!o8( zAY)08UoeBivm0qZj=iUgV@O4uYmXt{0RtYU%{$(IE|UCZFBTVhJ5)sMh+@Rjn8gpu zB|hB9x?k3OLoDR>%r!dQ?<(gSyQkOxKlh*gg{9DKmHlnfK%*EuUHx3vIVCg!0NIW< A9{>OV literal 0 HcmV?d00001 diff --git a/src/common/card/NitroCardView.tsx b/src/common/card/NitroCardView.tsx index 2735f61..8684ccd 100644 --- a/src/common/card/NitroCardView.tsx +++ b/src/common/card/NitroCardView.tsx @@ -6,16 +6,17 @@ import { NitroCardContextProvider } from './NitroCardContext'; export interface NitroCardViewProps extends DraggableWindowProps, ColumnProps { theme?: string; + isResizable?: boolean; } export const NitroCardView: FC = props => { - const { theme = 'primary', uniqueKey = null, handleSelector = '.drag-handler', windowPosition = DraggableWindowPosition.CENTER, disableDrag = false, overflow = 'hidden', position = 'relative', gap = 0, classNames = [], ...rest } = props; + const { theme = 'primary', uniqueKey = null, handleSelector = '.drag-handler', windowPosition = DraggableWindowPosition.CENTER, disableDrag = false, overflow = 'hidden', position = 'relative', gap = 0, classNames = [], isResizable = true, ...rest } = props; const elementRef = useRef(); const getClassNames = useMemo(() => { - const newClassNames: string[] = [ 'resize', 'rounded', 'shadow', ]; + const newClassNames: string[] = [ isResizable ? 'resize' : 'resize-none', 'rounded', 'shadow' ]; // Card Theme Changer newClassNames.push('border border-[#283F5D]'); @@ -23,7 +24,7 @@ export const NitroCardView: FC = props => if(classNames.length) newClassNames.push(...classNames); return newClassNames; - }, [ classNames ]); + }, [ classNames, isResizable ]); return ( diff --git a/src/common/draggable-window/DraggableWindow.tsx b/src/common/draggable-window/DraggableWindow.tsx index 7400a5f..9bb7e34 100644 --- a/src/common/draggable-window/DraggableWindow.tsx +++ b/src/common/draggable-window/DraggableWindow.tsx @@ -232,7 +232,9 @@ export const DraggableWindow: FC = props => { ...dragStyle, left: 0, top: 0, - transform: `translate(${offset.x + delta.x}px, ${offset.y + delta.y}px)`, + transform: `translate3d(${offset.x + delta.x}px, ${offset.y + delta.y}px, 0)`, + willChange: isDragging ? 'transform' : undefined, + backfaceVisibility: 'hidden', visibility: isPositioned ? 'visible' : 'hidden' }} onMouseDownCapture={onMouseDown} diff --git a/src/components/room/widgets/avatar-info/AvatarInfoWidgetView.tsx b/src/components/room/widgets/avatar-info/AvatarInfoWidgetView.tsx index c12ed78..fd81f96 100644 --- a/src/components/room/widgets/avatar-info/AvatarInfoWidgetView.tsx +++ b/src/components/room/widgets/avatar-info/AvatarInfoWidgetView.tsx @@ -1,5 +1,5 @@ -import { GetSessionDataManager, RoomEngineEvent, RoomEnterEffect, RoomSessionDanceEvent } from '@nitrots/nitro-renderer'; -import { FC, useState } from 'react'; +import { AddLinkEventTracker, GetRoomEngine, GetSessionDataManager, ILinkEventTracker, RemoveLinkEventTracker, RoomEngineEvent, RoomEnterEffect, RoomSessionDanceEvent } from '@nitrots/nitro-renderer'; +import { FC, useEffect, useState } from 'react'; import { AvatarInfoFurni, AvatarInfoPet, AvatarInfoRentableBot, AvatarInfoUser, GetConfigurationValue, RoomWidgetUpdateRentableBotChatEvent } from '../../../../api'; import { Column } from '../../../../common'; import { useAvatarInfoWidget, useNitroEvent, useRoom, useUiEvent } from '../../../../hooks'; @@ -23,12 +23,29 @@ import { AvatarInfoWidgetRentableBotView } from './menu/AvatarInfoWidgetRentable export const AvatarInfoWidgetView: FC<{}> = props => { + const BLOCK_MENU_WINDOW_MS = 500; + const BLOCK_ROTATE_WINDOW_MS = 500; const [ isGameMode, setGameMode ] = useState(false); const [ isDancing, setIsDancing ] = useState(false); const [ rentableBotChatEvent, setRentableBotChatEvent ] = useState(null); const { avatarInfo = null, setAvatarInfo = null, activeNameBubble = null, setActiveNameBubble = null, nameBubbles = [], removeNameBubble = null, productBubbles = [], confirmingProduct = null, updateConfirmingProduct = null, removeProductBubble = null, isDecorating = false, setIsDecorating = null } = useAvatarInfoWidget(); const { roomSession = null } = useRoom(); + const updateAvatarClickControl = (updates: { suppressMenuUntil?: number; suppressRotateUntil?: number; }) => + { + const globalScope = (globalThis as any); + + if(!globalScope.__nitroAvatarClickControl) + { + globalScope.__nitroAvatarClickControl = { + suppressMenuUntil: 0, + suppressRotateUntil: 0 + }; + } + + Object.assign(globalScope.__nitroAvatarClickControl, updates); + }; + useNitroEvent(RoomEngineEvent.NORMAL_MODE, event => { if(isGameMode) setGameMode(false); @@ -48,6 +65,41 @@ export const AvatarInfoWidgetView: FC<{}> = props => useUiEvent(RoomWidgetUpdateRentableBotChatEvent.UPDATE_CHAT, event => setRentableBotChatEvent(event)); + useEffect(() => + { + const linkTracker: ILinkEventTracker = { + linkReceived: (url: string) => + { + const parts = url.split('/'); + + if(parts.length < 2) return; + + switch(parts[1]) + { + case 'hide': + setAvatarInfo(null); + setActiveNameBubble(null); + + if(roomSession) GetRoomEngine().clearSelectedAvatar(roomSession.roomId); + return; + case 'block-menu': + updateAvatarClickControl({ suppressMenuUntil: Date.now() + BLOCK_MENU_WINDOW_MS }); + setAvatarInfo(null); + setActiveNameBubble(null); + return; + case 'block-rotate': + updateAvatarClickControl({ suppressRotateUntil: Date.now() + BLOCK_ROTATE_WINDOW_MS }); + return; + } + }, + eventUrlPrefix: 'avatar-info/' + }; + + AddLinkEventTracker(linkTracker); + + return () => RemoveLinkEventTracker(linkTracker); + }, [ roomSession, setActiveNameBubble, setAvatarInfo ]); + const getMenuView = () => { if(!roomSession || isGameMode) return null; diff --git a/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx b/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx index 1791979..12aca46 100644 --- a/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx +++ b/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx @@ -61,6 +61,8 @@ export const InfoStandWidgetUserView: FC = props = if (oldBadges === event.badges.join('')) return; setAvatarInfo(prevValue => { + if (!prevValue) return prevValue; + const newValue = CloneObject(prevValue); newValue.badges = event.badges; return newValue; @@ -71,6 +73,8 @@ export const InfoStandWidgetUserView: FC = props = if (!avatarInfo || avatarInfo.roomIndex !== event.roomIndex) return; setAvatarInfo(prevValue => { + if (!prevValue) return prevValue; + const newValue = CloneObject(prevValue); newValue.figure = event.figure; newValue.motto = event.customInfo; @@ -86,6 +90,8 @@ export const InfoStandWidgetUserView: FC = props = if (!avatarInfo || avatarInfo.roomIndex !== event.roomIndex) return; setAvatarInfo(prevValue => { + if (!prevValue) return prevValue; + const newValue = CloneObject(prevValue); const clearGroup = (event.status === -1) || (event.habboGroupId <= 0); @@ -272,4 +278,4 @@ export const InfoStandWidgetUserView: FC = props = )} ); -}; \ No newline at end of file +}; diff --git a/src/components/wired/views/WiredBaseView.tsx b/src/components/wired/views/WiredBaseView.tsx index f9477af..96f302a 100644 --- a/src/components/wired/views/WiredBaseView.tsx +++ b/src/components/wired/views/WiredBaseView.tsx @@ -1,4 +1,4 @@ -import { GetSessionDataManager } from '@nitrots/nitro-renderer'; +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'; @@ -24,8 +24,15 @@ export const WiredBaseView: FC> = props => const [ needsSave, setNeedsSave ] = useState(false); const { trigger = null, setTrigger = null, setIntParams = null, setStringParam = null, setFurniIds = null, setAllowsFurni = null, saveWired = null } = useWired(); + const clearRoomAreaSelection = () => + { + GetRoomEngine().areaSelectionManager.clearHighlight(); + GetRoomEngine().areaSelectionManager.deactivate(); + }; + const onClose = () => { + clearRoomAreaSelection(); WiredSelectionVisualizer.clearAllSelectionShaders(); setTrigger(null); }; @@ -92,6 +99,11 @@ export const WiredBaseView: FC> = props => } }, [ trigger, hasSpecialInput, setIntParams, setStringParam, setFurniIds ]); + useEffect(() => + { + return () => clearRoomAreaSelection(); + }, []); + useEffect(() => { if(!trigger) return; @@ -99,8 +111,16 @@ export const WiredBaseView: FC> = props => setAllowsFurni(requiresFurni); }, [ trigger, requiresFurni, setAllowsFurni ]); + const resolvedCardStyle: CSSProperties = { ...cardStyle }; + + if(resolvedCardStyle.width !== undefined) + { + resolvedCardStyle.minWidth = resolvedCardStyle.width; + delete resolvedCardStyle.width; + } + return ( - +
diff --git a/src/components/wired/views/WiredDirectionIcon.tsx b/src/components/wired/views/WiredDirectionIcon.tsx new file mode 100644 index 0000000..8179ab8 --- /dev/null +++ b/src/components/wired/views/WiredDirectionIcon.tsx @@ -0,0 +1,53 @@ +import { FC } from 'react'; +import iconWiredDirE from '../../../assets/images/wired/icon_wired_dir_e.png'; +import iconWiredDirN from '../../../assets/images/wired/icon_wired_dir_n.png'; +import iconWiredDirNe from '../../../assets/images/wired/icon_wired_dir_ne.png'; +import iconWiredDirNw from '../../../assets/images/wired/icon_wired_dir_nw.png'; +import iconWiredDirS from '../../../assets/images/wired/icon_wired_dir_s.png'; +import iconWiredDirSe from '../../../assets/images/wired/icon_wired_dir_se.png'; +import iconWiredDirSw from '../../../assets/images/wired/icon_wired_dir_sw.png'; +import iconWiredDirW from '../../../assets/images/wired/icon_wired_dir_w.png'; + +export const WIRED_DIRECTION_GRID = [ + [ 7, 0, 1, 2 ], + [ 6, 5, 4, 3 ] +]; + +interface WiredDirectionIconProps +{ + direction: number; + selected?: boolean; + debugValue?: number | string; + iconSrc?: string; + showImage?: boolean; +} + +const DIRECTION_ICON_MAP: Record = { + 0: iconWiredDirN, + 1: iconWiredDirNe, + 2: iconWiredDirE, + 3: iconWiredDirSe, + 4: iconWiredDirS, + 5: iconWiredDirSw, + 6: iconWiredDirW, + 7: iconWiredDirNw +}; + +export const WiredDirectionIcon: FC = props => +{ + const { direction = 0, selected = false, debugValue = null, iconSrc = null, showImage = true } = props; + const icon = iconSrc ?? DIRECTION_ICON_MAP[direction]; + + return ( + + { showImage && + } + { (debugValue !== null && debugValue !== undefined) && + { debugValue } } + + ); +}; diff --git a/src/components/wired/views/WiredHandItemField.tsx b/src/components/wired/views/WiredHandItemField.tsx new file mode 100644 index 0000000..b4b17ac --- /dev/null +++ b/src/components/wired/views/WiredHandItemField.tsx @@ -0,0 +1,59 @@ +import { RoomObjectVariable } from '@nitrots/nitro-renderer'; +import { FC, useMemo } from 'react'; +import { GetOwnRoomObject, LocalizeText } from '../../../api'; +import { Button, Text } from '../../../common'; + +export const DEFAULT_HAND_ITEM_IDS: number[] = [ 2, 5, 7, 8, 9, 10, 27 ]; + +interface WiredHandItemFieldProps +{ + handItemId: number; + onChange: (value: number) => void; + labelKey?: string; + showCopyButton?: boolean; +} + +export const WiredHandItemField: FC = props => +{ + const { handItemId = 0, onChange = null, labelKey = 'wiredfurni.params.handitem', showCopyButton = false } = props; + + const options = useMemo(() => + { + const values = [ ...DEFAULT_HAND_ITEM_IDS ]; + + if(handItemId > 0 && !values.includes(handItemId)) values.unshift(handItemId); + + return values; + }, [ handItemId ]); + + const getLabel = (value: number) => + { + const key = `handitem${ value }`; + const localized = LocalizeText(key); + + if(localized && localized !== key) return localized; + + return `${ value }`; + }; + + const copyOwnHandItem = () => + { + const roomObject = GetOwnRoomObject(); + const copiedHandItem = (roomObject?.model?.getValue(RoomObjectVariable.FIGURE_CARRY_OBJECT) || 0); + + onChange && onChange(copiedHandItem); + }; + + return ( +
+ { LocalizeText(labelKey) } +
+ + { showCopyButton && } +
+
+ ); +}; diff --git a/src/components/wired/views/WiredSourcesSelector.tsx b/src/components/wired/views/WiredSourcesSelector.tsx index 2d5ebc1..ab1288e 100644 --- a/src/components/wired/views/WiredSourcesSelector.tsx +++ b/src/components/wired/views/WiredSourcesSelector.tsx @@ -16,6 +16,13 @@ export const USER_SOURCES = [ { value: 201, label: 'wiredfurni.params.sources.users.201' } ]; +export const BOT_SOURCES = [ + { value: 0, label: 'wiredfurni.params.sources.users.0' }, + { value: 100, label: 'wiredfurni.params.sources.users.100' }, + { value: 200, label: 'wiredfurni.params.sources.users.200' }, + { value: 201, label: 'wiredfurni.params.sources.users.201' } +]; + export interface WiredSourceOption { value: number; diff --git a/src/components/wired/views/actions/WiredActionBotChangeFigureView.tsx b/src/components/wired/views/actions/WiredActionBotChangeFigureView.tsx index e359037..eaf2037 100644 --- a/src/components/wired/views/actions/WiredActionBotChangeFigureView.tsx +++ b/src/components/wired/views/actions/WiredActionBotChangeFigureView.tsx @@ -5,31 +5,43 @@ import { Button, LayoutAvatarImageView, Text } from '../../../../common'; import { useWired } from '../../../../hooks'; import { NitroInput } from '../../../../layout'; import { WiredActionBaseView } from './WiredActionBaseView'; +import { BOT_SOURCES, WiredSourcesSelector } from '../WiredSourcesSelector'; const DEFAULT_FIGURE: string = 'hd-180-1.ch-210-66.lg-270-82.sh-290-81'; +const normalizeBotSource = (value: number, hasBotName = false) => (BOT_SOURCES.some(option => (option.value === value)) ? value : (hasBotName ? 100 : 0)); export const WiredActionBotChangeFigureView: FC<{}> = props => { const [ botName, setBotName ] = useState(''); const [ figure, setFigure ] = useState(''); - const { trigger = null, setStringParam = null } = useWired(); + const [ botSource, setBotSource ] = useState(100); + const { trigger = null, setStringParam = null, setIntParams = null } = useWired(); - const save = () => setStringParam((botName + WIRED_STRING_DELIMETER + figure)); + const save = () => + { + setStringParam(((botSource === 100) ? botName : '') + WIRED_STRING_DELIMETER + figure); + setIntParams([ botSource ]); + }; useEffect(() => { const data = trigger.stringData.split(WIRED_STRING_DELIMETER); + const nextBotName = (data.length > 0) ? data[0] : ''; - if(data.length > 0) setBotName(data[0]); + if(data.length > 0) setBotName(nextBotName); if(data.length > 1) setFigure(data[1].length > 0 ? data[1] : DEFAULT_FIGURE); + else setFigure(DEFAULT_FIGURE); + + setBotSource((trigger.intData.length > 0) ? normalizeBotSource(trigger.intData[0], (nextBotName.length > 0)) : normalizeBotSource(-1, (nextBotName.length > 0))); }, [ trigger ]); return ( - -
- { LocalizeText('wiredfurni.params.bot.name') } - setBotName(event.target.value) } /> -
+ setBotSource(normalizeBotSource(value, (botName.length > 0))) } /> }> + { (botSource === 100) && +
+ { LocalizeText('wiredfurni.params.bot.name') } + setBotName(event.target.value) } /> +
}
diff --git a/src/components/wired/views/actions/WiredActionBotFollowAvatarView.tsx b/src/components/wired/views/actions/WiredActionBotFollowAvatarView.tsx index e78ce8a..db00b7d 100644 --- a/src/components/wired/views/actions/WiredActionBotFollowAvatarView.tsx +++ b/src/components/wired/views/actions/WiredActionBotFollowAvatarView.tsx @@ -4,13 +4,16 @@ import { Text } from '../../../../common'; import { useWired } from '../../../../hooks'; import { NitroInput } from '../../../../layout'; import { WiredActionBaseView } from './WiredActionBaseView'; -import { WiredSourcesSelector } from '../WiredSourcesSelector'; +import { BOT_SOURCES, WiredSourcesSelector } from '../WiredSourcesSelector'; + +const normalizeBotSource = (value: number, hasBotName = false) => (BOT_SOURCES.some(option => (option.value === value)) ? value : (hasBotName ? 100 : 0)); export const WiredActionBotFollowAvatarView: FC<{}> = props => { const [ botName, setBotName ] = useState(''); const [ followMode, setFollowMode ] = useState(-1); const { trigger = null, setStringParam = null, setIntParams = null } = useWired(); + const [ botSource, setBotSource ] = useState(100); const [ userSource, setUserSource ] = useState(() => { if(trigger?.intData?.length > 1) return trigger.intData[1]; @@ -19,15 +22,17 @@ export const WiredActionBotFollowAvatarView: FC<{}> = props => const save = () => { - setStringParam(botName); - setIntParams([ followMode, userSource ]); + setStringParam((botSource === 100) ? botName : ''); + setIntParams([ followMode, userSource, botSource ]); }; useEffect(() => { - setBotName(trigger.stringData); + const nextBotName = trigger.stringData || ''; + setBotName(nextBotName); setFollowMode((trigger.intData.length > 0) ? trigger.intData[0] : 0); setUserSource((trigger.intData.length > 1) ? trigger.intData[1] : 0); + setBotSource((trigger.intData.length > 2) ? normalizeBotSource(trigger.intData[2], (nextBotName.length > 0)) : normalizeBotSource(-1, (nextBotName.length > 0))); }, [ trigger ]); return ( @@ -35,11 +40,18 @@ export const WiredActionBotFollowAvatarView: FC<{}> = props => hasSpecialInput={ true } requiresFurni={ WiredFurniType.STUFF_SELECTION_OPTION_NONE } save={ save } - footer={ }> -
- { LocalizeText('wiredfurni.params.bot.name') } - setBotName(event.target.value) } /> -
+ footer={ +
+ +
+ setBotSource(normalizeBotSource(value, (botName.length > 0))) } /> +
+ }> + { (botSource === 100) && +
+ { LocalizeText('wiredfurni.params.bot.name') } + setBotName(event.target.value) } /> +
}
setFollowMode(1) } /> diff --git a/src/components/wired/views/actions/WiredActionBotGiveHandItemView.tsx b/src/components/wired/views/actions/WiredActionBotGiveHandItemView.tsx index 472e46f..4b25adb 100644 --- a/src/components/wired/views/actions/WiredActionBotGiveHandItemView.tsx +++ b/src/components/wired/views/actions/WiredActionBotGiveHandItemView.tsx @@ -4,32 +4,47 @@ import { Text } from '../../../../common'; import { useWired } from '../../../../hooks'; import { NitroInput } from '../../../../layout'; import { WiredActionBaseView } from './WiredActionBaseView'; -import { WiredSourcesSelector } from '../WiredSourcesSelector'; +import { WiredSourceOption, WiredSourcesSelector } from '../WiredSourcesSelector'; +import { WiredHandItemField } from '../WiredHandItemField'; -const ALLOWED_HAND_ITEM_IDS: number[] = [ 2, 5, 7, 8, 9, 10, 27 ]; +const USER_SOURCE_OPTIONS: WiredSourceOption[] = [ + { value: 0, label: 'wiredfurni.params.sources.users.0' }, + { value: 200, label: 'wiredfurni.params.sources.users.200' }, + { value: 201, label: 'wiredfurni.params.sources.users.201' } +]; + +const BOT_SOURCE_OPTIONS: WiredSourceOption[] = [ + { value: 0, label: 'wiredfurni.params.sources.users.0' }, + { value: 100, label: 'wiredfurni.params.sources.users.100' }, + { value: 200, label: 'wiredfurni.params.sources.users.200' }, + { value: 201, label: 'wiredfurni.params.sources.users.201' } +]; + +const normalizeUserSource = (value: number) => (USER_SOURCE_OPTIONS.some(option => (option.value === value)) ? value : 0); +const normalizeBotSource = (value: number, hasBotName = false) => (BOT_SOURCE_OPTIONS.some(option => (option.value === value)) ? value : (hasBotName ? 100 : 0)); export const WiredActionBotGiveHandItemView: FC<{}> = props => { const [ botName, setBotName ] = useState(''); const [ handItemId, setHandItemId ] = useState(-1); const { trigger = null, setStringParam = null, setIntParams = null } = useWired(); - const [ userSource, setUserSource ] = useState(() => - { - if(trigger?.intData?.length > 1) return trigger.intData[1]; - return 0; - }); + const [ userSource, setUserSource ] = useState(0); + const [ botSource, setBotSource ] = useState(100); const save = () => { - setStringParam(botName); - setIntParams([ handItemId, userSource ]); + setStringParam((botSource === 100) ? botName : ''); + setIntParams([ handItemId, userSource, botSource ]); }; useEffect(() => { - setBotName(trigger.stringData); + const nextBotName = trigger.stringData || ''; + + setBotName(nextBotName); setHandItemId((trigger.intData.length > 0) ? trigger.intData[0] : 0); - setUserSource((trigger.intData.length > 1) ? trigger.intData[1] : 0); + setUserSource((trigger.intData.length > 1) ? normalizeUserSource(trigger.intData[1]) : 0); + setBotSource((trigger.intData.length > 2) ? normalizeBotSource(trigger.intData[2], (nextBotName.length > 0)) : normalizeBotSource(-1, (nextBotName.length > 0))); }, [ trigger ]); return ( @@ -37,18 +52,23 @@ export const WiredActionBotGiveHandItemView: FC<{}> = props => hasSpecialInput={ true } requiresFurni={ WiredFurniType.STUFF_SELECTION_OPTION_NONE } save={ save } - footer={ }> -
- { LocalizeText('wiredfurni.params.bot.name') } - setBotName(event.target.value) } /> -
-
- { LocalizeText('wiredfurni.params.handitem') } - + footer={ +
+ +
+ setBotSource(normalizeBotSource(value, (botName.length > 0))) } /> +
+ }> +
+ setBotSource(event.target.checked ? 100 : 0) } /> +
+ { (botSource === 100) && +
+ { LocalizeText('wiredfurni.params.bot.name') } + setBotName(event.target.value) } /> +
} + ); }; diff --git a/src/components/wired/views/actions/WiredActionBotMoveView.tsx b/src/components/wired/views/actions/WiredActionBotMoveView.tsx index d07bc7b..b80e92e 100644 --- a/src/components/wired/views/actions/WiredActionBotMoveView.tsx +++ b/src/components/wired/views/actions/WiredActionBotMoveView.tsx @@ -4,11 +4,14 @@ import { Text } from '../../../../common'; import { useWired } from '../../../../hooks'; import { NitroInput } from '../../../../layout'; import { WiredActionBaseView } from './WiredActionBaseView'; -import { WiredSourcesSelector } from '../WiredSourcesSelector'; +import { BOT_SOURCES, WiredSourcesSelector } from '../WiredSourcesSelector'; + +const normalizeBotSource = (value: number, hasBotName = false) => (BOT_SOURCES.some(option => (option.value === value)) ? value : (hasBotName ? 100 : 0)); export const WiredActionBotMoveView: FC<{}> = props => { const [ botName, setBotName ] = useState(''); + const [ botSource, setBotSource ] = useState(100); const { trigger = null, setStringParam = null, setIntParams = null } = useWired(); const [ furniSource, setFurniSource ] = useState(() => @@ -19,18 +22,21 @@ export const WiredActionBotMoveView: FC<{}> = props => const save = () => { - setStringParam(botName); - setIntParams([ furniSource ]); + setStringParam((botSource === 100) ? botName : ''); + setIntParams([ furniSource, botSource ]); }; useEffect(() => { if(!trigger) return; - setBotName(trigger.stringData); + const nextBotName = trigger.stringData || ''; + setBotName(nextBotName); if(trigger.intData.length >= 1) setFurniSource(trigger.intData[0]); else setFurniSource((trigger.selectedItems?.length ?? 0) > 0 ? 100 : 0); + + setBotSource((trigger.intData.length >= 2) ? normalizeBotSource(trigger.intData[1], (nextBotName.length > 0)) : normalizeBotSource(-1, (nextBotName.length > 0))); }, [ trigger ]); const onChangeFurniSource = (next: number) => setFurniSource(next); @@ -42,11 +48,18 @@ export const WiredActionBotMoveView: FC<{}> = props => hasSpecialInput={ true } requiresFurni={ requiresFurni } save={ save } - footer={ }> -
- { LocalizeText('wiredfurni.params.bot.name') } - setBotName(event.target.value) } /> -
+ footer={ +
+ +
+ setBotSource(normalizeBotSource(value, (botName.length > 0))) } /> +
+ }> + { (botSource === 100) && +
+ { LocalizeText('wiredfurni.params.bot.name') } + setBotName(event.target.value) } /> +
} ); }; diff --git a/src/components/wired/views/actions/WiredActionBotTalkToAvatarView.tsx b/src/components/wired/views/actions/WiredActionBotTalkToAvatarView.tsx index 4961154..5d7db10 100644 --- a/src/components/wired/views/actions/WiredActionBotTalkToAvatarView.tsx +++ b/src/components/wired/views/actions/WiredActionBotTalkToAvatarView.tsx @@ -4,13 +4,16 @@ import { Text } from '../../../../common'; import { useWired } from '../../../../hooks'; import { NitroInput } from '../../../../layout'; import { WiredActionBaseView } from './WiredActionBaseView'; -import { WiredSourcesSelector } from '../WiredSourcesSelector'; +import { BOT_SOURCES, WiredSourcesSelector } from '../WiredSourcesSelector'; + +const normalizeBotSource = (value: number, hasBotName = false) => (BOT_SOURCES.some(option => (option.value === value)) ? value : (hasBotName ? 100 : 0)); export const WiredActionBotTalkToAvatarView: FC<{}> = props => { const [ botName, setBotName ] = useState(''); const [ message, setMessage ] = useState(''); const [ talkMode, setTalkMode ] = useState(-1); + const [ botSource, setBotSource ] = useState(100); const { trigger = null, setStringParam = null, setIntParams = null } = useWired(); const [ userSource, setUserSource ] = useState(() => { @@ -20,19 +23,21 @@ export const WiredActionBotTalkToAvatarView: FC<{}> = props => const save = () => { - setStringParam(botName + WIRED_STRING_DELIMETER + message); - setIntParams([ talkMode, userSource ]); + setStringParam(((botSource === 100) ? botName : '') + WIRED_STRING_DELIMETER + message); + setIntParams([ talkMode, userSource, botSource ]); }; useEffect(() => { const data = trigger.stringData.split(WIRED_STRING_DELIMETER); + const nextBotName = (data.length > 0) ? data[0] : ''; - if(data.length > 0) setBotName(data[0]); + if(data.length > 0) setBotName(nextBotName); if(data.length > 1) setMessage(data[1].length > 0 ? data[1] : ''); setTalkMode((trigger.intData.length > 0) ? trigger.intData[0] : 0); setUserSource((trigger.intData.length > 1) ? trigger.intData[1] : 0); + setBotSource((trigger.intData.length > 2) ? normalizeBotSource(trigger.intData[2], (nextBotName.length > 0)) : normalizeBotSource(-1, (nextBotName.length > 0))); }, [ trigger ]); return ( @@ -40,11 +45,18 @@ export const WiredActionBotTalkToAvatarView: FC<{}> = props => hasSpecialInput={ true } requiresFurni={ WiredFurniType.STUFF_SELECTION_OPTION_NONE } save={ save } - footer={ }> -
- { LocalizeText('wiredfurni.params.bot.name') } - setBotName(event.target.value) } /> -
+ footer={ +
+ +
+ setBotSource(normalizeBotSource(value, (botName.length > 0))) } /> +
+ }> + { (botSource === 100) && +
+ { LocalizeText('wiredfurni.params.bot.name') } + setBotName(event.target.value) } /> +
}
{ LocalizeText('wiredfurni.params.message') } ('wired.action.bot.talk.to.avatar.max.length', 64) } type="text" value={ message } onChange={ event => setMessage(event.target.value) } /> diff --git a/src/components/wired/views/actions/WiredActionBotTalkView.tsx b/src/components/wired/views/actions/WiredActionBotTalkView.tsx index 31b6a14..ab1ac60 100644 --- a/src/components/wired/views/actions/WiredActionBotTalkView.tsx +++ b/src/components/wired/views/actions/WiredActionBotTalkView.tsx @@ -4,36 +4,43 @@ import { Text } from '../../../../common'; import { useWired } from '../../../../hooks'; import { NitroInput } from '../../../../layout'; import { WiredActionBaseView } from './WiredActionBaseView'; +import { BOT_SOURCES, WiredSourcesSelector } from '../WiredSourcesSelector'; + +const normalizeBotSource = (value: number, hasBotName = false) => (BOT_SOURCES.some(option => (option.value === value)) ? value : (hasBotName ? 100 : 0)); export const WiredActionBotTalkView: FC<{}> = props => { const [ botName, setBotName ] = useState(''); const [ message, setMessage ] = useState(''); const [ talkMode, setTalkMode ] = useState(-1); + const [ botSource, setBotSource ] = useState(100); const { trigger = null, setStringParam = null, setIntParams = null } = useWired(); const save = () => { - setStringParam(botName + WIRED_STRING_DELIMETER + message); - setIntParams([ talkMode ]); + setStringParam(((botSource === 100) ? botName : '') + WIRED_STRING_DELIMETER + message); + setIntParams([ talkMode, botSource ]); }; useEffect(() => { const data = trigger.stringData.split(WIRED_STRING_DELIMETER); + const nextBotName = (data.length > 0) ? data[0] : ''; - if(data.length > 0) setBotName(data[0]); + if(data.length > 0) setBotName(nextBotName); if(data.length > 1) setMessage(data[1].length > 0 ? data[1] : ''); setTalkMode((trigger.intData.length > 0) ? trigger.intData[0] : 0); + setBotSource((trigger.intData.length > 1) ? normalizeBotSource(trigger.intData[1], (nextBotName.length > 0)) : normalizeBotSource(-1, (nextBotName.length > 0))); }, [ trigger ]); return ( - -
- { LocalizeText('wiredfurni.params.bot.name') } - setBotName(event.target.value) } /> -
+ setBotSource(normalizeBotSource(value, (botName.length > 0))) } /> }> + { (botSource === 100) && +
+ { LocalizeText('wiredfurni.params.bot.name') } + setBotName(event.target.value) } /> +
}
{ LocalizeText('wiredfurni.params.message') } ('wired.action.bot.talk.max.length', 64) } type="text" value={ message } onChange={ event => setMessage(event.target.value) } /> diff --git a/src/components/wired/views/actions/WiredActionBotTeleportView.tsx b/src/components/wired/views/actions/WiredActionBotTeleportView.tsx index 725f236..1422b36 100644 --- a/src/components/wired/views/actions/WiredActionBotTeleportView.tsx +++ b/src/components/wired/views/actions/WiredActionBotTeleportView.tsx @@ -4,11 +4,14 @@ import { Text } from '../../../../common'; import { useWired } from '../../../../hooks'; import { NitroInput } from '../../../../layout'; import { WiredActionBaseView } from './WiredActionBaseView'; -import { WiredSourcesSelector } from '../WiredSourcesSelector'; +import { BOT_SOURCES, WiredSourcesSelector } from '../WiredSourcesSelector'; + +const normalizeBotSource = (value: number, hasBotName = false) => (BOT_SOURCES.some(option => (option.value === value)) ? value : (hasBotName ? 100 : 0)); export const WiredActionBotTeleportView: FC<{}> = props => { const [ botName, setBotName ] = useState(''); + const [ botSource, setBotSource ] = useState(100); const { trigger = null, setStringParam = null, setIntParams = null } = useWired(); const [ furniSource, setFurniSource ] = useState(() => @@ -19,18 +22,21 @@ export const WiredActionBotTeleportView: FC<{}> = props => const save = () => { - setStringParam(botName); - setIntParams([ furniSource ]); + setStringParam((botSource === 100) ? botName : ''); + setIntParams([ furniSource, botSource ]); }; useEffect(() => { if(!trigger) return; - setBotName(trigger.stringData); + const nextBotName = trigger.stringData || ''; + setBotName(nextBotName); if(trigger.intData.length >= 1) setFurniSource(trigger.intData[0]); else setFurniSource((trigger.selectedItems?.length ?? 0) > 0 ? 100 : 0); + + setBotSource((trigger.intData.length >= 2) ? normalizeBotSource(trigger.intData[1], (nextBotName.length > 0)) : normalizeBotSource(-1, (nextBotName.length > 0))); }, [ trigger ]); const onChangeFurniSource = (next: number) => setFurniSource(next); @@ -42,11 +48,18 @@ export const WiredActionBotTeleportView: FC<{}> = props => hasSpecialInput={ true } requiresFurni={ requiresFurni } save={ save } - footer={ }> -
- { LocalizeText('wiredfurni.params.bot.name') } - setBotName(event.target.value) } /> -
+ footer={ +
+ +
+ setBotSource(normalizeBotSource(value, (botName.length > 0))) } /> +
+ }> + { (botSource === 100) && +
+ { LocalizeText('wiredfurni.params.bot.name') } + setBotName(event.target.value) } /> +
} ); }; diff --git a/src/components/wired/views/actions/WiredActionGiveScoreToPredefinedTeamView.tsx b/src/components/wired/views/actions/WiredActionGiveScoreToPredefinedTeamView.tsx index 1cfe779..be20004 100644 --- a/src/components/wired/views/actions/WiredActionGiveScoreToPredefinedTeamView.tsx +++ b/src/components/wired/views/actions/WiredActionGiveScoreToPredefinedTeamView.tsx @@ -7,24 +7,24 @@ import { WiredActionBaseView } from './WiredActionBaseView'; export const WiredActionGiveScoreToPredefinedTeamView: FC<{}> = props => { const [ points, setPoints ] = useState(1); - const [ time, setTime ] = useState(1); + const [ operation, setOperation ] = useState(0); const [ selectedTeam, setSelectedTeam ] = useState(1); const { trigger = null, setIntParams = null } = useWired(); - const save = () => setIntParams([ points, time, selectedTeam ]); + const save = () => setIntParams([ points, operation, selectedTeam ]); useEffect(() => { - if(trigger.intData.length >= 2) + if(trigger.intData.length >= 3) { setPoints(trigger.intData[0]); - setTime(trigger.intData[1]); + setOperation(trigger.intData[1]); setSelectedTeam(trigger.intData[2]); } else { setPoints(1); - setTime(1); + setOperation(0); setSelectedTeam(1); } }, [ trigger ]); @@ -40,12 +40,13 @@ export const WiredActionGiveScoreToPredefinedTeamView: FC<{}> = props => onChange={ event => setPoints(event) } />
- { LocalizeText('wiredfurni.params.settimesingame', [ 'times' ], [ time.toString() ]) } - setTime(event) } /> + { LocalizeText('wiredfurni.params.choose_type') } + { [ 0, 1 ].map(value => ( + + )) }
{ LocalizeText('wiredfurni.params.team') } diff --git a/src/components/wired/views/actions/WiredActionGiveScoreView.tsx b/src/components/wired/views/actions/WiredActionGiveScoreView.tsx index 402b69d..0377a10 100644 --- a/src/components/wired/views/actions/WiredActionGiveScoreView.tsx +++ b/src/components/wired/views/actions/WiredActionGiveScoreView.tsx @@ -8,7 +8,7 @@ import { WiredSourcesSelector } from '../WiredSourcesSelector'; export const WiredActionGiveScoreView: FC<{}> = props => { const [ points, setPoints ] = useState(1); - const [ time, setTime ] = useState(1); + const [ operation, setOperation ] = useState(0); const { trigger = null, setIntParams = null } = useWired(); const [ userSource, setUserSource ] = useState(() => { @@ -16,19 +16,19 @@ export const WiredActionGiveScoreView: FC<{}> = props => return 0; }); - const save = () => setIntParams([ points, time, userSource ]); + const save = () => setIntParams([ points, operation, userSource ]); useEffect(() => { if(trigger.intData.length >= 2) { setPoints(trigger.intData[0]); - setTime(trigger.intData[1]); + setOperation(trigger.intData[1]); } else { setPoints(1); - setTime(1); + setOperation(0); } setUserSource((trigger.intData.length > 2) ? trigger.intData[2] : 0); @@ -49,12 +49,13 @@ export const WiredActionGiveScoreView: FC<{}> = props => onChange={ event => setPoints(event) } />
- { LocalizeText('wiredfurni.params.settimesingame', [ 'times' ], [ time.toString() ]) } - setTime(event) } /> + { LocalizeText('wiredfurni.params.choose_type') } + { [ 0, 1 ].map(value => ( + + )) }
); diff --git a/src/components/wired/views/actions/WiredActionJoinTeamView.tsx b/src/components/wired/views/actions/WiredActionJoinTeamView.tsx index 3468f26..c0c9b74 100644 --- a/src/components/wired/views/actions/WiredActionJoinTeamView.tsx +++ b/src/components/wired/views/actions/WiredActionJoinTeamView.tsx @@ -7,20 +7,32 @@ import { WiredSourcesSelector } from '../WiredSourcesSelector'; export const WiredActionJoinTeamView: FC<{}> = props => { - const [ selectedTeam, setSelectedTeam ] = useState(-1); + const [ selectedTeamType, setSelectedTeamType ] = useState(0); + const [ selectedTeam, setSelectedTeam ] = useState(1); const { trigger = null, setIntParams = null } = useWired(); const [ userSource, setUserSource ] = useState(() => { + if(trigger?.intData?.length > 2) return trigger.intData[2]; if(trigger?.intData?.length > 1) return trigger.intData[1]; return 0; }); - const save = () => setIntParams([ selectedTeam, userSource ]); + const save = () => setIntParams([ selectedTeamType, selectedTeam, userSource ]); useEffect(() => { - setSelectedTeam((trigger.intData.length > 0) ? trigger.intData[0] : 0); - setUserSource((trigger.intData.length > 1) ? trigger.intData[1] : 0); + if(trigger.intData.length > 2) + { + setSelectedTeamType(trigger.intData[0]); + setSelectedTeam(trigger.intData[1]); + setUserSource(trigger.intData[2]); + } + else + { + setSelectedTeamType(0); + setSelectedTeam((trigger.intData.length > 0) ? trigger.intData[0] : 1); + setUserSource((trigger.intData.length > 1) ? trigger.intData[1] : 0); + } }, [ trigger ]); return ( @@ -29,6 +41,22 @@ export const WiredActionJoinTeamView: FC<{}> = props => requiresFurni={ WiredFurniType.STUFF_SELECTION_OPTION_NONE } save={ save } footer={ }> +
+ { LocalizeText('wiredfurni.params.choose_type') } + { [ + { value: 0, label: 'Wired' }, + { value: 1, label: 'Banzai' }, + { value: 2, label: 'Freeze' } + ].map(option => + { + return ( +
+ setSelectedTeamType(option.value) } /> + { option.label } +
+ ); + }) } +
{ LocalizeText('wiredfurni.params.team') } { [ 1, 2, 3, 4 ].map(team => diff --git a/src/components/wired/views/actions/WiredActionLayoutView.tsx b/src/components/wired/views/actions/WiredActionLayoutView.tsx index c445dbb..baa0278 100644 --- a/src/components/wired/views/actions/WiredActionLayoutView.tsx +++ b/src/components/wired/views/actions/WiredActionLayoutView.tsx @@ -9,8 +9,20 @@ import { WiredActionSendSignalView } from './WiredActionSendSignalView'; import { WiredActionFurniAreaView } from '../selectors/WiredActionFurniAreaView'; import { WiredSelectorFurniNeighborhoodView } from '../selectors/WiredSelectorFurniNeighborhoodView'; import { WiredSelectorFurniByTypeView } from '../selectors/WiredSelectorFurniByTypeView'; +import { WiredSelectorFurniAltitudeView } from '../selectors/WiredSelectorFurniAltitudeView'; +import { WiredSelectorFurniOnFurniView } from '../selectors/WiredSelectorFurniOnFurniView'; +import { WiredSelectorFurniPicksView } from '../selectors/WiredSelectorFurniPicksView'; +import { WiredSelectorFurniSignalView } from '../selectors/WiredSelectorFurniSignalView'; import { WiredSelectorUsersAreaView } from '../selectors/WiredSelectorUsersAreaView'; +import { WiredSelectorUsersByTypeView } from '../selectors/WiredSelectorUsersByTypeView'; +import { WiredSelectorUsersByActionView } from '../selectors/WiredSelectorUsersByActionView'; +import { WiredSelectorUsersByNameView } from '../selectors/WiredSelectorUsersByNameView'; +import { WiredSelectorUsersOnFurniView } from '../selectors/WiredSelectorUsersOnFurniView'; +import { WiredSelectorUsersGroupView } from '../selectors/WiredSelectorUsersGroupView'; +import { WiredSelectorUsersHandItemView } from '../selectors/WiredSelectorUsersHandItemView'; import { WiredSelectorUsersNeighborhoodView } from '../selectors/WiredSelectorUsersNeighborhoodView'; +import { WiredSelectorUsersSignalView } from '../selectors/WiredSelectorUsersSignalView'; +import { WiredSelectorUsersTeamView } from '../selectors/WiredSelectorUsersTeamView'; import { WiredActionBotFollowAvatarView } from './WiredActionBotFollowAvatarView'; import { WiredActionBotGiveHandItemView } from './WiredActionBotGiveHandItemView'; import { WiredActionBotMoveView } from './WiredActionBotMoveView'; @@ -28,6 +40,7 @@ import { WiredActionJoinTeamView } from './WiredActionJoinTeamView'; import { WiredActionKickFromRoomView } from './WiredActionKickFromRoomView'; import { WiredActionLeaveTeamView } from './WiredActionLeaveTeamView'; import { WiredActionMoveAndRotateFurniView } from './WiredActionMoveAndRotateFurniView'; +import { WiredActionMoveRotateUserView } from './WiredActionMoveRotateUserView'; import { WiredActionMoveFurniToView } from './WiredActionMoveFurniToView'; import { WiredActionMoveFurniView } from './WiredActionMoveFurniView'; import { WiredActionMuteUserView } from './WiredActionMuteUserView'; @@ -37,6 +50,8 @@ import { WiredActionSetFurniStateToView } from './WiredActionSetFurniStateToView import { WiredActionTeleportView } from './WiredActionTeleportView'; import { WiredActionToggleFurniStateView } from './WiredActionToggleFurniStateView'; import { WiredActionUnfreezeView } from './WiredActionUnfreezeView'; +import { WiredExtraFilterFurniView } from '../extras/WiredExtraFilterFurniView'; +import { WiredExtraFilterUserView } from '../extras/WiredExtraFilterUserView'; export const WiredActionLayoutView = (code: number) => { @@ -92,6 +107,8 @@ export const WiredActionLayoutView = (code: number) => return ; case WiredActionLayoutCode.MOVE_AND_ROTATE_FURNI: return ; + case WiredActionLayoutCode.MOVE_ROTATE_USER: + return ; case WiredActionLayoutCode.MOVE_FURNI_TO: return ; case WiredActionLayoutCode.MUTE_USER: @@ -116,10 +133,38 @@ export const WiredActionLayoutView = (code: number) => return ; case WiredActionLayoutCode.FURNI_BYTYPE_SELECTOR: return ; + case WiredActionLayoutCode.FURNI_ALTITUDE_SELECTOR: + return ; + case WiredActionLayoutCode.FURNI_ON_FURNI_SELECTOR: + return ; + case WiredActionLayoutCode.FURNI_PICKS_SELECTOR: + return ; + case WiredActionLayoutCode.FURNI_SIGNAL_SELECTOR: + return ; case WiredActionLayoutCode.USERS_AREA_SELECTOR: return ; case WiredActionLayoutCode.USERS_NEIGHBORHOOD_SELECTOR: return ; + case WiredActionLayoutCode.USERS_SIGNAL_SELECTOR: + return ; + case WiredActionLayoutCode.USERS_BY_TYPE_SELECTOR: + return ; + case WiredActionLayoutCode.USERS_BY_ACTION_SELECTOR: + return ; + case WiredActionLayoutCode.USERS_BY_NAME_SELECTOR: + return ; + case WiredActionLayoutCode.USERS_ON_FURNI_SELECTOR: + return ; + case WiredActionLayoutCode.USERS_GROUP_SELECTOR: + return ; + case WiredActionLayoutCode.USERS_HANDITEM_SELECTOR: + return ; + case WiredActionLayoutCode.USERS_TEAM_SELECTOR: + return ; + case WiredActionLayoutCode.FILTER_FURNI_EXTRA: + return ; + case WiredActionLayoutCode.FILTER_USER_EXTRA: + return ; case WiredActionLayoutCode.SEND_SIGNAL: return ; } diff --git a/src/components/wired/views/actions/WiredActionMoveAndRotateFurniView.tsx b/src/components/wired/views/actions/WiredActionMoveAndRotateFurniView.tsx index c7fff0d..b253590 100644 --- a/src/components/wired/views/actions/WiredActionMoveAndRotateFurniView.tsx +++ b/src/components/wired/views/actions/WiredActionMoveAndRotateFurniView.tsx @@ -2,34 +2,17 @@ import { FC, useEffect, useState } from 'react'; import { LocalizeText, WiredFurniType } from '../../../../api'; import { Text } from '../../../../common'; import { useWired } from '../../../../hooks'; +import { WiredDirectionIcon, WIRED_DIRECTION_GRID } from '../WiredDirectionIcon'; import { WiredActionBaseView } from './WiredActionBaseView'; import { WiredSourcesSelector } from '../WiredSourcesSelector'; -const directionOptions: { value: number, icon: string }[] = [ - { - value: 0, - icon: 'ne' - }, - { - value: 2, - icon: 'se' - }, - { - value: 4, - icon: 'sw' - }, - { - value: 6, - icon: 'nw' - } -]; - const rotationOptions: number[] = [ 0, 1, 2, 3, 4, 5, 6 ]; export const WiredActionMoveAndRotateFurniView: FC<{}> = props => { const [ movement, setMovement ] = useState(-1); const [ rotation, setRotation ] = useState(-1); + const [ blockOnUserCollision, setBlockOnUserCollision ] = useState(false); const { trigger = null, setIntParams = null } = useWired(); const [ furniSource, setFurniSource ] = useState(() => { @@ -37,7 +20,7 @@ export const WiredActionMoveAndRotateFurniView: FC<{}> = props => return (trigger?.selectedItems?.length ?? 0) > 0 ? 100 : 0; }); - const save = () => setIntParams([ movement, rotation, furniSource ]); + const save = () => setIntParams([ movement, rotation, furniSource, blockOnUserCollision ? 1 : 0 ]); useEffect(() => { @@ -54,6 +37,8 @@ export const WiredActionMoveAndRotateFurniView: FC<{}> = props => if(trigger.intData.length > 2) setFurniSource(trigger.intData[2]); else setFurniSource((trigger.selectedItems?.length ?? 0) > 0 ? 100 : 0); + + setBlockOnUserCollision((trigger.intData?.length ?? 0) > 3 ? trigger.intData[3] === 1 : false); }, [ trigger ]); const onChangeFurniSource = (next: number) => setFurniSource(next); @@ -68,18 +53,23 @@ export const WiredActionMoveAndRotateFurniView: FC<{}> = props => footer={ }>
{ LocalizeText('wiredfurni.params.startdir') } -
- { directionOptions.map(option => +
+ { WIRED_DIRECTION_GRID.flatMap((row, rowIndex) => row.map((direction, columnIndex) => { + if(direction === null) + { + return
; + } + return ( -
- setMovement(option.value) } /> - - - -
+ ); - }) } + })) }
@@ -94,6 +84,13 @@ export const WiredActionMoveAndRotateFurniView: FC<{}> = props => ); }) }
+
+ { LocalizeText('wiredfurni.params.user_collide') } + +
); }; diff --git a/src/components/wired/views/actions/WiredActionMoveFurniView.tsx b/src/components/wired/views/actions/WiredActionMoveFurniView.tsx index e7285a3..391e726 100644 --- a/src/components/wired/views/actions/WiredActionMoveFurniView.tsx +++ b/src/components/wired/views/actions/WiredActionMoveFurniView.tsx @@ -2,37 +2,44 @@ import { FC, useEffect, useState } from 'react'; import { LocalizeText, WiredFurniType } from '../../../../api'; import { Text } from '../../../../common'; import { useWired } from '../../../../hooks'; +import iconWiredDirE from '../../../../assets/images/wired/icon_wired_dir_e.png'; +import iconWiredDirHorizontalRandom from '../../../../assets/images/wired/icon_wired_dir_horizontal_random.png'; +import iconWiredDirN from '../../../../assets/images/wired/icon_wired_dir_n.png'; +import iconWiredDirNe from '../../../../assets/images/wired/icon_wired_dir_ne.png'; +import iconWiredDirNw from '../../../../assets/images/wired/icon_wired_dir_nw.png'; +import iconWiredDirRandom from '../../../../assets/images/wired/icon_wired_dir_random.png'; +import iconWiredDirS from '../../../../assets/images/wired/icon_wired_dir_s.png'; +import iconWiredDirSe from '../../../../assets/images/wired/icon_wired_dir_se.png'; +import iconWiredDirSw from '../../../../assets/images/wired/icon_wired_dir_sw.png'; +import iconWiredDirVerticalRandom from '../../../../assets/images/wired/icon_wired_dir_vertical_random.png'; +import iconWiredDirW from '../../../../assets/images/wired/icon_wired_dir_w.png'; +import { WiredDirectionIcon, WIRED_DIRECTION_GRID } from '../WiredDirectionIcon'; import { WiredActionBaseView } from './WiredActionBaseView'; import { WiredSourcesSelector } from '../WiredSourcesSelector'; -const directionOptions: { value: number, icon: string }[] = [ +const NORMAL_DIRECTION_VALUE_MAP: Record = { + 0: { value: 6, icon: iconWiredDirN }, + 1: { value: 8, icon: iconWiredDirNe }, + 2: { value: 5, icon: iconWiredDirE }, + 3: { value: 9, icon: iconWiredDirSe }, + 4: { value: 4, icon: iconWiredDirS }, + 5: { value: 10, icon: iconWiredDirSw }, + 6: { value: 7, icon: iconWiredDirW }, + 7: { value: 11, icon: iconWiredDirNw } +}; + +const extraDirectionOptions: { value: number, icon: string }[] = [ { - value: 4, - icon: 'ne' - }, - { - value: 5, - icon: 'se' - }, - { - value: 6, - icon: 'sw' - }, - { - value: 7, - icon: 'nw' + value: 1, + icon: iconWiredDirRandom }, { value: 2, - icon: 'mv-2' + icon: iconWiredDirHorizontalRandom }, { value: 3, - icon: 'mv-3' - }, - { - value: 1, - icon: 'mv-1' + icon: iconWiredDirVerticalRandom } ]; @@ -84,17 +91,38 @@ export const WiredActionMoveFurniView: FC<{}> = props => setMovement(0) } /> { LocalizeText('wiredfurni.params.movefurni.0') }
-
- { directionOptions.map(option => +
+ { WIRED_DIRECTION_GRID.flatMap((row, rowIndex) => row.map((direction, columnIndex) => + { + if(direction === null) + { + return
; + } + + const option = NORMAL_DIRECTION_VALUE_MAP[direction]; + + return ( + + ); + })) } +
+
+ { extraDirectionOptions.map(option => { return ( -
+
+ + + + ); }) } -
diff --git a/src/components/wired/views/actions/WiredActionMoveRotateUserView.tsx b/src/components/wired/views/actions/WiredActionMoveRotateUserView.tsx new file mode 100644 index 0000000..c39d5f6 --- /dev/null +++ b/src/components/wired/views/actions/WiredActionMoveRotateUserView.tsx @@ -0,0 +1,121 @@ +import { FC, useEffect, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import iconRotateClockwise from '../../../../assets/images/wired/icon_wired_rotate_clockwise.png'; +import iconRotateCounterClockwise from '../../../../assets/images/wired/icon_wired_rotate_counter_clockwise.png'; +import { WiredDirectionIcon, WIRED_DIRECTION_GRID } from '../WiredDirectionIcon'; +import { WiredActionBaseView } from './WiredActionBaseView'; +import { WiredSourcesSelector } from '../WiredSourcesSelector'; + +const ROTATION_CLOCKWISE = 8; +const ROTATION_COUNTER_CLOCKWISE = 9; + +interface DirectionExtraOption +{ + value: number; + icon: string; + label: string; +} + +interface DirectionPickerProps +{ + name: string; + title: string; + noneLabel: string; + value: number; + onChange: (value: number) => void; + extraOptions?: DirectionExtraOption[]; +} + +const DirectionPicker: FC = props => +{ + const { name = '', title = '', noneLabel = '', value = -1, onChange = null, extraOptions = [] } = props; + + return ( +
+ { title } + +
+ { WIRED_DIRECTION_GRID.flatMap((row, rowIndex) => row.map((direction, columnIndex) => + { + if(direction === null) + { + return
; + } + + const selected = (value === direction); + + return ( + + ); + })) } +
+ { extraOptions.length > 0 && +
+ { extraOptions.map(option => ( + + )) } +
} +
+ ); +}; + +export const WiredActionMoveRotateUserView: FC<{}> = props => +{ + const [ movementDirection, setMovementDirection ] = useState(-1); + const [ rotationDirection, setRotationDirection ] = useState(-1); + const { trigger = null, setIntParams = null } = useWired(); + const [ userSource, setUserSource ] = useState(() => + { + if(trigger?.intData?.length > 2) return trigger.intData[2]; + return 0; + }); + + const save = () => setIntParams([ movementDirection, rotationDirection, userSource ]); + + const rotationExtraOptions: DirectionExtraOption[] = [ + { value: ROTATION_CLOCKWISE, icon: iconRotateClockwise, label: LocalizeText('wiredfurni.params.rotatefurni.1') }, + { value: ROTATION_COUNTER_CLOCKWISE, icon: iconRotateCounterClockwise, label: LocalizeText('wiredfurni.params.rotatefurni.2') } + ]; + + useEffect(() => + { + setMovementDirection((trigger.intData.length > 0) ? trigger.intData[0] : -1); + setRotationDirection((trigger.intData.length > 1) ? trigger.intData[1] : -1); + setUserSource((trigger.intData.length > 2) ? trigger.intData[2] : 0); + }, [ trigger ]); + + return ( + }> + + + + ); +}; diff --git a/src/components/wired/views/actions/WiredActionSendSignalView.tsx b/src/components/wired/views/actions/WiredActionSendSignalView.tsx index 6ed3df5..af6a253 100644 --- a/src/components/wired/views/actions/WiredActionSendSignalView.tsx +++ b/src/components/wired/views/actions/WiredActionSendSignalView.tsx @@ -1,4 +1,4 @@ -import { FC, useCallback, useEffect, useState } from 'react'; +import { FC, useCallback, useEffect, useRef, useState } from 'react'; import { LocalizeText, WiredFurniType, WiredSelectionVisualizer } from '../../../../api'; import { Button, Text } from '../../../../common'; import { useWired } from '../../../../hooks'; @@ -48,6 +48,7 @@ export const WiredActionSendSignalView: FC<{}> = () => const [ antennaIds, setAntennaIds ] = useState([]); const [ forwardFurniIds, setForwardFurniIds ] = useState([]); const [ selectionMode, setSelectionMode ] = useState('antenna'); + const highlightedIds = useRef([]); const { trigger = null, furniIds = [], setFurniIds = null, setIntParams = null, setStringParam = null, setAllowedInteractionTypes = null } = useWired(); @@ -84,28 +85,39 @@ export const WiredActionSendSignalView: FC<{}> = () => else setForwardFurniIds(furniIds); }, [ furniIds, selectionMode ]); - const applySelection = useCallback((nextIds: number[]) => + const syncHighlights = useCallback((nextAntennaIds: number[], nextForwardFurniIds: number[], nextSelectionMode: SelectionMode, nextFurniSource: number) => { - if(!setFurniIds) return; - - setFurniIds(prev => + if(highlightedIds.current.length) { - if(prev && prev.length) WiredSelectionVisualizer.clearSelectionShaderFromFurni(prev); - if(nextIds && nextIds.length) WiredSelectionVisualizer.applySelectionShaderToFurni(nextIds); + WiredSelectionVisualizer.clearSelectionShaderFromFurni(highlightedIds.current); + WiredSelectionVisualizer.clearSecondarySelectionShaderFromFurni(highlightedIds.current); + } - return [ ...nextIds ]; - }); - }, [ setFurniIds ]); + const visibleForwardIds = (nextFurniSource === SOURCE_SELECTED) ? nextForwardFurniIds : []; + const activeIds = (nextSelectionMode === 'antenna') ? nextAntennaIds : visibleForwardIds; + const passiveIds = (nextSelectionMode === 'antenna') ? visibleForwardIds : nextAntennaIds; + const activeSet = new Set(activeIds); + const passiveOnlyIds = passiveIds.filter(id => !activeSet.has(id)); + + if(activeIds.length) WiredSelectionVisualizer.applySelectionShaderToFurni(activeIds); + if(passiveOnlyIds.length) WiredSelectionVisualizer.applySecondarySelectionShaderToFurni(passiveOnlyIds); + + highlightedIds.current = Array.from(new Set([ ...activeIds, ...passiveOnlyIds ])); + }, []); + + useEffect(() => + { + syncHighlights(antennaIds, forwardFurniIds, selectionMode, furniSource); + }, [ antennaIds, forwardFurniIds, selectionMode, furniSource, syncHighlights ]); const switchSelection = useCallback((mode: SelectionMode) => { if(mode === selectionMode) return; if(mode === 'furni' && furniSource !== SOURCE_SELECTED) return; - const nextIds = (mode === 'antenna') ? antennaIds : forwardFurniIds; - applySelection(nextIds); setSelectionMode(mode); - }, [ selectionMode, furniSource, antennaIds, forwardFurniIds, applySelection ]); + if(setFurniIds) setFurniIds([ ...((mode === 'antenna') ? antennaIds : forwardFurniIds) ]); + }, [ selectionMode, furniSource, antennaIds, forwardFurniIds, setFurniIds ]); const onChangeFurniSource = (next: number) => { @@ -113,7 +125,7 @@ export const WiredActionSendSignalView: FC<{}> = () => if(selectionMode === 'furni') { - applySelection(antennaIds); + if(setFurniIds) setFurniIds([ ...antennaIds ]); setSelectionMode('antenna'); } @@ -122,12 +134,6 @@ export const WiredActionSendSignalView: FC<{}> = () => const save = useCallback(() => { - if(selectionMode === 'furni') - { - setSelectionMode('antenna'); - applySelection(antennaIds); - } - const antennaSource = (antennaIds && antennaIds.length) ? antennaIds[0] : 0; setIntParams([ @@ -140,7 +146,19 @@ export const WiredActionSendSignalView: FC<{}> = () => ]); setStringParam(serializeForwardIds(forwardFurniIds)); - }, [ selectionMode, antennaIds, furniSource, userSource, signalPerFurni, signalPerUser, forwardFurniIds, setIntParams, setStringParam, applySelection, setSelectionMode ]); + }, [ antennaIds, furniSource, userSource, signalPerFurni, signalPerUser, forwardFurniIds, setIntParams, setStringParam ]); + + useEffect(() => + { + return () => + { + if(!highlightedIds.current.length) return; + + WiredSelectionVisualizer.clearSelectionShaderFromFurni(highlightedIds.current); + WiredSelectionVisualizer.clearSecondarySelectionShaderFromFurni(highlightedIds.current); + highlightedIds.current = []; + }; + }, []); const selectionLimit = trigger?.maximumItemSelectionCount ?? 0; const forwardSelectionEnabled = (furniSource === SOURCE_SELECTED); diff --git a/src/components/wired/views/actions/WiredActionSetFurniStateToView.tsx b/src/components/wired/views/actions/WiredActionSetFurniStateToView.tsx index 8f31754..3f2c8cb 100644 --- a/src/components/wired/views/actions/WiredActionSetFurniStateToView.tsx +++ b/src/components/wired/views/actions/WiredActionSetFurniStateToView.tsx @@ -10,22 +10,26 @@ export const WiredActionSetFurniStateToView: FC<{}> = props => const [ stateFlag, setStateFlag ] = useState(0); const [ directionFlag, setDirectionFlag ] = useState(0); const [ positionFlag, setPositionFlag ] = useState(0); + const [ altitudeFlag, setAltitudeFlag ] = useState(0); const { trigger = null, setIntParams = null } = useWired(); const [ furniSource, setFurniSource ] = useState(() => { + if(trigger?.intData?.length > 4) return trigger.intData[4]; if(trigger?.intData?.length > 3) return trigger.intData[3]; return (trigger?.selectedItems?.length ?? 0) > 0 ? 100 : 0; }); - const save = () => setIntParams([ stateFlag, directionFlag, positionFlag, furniSource ]); + const save = () => setIntParams([ stateFlag, directionFlag, positionFlag, altitudeFlag, furniSource ]); useEffect(() => { setStateFlag(trigger.getBoolean(0) ? 1 : 0); setDirectionFlag(trigger.getBoolean(1) ? 1 : 0); setPositionFlag(trigger.getBoolean(2) ? 1 : 0); + setAltitudeFlag((trigger.intData.length > 4 && trigger.getBoolean(3)) ? 1 : 0); - if(trigger.intData.length > 3) setFurniSource(trigger.intData[3]); + if(trigger.intData.length > 4) setFurniSource(trigger.intData[4]); + else if(trigger.intData.length > 3) setFurniSource(trigger.intData[3]); else setFurniSource((trigger.selectedItems?.length ?? 0) > 0 ? 100 : 0); }, [ trigger ]); @@ -53,6 +57,10 @@ export const WiredActionSetFurniStateToView: FC<{}> = props => setPositionFlag(event.target.checked ? 1 : 0) } /> { LocalizeText('wiredfurni.params.condition.position') }
+
+ setAltitudeFlag(event.target.checked ? 1 : 0) } /> + { LocalizeText('wiredfurni.params.condition.altitude') } +
); diff --git a/src/components/wired/views/actions/WiredActionTeleportView.tsx b/src/components/wired/views/actions/WiredActionTeleportView.tsx index b526092..c26c7bb 100644 --- a/src/components/wired/views/actions/WiredActionTeleportView.tsx +++ b/src/components/wired/views/actions/WiredActionTeleportView.tsx @@ -1,21 +1,39 @@ import { FC, useEffect, useState } from 'react'; -import { WiredFurniType } from '../../../../api'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Text } from '../../../../common'; import { useWired } from '../../../../hooks'; import { WiredActionBaseView } from './WiredActionBaseView'; +import { WiredActionLayoutCode } from '../../../../api/wired/WiredActionLayoutCode'; import { WiredSourcesSelector } from '../WiredSourcesSelector'; export const WiredActionTeleportView: FC<{}> = props => { const { trigger = null, setIntParams = null } = useWired(); + const isTeleportEffect = (trigger?.code === WiredActionLayoutCode.TELEPORT); + const isUserToFurniEffect = (trigger?.code === WiredActionLayoutCode.USER_TO_FURNI); + const [ fastTeleport, setFastTeleport ] = useState(() => + { + if(isTeleportEffect && trigger?.intData?.length >= 3) return (trigger.intData[0] === 1); + return false; + }); + const [ walkMode, setWalkMode ] = useState(() => + { + if(isUserToFurniEffect && trigger?.intData?.length >= 3) return trigger.intData[2]; + return 1; + }); const [ furniSource, setFurniSource ] = useState(() => { + if(isTeleportEffect && trigger?.intData?.length >= 3) return trigger.intData[1]; + if(isUserToFurniEffect && trigger?.intData?.length >= 3) return trigger.intData[0]; if(trigger?.intData?.length >= 1) return trigger.intData[0]; return (trigger?.selectedItems?.length ?? 0) > 0 ? 100 : 0; }); const [ userSource, setUserSource ] = useState(() => { + if(isTeleportEffect && trigger?.intData?.length >= 3) return trigger.intData[2]; + if(isUserToFurniEffect && trigger?.intData?.length >= 3) return trigger.intData[1]; if(trigger?.intData?.length >= 2) return trigger.intData[1]; return 0; }); @@ -24,16 +42,37 @@ export const WiredActionTeleportView: FC<{}> = props => { if(!trigger) return; + if(isTeleportEffect && trigger.intData.length >= 3) + { + setFastTeleport(trigger.intData[0] === 1); + setFurniSource(trigger.intData[1]); + setUserSource(trigger.intData[2]); + setWalkMode(1); + return; + } + + if(isUserToFurniEffect && trigger.intData.length >= 3) + { + setFastTeleport(false); + setFurniSource(trigger.intData[0]); + setUserSource(trigger.intData[1]); + setWalkMode(trigger.intData[2]); + return; + } + + setFastTeleport(false); + setWalkMode(1); + if(trigger.intData.length >= 1) setFurniSource(trigger.intData[0]); else setFurniSource((trigger.selectedItems?.length ?? 0) > 0 ? 100 : 0); if(trigger.intData.length >= 2) setUserSource(trigger.intData[1]); else setUserSource(0); - }, [ trigger ]); + }, [ isTeleportEffect, isUserToFurniEffect, trigger ]); const onChangeFurniSource = (next: number) => setFurniSource(next); - const save = () => setIntParams([ furniSource, userSource ]); + const save = () => setIntParams(isTeleportEffect ? [ fastTeleport ? 1 : 0, furniSource, userSource ] : (isUserToFurniEffect ? [ furniSource, userSource, walkMode ] : [ furniSource, userSource ])); const requiresFurni = WiredFurniType.STUFF_SELECTION_OPTION_BY_ID_BY_TYPE_OR_FROM_CONTEXT; @@ -50,6 +89,21 @@ export const WiredActionTeleportView: FC<{}> = props => userSource={ userSource } onChangeFurni={ onChangeFurniSource } onChangeUsers={ setUserSource } /> - } /> + }> + { isTeleportEffect && +
+ setFastTeleport(event.target.checked) } /> + { LocalizeText('wiredfurni.params.teleport.options.0') } +
} + { isUserToFurniEffect && +
+ { [ 0, 1, 2 ].map(option => ( + + )) } +
} + ); }; diff --git a/src/components/wired/views/actions/WiredActionToggleFurniStateView.tsx b/src/components/wired/views/actions/WiredActionToggleFurniStateView.tsx index 2055acf..4450906 100644 --- a/src/components/wired/views/actions/WiredActionToggleFurniStateView.tsx +++ b/src/components/wired/views/actions/WiredActionToggleFurniStateView.tsx @@ -1,5 +1,6 @@ import { FC, useEffect, useState } from 'react'; -import { WiredFurniType } from '../../../../api'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Text } from '../../../../common'; import { WiredActionBaseView } from './WiredActionBaseView'; import { WiredSourcesSelector } from '../WiredSourcesSelector'; import { useWired } from '../../../../hooks'; @@ -7,8 +8,14 @@ import { useWired } from '../../../../hooks'; export const WiredActionToggleFurniStateView: FC<{}> = props => { const { trigger = null, setIntParams = null } = useWired(); + const [ toggleType, setToggleType ] = useState(() => + { + if(trigger?.intData?.length > 1) return trigger.intData[0]; + return 0; + }); const [ furniSource, setFurniSource ] = useState(() => { + if(trigger?.intData?.length > 1) return trigger.intData[1]; if(trigger?.intData?.length >= 1) return trigger.intData[0]; return (trigger?.selectedItems?.length ?? 0) > 0 ? 100 : 0; }); @@ -17,13 +24,26 @@ export const WiredActionToggleFurniStateView: FC<{}> = props => { if(!trigger) return; - if(trigger.intData.length >= 1) setFurniSource(trigger.intData[0]); - else setFurniSource((trigger.selectedItems?.length ?? 0) > 0 ? 100 : 0); + if(trigger.intData.length > 1) + { + setToggleType(trigger.intData[0]); + setFurniSource(trigger.intData[1]); + } + else if(trigger.intData.length >= 1) + { + setToggleType(0); + setFurniSource(trigger.intData[0]); + } + else + { + setToggleType(0); + setFurniSource((trigger.selectedItems?.length ?? 0) > 0 ? 100 : 0); + } }, [ trigger ]); const onChangeFurniSource = (next: number) => setFurniSource(next); - const save = () => setIntParams([ furniSource ]); + const save = () => setIntParams([ toggleType, furniSource ]); const requiresFurni = WiredFurniType.STUFF_SELECTION_OPTION_BY_ID_BY_TYPE_OR_FROM_CONTEXT; @@ -32,6 +52,16 @@ export const WiredActionToggleFurniStateView: FC<{}> = props => hasSpecialInput={ true } requiresFurni={ requiresFurni } save={ save } - footer={ } /> + footer={ }> +
+ { LocalizeText('wiredfurni.params.operator.2') } + { [ 0, 1 ].map(option => ( + + )) } +
+ ); }; diff --git a/src/components/wired/views/conditions/WiredConditionActorDirView.tsx b/src/components/wired/views/conditions/WiredConditionActorDirView.tsx new file mode 100644 index 0000000..127e754 --- /dev/null +++ b/src/components/wired/views/conditions/WiredConditionActorDirView.tsx @@ -0,0 +1,100 @@ +import { FC, useEffect, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredDirectionIcon, WIRED_DIRECTION_GRID } from '../WiredDirectionIcon'; +import { WiredSourcesSelector } from '../WiredSourcesSelector'; +import { WiredConditionBaseView } from './WiredConditionBaseView'; + +const toggleDirection = (mask: number, direction: number, checked: boolean) => +{ + if(checked) return (mask | (1 << direction)); + + return (mask & ~(1 << direction)); +}; + +export const WiredConditionActorDirView: FC<{}> = () => +{ + const { trigger = null, setIntParams = null } = useWired(); + const [ directionMask, setDirectionMask ] = useState(0); + const [ userSource, setUserSource ] = useState(0); + const [ quantifier, setQuantifier ] = useState(0); + const [ showAdvanced, setShowAdvanced ] = useState(false); + + useEffect(() => + { + if(!trigger) return; + + const nextDirectionMask = trigger.intData.length > 0 ? trigger.intData[0] : 0; + const nextUserSource = trigger.intData.length > 1 ? trigger.intData[1] : 0; + const nextQuantifier = trigger.intData.length > 2 ? trigger.intData[2] : 0; + + setDirectionMask(nextDirectionMask); + setUserSource(nextUserSource); + setQuantifier((nextQuantifier === 1) ? 1 : 0); + setShowAdvanced(nextUserSource !== 0 || nextQuantifier !== 0); + }, [ trigger ]); + + const save = () => + { + setIntParams([ + directionMask, + userSource, + quantifier + ]); + }; + + return ( + + + { showAdvanced && + <> +
+ { LocalizeText('wiredfurni.params.quantifier_selection') } + { [ 0, 1 ].map(value => + { + return ( + + ); + }) } +
+ + } +
+ }> +
+ { LocalizeText('wiredfurni.params.direction_selection') } +
+ { WIRED_DIRECTION_GRID.flatMap((row, rowIndex) => row.map((direction, columnIndex) => + { + if(direction === null) + { + return
; + } + + const checked = ((directionMask & (1 << direction)) !== 0); + + return ( + + ); + })) } +
+
+ + ); +}; diff --git a/src/components/wired/views/conditions/WiredConditionActorHasHandItem.tsx b/src/components/wired/views/conditions/WiredConditionActorHasHandItem.tsx index 41df779..f190085 100644 --- a/src/components/wired/views/conditions/WiredConditionActorHasHandItem.tsx +++ b/src/components/wired/views/conditions/WiredConditionActorHasHandItem.tsx @@ -2,14 +2,19 @@ import { FC, useEffect, useState } from 'react'; import { LocalizeText, WiredFurniType } from '../../../../api'; import { Text } from '../../../../common'; import { useWired } from '../../../../hooks'; +import { WiredHandItemField } from '../WiredHandItemField'; import { WiredConditionBaseView } from './WiredConditionBaseView'; import { WiredSourcesSelector } from '../WiredSourcesSelector'; -const ALLOWED_HAND_ITEM_IDS: number[] = [ 2, 5, 7, 8, 9, 10, 27 ]; +interface WiredConditionActorHasHandItemViewProps +{ + negative?: boolean; +} -export const WiredConditionActorHasHandItemView: FC<{}> = props => +export const WiredConditionActorHasHandItemView: FC = ({ negative = false }) => { const [ handItemId, setHandItemId ] = useState(-1); + const [ quantifier, setQuantifier ] = useState(0); const { trigger = null, setIntParams = null } = useWired(); const [ userSource, setUserSource ] = useState(() => { @@ -17,12 +22,13 @@ export const WiredConditionActorHasHandItemView: FC<{}> = props => return 0; }); - const save = () => setIntParams([ handItemId, userSource ]); + const save = () => setIntParams([ handItemId, userSource, quantifier ]); useEffect(() => { setHandItemId((trigger.intData.length > 0) ? trigger.intData[0] : 0); setUserSource((trigger.intData.length > 1) ? trigger.intData[1] : 0); + setQuantifier((trigger.intData.length > 2 && trigger.intData[2] === 1) ? 1 : 0); }, [ trigger ]); return ( @@ -32,14 +38,15 @@ export const WiredConditionActorHasHandItemView: FC<{}> = props => save={ save } footer={ }>
- { LocalizeText('wiredfurni.params.handitem') } - + { LocalizeText('wiredfurni.params.quantifier_selection') } + { [ 0, 1 ].map(value => ( + + )) }
+ ); }; diff --git a/src/components/wired/views/conditions/WiredConditionActorIsGroupMemberView.tsx b/src/components/wired/views/conditions/WiredConditionActorIsGroupMemberView.tsx index b75608d..c6f33af 100644 --- a/src/components/wired/views/conditions/WiredConditionActorIsGroupMemberView.tsx +++ b/src/components/wired/views/conditions/WiredConditionActorIsGroupMemberView.tsx @@ -1,32 +1,96 @@ -import { FC, useEffect, useState } from 'react'; -import { WiredFurniType } from '../../../../api'; -import { useWired } from '../../../../hooks'; -import { WiredConditionBaseView } from './WiredConditionBaseView'; +import { CatalogGroupsComposer, GuildMembershipsMessageEvent, HabboGroupEntryData } from '@nitrots/nitro-renderer'; +import { FC, useEffect, useMemo, useState } from 'react'; +import { LocalizeText, SendMessageComposer, WiredFurniType } from '../../../../api'; +import { Text } from '../../../../common'; +import { useMessageEvent, useWired } from '../../../../hooks'; import { WiredSourcesSelector } from '../WiredSourcesSelector'; +import { WiredConditionBaseView } from './WiredConditionBaseView'; -export const WiredConditionActorIsGroupMemberView: FC<{}> = props => +const GROUP_CURRENT_ROOM = 0; +const GROUP_SELECTED = 1; + +interface WiredConditionActorIsGroupMemberViewProps { + negative?: boolean; +} + +export const WiredConditionActorIsGroupMemberView: FC = ({ negative = false }) => +{ + const [ groups, setGroups ] = useState([]); + const [ userSource, setUserSource ] = useState(0); + const [ groupType, setGroupType ] = useState(GROUP_CURRENT_ROOM); + const [ selectedGroupId, setSelectedGroupId ] = useState(0); + const [ quantifier, setQuantifier ] = useState(0); const { trigger = null, setIntParams = null } = useWired(); - const [ userSource, setUserSource ] = useState(() => + + useMessageEvent(GuildMembershipsMessageEvent, event => { - if(trigger?.intData?.length >= 1) return trigger.intData[0]; - return 0; + const parser = event.getParser(); + + setGroups(parser.groups || []); }); + useEffect(() => + { + SendMessageComposer(new CatalogGroupsComposer()); + }, []); + useEffect(() => { if(!trigger) return; - if(trigger.intData.length >= 1) setUserSource(trigger.intData[0]); - else setUserSource(0); + + const params = trigger.intData; + + setUserSource(params.length > 0 ? params[0] : 0); + setGroupType(params.length > 1 ? params[1] : GROUP_CURRENT_ROOM); + setSelectedGroupId(params.length > 2 ? params[2] : 0); + setQuantifier((params.length > 3 && params[3] === 1) ? 1 : 0); }, [ trigger ]); - const save = () => setIntParams([ userSource ]); + useEffect(() => + { + if((groupType !== GROUP_SELECTED) || selectedGroupId || !groups.length) return; + + setSelectedGroupId(groups[0].groupId); + }, [ groupType, selectedGroupId, groups ]); + + const selectedGroupOptions = useMemo(() => groups.map(group => ({ value: group.groupId, label: group.groupName })), [ groups ]); + + const save = () => setIntParams([ userSource, groupType, selectedGroupId, quantifier ]); return ( } /> + footer={ }> +
+
+ { LocalizeText('wiredfurni.params.quantifier_selection') } + { [ 0, 1 ].map(value => ( + + )) } +
+ +
+ { LocalizeText('wiredfurni.params.groupselection') } + { [ GROUP_CURRENT_ROOM, GROUP_SELECTED ].map(value => ( + + )) } +
+ + { (groupType === GROUP_SELECTED) && + } +
+
); }; diff --git a/src/components/wired/views/conditions/WiredConditionActorIsOnFurniView.tsx b/src/components/wired/views/conditions/WiredConditionActorIsOnFurniView.tsx index 7796d23..90c245a 100644 --- a/src/components/wired/views/conditions/WiredConditionActorIsOnFurniView.tsx +++ b/src/components/wired/views/conditions/WiredConditionActorIsOnFurniView.tsx @@ -1,10 +1,16 @@ import { FC, useEffect, useState } from 'react'; -import { WiredFurniType } from '../../../../api'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Text } from '../../../../common'; import { useWired } from '../../../../hooks'; import { WiredConditionBaseView } from './WiredConditionBaseView'; import { WiredSourcesSelector } from '../WiredSourcesSelector'; -export const WiredConditionActorIsOnFurniView: FC<{}> = props => +interface WiredConditionActorIsOnFurniViewProps +{ + negative?: boolean; +} + +export const WiredConditionActorIsOnFurniView: FC = ({ negative = false }) => { const { trigger = null, setIntParams = null } = useWired(); const [ furniSource, setFurniSource ] = useState(() => @@ -17,6 +23,11 @@ export const WiredConditionActorIsOnFurniView: FC<{}> = props => if(trigger?.intData?.length > 1) return trigger.intData[1]; return 0; }); + const [ quantifier, setQuantifier ] = useState(() => + { + if(trigger?.intData?.length > 2) return trigger.intData[2]; + return 0; + }); useEffect(() => { @@ -27,11 +38,14 @@ export const WiredConditionActorIsOnFurniView: FC<{}> = props => if(trigger.intData.length > 1) setUserSource(trigger.intData[1]); else setUserSource(0); + + if(trigger.intData.length > 2) setQuantifier(trigger.intData[2] === 1 ? 1 : 0); + else setQuantifier(0); }, [ trigger ]); const onChangeFurniSource = (next: number) => setFurniSource(next); - const save = () => setIntParams([ furniSource, userSource ]); + const save = () => setIntParams([ furniSource, userSource, quantifier ]); const requiresFurni = WiredFurniType.STUFF_SELECTION_OPTION_BY_ID; @@ -41,13 +55,24 @@ export const WiredConditionActorIsOnFurniView: FC<{}> = props => requiresFurni={ requiresFurni } save={ save } footer={ ( - +
+
+ { LocalizeText('wiredfurni.params.quantifier_selection') } + { [ 0, 1 ].map(value => ( + + )) } +
+ +
) } /> ); }; diff --git a/src/components/wired/views/conditions/WiredConditionActorIsTeamMemberView.tsx b/src/components/wired/views/conditions/WiredConditionActorIsTeamMemberView.tsx index 43ec3c9..dfac1da 100644 --- a/src/components/wired/views/conditions/WiredConditionActorIsTeamMemberView.tsx +++ b/src/components/wired/views/conditions/WiredConditionActorIsTeamMemberView.tsx @@ -7,9 +7,15 @@ import { WiredSourcesSelector } from '../WiredSourcesSelector'; const teamIds: number[] = [ 1, 2, 3, 4 ]; -export const WiredConditionActorIsTeamMemberView: FC<{}> = props => +interface WiredConditionActorIsTeamMemberViewProps +{ + negative?: boolean; +} + +export const WiredConditionActorIsTeamMemberView: FC = ({ negative = false }) => { const [ selectedTeam, setSelectedTeam ] = useState(-1); + const [ quantifier, setQuantifier ] = useState(1); const { trigger = null, setIntParams = null } = useWired(); const [ userSource, setUserSource ] = useState(() => { @@ -17,12 +23,13 @@ export const WiredConditionActorIsTeamMemberView: FC<{}> = props => return 0; }); - const save = () => setIntParams([ selectedTeam, userSource ]); + const save = () => setIntParams([ selectedTeam, userSource, quantifier ]); useEffect(() => { setSelectedTeam((trigger.intData.length > 0) ? trigger.intData[0] : 0); setUserSource((trigger.intData.length > 1) ? trigger.intData[1] : 0); + setQuantifier((trigger.intData.length > 2) ? (trigger.intData[2] === 1 ? 1 : 0) : 1); }, [ trigger ]); return ( @@ -31,6 +38,15 @@ export const WiredConditionActorIsTeamMemberView: FC<{}> = props => requiresFurni={ WiredFurniType.STUFF_SELECTION_OPTION_NONE } save={ save } footer={ }> +
+ { LocalizeText('wiredfurni.params.quantifier_selection') } + { [ 0, 1 ].map(value => ( + + )) } +
{ LocalizeText('wiredfurni.params.team') } { teamIds.map(value => diff --git a/src/components/wired/views/conditions/WiredConditionActorIsWearingBadgeView.tsx b/src/components/wired/views/conditions/WiredConditionActorIsWearingBadgeView.tsx index d4f370e..34df63f 100644 --- a/src/components/wired/views/conditions/WiredConditionActorIsWearingBadgeView.tsx +++ b/src/components/wired/views/conditions/WiredConditionActorIsWearingBadgeView.tsx @@ -6,9 +6,15 @@ import { NitroInput } from '../../../../layout'; import { WiredConditionBaseView } from './WiredConditionBaseView'; import { WiredSourcesSelector } from '../WiredSourcesSelector'; -export const WiredConditionActorIsWearingBadgeView: FC<{}> = props => +interface WiredConditionActorIsWearingBadgeViewProps +{ + negative?: boolean; +} + +export const WiredConditionActorIsWearingBadgeView: FC = ({ negative = false }) => { const [ badge, setBadge ] = useState(''); + const [ quantifier, setQuantifier ] = useState(1); const { trigger = null, setStringParam = null, setIntParams = null } = useWired(); const [ userSource, setUserSource ] = useState(() => { @@ -19,7 +25,7 @@ export const WiredConditionActorIsWearingBadgeView: FC<{}> = props => const save = () => { setStringParam(badge); - setIntParams([ userSource ]); + setIntParams([ userSource, quantifier ]); }; useEffect(() => @@ -27,6 +33,7 @@ export const WiredConditionActorIsWearingBadgeView: FC<{}> = props => setBadge(trigger.stringData); if(trigger.intData.length >= 1) setUserSource(trigger.intData[0]); else setUserSource(0); + setQuantifier((trigger.intData.length >= 2) ? (trigger.intData[1] === 1 ? 1 : 0) : 1); }, [ trigger ]); return ( @@ -35,6 +42,15 @@ export const WiredConditionActorIsWearingBadgeView: FC<{}> = props => requiresFurni={ WiredFurniType.STUFF_SELECTION_OPTION_NONE } save={ save } footer={ }> +
+ { LocalizeText('wiredfurni.params.quantifier_selection') } + { [ 0, 1 ].map(value => ( + + )) } +
{ LocalizeText('wiredfurni.params.badgecode') } setBadge(event.target.value) } /> diff --git a/src/components/wired/views/conditions/WiredConditionActorIsWearingEffectView.tsx b/src/components/wired/views/conditions/WiredConditionActorIsWearingEffectView.tsx index d0e19e1..e18e82e 100644 --- a/src/components/wired/views/conditions/WiredConditionActorIsWearingEffectView.tsx +++ b/src/components/wired/views/conditions/WiredConditionActorIsWearingEffectView.tsx @@ -6,9 +6,15 @@ import { NitroInput } from '../../../../layout'; import { WiredConditionBaseView } from './WiredConditionBaseView'; import { WiredSourcesSelector } from '../WiredSourcesSelector'; -export const WiredConditionActorIsWearingEffectView: FC<{}> = props => +interface WiredConditionActorIsWearingEffectViewProps +{ + negative?: boolean; +} + +export const WiredConditionActorIsWearingEffectView: FC = ({ negative = false }) => { const [ effect, setEffect ] = useState(-1); + const [ quantifier, setQuantifier ] = useState(1); const { trigger = null, setIntParams = null } = useWired(); const [ userSource, setUserSource ] = useState(() => { @@ -16,13 +22,14 @@ export const WiredConditionActorIsWearingEffectView: FC<{}> = props => return 0; }); - const save = () => setIntParams([ effect, userSource ]); + const save = () => setIntParams([ effect, userSource, quantifier ]); useEffect(() => { setEffect(trigger?.intData[0] ?? 0); if(trigger?.intData?.length > 1) setUserSource(trigger.intData[1]); else setUserSource(0); + setQuantifier((trigger?.intData?.length > 2) ? (trigger.intData[2] === 1 ? 1 : 0) : 1); }, [ trigger ]); return ( @@ -31,6 +38,15 @@ export const WiredConditionActorIsWearingEffectView: FC<{}> = props => requiresFurni={ WiredFurniType.STUFF_SELECTION_OPTION_NONE } save={ save } footer={ }> +
+ { LocalizeText('wiredfurni.params.quantifier_selection') } + { [ 0, 1 ].map(value => ( + + )) } +
{ LocalizeText('wiredfurni.tooltip.effectid') } setEffect(parseInt(event.target.value)) } /> diff --git a/src/components/wired/views/conditions/WiredConditionCounterTimeMatchesView.tsx b/src/components/wired/views/conditions/WiredConditionCounterTimeMatchesView.tsx index fb68c4e..6631f28 100644 --- a/src/components/wired/views/conditions/WiredConditionCounterTimeMatchesView.tsx +++ b/src/components/wired/views/conditions/WiredConditionCounterTimeMatchesView.tsx @@ -46,6 +46,7 @@ export const WiredConditionCounterTimeMatchesView: FC<{}> = () => }); const [ minutes, setMinutes ] = useState(0); const [ halfSeconds, setHalfSeconds ] = useState(0); + const [ quantifier, setQuantifier ] = useState(0); const secondsLabel = useMemo(() => formatSeconds(halfSeconds), [ halfSeconds ]); @@ -55,7 +56,8 @@ export const WiredConditionCounterTimeMatchesView: FC<{}> = () => normalizeComparison(comparison), normalizeMinutes(minutes), normalizeHalfSeconds(halfSeconds), - furniSource + furniSource, + quantifier ]); }; @@ -67,6 +69,7 @@ export const WiredConditionCounterTimeMatchesView: FC<{}> = () => setMinutes((trigger.intData.length > 1) ? normalizeMinutes(trigger.intData[1]) : 0); setHalfSeconds((trigger.intData.length > 2) ? normalizeHalfSeconds(trigger.intData[2]) : 0); setFurniSource((trigger.intData.length > 3) ? trigger.intData[3] : ((trigger.selectedItems?.length ?? 0) > 0 ? 100 : 0)); + setQuantifier((trigger.intData.length > 4 && trigger.intData[4] === 1) ? 1 : 0); }, [ trigger ]); useEffect(() => @@ -86,7 +89,20 @@ export const WiredConditionCounterTimeMatchesView: FC<{}> = () => hasSpecialInput={ true } requiresFurni={ WiredFurniType.STUFF_SELECTION_OPTION_BY_ID_BY_TYPE_OR_FROM_CONTEXT } save={ save } - footer={ }> + footer={ +
+
+ { LocalizeText('wiredfurni.params.quantifier_selection') } + { [ 0, 1 ].map(value => ( + + )) } +
+ +
+ }>
{ COMPARISON_OPTIONS.map(option => { diff --git a/src/components/wired/views/conditions/WiredConditionFurniHasAvatarOnView.tsx b/src/components/wired/views/conditions/WiredConditionFurniHasAvatarOnView.tsx index aa4505f..76db5a9 100644 --- a/src/components/wired/views/conditions/WiredConditionFurniHasAvatarOnView.tsx +++ b/src/components/wired/views/conditions/WiredConditionFurniHasAvatarOnView.tsx @@ -1,28 +1,41 @@ import { FC, useEffect, useState } from 'react'; -import { WiredFurniType } from '../../../../api'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Text } from '../../../../common'; import { useWired } from '../../../../hooks'; import { WiredConditionBaseView } from './WiredConditionBaseView'; import { WiredSourcesSelector } from '../WiredSourcesSelector'; -export const WiredConditionFurniHasAvatarOnView: FC<{}> = props => +interface WiredConditionFurniHasAvatarOnViewProps +{ + negative?: boolean; +} + +export const WiredConditionFurniHasAvatarOnView: FC = ({ negative = false }) => { const { trigger = null, setIntParams = null } = useWired(); + const [ requireAll, setRequireAll ] = useState(0); const [ furniSource, setFurniSource ] = useState(() => { - if(trigger?.intData?.length >= 1) return trigger.intData[0]; + if(trigger?.intData?.length > 1) return trigger.intData[1]; + if(trigger?.intData?.length >= 1 && trigger.intData[0] > 1) return trigger.intData[0]; return (trigger?.selectedItems?.length ?? 0) > 0 ? 100 : 0; }); useEffect(() => { if(!trigger) return; - if(trigger.intData.length >= 1) setFurniSource(trigger.intData[0]); + + if(trigger.intData.length >= 1) setRequireAll(trigger.intData[0] === 1 ? 1 : 0); + else setRequireAll(0); + + if(trigger.intData.length > 1) setFurniSource(trigger.intData[1]); + else if(trigger.intData.length >= 1 && trigger.intData[0] > 1) setFurniSource(trigger.intData[0]); else setFurniSource((trigger.selectedItems?.length ?? 0) > 0 ? 100 : 0); }, [ trigger ]); const onChangeFurniSource = (next: number) => setFurniSource(next); - const save = () => setIntParams([ furniSource ]); + const save = () => setIntParams([ requireAll, furniSource ]); const requiresFurni = WiredFurniType.STUFF_SELECTION_OPTION_BY_ID; @@ -31,6 +44,16 @@ export const WiredConditionFurniHasAvatarOnView: FC<{}> = props => hasSpecialInput={ true } requiresFurni={ requiresFurni } save={ save } - footer={ } /> + footer={ }> +
+ { LocalizeText('wiredfurni.params.requireall') } + { [ 0, 1 ].map(value => ( + + )) } +
+ ); }; diff --git a/src/components/wired/views/conditions/WiredConditionFurniIsOfTypeView.tsx b/src/components/wired/views/conditions/WiredConditionFurniIsOfTypeView.tsx index ac62b15..1b5f413 100644 --- a/src/components/wired/views/conditions/WiredConditionFurniIsOfTypeView.tsx +++ b/src/components/wired/views/conditions/WiredConditionFurniIsOfTypeView.tsx @@ -1,36 +1,240 @@ -import { FC, useEffect, useState } from 'react'; -import { WiredFurniType } from '../../../../api'; +import { FC, useCallback, useEffect, useRef, useState } from 'react'; +import { LocalizeText, WiredFurniType, WiredSelectionVisualizer } from '../../../../api'; +import { Button, Text } from '../../../../common'; import { useWired } from '../../../../hooks'; +import { FURNI_SOURCES, WiredSourceOption, WiredSourcesSelector } from '../WiredSourcesSelector'; import { WiredConditionBaseView } from './WiredConditionBaseView'; -import { WiredSourcesSelector } from '../WiredSourcesSelector'; -export const WiredConditionFurniIsOfTypeView: FC<{}> = props => +const SOURCE_TRIGGER = 0; +const SOURCE_SELECTED = 100; +const SOURCE_SECONDARY_SELECTED = 101; +const FURNI_DELIMITER = ';'; + +const MATCH_FURNI_SOURCES: WiredSourceOption[] = [ + ...FURNI_SOURCES, + { value: SOURCE_SECONDARY_SELECTED, label: 'wiredfurni.params.sources.furni.101' } +]; + +type SelectionMode = 'primary' | 'secondary'; + +const parseIds = (data: string): number[] => { - const { trigger = null, setIntParams = null } = useWired(); - const [ furniSource, setFurniSource ] = useState(() => + if(!data || !data.length) return []; + + const ids = new Set(); + + for(const part of data.split(/[;,\t]/)) { - if(trigger?.intData?.length >= 1) return trigger.intData[0]; - return (trigger?.selectedItems?.length ?? 0) > 0 ? 100 : 0; - }); + const trimmed = part.trim(); + if(!trimmed.length) continue; + + const value = parseInt(trimmed, 10); + if(!isNaN(value) && value > 0) ids.add(value); + } + + return Array.from(ids); +}; + +const serializeIds = (ids: number[]): string => +{ + if(!ids || !ids.length) return ''; + + return ids.filter(id => (id > 0)).join(FURNI_DELIMITER); +}; + +interface WiredConditionFurniIsOfTypeViewProps +{ + negative?: boolean; +} + +export const WiredConditionFurniIsOfTypeView: FC = ({ negative = false }) => +{ + const [ matchSource, setMatchSource ] = useState(SOURCE_TRIGGER); + const [ compareSource, setCompareSource ] = useState(SOURCE_TRIGGER); + const [ quantifier, setQuantifier ] = useState(0); + const [ primaryFurniIds, setPrimaryFurniIds ] = useState([]); + const [ secondaryFurniIds, setSecondaryFurniIds ] = useState([]); + const [ selectionMode, setSelectionMode ] = useState('primary'); + + const highlightedIds = useRef([]); + + const { trigger = null, furniIds = [], setFurniIds, setIntParams, setStringParam, setAllowsFurni } = useWired(); + + const syncHighlights = useCallback((nextPrimaryIds: number[], nextSecondaryIds: number[]) => + { + if(highlightedIds.current.length) + { + WiredSelectionVisualizer.clearSelectionShaderFromFurni(highlightedIds.current); + WiredSelectionVisualizer.clearSecondarySelectionShaderFromFurni(highlightedIds.current); + } + + const secondarySet = new Set(nextSecondaryIds); + const primaryOnlyIds = nextPrimaryIds.filter(id => !secondarySet.has(id)); + + if(primaryOnlyIds.length) WiredSelectionVisualizer.applySelectionShaderToFurni(primaryOnlyIds); + if(nextSecondaryIds.length) WiredSelectionVisualizer.applySecondarySelectionShaderToFurni(nextSecondaryIds); + + highlightedIds.current = Array.from(new Set([ ...nextPrimaryIds, ...nextSecondaryIds ])); + }, []); + + const switchSelection = useCallback((mode: SelectionMode) => + { + const canEditPrimary = (matchSource === SOURCE_SELECTED) || (compareSource === SOURCE_SELECTED); + const canEditSecondary = (matchSource === SOURCE_SECONDARY_SELECTED) || (compareSource === SOURCE_SECONDARY_SELECTED); + + if(mode === 'primary' && !canEditPrimary) return; + if(mode === 'secondary' && !canEditSecondary) return; + + setSelectionMode(mode); + setFurniIds([ ...(mode === 'primary' ? primaryFurniIds : secondaryFurniIds) ]); + }, [ matchSource, compareSource, primaryFurniIds, secondaryFurniIds, setFurniIds ]); useEffect(() => { if(!trigger) return; - if(trigger.intData.length >= 1) setFurniSource(trigger.intData[0]); - else setFurniSource((trigger.selectedItems?.length ?? 0) > 0 ? 100 : 0); - }, [ trigger ]); - const onChangeFurniSource = (next: number) => setFurniSource(next); + const nextPrimaryIds = trigger.selectedItems ?? []; + const nextSecondaryIds = parseIds(trigger.stringData); + const nextMatchSource = (trigger.intData.length >= 1) + ? trigger.intData[0] + : (nextPrimaryIds.length ? SOURCE_SELECTED : SOURCE_TRIGGER); + const nextCompareSource = (trigger.intData.length >= 2) + ? trigger.intData[1] + : (nextSecondaryIds.length ? SOURCE_SECONDARY_SELECTED : SOURCE_TRIGGER); + const nextQuantifier = (trigger.intData.length >= 3) + ? (trigger.intData[2] === 1 ? 1 : 0) + : 0; - const save = () => setIntParams([ furniSource ]); + setMatchSource(nextMatchSource); + setCompareSource(nextCompareSource); + setQuantifier(nextQuantifier); + setPrimaryFurniIds(nextPrimaryIds); + setSecondaryFurniIds(nextSecondaryIds); + setSelectionMode('primary'); + setFurniIds([ ...nextPrimaryIds ]); + }, [ trigger, setFurniIds, negative ]); - const requiresFurni = WiredFurniType.STUFF_SELECTION_OPTION_BY_ID_OR_BY_TYPE; + useEffect(() => + { + if(selectionMode === 'primary') setPrimaryFurniIds(furniIds); + else setSecondaryFurniIds(furniIds); + }, [ furniIds, selectionMode ]); + + useEffect(() => + { + syncHighlights(primaryFurniIds, secondaryFurniIds); + }, [ primaryFurniIds, secondaryFurniIds, syncHighlights ]); + + useEffect(() => + { + const canEditPrimary = (matchSource === SOURCE_SELECTED) || (compareSource === SOURCE_SELECTED); + const canEditSecondary = (matchSource === SOURCE_SECONDARY_SELECTED) || (compareSource === SOURCE_SECONDARY_SELECTED); + + if(selectionMode === 'primary' && !canEditPrimary && canEditSecondary) + { + switchSelection('secondary'); + return; + } + + if(selectionMode === 'secondary' && !canEditSecondary && canEditPrimary) + { + switchSelection('primary'); + return; + } + + const canEditCurrent = ((selectionMode === 'primary') ? canEditPrimary : canEditSecondary); + + setAllowsFurni(canEditCurrent ? WiredFurniType.STUFF_SELECTION_OPTION_BY_ID_OR_BY_TYPE : WiredFurniType.STUFF_SELECTION_OPTION_NONE); + }, [ selectionMode, matchSource, compareSource, switchSelection, setAllowsFurni ]); + + useEffect(() => + { + return () => + { + if(!highlightedIds.current.length) return; + + WiredSelectionVisualizer.clearSelectionShaderFromFurni(highlightedIds.current); + WiredSelectionVisualizer.clearSecondarySelectionShaderFromFurni(highlightedIds.current); + highlightedIds.current = []; + }; + }, []); + + const save = useCallback(() => + { + if(selectionMode === 'secondary') + { + setSelectionMode('primary'); + setFurniIds([ ...primaryFurniIds ]); + } + + setIntParams([ + matchSource, + compareSource, + quantifier + ]); + setStringParam(serializeIds(secondaryFurniIds)); + }, [ selectionMode, primaryFurniIds, matchSource, compareSource, quantifier, secondaryFurniIds, setFurniIds, setIntParams, setStringParam ]); + + const selectionLimit = trigger?.maximumItemSelectionCount ?? 0; + const quantifierKeyPrefix = negative ? 'wiredfurni.params.quantifier.furni.neg' : 'wiredfurni.params.quantifier.furni'; return ( } /> + footer={ +
+ +
+ +
+ }> +
+ { LocalizeText('wiredfurni.params.quantifier_selection') } + { [ 0, 1 ].map(value => ( + + )) } +
+
+
+ { LocalizeText('wiredfurni.params.sources.furni.title.match.0') } +
+ + { selectionLimit ? `${ primaryFurniIds.length }/${ selectionLimit }` : primaryFurniIds.length } +
+
+
+ { LocalizeText('wiredfurni.params.sources.furni.title.match.1') } +
+ + { selectionLimit ? `${ secondaryFurniIds.length }/${ selectionLimit }` : secondaryFurniIds.length } +
+
+
+
); }; diff --git a/src/components/wired/views/conditions/WiredConditionFurniMatchesSnapshotView.tsx b/src/components/wired/views/conditions/WiredConditionFurniMatchesSnapshotView.tsx index 3bedd0c..4e7935d 100644 --- a/src/components/wired/views/conditions/WiredConditionFurniMatchesSnapshotView.tsx +++ b/src/components/wired/views/conditions/WiredConditionFurniMatchesSnapshotView.tsx @@ -5,27 +5,38 @@ import { useWired } from '../../../../hooks'; import { WiredConditionBaseView } from './WiredConditionBaseView'; import { WiredSourcesSelector } from '../WiredSourcesSelector'; -export const WiredConditionFurniMatchesSnapshotView: FC<{}> = props => +interface WiredConditionFurniMatchesSnapshotViewProps +{ + negative?: boolean; +} + +export const WiredConditionFurniMatchesSnapshotView: FC = ({ negative = false }) => { const [ stateFlag, setStateFlag ] = useState(0); const [ directionFlag, setDirectionFlag ] = useState(0); const [ positionFlag, setPositionFlag ] = useState(0); + const [ altitudeFlag, setAltitudeFlag ] = useState(0); + const [ quantifier, setQuantifier ] = useState(0); const { trigger = null, setIntParams = null } = useWired(); const [ furniSource, setFurniSource ] = useState(() => { + if(trigger?.intData?.length > 4) return trigger.intData[4]; if(trigger?.intData?.length > 3) return trigger.intData[3]; return (trigger?.selectedItems?.length ?? 0) > 0 ? 100 : 0; }); - const save = () => setIntParams([ stateFlag, directionFlag, positionFlag, furniSource ]); + const save = () => setIntParams([ stateFlag, directionFlag, positionFlag, altitudeFlag, furniSource, quantifier ]); useEffect(() => { setStateFlag(trigger.getBoolean(0) ? 1 : 0); setDirectionFlag(trigger.getBoolean(1) ? 1 : 0); setPositionFlag(trigger.getBoolean(2) ? 1 : 0); - if(trigger.intData.length > 3) setFurniSource(trigger.intData[3]); + setAltitudeFlag((trigger.intData.length > 4 && trigger.getBoolean(3)) ? 1 : 0); + if(trigger.intData.length > 4) setFurniSource(trigger.intData[4]); + else if(trigger.intData.length > 3) setFurniSource(trigger.intData[3]); else setFurniSource((trigger.selectedItems?.length ?? 0) > 0 ? 100 : 0); + setQuantifier((trigger.intData.length > 5 && trigger.intData[5] === 1) ? 1 : 0); }, [ trigger ]); const onChangeFurniSource = (next: number) => setFurniSource(next); @@ -37,7 +48,23 @@ export const WiredConditionFurniMatchesSnapshotView: FC<{}> = props => hasSpecialInput={ true } requiresFurni={ requiresFurni } save={ save } - footer={ }> + footer={ +
+
+ { LocalizeText('wiredfurni.params.quantifier_selection') } + { [ 0, 1 ].map(value => + { + return ( + + ); + }) } +
+ +
+ }>
{ LocalizeText('wiredfurni.params.conditions') }
@@ -52,6 +79,10 @@ export const WiredConditionFurniMatchesSnapshotView: FC<{}> = props => setPositionFlag(event.target.checked ? 1 : 0) } /> { LocalizeText('wiredfurni.params.condition.position') }
+
+ setAltitudeFlag(event.target.checked ? 1 : 0) } /> + { LocalizeText('wiredfurni.params.condition.altitude') } +
); diff --git a/src/components/wired/views/conditions/WiredConditionLayoutView.tsx b/src/components/wired/views/conditions/WiredConditionLayoutView.tsx index 888df16..0839d67 100644 --- a/src/components/wired/views/conditions/WiredConditionLayoutView.tsx +++ b/src/components/wired/views/conditions/WiredConditionLayoutView.tsx @@ -1,5 +1,6 @@ import { WiredConditionlayout } from '../../../../api'; import { WiredConditionActorHasHandItemView } from './WiredConditionActorHasHandItem'; +import { WiredConditionActorDirView } from './WiredConditionActorDirView'; import { WiredConditionActorIsGroupMemberView } from './WiredConditionActorIsGroupMemberView'; import { WiredConditionActorIsOnFurniView } from './WiredConditionActorIsOnFurniView'; import { WiredConditionActorIsTeamMemberView } from './WiredConditionActorIsTeamMemberView'; @@ -10,6 +11,7 @@ import { WiredConditionDateRangeView } from './WiredConditionDateRangeView'; import { WiredConditionMatchDateView } from './WiredConditionMatchDateView'; import { WiredConditionMatchTimeView } from './WiredConditionMatchTimeView'; import { WiredConditionHasAltitudeView } from './WiredConditionHasAltitudeView'; +import { WiredConditionMovementValidationView } from './WiredConditionMovementValidationView'; import { WiredConditionFurniHasAvatarOnView } from './WiredConditionFurniHasAvatarOnView'; import { WiredConditionFurniHasFurniOnView } from './WiredConditionFurniHasFurniOnView'; import { WiredConditionFurniHasNotFurniOnView } from './WiredConditionFurniHasNotFurniOnView'; @@ -22,51 +24,68 @@ import { WiredConditionTeamHasScoreView } from './WiredConditionTeamHasScoreView import { WiredConditionTriggererMatchView } from './WiredConditionTriggererMatchView'; import { WiredConditionUserPerformsActionView } from './WiredConditionUserPerformsActionView'; import { WiredConditionUserCountInRoomView } from './WiredConditionUserCountInRoomView'; +import { WiredConditionSelectionQuantityView } from './WiredConditionSelectionQuantityView'; export const WiredConditionLayoutView = (code: number) => { switch(code) { case WiredConditionlayout.ACTOR_HAS_HANDITEM: - case WiredConditionlayout.NOT_ACTOR_HAS_HANDITEM: return ; + case WiredConditionlayout.NOT_ACTOR_HAS_HANDITEM: + return ; + case WiredConditionlayout.ACTOR_DIR: + return ; + case WiredConditionlayout.SLC_QUANTITY: + return ; case WiredConditionlayout.TRIGGERER_MATCH: - case WiredConditionlayout.NOT_TRIGGERER_MATCH: return ; + case WiredConditionlayout.NOT_TRIGGERER_MATCH: + return ; case WiredConditionlayout.ACTOR_IS_GROUP_MEMBER: - case WiredConditionlayout.NOT_ACTOR_IN_GROUP: return ; + case WiredConditionlayout.NOT_ACTOR_IN_GROUP: + return ; case WiredConditionlayout.ACTOR_IS_ON_FURNI: - case WiredConditionlayout.NOT_ACTOR_ON_FURNI: return ; + case WiredConditionlayout.NOT_ACTOR_ON_FURNI: + return ; case WiredConditionlayout.ACTOR_IS_IN_TEAM: - case WiredConditionlayout.NOT_ACTOR_IN_TEAM: return ; + case WiredConditionlayout.NOT_ACTOR_IN_TEAM: + return ; case WiredConditionlayout.ACTOR_IS_WEARING_BADGE: - case WiredConditionlayout.NOT_ACTOR_WEARS_BADGE: return ; + case WiredConditionlayout.NOT_ACTOR_WEARS_BADGE: + return ; case WiredConditionlayout.ACTOR_IS_WEARING_EFFECT: - case WiredConditionlayout.NOT_ACTOR_WEARING_EFFECT: return ; + case WiredConditionlayout.NOT_ACTOR_WEARING_EFFECT: + return ; case WiredConditionlayout.DATE_RANGE_ACTIVE: return ; + case WiredConditionlayout.MOVEMENT_VALIDATION: + return ; case WiredConditionlayout.MATCH_TIME: return ; case WiredConditionlayout.MATCH_DATE: return ; case WiredConditionlayout.FURNIS_HAVE_AVATARS: - case WiredConditionlayout.FURNI_NOT_HAVE_HABBO: return ; + case WiredConditionlayout.FURNI_NOT_HAVE_HABBO: + return ; case WiredConditionlayout.HAS_STACKED_FURNIS: return ; case WiredConditionlayout.NOT_HAS_STACKED_FURNIS: return ; case WiredConditionlayout.STUFF_TYPE_MATCHES: - case WiredConditionlayout.NOT_FURNI_IS_OF_TYPE: return ; + case WiredConditionlayout.NOT_FURNI_IS_OF_TYPE: + return ; case WiredConditionlayout.STATES_MATCH: - case WiredConditionlayout.NOT_STATES_MATCH: return ; + case WiredConditionlayout.NOT_STATES_MATCH: + return ; case WiredConditionlayout.TIME_ELAPSED_LESS: return ; case WiredConditionlayout.TIME_ELAPSED_MORE: diff --git a/src/components/wired/views/conditions/WiredConditionMovementValidationView.tsx b/src/components/wired/views/conditions/WiredConditionMovementValidationView.tsx new file mode 100644 index 0000000..03ca6e8 --- /dev/null +++ b/src/components/wired/views/conditions/WiredConditionMovementValidationView.tsx @@ -0,0 +1,22 @@ +import { FC } from 'react'; +import { WiredFurniType } from '../../../../api'; +import { useWired } from '../../../../hooks'; +import { WiredConditionBaseView } from './WiredConditionBaseView'; + +export const WiredConditionMovementValidationView: FC<{}> = () => +{ + const { setIntParams = null, setStringParam = null } = useWired(); + + const save = () => + { + setIntParams([]); + setStringParam(''); + }; + + return ( + + ); +}; diff --git a/src/components/wired/views/conditions/WiredConditionSelectionQuantityView.tsx b/src/components/wired/views/conditions/WiredConditionSelectionQuantityView.tsx new file mode 100644 index 0000000..11d259c --- /dev/null +++ b/src/components/wired/views/conditions/WiredConditionSelectionQuantityView.tsx @@ -0,0 +1,178 @@ +import { FC, useEffect, useMemo, useState } from 'react'; +import { FaChevronLeft, FaChevronRight } from 'react-icons/fa'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Button, Slider, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredConditionBaseView } from './WiredConditionBaseView'; + +const SOURCE_GROUP_USERS = 0; +const SOURCE_GROUP_FURNI = 1; +const COMPARISON_OPTIONS = [ 0, 1, 2 ]; +const MIN_QUANTITY = 0; +const MAX_QUANTITY = 100; +const QUANTITY_PATTERN = /^\d*$/; + +const USER_SOURCES = [ + { value: 0, label: 'wiredfurni.params.sources.users.0' }, + { value: 200, label: 'wiredfurni.params.sources.users.200' }, + { value: 201, label: 'wiredfurni.params.sources.users.201' } +]; + +const FURNI_SOURCES = [ + { value: 0, label: 'wiredfurni.params.sources.furni.0' }, + { value: 200, label: 'wiredfurni.params.sources.furni.200' }, + { value: 201, label: 'wiredfurni.params.sources.furni.201' } +]; + +const clampQuantity = (value: number) => +{ + if(isNaN(value)) return MIN_QUANTITY; + + return Math.max(MIN_QUANTITY, Math.min(MAX_QUANTITY, Math.floor(value))); +}; + +const normalizeSource = (value: number, allowed: number[]) => +{ + return allowed.includes(value) ? value : 0; +}; + +export const WiredConditionSelectionQuantityView: FC<{}> = () => +{ + const { trigger = null, setIntParams = null, setStringParam = null } = useWired(); + const [ comparison, setComparison ] = useState(1); + const [ quantity, setQuantity ] = useState(0); + const [ quantityInput, setQuantityInput ] = useState('0'); + const [ sourceGroup, setSourceGroup ] = useState(SOURCE_GROUP_USERS); + const [ userSource, setUserSource ] = useState(0); + const [ furniSource, setFurniSource ] = useState(0); + + useEffect(() => + { + if(!trigger) return; + + const nextComparison = (trigger.intData.length > 0) ? trigger.intData[0] : 1; + const nextQuantity = clampQuantity((trigger.intData.length > 1) ? trigger.intData[1] : 0); + const nextSourceGroup = (trigger.intData.length > 2 && trigger.intData[2] === SOURCE_GROUP_FURNI) ? SOURCE_GROUP_FURNI : SOURCE_GROUP_USERS; + const nextSourceType = (trigger.intData.length > 3) ? trigger.intData[3] : 0; + + setComparison(COMPARISON_OPTIONS.includes(nextComparison) ? nextComparison : 1); + setQuantity(nextQuantity); + setQuantityInput(nextQuantity.toString()); + setSourceGroup(nextSourceGroup); + setUserSource(nextSourceGroup === SOURCE_GROUP_USERS ? normalizeSource(nextSourceType, USER_SOURCES.map(source => source.value)) : 0); + setFurniSource(nextSourceGroup === SOURCE_GROUP_FURNI ? normalizeSource(nextSourceType, FURNI_SOURCES.map(source => source.value)) : 0); + }, [ trigger ]); + + const activeSources = useMemo(() => (sourceGroup === SOURCE_GROUP_FURNI) ? FURNI_SOURCES : USER_SOURCES, [ sourceGroup ]); + const activeSource = (sourceGroup === SOURCE_GROUP_FURNI) ? furniSource : userSource; + const activeSourceIndex = Math.max(0, activeSources.findIndex(source => source.value === activeSource)); + + const updateQuantity = (value: number) => + { + const nextValue = clampQuantity(value); + + setQuantity(nextValue); + setQuantityInput(nextValue.toString()); + }; + + const updateQuantityInput = (value: string) => + { + if(!QUANTITY_PATTERN.test(value)) return; + + setQuantityInput(value); + + if(!value.length) + { + setQuantity(0); + return; + } + + updateQuantity(parseInt(value)); + }; + + const cycleSource = (direction: -1 | 1) => + { + const nextIndex = (activeSourceIndex + direction + activeSources.length) % activeSources.length; + const nextValue = activeSources[nextIndex].value; + + if(sourceGroup === SOURCE_GROUP_FURNI) setFurniSource(nextValue); + else setUserSource(nextValue); + }; + + const save = () => + { + setIntParams([ + comparison, + clampQuantity(quantity), + sourceGroup, + (sourceGroup === SOURCE_GROUP_FURNI) ? furniSource : userSource + ]); + setStringParam(''); + }; + + return ( + +
+ { LocalizeText('wiredfurni.params.comparison_selection') } + { COMPARISON_OPTIONS.map(value => + { + return ( + + ); + }) } +
+
+ setQuantityInput(clampQuantity(quantity).toString()) } + onChange={ event => updateQuantityInput(event.target.value) } /> + updateQuantity(event as number) } /> + { quantity } +
+
+ { LocalizeText('wiredfurni.params.sources.merged.title') } +
+ + +
+
+ +
+ { LocalizeText(activeSources[activeSourceIndex].label) } +
+ +
+
+
+ ); +}; diff --git a/src/components/wired/views/conditions/WiredConditionTriggererMatchView.tsx b/src/components/wired/views/conditions/WiredConditionTriggererMatchView.tsx index d66403b..d8bdfa4 100644 --- a/src/components/wired/views/conditions/WiredConditionTriggererMatchView.tsx +++ b/src/components/wired/views/conditions/WiredConditionTriggererMatchView.tsx @@ -24,7 +24,12 @@ const COMPARE_USER_SOURCES: WiredSourceOption[] = [ { value: SOURCE_SPECIFIED_USERNAME, label: 'wiredfurni.params.sources.users.101' } ]; -export const WiredConditionTriggererMatchView: FC<{}> = () => +interface WiredConditionTriggererMatchViewProps +{ + negative?: boolean; +} + +export const WiredConditionTriggererMatchView: FC = ({ negative = false }) => { const [ entityType, setEntityType ] = useState(ENTITY_HABBO); const [ avatarMode, setAvatarMode ] = useState(AVATAR_MODE_ANY); @@ -36,6 +41,7 @@ export const WiredConditionTriggererMatchView: FC<{}> = () => const { trigger = null, setIntParams = null, setStringParam = null } = useWired(); const needsUsername = (avatarMode === AVATAR_MODE_CERTAIN) || (compareUserSource === SOURCE_SPECIFIED_USERNAME); + const quantifierKeyPrefix = negative ? 'wiredfurni.params.quantifier.users.neg' : 'wiredfurni.params.quantifier.users'; const save = () => { @@ -81,7 +87,7 @@ export const WiredConditionTriggererMatchView: FC<{}> = () => return (
setQuantifier(value) } /> - { LocalizeText(`wiredfurni.params.quantifier.users.${ value }`) } + { LocalizeText(`${ quantifierKeyPrefix }.${ value }`) }
); }) } diff --git a/src/components/wired/views/extras/WiredExtraBaseView.tsx b/src/components/wired/views/extras/WiredExtraBaseView.tsx new file mode 100644 index 0000000..22b5394 --- /dev/null +++ b/src/components/wired/views/extras/WiredExtraBaseView.tsx @@ -0,0 +1,32 @@ +import { WiredActionDefinition } from '@nitrots/nitro-renderer'; +import { CSSProperties, FC, PropsWithChildren, ReactNode, useEffect } from 'react'; +import { WiredFurniType } from '../../../../api'; +import { useWired } from '../../../../hooks'; +import { WiredBaseView } from '../WiredBaseView'; + +export interface WiredExtraBaseViewProps +{ + hasSpecialInput: boolean; + requiresFurni: number; + save: () => void; + validate?: () => boolean; + cardStyle?: CSSProperties; + footer?: ReactNode; +} + +export const WiredExtraBaseView: FC> = props => +{ + const { requiresFurni = WiredFurniType.STUFF_SELECTION_OPTION_NONE, save = null, validate = null, hasSpecialInput = false, children = null, cardStyle = undefined, footer = null } = props; + const { trigger = null, setActionDelay = null } = useWired(); + + useEffect(() => + { + setActionDelay((trigger as WiredActionDefinition)?.delayInPulses ?? 0); + }, [ trigger, setActionDelay ]); + + return ( + + { children } + + ); +}; diff --git a/src/components/wired/views/extras/WiredExtraFilterFurniView.tsx b/src/components/wired/views/extras/WiredExtraFilterFurniView.tsx new file mode 100644 index 0000000..687f368 --- /dev/null +++ b/src/components/wired/views/extras/WiredExtraFilterFurniView.tsx @@ -0,0 +1,48 @@ +import { ChangeEvent, FC, useEffect, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredExtraBaseView } from './WiredExtraBaseView'; + +const MIN_FILTER = 0; +const MAX_FILTER = 10000; + +const clampValue = (value: number) => +{ + if(isNaN(value)) return MIN_FILTER; + + return Math.max(MIN_FILTER, Math.min(MAX_FILTER, Math.floor(value))); +}; + +export const WiredExtraFilterFurniView: FC<{}> = () => +{ + const { trigger = null, setIntParams = null, setStringParam = null } = useWired(); + const [ amount, setAmount ] = useState(0); + + useEffect(() => + { + if(!trigger) return; + + setAmount(clampValue((trigger.intData.length > 0) ? trigger.intData[0] : 0)); + }, [ trigger ]); + + const onChange = (event: ChangeEvent) => + { + setAmount(clampValue(Number(event.target.value))); + }; + + const save = () => + { + setIntParams([ clampValue(amount) ]); + setStringParam(''); + }; + + return ( + +
+ { LocalizeText('wiredfurni.params.setfilter') } + +
+
+ ); +}; diff --git a/src/components/wired/views/extras/WiredExtraFilterUserView.tsx b/src/components/wired/views/extras/WiredExtraFilterUserView.tsx new file mode 100644 index 0000000..345c708 --- /dev/null +++ b/src/components/wired/views/extras/WiredExtraFilterUserView.tsx @@ -0,0 +1,48 @@ +import { ChangeEvent, FC, useEffect, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredExtraBaseView } from './WiredExtraBaseView'; + +const MIN_FILTER = 0; +const MAX_FILTER = 10000; + +const clampValue = (value: number) => +{ + if(isNaN(value)) return MIN_FILTER; + + return Math.max(MIN_FILTER, Math.min(MAX_FILTER, Math.floor(value))); +}; + +export const WiredExtraFilterUserView: FC<{}> = () => +{ + const { trigger = null, setIntParams = null, setStringParam = null } = useWired(); + const [ amount, setAmount ] = useState(0); + + useEffect(() => + { + if(!trigger) return; + + setAmount(clampValue((trigger.intData.length > 0) ? trigger.intData[0] : 0)); + }, [ trigger ]); + + const onChange = (event: ChangeEvent) => + { + setAmount(clampValue(Number(event.target.value))); + }; + + const save = () => + { + setIntParams([ clampValue(amount) ]); + setStringParam(''); + }; + + return ( + +
+ { LocalizeText('wiredfurni.params.setfilter') } + +
+
+ ); +}; diff --git a/src/components/wired/views/selectors/WiredActionFurniAreaView.tsx b/src/components/wired/views/selectors/WiredActionFurniAreaView.tsx index 3b8d759..9eb8777 100644 --- a/src/components/wired/views/selectors/WiredActionFurniAreaView.tsx +++ b/src/components/wired/views/selectors/WiredActionFurniAreaView.tsx @@ -3,7 +3,7 @@ import { FC, useCallback, useEffect, useState } from 'react'; import { LocalizeText } from '../../../../api'; import { Button, Text } from '../../../../common'; import { useWired } from '../../../../hooks'; -import { WiredActionBaseView } from '../actions/WiredActionBaseView'; +import { WiredSelectorBaseView } from './WiredSelectorBaseView'; export const WiredActionFurniAreaView: FC<{}> = props => { @@ -101,7 +101,7 @@ export const WiredActionFurniAreaView: FC<{}> = props => : 0; return ( - +
{ LocalizeText('wiredfurni.params.area_selection') } { LocalizeText('wiredfurni.params.area_selection.info') } @@ -154,6 +154,6 @@ export const WiredActionFurniAreaView: FC<{}> = props => { LocalizeText('wiredfurni.params.selector_option.1') }
-
+ ); }; diff --git a/src/components/wired/views/selectors/WiredSelectorBaseView.tsx b/src/components/wired/views/selectors/WiredSelectorBaseView.tsx new file mode 100644 index 0000000..ab12ba4 --- /dev/null +++ b/src/components/wired/views/selectors/WiredSelectorBaseView.tsx @@ -0,0 +1,25 @@ +import { CSSProperties, FC, PropsWithChildren, ReactNode } from 'react'; +import { WiredFurniType } from '../../../../api'; +import { WiredBaseView } from '../WiredBaseView'; + +export interface WiredSelectorBaseViewProps +{ + hasSpecialInput: boolean; + requiresFurni: number; + save: () => void; + validate?: () => boolean; + cardStyle?: CSSProperties; + hideDelay?: boolean; + footer?: ReactNode; +} + +export const WiredSelectorBaseView: FC> = props => +{ + const { requiresFurni = WiredFurniType.STUFF_SELECTION_OPTION_NONE, save = null, validate = null, hasSpecialInput = false, children = null, cardStyle = undefined, footer = null } = props; + + return ( + + { children } + + ); +}; diff --git a/src/components/wired/views/selectors/WiredSelectorFurniAltitudeView.tsx b/src/components/wired/views/selectors/WiredSelectorFurniAltitudeView.tsx new file mode 100644 index 0000000..d9bc0cb --- /dev/null +++ b/src/components/wired/views/selectors/WiredSelectorFurniAltitudeView.tsx @@ -0,0 +1,165 @@ +import { FC, useCallback, useEffect, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Slider, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredSelectorBaseView } from './WiredSelectorBaseView'; + +const MIN_ALTITUDE = 0; +const MAX_ALTITUDE = 40; +const ALTITUDE_STEP = 0.5; +const ALTITUDE_PATTERN = /^\d*(\.\d{0,2})?$/; + +const clampAltitude = (value: number) => +{ + if(isNaN(value)) return MIN_ALTITUDE; + + const clamped = Math.min(MAX_ALTITUDE, Math.max(MIN_ALTITUDE, value)); + + return parseFloat(clamped.toFixed(2)); +}; + +const formatAltitude = (value: number) => +{ + const normalized = clampAltitude(value); + const text = normalized.toFixed(2); + + return text.replace(/\.00$/, '').replace(/(\.\d)0$/, '$1'); +}; + +const parseAltitude = (value: string) => +{ + if(!value || !value.trim().length) return 0; + + const parsed = parseFloat(value); + + if(isNaN(parsed)) return 0; + + return clampAltitude(parsed); +}; + +export const WiredSelectorFurniAltitudeView: FC<{}> = () => +{ + const { trigger = null, setIntParams = null, setStringParam = null } = useWired(); + const [ comparison, setComparison ] = useState(1); + const [ filterExisting, setFilterExisting ] = useState(false); + const [ invert, setInvert ] = useState(false); + const [ altitude, setAltitude ] = useState(0); + const [ altitudeInput, setAltitudeInput ] = useState('0'); + + useEffect(() => + { + if(!trigger) return; + + const nextAltitude = parseAltitude(trigger.stringData); + + setComparison((trigger.intData.length > 0) ? trigger.intData[0] : 1); + setFilterExisting((trigger.intData.length > 1) ? (trigger.intData[1] === 1) : false); + setInvert((trigger.intData.length > 2) ? (trigger.intData[2] === 1) : false); + setAltitude(nextAltitude); + setAltitudeInput(formatAltitude(nextAltitude)); + }, [ trigger ]); + + const updateAltitude = (value: number) => + { + const nextValue = clampAltitude(value); + + setAltitude(nextValue); + setAltitudeInput(formatAltitude(nextValue)); + }; + + const updateAltitudeInput = (value: string) => + { + if(!ALTITUDE_PATTERN.test(value)) return; + + setAltitudeInput(value); + + if(!value.length) + { + setAltitude(0); + return; + } + + const parsedValue = parseFloat(value); + + if(isNaN(parsedValue)) return; + + if(parsedValue > MAX_ALTITUDE) + { + updateAltitude(MAX_ALTITUDE); + return; + } + + setAltitude(clampAltitude(parsedValue)); + }; + + const save = useCallback(() => + { + setIntParams([ + comparison, + filterExisting ? 1 : 0, + invert ? 1 : 0 + ]); + setStringParam(formatAltitude(altitude)); + }, [ altitude, comparison, filterExisting, invert, setIntParams, setStringParam ]); + + return ( + +
+
+ { [ 0, 1, 2 ].map(value => + { + return ( + + ); + }) } +
+ +
+ { LocalizeText('wiredfurni.params.setaltitude') } + setAltitudeInput(formatAltitude(altitude)) } + onChange={ event => updateAltitudeInput(event.target.value) } /> +
+ +
+ updateAltitude(event as number) } /> + { formatAltitude(altitude) } +
+ +
+ + { LocalizeText('wiredfurni.params.selector_options_selector') } + + + + +
+
+ ); +}; diff --git a/src/components/wired/views/selectors/WiredSelectorFurniByTypeView.tsx b/src/components/wired/views/selectors/WiredSelectorFurniByTypeView.tsx index cc0f338..d39b4df 100644 --- a/src/components/wired/views/selectors/WiredSelectorFurniByTypeView.tsx +++ b/src/components/wired/views/selectors/WiredSelectorFurniByTypeView.tsx @@ -2,7 +2,7 @@ import { FC, useCallback, useEffect, useState } from 'react'; import { LocalizeText, WiredFurniType } from '../../../../api'; import { Text } from '../../../../common'; import { useWired } from '../../../../hooks'; -import { WiredActionBaseView } from '../actions/WiredActionBaseView'; +import { WiredSelectorBaseView } from './WiredSelectorBaseView'; const SOURCE_FURNI_PICKED = 0; @@ -42,7 +42,7 @@ export const WiredSelectorFurniByTypeView: FC<{}> = () => const requiresFurni = WiredFurniType.STUFF_SELECTION_OPTION_BY_ID; return ( - +
-
+ ); }; diff --git a/src/components/wired/views/selectors/WiredSelectorFurniNeighborhoodView.tsx b/src/components/wired/views/selectors/WiredSelectorFurniNeighborhoodView.tsx index 27bf7d6..a4617e6 100644 --- a/src/components/wired/views/selectors/WiredSelectorFurniNeighborhoodView.tsx +++ b/src/components/wired/views/selectors/WiredSelectorFurniNeighborhoodView.tsx @@ -1,11 +1,11 @@ -import { GetRoomEngine, RoomObjectCategory } from '@nitrots/nitro-renderer'; -import { CSSProperties, FC, useCallback, useEffect, useState } from 'react'; +import { GetRoomEngine } from '@nitrots/nitro-renderer'; +import { CSSProperties, FC, MouseEvent as ReactMouseEvent, useCallback, useEffect, useState } from 'react'; import { FaMinus, FaPlus, FaTimes } from 'react-icons/fa'; import { MdGridOn } from 'react-icons/md'; -import { GetRoomSession, LocalizeText, WiredFurniType, WiredSelectionVisualizer } from '../../../../api'; +import { LocalizeText, WiredFurniType } from '../../../../api'; import { Button, Text } from '../../../../common'; import { useWired } from '../../../../hooks'; -import { WiredActionBaseView } from '../actions/WiredActionBaseView'; +import { WiredSelectorBaseView } from './WiredSelectorBaseView'; const SOURCE_USER_TRIGGER = 0; const SOURCE_USER_SIGNAL = 1; @@ -47,33 +47,74 @@ const tileTop = (rx: number, ry: number) => interface GridProps { selectedTiles: Tile[]; + targetTile: Tile; invert: boolean; - onToggle: (x: number, y: number) => void; + onSetTile: (x: number, y: number, selected: boolean) => void; + onMoveTarget: (x: number, y: number) => void; + targetPlacementMode: boolean; } -const NeighborhoodGrid: FC = ({ selectedTiles, invert, onToggle }) => +const NeighborhoodGrid: FC = ({ selectedTiles, targetTile, invert, onSetTile, onMoveTarget, targetPlacementMode }) => { + const [ dragMode, setDragMode ] = useState<'add' | 'remove' | 'target' | null>(null); const tiles: JSX.Element[] = []; + useEffect(() => + { + const stopDragging = () => setDragMode(null); + + window.addEventListener('mouseup', stopDragging); + + return () => window.removeEventListener('mouseup', stopDragging); + }, []); + + const beginTileDrag = (event: ReactMouseEvent, rx: number, ry: number, isTarget: boolean, isSelected: boolean) => + { + event.preventDefault(); + + if(targetPlacementMode) + { + setDragMode('target'); + onMoveTarget(rx, ry); + return; + } + + const nextMode = isSelected ? 'remove' : 'add'; + + setDragMode(nextMode); + onSetTile(rx, ry, nextMode === 'add'); + }; + + const continueTileDrag = (event: ReactMouseEvent, rx: number, ry: number, isTarget: boolean) => + { + if(!(event.buttons & 1) || !dragMode) return; + + if(dragMode === 'target') + { + onMoveTarget(rx, ry); + return; + } + + onSetTile(rx, ry, dragMode === 'add'); + }; + for (let ry = -GRID_RANGE; ry <= GRID_RANGE; ry++) { for (let rx = -GRID_RANGE; rx <= GRID_RANGE; rx++) { - const isCenter = rx === 0 && ry === 0; + const isTarget = rx === targetTile.x && ry === targetTile.y; const isSelected = tileIncluded(selectedTiles, rx, ry); const isActive = invert ? !isSelected : isSelected; const left = tileLeft(rx, ry); const top_ = tileTop(rx, ry); const zIdx = rx + ry + GRID_RANGE * 2 + 10; - const bgColor = isCenter - ? '#ff9500' - : isActive - ? '#3399ff' - : '#2a3042'; + const bgColor = isActive + ? '#3399ff' + : '#2a3042'; - const borderColor = isCenter - ? '#cc6600' + const borderColor = isTarget + ? '#ffffff' : isActive ? '#1166cc' : '#1a2032'; @@ -86,36 +127,61 @@ const NeighborhoodGrid: FC = ({ selectedTiles, invert, onToggle }) => top: top_, clipPath: 'polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)', backgroundColor: bgColor, - cursor: isCenter ? 'default' : 'pointer', + cursor: 'pointer', zIndex: zIdx, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + color: isTarget ? '#ffffff' : 'transparent', + fontSize: 10 }; const border: CSSProperties = { position: 'absolute', - width: TILE_W + 2, - height: TILE_H + 2, - left: left - 1, - top: top_ - 1, + width: TILE_W + (isTarget ? 6 : 2), + height: TILE_H + (isTarget ? 6 : 2), + left: left - (isTarget ? 3 : 1), + top: top_ - (isTarget ? 3 : 1), clipPath: 'polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)', backgroundColor: borderColor, zIndex: zIdx - 1, pointerEvents: 'none', }; + const targetOutline: CSSProperties = { + position: 'absolute', + width: TILE_W + 4, + height: TILE_H + 4, + left: left - 2, + top: top_ - 2, + zIndex: zIdx + 1, + pointerEvents: 'none', + overflow: 'visible' + }; + tiles.push(
, + isTarget && ( + + + + ),
!isCenter && onToggle(rx, ry) } - />, + onMouseDown={ event => beginTileDrag(event, rx, ry, isTarget, isSelected) } + onMouseEnter={ event => continueTileDrag(event, rx, ry, isTarget) } />, ); } } return ( -
+
event.preventDefault() }> { tiles }
); @@ -127,10 +193,18 @@ export const WiredSelectorFurniNeighborhoodView: FC<{}> = () => const [ filterExisting, setFilterExisting ] = useState(false); const [ invert, setInvert ] = useState(false); const [ sourceType, setSourceType ] = useState(SOURCE_USER_TRIGGER); + const [ targetTile, setTargetTile ] = useState({ x: 0, y: 0 }); + const [ targetPlacementMode, setTargetPlacementMode ] = useState(false); const [ curX, setCurX ] = useState(0); const [ curY, setCurY ] = useState(0); - const { trigger = null, furniIds = [], setIntParams, setFurniIds } = useWired(); + const { trigger = null, furniIds = [], setIntParams } = useWired(); + + useEffect(() => + { + GetRoomEngine().areaSelectionManager.clearHighlight(); + GetRoomEngine().areaSelectionManager.deactivate(); + }, []); useEffect(() => { @@ -140,15 +214,17 @@ export const WiredSelectorFurniNeighborhoodView: FC<{}> = () => if(p.length >= 1) setSourceType(p[0]); if(p.length >= 2) setFilterExisting(p[1] === 1); if(p.length >= 3) setInvert(p[2] === 1); + if(p.length >= 5) setTargetTile({ x: p[3], y: p[4] }); + else setTargetTile({ x: 0, y: 0 }); - if(p.length >= 4) + if(p.length >= 6) { - const n = p[3]; + const n = p[5]; const tiles: Tile[] = []; for(let i = 0; i < n; i++) { - const xi = 4 + i * 2; + const xi = 6 + i * 2; if(xi + 1 < p.length) tiles.push({ x: p[xi], y: p[xi + 1] }); } @@ -160,71 +236,47 @@ export const WiredSelectorFurniNeighborhoodView: FC<{}> = () => } }, [ trigger ]); - useEffect(() => - { - if(sourceType !== SOURCE_FURNI_PICKED || !trigger) return; - - const roomId = GetRoomSession().roomId; - const wiredObj = GetRoomEngine().getRoomObject(roomId, trigger.id, RoomObjectCategory.FLOOR); - - if(!wiredObj) return; - - const wiredPos = wiredObj.getLocation(); - const tileSet = new Set(selectedTiles.map(t => `${ t.x },${ t.y }`)); - const limit = trigger.maximumItemSelectionCount; - - const allFloorObjects = GetRoomEngine().getRoomObjects(roomId, RoomObjectCategory.FLOOR); - const newIds: number[] = []; - - for(const obj of allFloorObjects) - { - if(newIds.length >= limit) break; - if(obj.id < 0 || obj.id === trigger.id) continue; - - const pos = obj.getLocation(); - const relX = Math.round(pos.x - wiredPos.x); - const relY = Math.round(pos.y - wiredPos.y); - - const isInTiles = tileSet.has(`${ relX },${ relY }`); - - if(invert ? !isInTiles : isInTiles) newIds.push(obj.id); - } - - setFurniIds(prevValue => - { - if(prevValue && prevValue.length) WiredSelectionVisualizer.clearSelectionShaderFromFurni(prevValue); - - WiredSelectionVisualizer.applySelectionShaderToFurni(newIds); - - return newIds; - }); - }, [ sourceType, selectedTiles, invert, trigger, setFurniIds ]); - const save = useCallback(() => { const params: number[] = [ sourceType, filterExisting ? 1 : 0, invert ? 1 : 0, + targetTile.x, + targetTile.y, selectedTiles.length, ...selectedTiles.flatMap(t => [ t.x, t.y ]), ]; setIntParams(params); - }, [ sourceType, filterExisting, invert, selectedTiles, setIntParams ]); + }, [ sourceType, filterExisting, invert, selectedTiles, targetTile.x, targetTile.y, setIntParams ]); - const toggleTile = useCallback((x: number, y: number) => + const setTileSelection = useCallback((x: number, y: number, selected: boolean) => { setSelectedTiles(prev => - tileIncluded(prev, x, y) - ? prev.filter(t => !(t.x === x && t.y === y)) - : [ ...prev, { x, y } ] - ); + { + const alreadySelected = tileIncluded(prev, x, y); + + if(selected) + { + if(alreadySelected) return prev; + + return [ ...prev, { x, y } ]; + } + + if(!alreadySelected) return prev; + + return prev.filter(t => !(t.x === x && t.y === y)); + }); + }, []); + + const moveTargetTile = useCallback((x: number, y: number) => + { + setTargetTile({ x, y }); }, []); const addTile = useCallback(() => { - if(curX === 0 && curY === 0) return; if(!tileIncluded(selectedTiles, curX, curY)) setSelectedTiles(prev => [ ...prev, { x: curX, y: curY } ]); }, [ curX, curY, selectedTiles ]); @@ -278,12 +330,23 @@ export const WiredSelectorFurniNeighborhoodView: FC<{}> = () => const pickedLimit = trigger?.maximumItemSelectionCount ?? 20; return ( - +
{ LocalizeText('wiredfurni.params.neighborhood_selection') }
+ @@ -299,7 +362,13 @@ export const WiredSelectorFurniNeighborhoodView: FC<{}> = () =>
- +
@@ -378,6 +447,6 @@ export const WiredSelectorFurniNeighborhoodView: FC<{}> = () => }
- + ); }; diff --git a/src/components/wired/views/selectors/WiredSelectorFurniOnFurniView.tsx b/src/components/wired/views/selectors/WiredSelectorFurniOnFurniView.tsx new file mode 100644 index 0000000..585b70d --- /dev/null +++ b/src/components/wired/views/selectors/WiredSelectorFurniOnFurniView.tsx @@ -0,0 +1,82 @@ +import { FC, useEffect, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredSourcesSelector } from '../WiredSourcesSelector'; +import { WiredSelectorBaseView } from './WiredSelectorBaseView'; + +export const WiredSelectorFurniOnFurniView: FC<{}> = () => +{ + const { trigger = null, setIntParams = null } = useWired(); + const [ selectionType, setSelectionType ] = useState(0); + const [ filterExisting, setFilterExisting ] = useState(false); + const [ invert, setInvert ] = useState(false); + const [ furniSource, setFurniSource ] = useState(() => + { + if(trigger?.intData?.length > 1) return trigger.intData[1]; + return (trigger?.selectedItems?.length ?? 0) > 0 ? 100 : 0; + }); + + useEffect(() => + { + if(!trigger) return; + + setSelectionType((trigger.intData.length > 0) ? trigger.intData[0] : 0); + setFurniSource((trigger.intData.length > 1) ? trigger.intData[1] : ((trigger.selectedItems?.length ?? 0) > 0 ? 100 : 0)); + setFilterExisting((trigger.intData.length > 2) ? (trigger.intData[2] === 1) : false); + setInvert((trigger.intData.length > 3) ? (trigger.intData[3] === 1) : false); + }, [ trigger ]); + + const save = () => setIntParams([ + selectionType, + furniSource, + filterExisting ? 1 : 0, + invert ? 1 : 0 + ]); + + return ( + }> +
+
+ { LocalizeText('wiredfurni.params.selection_type') } + { [ 0, 1, 2, 3 ].map(value => + { + return ( + + ); + }) } +
+ +
+ + { LocalizeText('wiredfurni.params.selector_options_selector') } + + + + +
+
+ ); +}; diff --git a/src/components/wired/views/selectors/WiredSelectorFurniPicksView.tsx b/src/components/wired/views/selectors/WiredSelectorFurniPicksView.tsx new file mode 100644 index 0000000..3487655 --- /dev/null +++ b/src/components/wired/views/selectors/WiredSelectorFurniPicksView.tsx @@ -0,0 +1,55 @@ +import { FC, useCallback, useEffect, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredSelectorBaseView } from './WiredSelectorBaseView'; + +export const WiredSelectorFurniPicksView: FC<{}> = () => +{ + const [ filterExisting, setFilterExisting ] = useState(false); + const [ invert, setInvert ] = useState(false); + const { trigger = null, setIntParams = null } = useWired(); + + useEffect(() => + { + if(!trigger) return; + + const params = trigger.intData; + setFilterExisting(params.length > 0 ? (params[0] === 1) : false); + setInvert(params.length > 1 ? (params[1] === 1) : false); + }, [ trigger ]); + + const save = useCallback(() => + { + setIntParams([ + filterExisting ? 1 : 0, + invert ? 1 : 0 + ]); + }, [ filterExisting, invert, setIntParams ]); + + return ( + +
+ { LocalizeText('wiredfurni.params.selector_options_selector') } + + + + +
+
+ ); +}; diff --git a/src/components/wired/views/selectors/WiredSelectorFurniSignalView.tsx b/src/components/wired/views/selectors/WiredSelectorFurniSignalView.tsx new file mode 100644 index 0000000..b822ece --- /dev/null +++ b/src/components/wired/views/selectors/WiredSelectorFurniSignalView.tsx @@ -0,0 +1,55 @@ +import { FC, useCallback, useEffect, useState } from 'react'; +import { LocalizeText } from '../../../../api'; +import { Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredSelectorBaseView } from './WiredSelectorBaseView'; + +export const WiredSelectorFurniSignalView: FC<{}> = () => +{ + const [ filterExisting, setFilterExisting ] = useState(false); + const [ invert, setInvert ] = useState(false); + const { trigger = null, setIntParams = null } = useWired(); + + useEffect(() => + { + if(!trigger) return; + + const params = trigger.intData; + setFilterExisting(params.length > 0 ? (params[0] === 1) : false); + setInvert(params.length > 1 ? (params[1] === 1) : false); + }, [ trigger ]); + + const save = useCallback(() => + { + setIntParams([ + filterExisting ? 1 : 0, + invert ? 1 : 0 + ]); + }, [ filterExisting, invert, setIntParams ]); + + return ( + +
+ { LocalizeText('wiredfurni.params.selector_options_selector') } + + + + +
+
+ ); +}; diff --git a/src/components/wired/views/selectors/WiredSelectorUsersAreaView.tsx b/src/components/wired/views/selectors/WiredSelectorUsersAreaView.tsx index d07af98..2ec0d9b 100644 --- a/src/components/wired/views/selectors/WiredSelectorUsersAreaView.tsx +++ b/src/components/wired/views/selectors/WiredSelectorUsersAreaView.tsx @@ -3,7 +3,7 @@ import { FC, useCallback, useEffect, useState } from 'react'; import { LocalizeText } from '../../../../api'; import { Button, Text } from '../../../../common'; import { useWired } from '../../../../hooks'; -import { WiredActionBaseView } from '../actions/WiredActionBaseView'; +import { WiredSelectorBaseView } from './WiredSelectorBaseView'; export const WiredSelectorUsersAreaView: FC<{}> = props => { @@ -13,14 +13,12 @@ export const WiredSelectorUsersAreaView: FC<{}> = props => const [ areaHeight, setAreaHeight ] = useState(0); const [ filterExisting, setFilterExisting ] = useState(false); const [ invert, setInvert ] = useState(false); - const [ excludeBots, setExcludeBots ] = useState(false); - const [ excludePets, setExcludePets ] = useState(false); const { trigger = null, setIntParams } = useWired(); const save = useCallback(() => { - setIntParams([ rootX, rootY, areaWidth, areaHeight, filterExisting ? 1 : 0, invert ? 1 : 0, excludeBots ? 1 : 0, excludePets ? 1 : 0 ]); - }, [ rootX, rootY, areaWidth, areaHeight, filterExisting, invert, excludeBots, excludePets, setIntParams ]); + setIntParams([ rootX, rootY, areaWidth, areaHeight, filterExisting ? 1 : 0, invert ? 1 : 0 ]); + }, [ rootX, rootY, areaWidth, areaHeight, filterExisting, invert, setIntParams ]); useEffect(() => { @@ -76,8 +74,6 @@ export const WiredSelectorUsersAreaView: FC<{}> = props => setFilterExisting(trigger.intData.length >= 5 && trigger.intData[4] === 1); setInvert(trigger.intData.length >= 6 && trigger.intData[5] === 1); - setExcludeBots(trigger.intData.length >= 7 && trigger.intData[6] === 1); - setExcludePets(trigger.intData.length >= 8 && trigger.intData[7] === 1); }, [ trigger ]); useEffect(() => @@ -92,7 +88,7 @@ export const WiredSelectorUsersAreaView: FC<{}> = props => const hasArea = areaWidth > 0 && areaHeight > 0; return ( - +
{ LocalizeText('wiredfurni.params.area_selection') } { LocalizeText('wiredfurni.params.area_selection.info') } @@ -134,25 +130,7 @@ export const WiredSelectorUsersAreaView: FC<{}> = props => onChange={ e => setInvert(e.target.checked) } /> { LocalizeText('wiredfurni.params.selector_option.1') } - - - -
-
+ ); }; diff --git a/src/components/wired/views/selectors/WiredSelectorUsersByActionView.tsx b/src/components/wired/views/selectors/WiredSelectorUsersByActionView.tsx new file mode 100644 index 0000000..496dc87 --- /dev/null +++ b/src/components/wired/views/selectors/WiredSelectorUsersByActionView.tsx @@ -0,0 +1,142 @@ +import { FC, useCallback, useEffect, useState } from 'react'; +import { LocalizeText } from '../../../../api'; +import { Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredSelectorBaseView } from './WiredSelectorBaseView'; + +const ACTION_WAVE = 1; +const ACTION_BLOW_KISS = 2; +const ACTION_LAUGH = 3; +const ACTION_AWAKE = 4; +const ACTION_RELAX = 5; +const ACTION_SIT = 6; +const ACTION_STAND = 7; +const ACTION_LAY = 8; +const ACTION_SIGN = 9; +const ACTION_DANCE = 10; +const ACTION_THUMB_UP = 11; + +const ACTION_OPTIONS = [ + { value: ACTION_WAVE, label: 'widget.memenu.wave' }, + { value: ACTION_BLOW_KISS, label: 'widget.memenu.blow' }, + { value: ACTION_LAUGH, label: 'widget.memenu.laugh' }, + { value: ACTION_THUMB_UP, label: 'widget.memenu.thumb' }, + { value: ACTION_AWAKE, label: 'wiredfurni.params.action.4' }, + { value: ACTION_RELAX, label: 'avatar.widget.random_walk' }, + { value: ACTION_SIT, label: 'widget.memenu.sit' }, + { value: ACTION_STAND, label: 'widget.memenu.stand' }, + { value: ACTION_LAY, label: 'wiredfurni.params.action.8' }, + { value: ACTION_SIGN, label: 'widget.memenu.sign' }, + { value: ACTION_DANCE, label: 'widget.memenu.dance' } +]; + +const SIGN_OPTIONS = Array.from({ length: 18 }, (_, value) => ({ + value, + label: `wiredfurni.params.action.sign.${ value }` +})); + +const DANCE_OPTIONS = [ + { value: 1, label: 'widget.memenu.dance1' }, + { value: 2, label: 'widget.memenu.dance2' }, + { value: 3, label: 'widget.memenu.dance3' }, + { value: 4, label: 'widget.memenu.dance4' } +]; + +export const WiredSelectorUsersByActionView: FC<{}> = () => +{ + const [ selectedAction, setSelectedAction ] = useState(ACTION_WAVE); + const [ signFilterEnabled, setSignFilterEnabled ] = useState(false); + const [ signId, setSignId ] = useState(0); + const [ danceFilterEnabled, setDanceFilterEnabled ] = useState(false); + const [ danceId, setDanceId ] = useState(1); + const [ filterExisting, setFilterExisting ] = useState(false); + const [ invert, setInvert ] = useState(false); + const { trigger = null, setIntParams = null } = useWired(); + + useEffect(() => + { + if(!trigger) return; + + const params = trigger.intData; + + setSelectedAction(params.length > 0 ? params[0] : ACTION_WAVE); + setSignFilterEnabled(params.length > 1 ? (params[1] === 1) : false); + setSignId(params.length > 2 ? params[2] : 0); + setDanceFilterEnabled(params.length > 3 ? (params[3] === 1) : false); + setDanceId(params.length > 4 ? params[4] : 1); + setFilterExisting(params.length > 5 ? (params[5] === 1) : false); + setInvert(params.length > 6 ? (params[6] === 1) : false); + }, [ trigger ]); + + const save = useCallback(() => + { + setIntParams([ + selectedAction, + signFilterEnabled ? 1 : 0, + signId, + danceFilterEnabled ? 1 : 0, + danceId, + filterExisting ? 1 : 0, + invert ? 1 : 0 + ]); + }, [ selectedAction, signFilterEnabled, signId, danceFilterEnabled, danceId, filterExisting, invert, setIntParams ]); + + return ( + +
+
+ Action + +
+ + { (selectedAction === ACTION_SIGN) && +
+ + { signFilterEnabled && + } +
} + + { (selectedAction === ACTION_DANCE) && +
+ + { danceFilterEnabled && + } +
} + +
+ + { LocalizeText('wiredfurni.params.selector_options_selector') } + + + + +
+
+ ); +}; diff --git a/src/components/wired/views/selectors/WiredSelectorUsersByNameView.tsx b/src/components/wired/views/selectors/WiredSelectorUsersByNameView.tsx new file mode 100644 index 0000000..c8ff653 --- /dev/null +++ b/src/components/wired/views/selectors/WiredSelectorUsersByNameView.tsx @@ -0,0 +1,69 @@ +import { FC, useCallback, useEffect, useState } from 'react'; +import { LocalizeText } from '../../../../api'; +import { Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredSelectorBaseView } from './WiredSelectorBaseView'; + +export const WiredSelectorUsersByNameView: FC<{}> = () => +{ + const [ namesText, setNamesText ] = useState(''); + const [ filterExisting, setFilterExisting ] = useState(false); + const [ invert, setInvert ] = useState(false); + const { trigger = null, setIntParams = null, setStringParam = null } = useWired(); + + useEffect(() => + { + if(!trigger) return; + + const params = trigger.intData; + + setNamesText(trigger.stringData || ''); + setFilterExisting(params.length > 0 ? (params[0] === 1) : false); + setInvert(params.length > 1 ? (params[1] === 1) : false); + }, [ trigger ]); + + const save = useCallback(() => + { + setStringParam(namesText); + setIntParams([ + filterExisting ? 1 : 0, + invert ? 1 : 0 + ]); + }, [ namesText, filterExisting, invert, setStringParam, setIntParams ]); + + return ( + +
+
+ { LocalizeText('wiredfurni.params.enter_names') } +