nitro-v36-with-renderer-v36
/
src /
components /
wired /
views /
actions /
WiredActionGiveVariableView.tsx
WiredActionGiveVariableView.tsx 18.1 Ko
Télécharger le fichier
import { FC, useEffect, useMemo, useState } from 'react';
import { FaChevronLeft, FaChevronRight } from 'react-icons/fa';
import { LocalizeText, localizeWithFallback, WiredFurniType } from '../../../../api';
import contextVariableIcon from '../../../../assets/images/wired/var/icon_source_context_clean.png';
import furniVariableIcon from '../../../../assets/images/wired/var/icon_source_furni.png';
import userVariableIcon from '../../../../assets/images/wired/var/icon_source_user.png';
import { Button, Text } from '../../../../common';
import { useWired, useWiredTools } from '../../../../hooks';
import { CLICKED_USER_SOURCE, FURNI_SOURCES, sortWiredSourceOptions, USER_SOURCES, useAvailableUserSources } from '../WiredSourcesSelector';
import { WiredVariablePicker } from '../WiredVariablePicker';
import {
buildWiredVariablePickerEntries,
createCustomVariableToken,
createFallbackVariableEntry,
flattenWiredVariablePickerEntries,
normalizeVariableTokenFromWire
} from '../WiredVariablePickerData';
import { isWiredSameHolderEligible, WiredValueOrVariableInput } from '../WiredValueOrVariableInput';
import { customVariablePickerEntries, normalizeWiredValueTarget, variableTokenFromId, WIRED_VALUE_TARGETS } from '../WiredValueOrVariableField';
import { fromWiredVariableId, toWiredVariableId, wiredVariableIdList } from '../WiredVariableIds';
import { WiredActionBaseView } from './WiredActionBaseView';
import { WiredActionDelaySection } from './WiredActionDelaySection';
import { BaseButton } from '../../../../common/BaseButton';
import { Checkbox } from '../../../../common/Checkbox';
import { NativeTextTree } from '../../../../common/native-text/NativeTextTree';
type VariableTargetType = 'user' | 'furni' | 'context';
// Official variable targets (1 user, 0 furni, -20 context).
const TARGET_USER = 1;
const TARGET_FURNI = 0;
const TARGET_CONTEXT = -20;
const REFERENCE_GLOBAL = -10;
const SOURCE_SELECTED = 100;
const TARGET_BUTTONS: Array<{ key: VariableTargetType; icon: string }> = [
{ key: 'furni', icon: furniVariableIcon },
{ key: 'user', icon: userVariableIcon },
{ key: 'context', icon: contextVariableIcon }
];
const normalizeTargetType = (value: number): VariableTargetType => {
switch (value) {
case TARGET_FURNI:
return 'furni';
case TARGET_CONTEXT:
return 'context';
default:
return 'user';
}
};
const getTargetValue = (value: VariableTargetType) => {
switch (value) {
case 'furni':
return TARGET_FURNI;
case 'context':
return TARGET_CONTEXT;
default:
return TARGET_USER;
}
};
export const WiredActionGiveVariableView: FC<{}> = () => {
const {
trigger = null,
furniIds = [],
actionDelay = 0,
setActionDelay = null,
setIntParams = null,
setFurniIds = null,
setStringParam = null,
setSourceTypes = null,
setVariableIds = null
} = useWired();
const { userVariableDefinitions = [], furniVariableDefinitions = [], roomVariableDefinitions = [], contextVariableDefinitions = [] } = useWiredTools();
const [selectedTargetType, setSelectedTargetType] = useState<VariableTargetType>('user');
const [selectedVariableToken, setSelectedVariableToken] = useState('');
const [overrideExisting, setOverrideExisting] = useState(false);
const [initialValueInput, setInitialValueInput] = useState('0');
const [userSource, setUserSource] = useState(0);
const [furniSource, setFurniSource] = useState(0);
const [fromVariable, setFromVariable] = useState(false);
const [referenceTarget, setReferenceTarget] = useState(REFERENCE_GLOBAL);
const [referenceToken, setReferenceToken] = useState('');
const [sameHolder, setSameHolder] = useState(false);
const [initialValueValid, setInitialValueValid] = useState(true);
const referenceTargetKey = WIRED_VALUE_TARGETS.find((entry) => entry.value === referenceTarget)?.key ?? 'global';
const referenceEntries = useMemo(() => {
const definitions = { furni: furniVariableDefinitions, user: userVariableDefinitions, global: roomVariableDefinitions, context: contextVariableDefinitions }[
referenceTargetKey
];
return customVariablePickerEntries(referenceTargetKey, 'change-reference', definitions.filter((definition) => definition.hasValue), referenceToken);
}, [contextVariableDefinitions, furniVariableDefinitions, referenceTargetKey, referenceToken, roomVariableDefinitions, userVariableDefinitions]);
// AIR ValueOrVariableSection.referenceVariableOnTarget: only for a variable on the destination's own furni or user holder.
const sameHolderActive = fromVariable && sameHolder && isWiredSameHolderEligible(selectedTargetType, referenceTargetKey);
const targetDefinitions = useMemo(() => {
if (selectedTargetType === 'furni') return furniVariableDefinitions;
if (selectedTargetType === 'context') return contextVariableDefinitions;
return userVariableDefinitions;
}, [contextVariableDefinitions, furniVariableDefinitions, selectedTargetType, userVariableDefinitions]);
const variableEntries = useMemo(
() => buildWiredVariablePickerEntries(selectedTargetType, 'give', targetDefinitions),
[selectedTargetType, targetDefinitions]
);
const resolvedVariableEntries = useMemo(() => {
if (!selectedVariableToken) return variableEntries;
if (flattenWiredVariablePickerEntries(variableEntries).some((entry) => entry.token === selectedVariableToken)) return variableEntries;
const fallbackEntry = createFallbackVariableEntry(selectedTargetType, selectedVariableToken);
return fallbackEntry ? [fallbackEntry, ...variableEntries] : variableEntries;
}, [selectedTargetType, selectedVariableToken, variableEntries]);
const selectedVariableDefinition = useMemo(
() => flattenWiredVariablePickerEntries(resolvedVariableEntries).find((entry) => entry.token === selectedVariableToken) ?? null,
[resolvedVariableEntries, selectedVariableToken]
);
const availableUserSources = useAvailableUserSources(trigger, USER_SOURCES);
const orderedUserSources = useMemo(() => sortWiredSourceOptions(availableUserSources, 'users'), [availableUserSources]);
const orderedFurniSources = useMemo(() => sortWiredSourceOptions(FURNI_SOURCES, 'furni'), []);
const sourceOptions = selectedTargetType === 'user' ? orderedUserSources : selectedTargetType === 'furni' ? orderedFurniSources : [];
const selectedSourceValue = selectedTargetType === 'user' ? userSource : furniSource;
const resolvedSourceOptions = useMemo(() => {
if (selectedTargetType === 'context') return [];
if (sourceOptions.some((option) => option.value === selectedSourceValue)) return sourceOptions;
const fallbackOptions = selectedTargetType === 'user' ? sortWiredSourceOptions([...USER_SOURCES, CLICKED_USER_SOURCE], 'users') : orderedFurniSources;
const fallbackOption = fallbackOptions.find((option) => option.value === selectedSourceValue);
if (!fallbackOption) return sourceOptions;
return [...sourceOptions, fallbackOption];
}, [orderedFurniSources, selectedSourceValue, selectedTargetType, sourceOptions]);
const selectedSourceIndex = resolvedSourceOptions.findIndex((option) => option.value === selectedSourceValue);
const selectedSourceOption = selectedSourceIndex >= 0 ? resolvedSourceOptions[selectedSourceIndex] : null;
const handleTargetTypeChange = (value: VariableTargetType) => {
if (value === selectedTargetType) return;
setSelectedTargetType(value);
setSelectedVariableToken('');
};
useEffect(() => {
if (!trigger) return;
// Official layout: [target, initialHigh, initialLow, override, fromVariable, referenceTarget, sameHolder];
// variable ids [variable, reference variable or none]; furni / user slot 0 are the targets. An older server
// wrote "variable\treferenceVariable" as the string.
const [variableText = '', referenceText = ''] = (trigger.stringData || '').split('\t');
const parsedVariableItemId = parseInt(variableText.trim(), 10);
const nextTargetType = normalizeTargetType(trigger.intData.length > 0 ? trigger.intData[0] : TARGET_USER);
const variableIds = trigger.variableIds ?? [];
setSelectedTargetType(nextTargetType);
setSelectedVariableToken(
fromWiredVariableId(variableIds[0] ?? '') ||
normalizeVariableTokenFromWire(
!Number.isNaN(parsedVariableItemId) && parsedVariableItemId > 0
? String(parsedVariableItemId)
: nextTargetType === 'user' && (trigger.selectedItems?.length ?? 0) > 0
? String(trigger.selectedItems[0])
: ''
)
);
setInitialValueInput((trigger.intData.length > 2 ? trigger.intData[2] : 0).toString());
setOverrideExisting(trigger.intData.length > 3 ? trigger.intData[3] === 1 : false);
setFromVariable(trigger.intData[4] === 1);
setReferenceTarget(trigger.intData.length > 5 ? normalizeWiredValueTarget(trigger.intData[5]) : REFERENCE_GLOBAL);
setSameHolder(trigger.intData[6] === 1);
setReferenceToken(fromWiredVariableId(variableIds[1] ?? '') || variableTokenFromId(referenceText));
setUserSource(trigger.userSourceTypes?.[0] ?? 0);
setFurniSource(trigger.furniSourceTypes?.[0] ?? ((trigger.selectedItems?.length ?? 0) > 0 ? SOURCE_SELECTED : 0));
}, [trigger]);
useEffect(() => {
if (!selectedVariableDefinition) return;
if (selectedVariableDefinition.hasValue) return;
setInitialValueInput('0');
}, [selectedVariableDefinition]);
const save = () => {
const targetValue = getTargetValue(selectedTargetType);
const parsedInitialValue = parseInt(initialValueInput.trim(), 10);
const initialValue = Number.isFinite(parsedInitialValue) ? parsedInitialValue : 0;
setStringParam('');
setVariableIds(
wiredVariableIdList(toWiredVariableId(selectedTargetType, selectedVariableToken), fromVariable ? toWiredVariableId(referenceTargetKey, referenceToken) : '')
);
setIntParams([targetValue, initialValue < 0 ? -1 : 0, initialValue, overrideExisting ? 1 : 0, fromVariable ? 1 : 0, referenceTarget, sameHolderActive ? 1 : 0]);
setSourceTypes({ furni: [furniSource], users: [userSource] });
setFurniIds(selectedTargetType === 'furni' && furniSource === SOURCE_SELECTED ? [...furniIds] : []);
};
// AIR saves any variable; one of another kind than the target just does nothing.
const validate = () => !!selectedVariableToken && (!selectedVariableDefinition?.hasValue || initialValueValid);
const requiresFurni =
selectedTargetType === 'furni' ? WiredFurniType.STUFF_SELECTION_OPTION_BY_ID_BY_TYPE_OR_FROM_CONTEXT : WiredFurniType.STUFF_SELECTION_OPTION_NONE;
const missingVariablesText = (() => {
switch (selectedTargetType) {
case 'furni':
return 'No wf_var_furni variables found in this room.';
case 'context':
return 'No wf_var_context variables found in this room.';
default:
return 'No wf_var_user variables found in this room.';
}
})();
const cycleSource = (direction: number) => {
if (!resolvedSourceOptions.length) return;
const currentIndex = selectedSourceIndex >= 0 ? selectedSourceIndex : 0;
const nextIndex = (currentIndex + direction + resolvedSourceOptions.length) % resolvedSourceOptions.length;
const nextSourceValue = resolvedSourceOptions[nextIndex].value;
if (selectedTargetType === 'user') {
setUserSource(nextSourceValue);
return;
}
if (selectedTargetType === 'furni') {
setFurniSource(nextSourceValue);
}
};
return (
<NativeTextTree>
<WiredActionBaseView hasSpecialInput={true} requiresFurni={requiresFurni} save={save} validate={validate} cardStyle={{ width: 244 }} hideDelay={true}>
<div className="octane-wired__give-var">
<div className="octane-wired__give-var-heading">
<Text>{LocalizeText('wiredfurni.params.variables.variable_selection')}</Text>
<div className="octane-wired__give-var-targets">
{TARGET_BUTTONS.map((button) => (
<BaseButton
key={button.key}
type="button"
className={`octane-wired__give-var-target octane-wired__give-var-target--${button.key} ${selectedTargetType === button.key ? 'is-active' : ''}`}
onClick={() => handleTargetTypeChange(button.key)}
>
<img src={button.icon} alt={button.key} />
</BaseButton>
))}
</div>
</div>
<>
<WiredVariablePicker
entries={resolvedVariableEntries}
recentScope="variable-effects"
selectedToken={selectedVariableToken}
onSelect={(entry) => setSelectedVariableToken(entry.token)}
/>
{!targetDefinitions.length && <Text small>{missingVariablesText}</Text>}
<label className="octane-wired__give-var-checkbox">
<Checkbox checked={overrideExisting} onCheckedChange={ nextChecked => setOverrideExisting(nextChecked) } />
<Text>{LocalizeText('wiredfurni.params.variables.value_settings.override_existing')}</Text>
</label>
<div className="octane-wired__divider" />
<div className="octane-wired__give-var-section">
<div className="octane-wired__give-var-section-title">{LocalizeText('wiredfurni.params.variables.value_settings.initial_value')}</div>
{/* AIR GiveVariable: the initial value is a value-or-variable section (createValueOrVariableSection). */}
<WiredValueOrVariableInput
disabled={!selectedVariableDefinition?.hasValue}
entries={referenceEntries}
isVariable={fromVariable}
label={LocalizeText('wiredfurni.params.variables.value_settings.initial_value')}
literal={initialValueInput}
primaryTarget={selectedTargetType}
sameHolder={sameHolder}
target={referenceTargetKey}
targets={WIRED_VALUE_TARGETS.map((entry) => entry.key)}
token={referenceToken}
onIsVariableChange={setFromVariable}
onLiteralChange={setInitialValueInput}
onSameHolderChange={setSameHolder}
onTargetChange={(key) => setReferenceTarget(WIRED_VALUE_TARGETS.find((entry) => entry.key === key)?.value ?? REFERENCE_GLOBAL)}
onTokenChange={setReferenceToken}
onValidityChange={setInitialValueValid}
/>
</div>
<div className="octane-wired__divider" />
<WiredActionDelaySection value={actionDelay} onChange={setActionDelay} />
{selectedTargetType !== 'context' && (
<>
<div className="octane-wired__divider" />
<div className="octane-wired__give-var-section">
<div className="octane-wired__give-var-section-title">{localizeWithFallback('wiredfurni.params.sources.merged.title.variables_destination', 'Destinazione variabile:')}</div>
<div className="flex items-center gap-1">
<Button
disabled={resolvedSourceOptions.length <= 1}
variant="primary"
classNames={['octane-wired__picker-button']}
className="px-2 py-1"
onClick={() => cycleSource(-1)}
>
<FaChevronLeft />
</Button>
<div className="flex min-w-0 flex-1 items-center justify-center octane-wired__picker-label">
<Text small className="text-center">
{selectedSourceOption ? LocalizeText(selectedSourceOption.label) : '-'}
</Text>
</div>
<Button
disabled={resolvedSourceOptions.length <= 1}
variant="primary"
classNames={['octane-wired__picker-button']}
className="px-2 py-1"
onClick={() => cycleSource(1)}
>
<FaChevronRight />
</Button>
</div>
</div>
</>
)}
</>
</div>
</WiredActionBaseView>
</NativeTextTree>
);
};
