feat(wired-ui): add altitude and relative move actions

This commit is contained in:
Lorenzune
2026-03-18 14:38:21 +01:00
parent 5f2e9af7fb
commit e35d06b248
5 changed files with 308 additions and 4 deletions
+2
View File
@@ -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;
}
+18 -4
View File
@@ -11,11 +11,25 @@ export interface SliderProps extends ReactSliderProps
export const Slider: FC<SliderProps> = 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 <Flex fullWidth gap={ 1 }>
{ !disabledButton && <Button disabled={ min >= value } onClick={ () => onChange(min < value ? value - 1 : min, 0) }><FaAngleLeft /></Button> }
<ReactSlider className={ 'nitro-slider' } max={ max } min={ min } value={ value } onChange={ onChange } { ...rest } />
{ !disabledButton && <Button disabled={ max <= value } onClick={ () => onChange(max > value ? value + 1 : max, 0) }><FaAngleRight /></Button> }
{ !disabledButton && <Button disabled={ minimum >= currentValue } onClick={ () => onChange(roundToStep(minimum < currentValue ? currentValue - buttonStep : minimum), 0) }><FaAngleLeft /></Button> }
<ReactSlider className={ 'nitro-slider' } max={ max } min={ min } step={ step } value={ value } onChange={ onChange } { ...rest } />
{ !disabledButton && <Button disabled={ maximum <= currentValue } onClick={ () => onChange(roundToStep(maximum > currentValue ? currentValue + buttonStep : maximum), 0) }><FaAngleRight /></Button> }
</Flex>;
}
@@ -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 <WiredActionTeleportView />;
case WiredActionLayoutCode.FURNI_TO_FURNI:
return <WiredActionFurniToFurniView />;
case WiredActionLayoutCode.SET_ALTITUDE:
return <WiredActionSetAltitudeView />;
case WiredActionLayoutCode.GIVE_REWARD:
return <WiredActionGiveRewardView />;
case WiredActionLayoutCode.GIVE_SCORE:
@@ -86,6 +90,8 @@ export const WiredActionLayoutView = (code: number) =>
return <WiredActionMoveFurniToView />;
case WiredActionLayoutCode.MUTE_USER:
return <WiredActionMuteUserView />;
case WiredActionLayoutCode.RELATIVE_MOVE:
return <WiredActionRelativeMoveView />;
case WiredActionLayoutCode.RESET:
return <WiredActionResetView />;
case WiredActionLayoutCode.SET_FURNI_STATE:
@@ -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: <FaArrowLeft /> },
{ value: 1, icon: <FaArrowRight /> }
];
const VERTICAL_OPTIONS = [
{ value: 0, icon: <FaArrowDown /> },
{ value: 1, icon: <FaArrowUp /> }
];
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<number>(() =>
{
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 (
<WiredActionBaseView
hasSpecialInput={ true }
requiresFurni={ WiredFurniType.STUFF_SELECTION_OPTION_BY_ID_BY_TYPE_OR_FROM_CONTEXT }
save={ save }
footer={ <WiredSourcesSelector showFurni={ true } furniSource={ furniSource } onChangeFurni={ setFurniSource } /> }>
<div className="flex flex-col gap-2">
<Text bold>{ LocalizeText('wiredfurni.params.movement.horizontal.selection') }</Text>
<div className="flex gap-2">
{ HORIZONTAL_OPTIONS.map(option =>
{
return (
<label key={ option.value } className="flex items-center gap-1">
<input checked={ (horizontalDirection === option.value) } className="form-check-input" name="relativeMoveHorizontal" type="radio" onChange={ () => setHorizontalDirection(option.value) } />
<Text>{ option.icon }</Text>
</label>
);
}) }
</div>
<Text>{ LocalizeText('wiredfurni.params.movement.horizontal.distance', [ 'distance' ], [ horizontalDistance.toString() ]) }</Text>
<Slider
max={ MAX_DISTANCE }
min={ 0 }
step={ 1 }
value={ horizontalDistance }
onChange={ value => setHorizontalDistance(value as number) } />
</div>
<div className="flex flex-col gap-2">
<Text bold>{ LocalizeText('wiredfurni.params.movement.vertical.selection') }</Text>
<div className="flex gap-2">
{ VERTICAL_OPTIONS.map(option =>
{
return (
<label key={ option.value } className="flex items-center gap-1">
<input checked={ (verticalDirection === option.value) } className="form-check-input" name="relativeMoveVertical" type="radio" onChange={ () => setVerticalDirection(option.value) } />
<Text>{ option.icon }</Text>
</label>
);
}) }
</div>
<Text>{ LocalizeText('wiredfurni.params.movement.vertical.distance', [ 'distance' ], [ verticalDistance.toString() ]) }</Text>
<Slider
max={ MAX_DISTANCE }
min={ 0 }
step={ 1 }
value={ verticalDistance }
onChange={ value => setVerticalDistance(value as number) } />
</div>
</WiredActionBaseView>
);
};
@@ -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<number>(() =>
{
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 (
<WiredActionBaseView
hasSpecialInput={ true }
requiresFurni={ WiredFurniType.STUFF_SELECTION_OPTION_BY_ID_BY_TYPE_OR_FROM_CONTEXT }
save={ save }
footer={ <WiredSourcesSelector showFurni={ true } furniSource={ furniSource } onChangeFurni={ setFurniSource } /> }>
<div className="flex flex-col gap-2">
{ OPERATOR_OPTIONS.map(option =>
{
return (
<div key={ option.value } className="flex items-center gap-1">
<input checked={ (operator === option.value) } className="form-check-input" id={ `setAltitudeOperator${ option.value }` } name="setAltitudeOperator" type="radio" onChange={ () => setOperator(option.value) } />
<Text>{ LocalizeText(option.label) }</Text>
</div>
);
}) }
</div>
<div className="flex flex-col gap-1">
<Text bold>{ LocalizeText('wiredfurni.params.setaltitude') }</Text>
<input
className="form-control form-control-sm"
inputMode="decimal"
type="text"
value={ altitudeInput }
onBlur={ () => setAltitudeInput(formatAltitude(altitude)) }
onChange={ event => updateAltitudeInput(event.target.value) } />
</div>
<div className="flex flex-col gap-1">
<Slider
max={ MAX_ALTITUDE }
min={ MIN_ALTITUDE }
step={ ALTITUDE_STEP }
value={ altitude }
onChange={ event => updateAltitude(event as number) } />
<Text small>{ normalizedAltitudeText }</Text>
</div>
</WiredActionBaseView>
);
};