From 95ec51b41da3a4f1c27e9949fa411afcf774195b Mon Sep 17 00:00:00 2001 From: Lorenzune Date: Wed, 18 Mar 2026 14:38:21 +0100 Subject: [PATCH] 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 3240a00..9e3cee2 100644 --- a/src/api/wired/WiredActionLayoutCode.ts +++ b/src/api/wired/WiredActionLayoutCode.ts @@ -32,4 +32,6 @@ 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 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 23c16d1..e4d14db 100644 --- a/src/components/wired/views/actions/WiredActionLayoutView.tsx +++ b/src/components/wired/views/actions/WiredActionLayoutView.tsx @@ -1,5 +1,6 @@ import { WiredActionLayoutCode } from '../../../../api'; import { WiredActionBotChangeFigureView } from './WiredActionBotChangeFigureView'; +import { WiredActionSetAltitudeView } from './WiredActionSetAltitudeView'; import { WiredActionSendSignalView } from './WiredActionSendSignalView'; import { WiredActionFurniAreaView } from '../selectors/WiredActionFurniAreaView'; import { WiredSelectorFurniNeighborhoodView } from '../selectors/WiredSelectorFurniNeighborhoodView'; @@ -26,6 +27,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'; @@ -57,6 +59,8 @@ export const WiredActionLayoutView = (code: number) => return ; case WiredActionLayoutCode.FLEE: return ; + case WiredActionLayoutCode.SET_ALTITUDE: + return ; case WiredActionLayoutCode.GIVE_REWARD: return ; case WiredActionLayoutCode.GIVE_SCORE: @@ -77,6 +81,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 } +
+
+ ); +};