nitro-v36-with-renderer-v36
/
src /
components /
wired /
views /
actions /
WiredActionPlaceTemporaryFurniView.tsx
WiredActionPlaceTemporaryFurniView.tsx 16 Ko
Télécharger le fichier
import { FC, useEffect, useMemo, useState } from 'react';
import { LocalizeText, localizeWithFallback, WiredFurniType } from '../../../../api';
import sourceFurniIcon from '../../../../assets/images/wired/source_furni.png';
import sourceUserIcon from '../../../../assets/images/wired/source_user.png';
import { Text } from '../../../../common';
import { useWired, useWiredTools } from '../../../../hooks';
import { WiredFurniSelectionSourceRow } from '../WiredFurniSelectionSourceRow';
import { sortWiredSourceOptions, useAvailableUserSources } from '../WiredSourcesSelector';
import { WiredVariablePicker } from '../WiredVariablePicker';
import { flattenWiredVariablePickerEntries, getCustomVariableItemId } from '../WiredVariablePickerData';
import { customVariablePickerEntries, normalizeWiredValueTarget, variableTokenFromId, WiredValueOrVariableField, wiredValueTargetKey } from '../WiredValueOrVariableField';
import { readWiredVariableToken, toWiredVariableId, wiredVariableIdList } from '../WiredVariableIds';
import { WiredActionBaseView } from './WiredActionBaseView';
import { Checkbox } from '../../../../common/Checkbox';
import { Radio } from '../../../../common/Radio';
import { NumberInput } from '../../../../common/NumberInput';
import { BaseButton } from '../../../../common/BaseButton';
import { Hint } from '../../../../common/Tooltip';
import { NativeTextTree } from '../../../../common/native-text/NativeTextTree';
const SOURCE_TRIGGER = 0;
const SOURCE_PICKED = 100;
const MAX_OFFSET = 64;
const MAX_OFFSET_ALTITUDE = 8000;
const TARGET_FURNI_SOURCES = sortWiredSourceOptions(
[
{ value: 0, label: 'wiredfurni.params.sources.furni.0' },
{ value: 200, label: 'wiredfurni.params.sources.furni.200' },
{ value: 201, label: 'wiredfurni.params.sources.furni.201' }
],
'furni'
);
const TARGET_USER_SOURCES = sortWiredSourceOptions(
[
{ value: 0, label: 'wiredfurni.params.sources.users.0' },
{ value: 11, label: 'wiredfurni.params.sources.users.11' },
{ value: 200, label: 'wiredfurni.params.sources.users.200' },
{ value: 201, label: 'wiredfurni.params.sources.users.201' }
],
'users'
);
const clamp = (value: number, bound: number) => Math.max(-bound, Math.min(bound, isNaN(value) ? 0 : value));
/**
* The official "place furni" (code 55): places temporary copies of the picked furni, as they were when
* the box was saved. Int params [targetIsUser, location, altitude, offsetX, offsetY, offsetAltitude,
* spawnWithVariable, valueIsVariable, value, valueTarget]; furni sources [picks, target furni, value
* furni], user sources [target user, value user]. Spawning with a variable gives every copy a furni
* variable; its value is a number or another variable's (value target 0 furni, 1 user, -10 global,
* -20 context). The two variables are AIR's variable ids [spawn variable, value variable]; an older server wrote
* them as the string "spawnId\tvalueId".
*/
export const WiredActionPlaceTemporaryFurniView: FC<{}> = () =>
{
const { trigger = null, setIntParams = null, setStringParam = null, setSourceTypes = null, setVariableIds = null } = useWired();
const [ targetIsUser, setTargetIsUser ] = useState(false);
const [ location, setLocation ] = useState(0);
const [ altitude, setAltitude ] = useState(0);
const [ offsetX, setOffsetX ] = useState(0);
const [ offsetY, setOffsetY ] = useState(0);
const [ offsetAltitude, setOffsetAltitude ] = useState(0);
const [ showOffsets, setShowOffsets ] = useState(false);
// Furni slot 0 holds the picks the copies are made from: the server opens it on their snapshot (110), as Habbo
// does, and our older saves hold 100. The editor has no row for it, so it goes back as the box sent it.
const [ templateSource, setTemplateSource ] = useState(SOURCE_PICKED);
const [ targetFurniSource, setTargetFurniSource ] = useState(SOURCE_TRIGGER);
const [ targetUserSource, setTargetUserSource ] = useState(SOURCE_TRIGGER);
const [ spawnWithVariable, setSpawnWithVariable ] = useState(false);
const [ spawnToken, setSpawnToken ] = useState('');
const [ valueIsVariable, setValueIsVariable ] = useState(false);
const [ value, setValue ] = useState('0');
const [ valueTarget, setValueTarget ] = useState(0);
const [ valueToken, setValueToken ] = useState('');
const [ valueFurniSource, setValueFurniSource ] = useState(SOURCE_TRIGGER);
const [ valueUserSource, setValueUserSource ] = useState(SOURCE_TRIGGER);
const [ valueValid, setValueValid ] = useState(true);
const availableUserSources = useAvailableUserSources(trigger, TARGET_USER_SOURCES);
const { furniVariableDefinitions = [] } = useWiredTools();
const spawnEntries = useMemo(
() => customVariablePickerEntries('furni', 'give', furniVariableDefinitions.filter(definition => !definition.isReadOnly), spawnToken),
[ furniVariableDefinitions, spawnToken ]
);
const spawnEntry = flattenWiredVariablePickerEntries(spawnEntries).find(entry => entry.token === spawnToken) ?? null;
// AIR ?_-9T?.updateSpawnValueState: the value is for a spawn variable with a value only (a saved one that is gone counts).
const spawnHasValue = spawnToken ? (spawnEntry?.hasValue ?? true) : false;
const valueDisabled = !spawnWithVariable || !spawnHasValue;
useEffect(() =>
{
if(!trigger) return;
const params = trigger.intData ?? [];
setTargetIsUser(params[0] === 1);
setLocation(params[1] === 1 ? 1 : 0);
setAltitude([ 1, 2 ].includes(params[2]) ? params[2] : 0);
setOffsetX(clamp(params[3] ?? 0, MAX_OFFSET));
setOffsetY(clamp(params[4] ?? 0, MAX_OFFSET));
setOffsetAltitude(clamp(params[5] ?? 0, MAX_OFFSET_ALTITUDE));
setShowOffsets(!!(params[3] || params[4] || params[5]));
setTemplateSource(trigger.furniSourceTypes?.[0] ?? trigger.inputSourcesConf?.furniDefaults?.[0] ?? SOURCE_PICKED);
setTargetFurniSource(trigger.furniSourceTypes?.[1] ?? SOURCE_TRIGGER);
setTargetUserSource(trigger.userSourceTypes?.[0] ?? SOURCE_TRIGGER);
const variables = (trigger.stringData ?? '').split('\t');
setSpawnWithVariable(params[6] === 1);
setValueIsVariable(params[7] === 1);
setValue(String(params[8] ?? 0));
setValueTarget(normalizeWiredValueTarget(params[9]));
setSpawnToken(readWiredVariableToken(trigger.variableIds, 0, variableTokenFromId(variables[0])));
setValueToken(readWiredVariableToken(trigger.variableIds, 1, variableTokenFromId(variables[1])));
setValueFurniSource(trigger.furniSourceTypes?.[2] ?? SOURCE_TRIGGER);
setValueUserSource(trigger.userSourceTypes?.[1] ?? SOURCE_TRIGGER);
}, [ trigger ]);
const save = () =>
{
const parsedValue = parseInt(value, 10);
const spawn = [ spawnWithVariable ? 1 : 0, valueIsVariable ? 1 : 0, Number.isFinite(parsedValue) ? parsedValue : 0, valueTarget ];
setIntParams([ targetIsUser ? 1 : 0, location, altitude, offsetX, offsetY, offsetAltitude, ...spawn ]);
setStringParam('');
setVariableIds(spawnWithVariable
? wiredVariableIdList(toWiredVariableId('furni', spawnToken), valueIsVariable ? toWiredVariableId(wiredValueTargetKey(valueTarget), valueToken) : '')
: []);
setSourceTypes({
furni: [ templateSource, targetFurniSource, valueFurniSource ],
users: [ targetUserSource, valueUserSource ]
});
};
const validate = () =>
{
if(!spawnWithVariable) return true;
if(getCustomVariableItemId(spawnToken) <= 0) return false;
if(valueDisabled) return true;
return valueValid && !(valueIsVariable && getCustomVariableItemId(valueToken) <= 0);
};
const sourceButtons = (isUser: boolean, onChange: (isUser: boolean) => void) => (
<div className="octane-wired__give-var-targets">
{ [ { key: 'furni', icon: sourceFurniIcon, isUser: false }, { key: 'user', icon: sourceUserIcon, isUser: true } ].map(button => (
<Hint key={ button.key } content={ LocalizeText(`wiredfurni.params.sources.${ button.isUser ? 'users' : 'furni' }.title`) }>
<BaseButton aria-label={ LocalizeText(`wiredfurni.params.sources.${ button.isUser ? 'users' : 'furni' }.title`) }
type="button"
className={ `octane-wired__give-var-target octane-wired__give-var-target--${ button.key } ${ isUser === button.isUser ? 'is-active' : '' }` }
onClick={ () => onChange(button.isUser) }>
<img src={ button.icon } alt={ button.key } />
</BaseButton>
</Hint>
)) }
</div>
);
const customTargetUsed = location === 1 || altitude === 2;
const radio = (name: string, value: number, current: number, onChange: (value: number) => void, label: string, info?: string) => (
<label key={ `${ name }${ value }` } className="flex items-start gap-1">
<Radio name={ name } checked={ current === value } onCheckedChange={() => onChange(value)} />
<span className="flex flex-col">
<Text>{ label }</Text>
{ info && <Text small className="text-muted">{ info }</Text> }
</span>
</label>
);
const offsetInput = (label: string, value: number, bound: number, onChange: (value: number) => void) => (
<div className="flex items-center gap-1">
<Text small>{ label }</Text>
<NumberInput className="form-control form-control-sm" style={ { width: 72 } } min={ -bound } max={ bound } value={ value } onChange={ event => onChange(clamp(parseInt(event.target.value, 10), bound)) } />
</div>
);
return (
<NativeTextTree>
<WiredActionBaseView hasSpecialInput={ true } requiresFurni={ WiredFurniType.STUFF_SELECTION_OPTION_BY_ID } save={ save } validate={ validate } widthModifier={ 1.2 } allowScrolling>
<div className="flex flex-col gap-2">
<Text small>{ localizeWithFallback('wiredfurni.params.place_furni.usage_info', 'Places temporary copies of the picked furni, as they are when you save.') }</Text>
<div className="flex flex-col gap-1">
<Text bold>{ localizeWithFallback('wiredfurni.params.place_furni.target_location', 'Location') }</Text>
{ radio('placeLocation', 0, location, setLocation, localizeWithFallback('wiredfurni.params.place_furni.target_location.0', 'Where the furni stood'), localizeWithFallback('wiredfurni.params.place_furni.target_location.0.info', '')) }
{ radio('placeLocation', 1, location, setLocation, localizeWithFallback('wiredfurni.params.place_furni.target_location.1', 'At the custom target')) }
</div>
<div className="flex flex-col gap-1">
<Text bold>{ localizeWithFallback('wiredfurni.params.place_furni.target_altitude', 'Altitude') }</Text>
{ [ 0, 1, 2 ].map(value => radio('placeAltitude', value, altitude, setAltitude, localizeWithFallback(`wiredfurni.params.place_furni.target_altitude.${ value }`, [ 'On top of the location', 'The furni\'s own altitude', 'The custom target\'s altitude' ][value]))) }
</div>
<div className="flex flex-col gap-1">
<BaseButton type="button" className="octane-wired__advanced-toggle" onClick={ () => setShowOffsets(value => !value) }>
<Text bold>{ localizeWithFallback('wiredfurni.params.place_furni.offsets', 'Offsets') }</Text>
</BaseButton>
{ showOffsets &&
<div className="flex flex-wrap items-center gap-3">
{ offsetInput(localizeWithFallback('wiredfurni.params.place_furni.offsets.x', 'x:'), offsetX, MAX_OFFSET, setOffsetX) }
{ offsetInput(localizeWithFallback('wiredfurni.params.place_furni.offsets.y', 'y:'), offsetY, MAX_OFFSET, setOffsetY) }
{ offsetInput(localizeWithFallback('wiredfurni.params.place_furni.offsets.altitude', 'altitude:'), offsetAltitude, MAX_OFFSET_ALTITUDE, setOffsetAltitude) }
</div> }
</div>
<hr className="m-0 bg-dark" />
<div className="flex flex-col gap-1">
<label className="flex items-center gap-1">
<Checkbox checked={ spawnWithVariable } onCheckedChange={(nextChecked) => setSpawnWithVariable(nextChecked)} />
<Text bold>{ localizeWithFallback('wiredfurni.params.place_furni.spawn_with_variable', 'Spawn with variable') }</Text>
</label>
{ spawnWithVariable &&
<>
<WiredVariablePicker entries={ spawnEntries } recentScope="variable-effects" selectedToken={ spawnToken } onSelect={ entry => setSpawnToken(entry.token) } />
{ !furniVariableDefinitions.length && <Text small>No wf_var_furni variables found in this room.</Text> }
{ /* AIR ?_-9T?: createValueOrVariableSection(1, mergedSourceOptions(1), spawn_with_value, int min, int max)
next to the spawn variable; the claimed merged slot 1 is the value's holder in the popup. */ }
<Text bold>{ localizeWithFallback('wiredfurni.params.place_furni.spawn_with_value', 'Spawn with value:') }</Text>
<WiredValueOrVariableField
disabled={ valueDisabled }
holderSource={ {
furniSource: valueFurniSource,
userSource: valueUserSource,
onFurniSourceChange: setValueFurniSource,
onUserSourceChange: setValueUserSource
} }
isVariable={ valueIsVariable }
label={ localizeWithFallback('wiredfurni.params.place_furni.spawn_with_value', 'Spawn with value:') }
literal={ value }
target={ valueTarget }
token={ valueToken }
onIsVariableChange={ setValueIsVariable }
onLiteralChange={ setValue }
onTargetChange={ setValueTarget }
onTokenChange={ setValueToken }
onValidityChange={ setValueValid } />
</> }
</div>
<hr className="m-0 bg-dark" />
<div className={ customTargetUsed ? '' : 'opacity-50 pointer-events-none' }>
<WiredFurniSelectionSourceRow
title="wiredfurni.params.sources.merged.title.custom_target"
options={ targetIsUser ? availableUserSources : TARGET_FURNI_SOURCES }
value={ targetIsUser ? targetUserSource : targetFurniSource }
selectionKind={ targetIsUser ? 'primary' : 'secondary' }
selectionActive={ false }
selectionCount={ 0 }
selectionLimit={ 0 }
selectionEnabledValues={ [] }
showSelectionToggle={ false }
headerContent={ sourceButtons(targetIsUser, setTargetIsUser) }
onChange={ value => (targetIsUser ? setTargetUserSource(value) : setTargetFurniSource(value)) } />
</div>
</div>
</WiredActionBaseView>
</NativeTextTree>
);
};
