mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-20 15:36:18 +00:00
feat: add advanced wired variable tools UI
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { LocalizeText, WiredFurniType } from '../../../../api';
|
||||
import { Text } from '../../../../common';
|
||||
import { useWired } from '../../../../hooks';
|
||||
import { NitroInput } from '../../../../layout';
|
||||
import { WiredExtraBaseView } from './WiredExtraBaseView';
|
||||
|
||||
const MAX_NAME_LENGTH = 40;
|
||||
|
||||
const normalizeVariableName = (value: string) =>
|
||||
{
|
||||
let normalizedValue = (value ?? '').trim().replace(/[\t\r\n]/g, '');
|
||||
|
||||
if(normalizedValue.includes('=')) normalizedValue = normalizedValue.substring(0, normalizedValue.indexOf('=')).trim();
|
||||
|
||||
while(normalizedValue.startsWith('@') || normalizedValue.startsWith('~'))
|
||||
{
|
||||
normalizedValue = normalizedValue.substring(1).trim();
|
||||
}
|
||||
|
||||
normalizedValue = normalizedValue.replace(/[^A-Za-z0-9_]/g, '');
|
||||
|
||||
return normalizedValue.slice(0, MAX_NAME_LENGTH);
|
||||
};
|
||||
|
||||
export const WiredExtraContextVariableView: FC<{}> = () =>
|
||||
{
|
||||
const { trigger = null, setIntParams = null, setStringParam = null } = useWired();
|
||||
const [ variableName, setVariableName ] = useState('');
|
||||
const [ hasValue, setHasValue ] = useState(false);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!trigger) return;
|
||||
|
||||
setVariableName(normalizeVariableName(trigger.stringData));
|
||||
setHasValue((trigger.intData.length > 0) ? (trigger.intData[0] === 1) : false);
|
||||
}, [ trigger ]);
|
||||
|
||||
const save = () =>
|
||||
{
|
||||
setStringParam(normalizeVariableName(variableName));
|
||||
setIntParams([ hasValue ? 1 : 0 ]);
|
||||
};
|
||||
|
||||
return (
|
||||
<WiredExtraBaseView hasSpecialInput={ true } requiresFurni={ WiredFurniType.STUFF_SELECTION_OPTION_NONE } save={ save } cardStyle={ { width: 400 } }>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<Text>{ LocalizeText('wiredfurni.params.variables.variable_name') }</Text>
|
||||
<NitroInput maxLength={ MAX_NAME_LENGTH } type="text" value={ variableName } onChange={ event => setVariableName(normalizeVariableName(event.target.value)) } />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<Text>{ LocalizeText('wiredfurni.params.variables.settings') }</Text>
|
||||
<label className="flex items-center gap-1 cursor-pointer">
|
||||
<input checked={ hasValue } className="form-check-input" type="checkbox" onChange={ event => setHasValue(event.target.checked) } />
|
||||
<Text>{ LocalizeText('wiredfurni.params.variables.settings.has_value') }</Text>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</WiredExtraBaseView>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,315 @@
|
||||
import { FC, useEffect, useMemo, useState } from 'react';
|
||||
import { LocalizeText, 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 globalVariableIcon from '../../../../assets/images/wired/var/icon_source_global.png';
|
||||
import userVariableIcon from '../../../../assets/images/wired/var/icon_source_user.png';
|
||||
import { Text } from '../../../../common';
|
||||
import { useWired, useWiredTools } from '../../../../hooks';
|
||||
import { NitroInput } from '../../../../layout';
|
||||
import { WiredFurniSelectionSourceRow } from '../WiredFurniSelectionSourceRow';
|
||||
import { WiredVariablePicker } from '../WiredVariablePicker';
|
||||
import { buildWiredVariablePickerEntries, createFallbackVariableEntry, flattenWiredVariablePickerEntries, normalizeVariableTokenFromWire, WiredVariablePickerTarget } from '../WiredVariablePickerData';
|
||||
import { CLICKED_USER_SOURCE, sortWiredSourceOptions, USER_SOURCES, useAvailableUserSources, WiredSourceOption } from '../WiredSourcesSelector';
|
||||
import { WiredExtraBaseView } from './WiredExtraBaseView';
|
||||
|
||||
type VariableTargetType = 'user' | 'furni' | 'global' | 'context';
|
||||
type AmountMode = 'constant' | 'variable';
|
||||
|
||||
interface IVariableDefinition
|
||||
{
|
||||
availability: number;
|
||||
hasValue: boolean;
|
||||
itemId: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface WiredExtraFilterByVariableViewProps
|
||||
{
|
||||
target: 'user' | 'furni';
|
||||
}
|
||||
|
||||
const TARGET_USER = 0;
|
||||
const TARGET_FURNI = 1;
|
||||
const TARGET_CONTEXT = 2;
|
||||
const TARGET_GLOBAL = 3;
|
||||
const AMOUNT_CONSTANT = 0;
|
||||
const AMOUNT_VARIABLE = 1;
|
||||
const SOURCE_TRIGGER = 0;
|
||||
const SOURCE_SECONDARY_SELECTED = 101;
|
||||
|
||||
const TARGET_BUTTONS: Array<{ key: VariableTargetType; icon: string; disabled?: boolean; }> = [
|
||||
{ key: 'furni', icon: furniVariableIcon },
|
||||
{ key: 'user', icon: userVariableIcon },
|
||||
{ key: 'global', icon: globalVariableIcon },
|
||||
{ key: 'context', icon: contextVariableIcon }
|
||||
];
|
||||
|
||||
const SORT_OPTIONS = [ 0, 1, 2, 3, 4, 5 ];
|
||||
|
||||
const SECONDARY_FURNI_SOURCES: WiredSourceOption[] = sortWiredSourceOptions([
|
||||
{ value: SOURCE_TRIGGER, 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' }
|
||||
], 'furni');
|
||||
|
||||
const GLOBAL_SOURCE_OPTIONS: WiredSourceOption[] = [ { value: SOURCE_TRIGGER, label: 'wiredfurni.params.sources.global' } ];
|
||||
const CONTEXT_SOURCE_OPTIONS: WiredSourceOption[] = [ { value: SOURCE_TRIGGER, label: 'Current execution' } ];
|
||||
|
||||
const parseStringData = (value: string) => (value?.length ? value.split('\t', -1) : []);
|
||||
|
||||
const normalizeTargetType = (value: number): VariableTargetType =>
|
||||
{
|
||||
switch(value)
|
||||
{
|
||||
case TARGET_FURNI: return 'furni';
|
||||
case TARGET_GLOBAL: return 'global';
|
||||
case TARGET_CONTEXT: return 'context';
|
||||
default: return 'user';
|
||||
}
|
||||
};
|
||||
|
||||
const getTargetValue = (value: VariableTargetType) =>
|
||||
{
|
||||
switch(value)
|
||||
{
|
||||
case 'furni': return TARGET_FURNI;
|
||||
case 'global': return TARGET_GLOBAL;
|
||||
case 'context': return TARGET_CONTEXT;
|
||||
default: return TARGET_USER;
|
||||
}
|
||||
};
|
||||
|
||||
const getReferenceDefinitions = (targetType: VariableTargetType, userDefinitions: IVariableDefinition[], furniDefinitions: IVariableDefinition[], roomDefinitions: IVariableDefinition[], contextDefinitions: IVariableDefinition[]) =>
|
||||
{
|
||||
switch(targetType)
|
||||
{
|
||||
case 'furni': return furniDefinitions;
|
||||
case 'global': return roomDefinitions;
|
||||
case 'context': return contextDefinitions;
|
||||
default: return userDefinitions;
|
||||
}
|
||||
};
|
||||
|
||||
const resolveSourceOptions = (baseOptions: WiredSourceOption[], selectedValue: number, fallbackOptions: WiredSourceOption[]) =>
|
||||
{
|
||||
if(!baseOptions.length) return baseOptions;
|
||||
if(baseOptions.some(option => (option.value === selectedValue))) return baseOptions;
|
||||
|
||||
const fallbackOption = fallbackOptions.find(option => (option.value === selectedValue));
|
||||
|
||||
return fallbackOption ? [ ...baseOptions, fallbackOption ] : baseOptions;
|
||||
};
|
||||
|
||||
export const WiredExtraFilterByVariableView: FC<WiredExtraFilterByVariableViewProps> = ({ target }) =>
|
||||
{
|
||||
const { trigger = null, furniIds = [], setFurniIds = null, setIntParams = null, setStringParam = null } = useWired();
|
||||
const { userVariableDefinitions = [], furniVariableDefinitions = [], roomVariableDefinitions = [], contextVariableDefinitions = [] } = useWiredTools();
|
||||
const [ variableToken, setVariableToken ] = useState('');
|
||||
const [ sortBy, setSortBy ] = useState(0);
|
||||
const [ amountMode, setAmountMode ] = useState<AmountMode>('constant');
|
||||
const [ amountInput, setAmountInput ] = useState('1');
|
||||
const [ referenceTargetType, setReferenceTargetType ] = useState<VariableTargetType>('user');
|
||||
const [ referenceVariableToken, setReferenceVariableToken ] = useState('');
|
||||
const [ referenceUserSource, setReferenceUserSource ] = useState(SOURCE_TRIGGER);
|
||||
const [ referenceFurniSource, setReferenceFurniSource ] = useState(SOURCE_TRIGGER);
|
||||
const [ referenceFurniIds, setReferenceFurniIds ] = useState<number[]>([]);
|
||||
|
||||
const availableUserSources = useAvailableUserSources(trigger, USER_SOURCES);
|
||||
const orderedUserSources = useMemo(() => sortWiredSourceOptions(availableUserSources, 'users'), [ availableUserSources ]);
|
||||
const userSourceFallbackOptions = useMemo(() => sortWiredSourceOptions([ ...USER_SOURCES, CLICKED_USER_SOURCE ], 'users'), []);
|
||||
|
||||
const mainDefinitions = target === 'user' ? userVariableDefinitions : furniVariableDefinitions;
|
||||
const mainEntries = useMemo(() => buildWiredVariablePickerEntries(target as WiredVariablePickerTarget, 'filter-main', mainDefinitions), [ mainDefinitions, target ]);
|
||||
const resolvedMainEntries = useMemo(() =>
|
||||
{
|
||||
if(!variableToken) return mainEntries;
|
||||
if(flattenWiredVariablePickerEntries(mainEntries).some(entry => (entry.token === variableToken))) return mainEntries;
|
||||
|
||||
const fallbackEntry = createFallbackVariableEntry(target as WiredVariablePickerTarget, variableToken);
|
||||
return fallbackEntry ? [ fallbackEntry, ...mainEntries ] : mainEntries;
|
||||
}, [ mainEntries, target, variableToken ]);
|
||||
|
||||
const referenceDefinitions = useMemo(() => getReferenceDefinitions(referenceTargetType, userVariableDefinitions, furniVariableDefinitions, roomVariableDefinitions, contextVariableDefinitions), [ contextVariableDefinitions, furniVariableDefinitions, referenceTargetType, roomVariableDefinitions, userVariableDefinitions ]);
|
||||
const referenceEntries = useMemo(() => buildWiredVariablePickerEntries(referenceTargetType, 'change-reference', referenceDefinitions), [ referenceDefinitions, referenceTargetType ]);
|
||||
const resolvedReferenceEntries = useMemo(() =>
|
||||
{
|
||||
if(!referenceVariableToken) return referenceEntries;
|
||||
if(flattenWiredVariablePickerEntries(referenceEntries).some(entry => (entry.token === referenceVariableToken))) return referenceEntries;
|
||||
|
||||
const fallbackEntry = createFallbackVariableEntry(referenceTargetType, referenceVariableToken);
|
||||
return fallbackEntry ? [ fallbackEntry, ...referenceEntries ] : referenceEntries;
|
||||
}, [ referenceEntries, referenceTargetType, referenceVariableToken ]);
|
||||
|
||||
const referenceSelectionEnabled = amountMode === 'variable' && referenceTargetType === 'furni' && referenceFurniSource === SOURCE_SECONDARY_SELECTED;
|
||||
const selectionLimit = trigger?.maximumItemSelectionCount ?? 0;
|
||||
const requiresFurni = referenceSelectionEnabled ? WiredFurniType.STUFF_SELECTION_OPTION_BY_ID : WiredFurniType.STUFF_SELECTION_OPTION_NONE;
|
||||
const selectedReferenceSourceValue = referenceTargetType === 'furni' ? referenceFurniSource : ((referenceTargetType === 'global' || referenceTargetType === 'context') ? SOURCE_TRIGGER : referenceUserSource);
|
||||
|
||||
const referenceSourceOptions = useMemo(() =>
|
||||
{
|
||||
if(referenceTargetType === 'furni') return resolveSourceOptions(SECONDARY_FURNI_SOURCES, selectedReferenceSourceValue, SECONDARY_FURNI_SOURCES);
|
||||
if(referenceTargetType === 'global') return GLOBAL_SOURCE_OPTIONS;
|
||||
if(referenceTargetType === 'context') return CONTEXT_SOURCE_OPTIONS;
|
||||
|
||||
return resolveSourceOptions(orderedUserSources, selectedReferenceSourceValue, userSourceFallbackOptions);
|
||||
}, [ orderedUserSources, referenceTargetType, selectedReferenceSourceValue, userSourceFallbackOptions ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!trigger) return;
|
||||
|
||||
const stringParts = parseStringData(trigger.stringData);
|
||||
const nextReferenceFurniIds = [ ...(trigger.selectedItems ?? []) ];
|
||||
|
||||
setVariableToken(normalizeVariableTokenFromWire((stringParts.length > 0) ? stringParts[0] : ''));
|
||||
setReferenceVariableToken(normalizeVariableTokenFromWire((stringParts.length > 1) ? stringParts[1] : ''));
|
||||
setSortBy((trigger.intData.length > 0) ? trigger.intData[0] : 0);
|
||||
setAmountMode(((trigger.intData.length > 1) ? trigger.intData[1] : AMOUNT_CONSTANT) === AMOUNT_VARIABLE ? 'variable' : 'constant');
|
||||
setAmountInput(((trigger.intData.length > 2) ? trigger.intData[2] : 1).toString());
|
||||
setReferenceTargetType(normalizeTargetType((trigger.intData.length > 3) ? trigger.intData[3] : TARGET_USER));
|
||||
setReferenceUserSource((trigger.intData.length > 4) ? trigger.intData[4] : SOURCE_TRIGGER);
|
||||
setReferenceFurniSource((trigger.intData.length > 5) ? trigger.intData[5] : (nextReferenceFurniIds.length ? SOURCE_SECONDARY_SELECTED : SOURCE_TRIGGER));
|
||||
setReferenceFurniIds(nextReferenceFurniIds);
|
||||
setFurniIds(nextReferenceFurniIds);
|
||||
}, [ setFurniIds, trigger ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(referenceSelectionEnabled) setReferenceFurniIds([ ...furniIds ]);
|
||||
}, [ furniIds, referenceSelectionEnabled ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(referenceTargetType !== 'user') return;
|
||||
if(orderedUserSources.some(option => (option.value === referenceUserSource))) return;
|
||||
|
||||
setReferenceUserSource(SOURCE_TRIGGER);
|
||||
}, [ orderedUserSources, referenceTargetType, referenceUserSource ]);
|
||||
|
||||
const save = () =>
|
||||
{
|
||||
const parsedAmount = parseInt(amountInput.trim(), 10);
|
||||
const nextReferenceFurniIds = referenceSelectionEnabled ? [ ...furniIds ] : [ ...referenceFurniIds ];
|
||||
|
||||
setReferenceFurniIds(nextReferenceFurniIds);
|
||||
setStringParam(`${ variableToken || '' }\t${ amountMode === 'variable' ? referenceVariableToken : '' }`);
|
||||
setIntParams([
|
||||
sortBy,
|
||||
amountMode === 'variable' ? AMOUNT_VARIABLE : AMOUNT_CONSTANT,
|
||||
Number.isFinite(parsedAmount) ? parsedAmount : 0,
|
||||
getTargetValue(referenceTargetType),
|
||||
referenceUserSource,
|
||||
referenceFurniSource
|
||||
]);
|
||||
setFurniIds(referenceSelectionEnabled ? nextReferenceFurniIds : []);
|
||||
};
|
||||
|
||||
const validate = () =>
|
||||
{
|
||||
if(!variableToken) return false;
|
||||
if(amountMode === 'variable' && !referenceVariableToken) return false;
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleReferenceTargetChange = (targetType: VariableTargetType) =>
|
||||
{
|
||||
if(targetType === referenceTargetType) return;
|
||||
|
||||
if(referenceTargetType === 'furni') setFurniIds([]);
|
||||
|
||||
setReferenceTargetType(targetType);
|
||||
setReferenceVariableToken('');
|
||||
};
|
||||
|
||||
return (
|
||||
<WiredExtraBaseView hasSpecialInput={ true } requiresFurni={ requiresFurni } save={ save } validate={ validate } cardStyle={ { width: 260 } }>
|
||||
<div className="nitro-wired__give-var">
|
||||
<div className="flex flex-col gap-1">
|
||||
<Text>{ LocalizeText('wiredfurni.params.variables.variable_selection') }</Text>
|
||||
<WiredVariablePicker
|
||||
entries={ resolvedMainEntries }
|
||||
recentScope={ `variable-extra-${ target }` }
|
||||
selectedToken={ variableToken }
|
||||
onSelect={ entry => setVariableToken(entry.token) } />
|
||||
</div>
|
||||
|
||||
<div className="nitro-wired__divider" />
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<Text>{ LocalizeText('wiredfurni.params.variables.sort_by') }</Text>
|
||||
<select className="form-select form-select-sm" value={ sortBy } onChange={ event => setSortBy(parseInt(event.target.value, 10) || 0) }>
|
||||
{ SORT_OPTIONS.map(option => <option key={ option } value={ option }>{ LocalizeText(`wiredfurni.params.variables.sort_by.${ option }`) }</option>) }
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="nitro-wired__divider" />
|
||||
|
||||
<div className="nitro-wired__give-var-section">
|
||||
<div className="nitro-wired__give-var-section-title">{ LocalizeText('wiredfurni.params.setfilter') }</div>
|
||||
<label className="nitro-wired__change-var-radio">
|
||||
<input checked={ amountMode === 'constant' } type="radio" onChange={ () => setAmountMode('constant') } />
|
||||
<Text>{ LocalizeText('wiredfurni.params.variables.reference_value.set_value') }</Text>
|
||||
<NitroInput className="nitro-wired__give-var-number" type="number" value={ amountInput } onChange={ event => setAmountInput(event.target.value) } />
|
||||
</label>
|
||||
|
||||
<div className="nitro-wired__change-var-reference-block">
|
||||
<label className="nitro-wired__change-var-radio">
|
||||
<input checked={ amountMode === 'variable' } type="radio" onChange={ () => setAmountMode('variable') } />
|
||||
<Text>{ LocalizeText('wiredfurni.params.variables.reference_value.from_variable') }</Text>
|
||||
<div className="nitro-wired__give-var-targets">
|
||||
{ TARGET_BUTTONS.map(button => (
|
||||
<button
|
||||
key={ `reference-${ button.key }` }
|
||||
type="button"
|
||||
disabled={ button.disabled || (amountMode !== 'variable') }
|
||||
className={ `nitro-wired__give-var-target nitro-wired__give-var-target--${ button.key } ${ referenceTargetType === button.key ? 'is-active' : '' }` }
|
||||
onClick={ () => handleReferenceTargetChange(button.key) }>
|
||||
<img src={ button.icon } alt={ button.key } />
|
||||
</button>
|
||||
)) }
|
||||
</div>
|
||||
</label>
|
||||
|
||||
{ amountMode === 'variable' &&
|
||||
<WiredVariablePicker
|
||||
entries={ resolvedReferenceEntries }
|
||||
recentScope={ `variable-extra-reference-${ target }` }
|
||||
selectedToken={ referenceVariableToken }
|
||||
onSelect={ entry => setReferenceVariableToken(entry.token) } /> }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ amountMode === 'variable' &&
|
||||
<>
|
||||
<div className="nitro-wired__divider" />
|
||||
<WiredFurniSelectionSourceRow
|
||||
title="wiredfurni.params.sources.merged.title.variables_reference"
|
||||
options={ referenceSourceOptions }
|
||||
value={ selectedReferenceSourceValue }
|
||||
selectionKind="primary"
|
||||
selectionActive={ true }
|
||||
selectionCount={ referenceSelectionEnabled ? furniIds.length : referenceFurniIds.length }
|
||||
selectionLimit={ selectionLimit }
|
||||
selectionEnabledValues={ [ SOURCE_SECONDARY_SELECTED ] }
|
||||
showSelectionToggle={ false }
|
||||
onChange={ value =>
|
||||
{
|
||||
if(referenceTargetType === 'furni')
|
||||
{
|
||||
if(referenceFurniSource === SOURCE_SECONDARY_SELECTED) setReferenceFurniIds([ ...furniIds ]);
|
||||
|
||||
setReferenceFurniSource(value);
|
||||
setFurniIds(value === SOURCE_SECONDARY_SELECTED ? [ ...referenceFurniIds ] : []);
|
||||
return;
|
||||
}
|
||||
|
||||
if(referenceTargetType === 'user') setReferenceUserSource(value);
|
||||
} } />
|
||||
</> }
|
||||
</div>
|
||||
</WiredExtraBaseView>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
import { FC } from 'react';
|
||||
import { WiredExtraFilterByVariableView } from './WiredExtraFilterByVariableView';
|
||||
|
||||
export const WiredExtraFilterFurniByVariableView: FC<{}> = () =>
|
||||
{
|
||||
return <WiredExtraFilterByVariableView target="furni" />;
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
import { FC } from 'react';
|
||||
import { WiredExtraFilterByVariableView } from './WiredExtraFilterByVariableView';
|
||||
|
||||
export const WiredExtraFilterUsersByVariableView: FC<{}> = () =>
|
||||
{
|
||||
return <WiredExtraFilterByVariableView target="user" />;
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import { FC } from 'react';
|
||||
import { LocalizeText } from '../../../../api';
|
||||
import { WiredExtraVariableView } from './WiredExtraUserVariableView';
|
||||
|
||||
export const WiredExtraFurniVariableView: FC<{}> = () =>
|
||||
{
|
||||
return <WiredExtraVariableView availabilityRadioName="wiredFurniVariableAvailability" availabilityRoomText={ LocalizeText('wiredfurni.params.variables.availability.1') } availabilityRoomValue={ 1 } />;
|
||||
};
|
||||
@@ -0,0 +1,86 @@
|
||||
import { FC, useEffect, useMemo, useState } from 'react';
|
||||
import { LocalizeText, WiredFurniType } from '../../../../api';
|
||||
import { Text } from '../../../../common';
|
||||
import { useWired } from '../../../../hooks';
|
||||
import { NitroInput } from '../../../../layout';
|
||||
import { WiredExtraBaseView } from './WiredExtraBaseView';
|
||||
|
||||
const AVAILABILITY_ROOM_ACTIVE = 1;
|
||||
const AVAILABILITY_PERMANENT = 10;
|
||||
const AVAILABILITY_SHARED = 11;
|
||||
const MAX_NAME_LENGTH = 40;
|
||||
|
||||
const normalizeVariableName = (value: string) =>
|
||||
{
|
||||
let normalizedValue = (value ?? '').trim().replace(/[\t\r\n]/g, '');
|
||||
|
||||
if(normalizedValue.includes('=')) normalizedValue = normalizedValue.substring(0, normalizedValue.indexOf('=')).trim();
|
||||
|
||||
while(normalizedValue.startsWith('@') || normalizedValue.startsWith('~'))
|
||||
{
|
||||
normalizedValue = normalizedValue.substring(1).trim();
|
||||
}
|
||||
|
||||
normalizedValue = normalizedValue.replace(/[^A-Za-z0-9_]/g, '');
|
||||
|
||||
return normalizedValue.slice(0, MAX_NAME_LENGTH);
|
||||
};
|
||||
|
||||
export const WiredExtraRoomVariableView: FC<{}> = () =>
|
||||
{
|
||||
const { trigger = null, setIntParams = null, setStringParam = null } = useWired();
|
||||
const [ variableName, setVariableName ] = useState('');
|
||||
const [ availability, setAvailability ] = useState(AVAILABILITY_ROOM_ACTIVE);
|
||||
const [ currentValue, setCurrentValue ] = useState(0);
|
||||
|
||||
const normalizedCurrentValue = useMemo(() => (Number.isFinite(currentValue) ? currentValue : 0), [ currentValue ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!trigger) return;
|
||||
|
||||
setVariableName(normalizeVariableName(trigger.stringData));
|
||||
const nextAvailability = (trigger.intData.length > 0) ? trigger.intData[0] : AVAILABILITY_ROOM_ACTIVE;
|
||||
|
||||
setAvailability((nextAvailability === AVAILABILITY_PERMANENT || nextAvailability === AVAILABILITY_SHARED) ? nextAvailability : AVAILABILITY_ROOM_ACTIVE);
|
||||
setCurrentValue((trigger.intData.length > 1) ? trigger.intData[1] : 0);
|
||||
}, [ trigger ]);
|
||||
|
||||
const save = () =>
|
||||
{
|
||||
setStringParam(normalizeVariableName(variableName));
|
||||
setIntParams([ availability, normalizedCurrentValue ]);
|
||||
};
|
||||
|
||||
return (
|
||||
<WiredExtraBaseView hasSpecialInput={ true } requiresFurni={ WiredFurniType.STUFF_SELECTION_OPTION_NONE } save={ save } cardStyle={ { width: 400 } }>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<Text>{ LocalizeText('wiredfurni.params.variables.variable_name') }</Text>
|
||||
<NitroInput maxLength={ MAX_NAME_LENGTH } type="text" value={ variableName } onChange={ event => setVariableName(normalizeVariableName(event.target.value)) } />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<Text>{ LocalizeText('wiredfurni.params.variables.availability') }</Text>
|
||||
<label className="flex items-center gap-1 cursor-pointer">
|
||||
<input checked={ (availability === AVAILABILITY_ROOM_ACTIVE) } className="form-check-input" name="wiredRoomVariableAvailability" type="radio" onChange={ () => setAvailability(AVAILABILITY_ROOM_ACTIVE) } />
|
||||
<Text>{ LocalizeText('wiredfurni.params.variables.availability.1') }</Text>
|
||||
</label>
|
||||
<label className="flex items-center gap-1 cursor-pointer">
|
||||
<input checked={ (availability === AVAILABILITY_PERMANENT) } className="form-check-input" name="wiredRoomVariableAvailability" type="radio" onChange={ () => setAvailability(AVAILABILITY_PERMANENT) } />
|
||||
<Text>{ LocalizeText('wiredfurni.params.variables.availability.10') }</Text>
|
||||
</label>
|
||||
<label className="flex items-center gap-1 cursor-pointer">
|
||||
<input checked={ (availability === AVAILABILITY_SHARED) } className="form-check-input" name="wiredRoomVariableAvailability" type="radio" onChange={ () => setAvailability(AVAILABILITY_SHARED) } />
|
||||
<Text>{ LocalizeText('wiredfurni.params.variables.availability.11') }</Text>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<Text>{ LocalizeText('wiredfurni.params.variables.inspection') }</Text>
|
||||
<Text>{ LocalizeText('wiredfurni.params.variables.inspection.current_value', [ 'value' ], [ normalizedCurrentValue.toString() ]) }</Text>
|
||||
</div>
|
||||
</div>
|
||||
</WiredExtraBaseView>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,155 @@
|
||||
import { FC, useEffect, useMemo, useState } from 'react';
|
||||
import { LocalizeText, WiredFurniType } from '../../../../api';
|
||||
import { Text } from '../../../../common';
|
||||
import { useWired, useWiredTools } from '../../../../hooks';
|
||||
import { NitroInput } from '../../../../layout';
|
||||
import { WiredVariablePicker } from '../WiredVariablePicker';
|
||||
import { buildWiredVariablePickerEntries, createFallbackVariableEntry, flattenWiredVariablePickerEntries, getCustomVariableItemId, isCustomVariableToken, normalizeVariableTokenFromWire } from '../WiredVariablePickerData';
|
||||
import { WiredExtraBaseView } from './WiredExtraBaseView';
|
||||
import { WiredPlaceholderPreview } from './WiredPlaceholderPreview';
|
||||
|
||||
interface IVariableDefinition
|
||||
{
|
||||
hasValue: boolean;
|
||||
isTextConnected: boolean;
|
||||
itemId: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const DISPLAY_NUMERIC = 1;
|
||||
const DISPLAY_TEXTUAL = 2;
|
||||
const DEFAULT_CAPTURER_NAME = '';
|
||||
const MAX_CAPTURER_NAME_LENGTH = 32;
|
||||
const PLACEHOLDER_WRAPPER_PATTERN = /^#(.*)#$/;
|
||||
|
||||
const splitStringData = (value: string) =>
|
||||
{
|
||||
if(!value?.length) return [ '', DEFAULT_CAPTURER_NAME ];
|
||||
|
||||
const parts = value.split('\t');
|
||||
|
||||
if(parts.length === 1) return [ parts[0], DEFAULT_CAPTURER_NAME ];
|
||||
|
||||
return [ parts[0], parts[1] ];
|
||||
};
|
||||
|
||||
const normalizeDisplayType = (value: number) => ((value === DISPLAY_TEXTUAL) ? DISPLAY_TEXTUAL : DISPLAY_NUMERIC);
|
||||
const normalizeCapturerName = (value: string) =>
|
||||
{
|
||||
let normalizedValue = (value ?? '').trim().replace(/[\t\r\n]/g, '');
|
||||
|
||||
if(PLACEHOLDER_WRAPPER_PATTERN.test(normalizedValue))
|
||||
{
|
||||
normalizedValue = normalizedValue.substring(1, normalizedValue.length - 1).trim();
|
||||
}
|
||||
|
||||
return normalizedValue.slice(0, MAX_CAPTURER_NAME_LENGTH);
|
||||
};
|
||||
|
||||
const escapeHtml = (value: string) => value
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
|
||||
export const WiredExtraTextInputVariableView: FC<{}> = () =>
|
||||
{
|
||||
const { trigger = null, setIntParams = null, setStringParam = null } = useWired();
|
||||
const { contextVariableDefinitions = [] } = useWiredTools();
|
||||
const [ variableToken, setVariableToken ] = useState('');
|
||||
const [ capturerName, setCapturerName ] = useState(DEFAULT_CAPTURER_NAME);
|
||||
const [ displayType, setDisplayType ] = useState(DISPLAY_NUMERIC);
|
||||
|
||||
const targetDefinitions = useMemo(() => contextVariableDefinitions.filter(definition => !!definition.hasValue), [ contextVariableDefinitions ]);
|
||||
const variableEntries = useMemo(() => buildWiredVariablePickerEntries('context', 'change-reference', targetDefinitions).filter(entry => (entry.kind === 'custom')), [ targetDefinitions ]);
|
||||
const resolvedVariableEntries = useMemo(() =>
|
||||
{
|
||||
if(!variableToken || !isCustomVariableToken(variableToken)) return variableEntries;
|
||||
if(flattenWiredVariablePickerEntries(variableEntries).some(entry => (entry.token === variableToken))) return variableEntries;
|
||||
|
||||
const fallbackEntry = createFallbackVariableEntry('context', variableToken);
|
||||
|
||||
return (fallbackEntry && (fallbackEntry.kind === 'custom')) ? [ fallbackEntry, ...variableEntries ] : variableEntries;
|
||||
}, [ variableEntries, variableToken ]);
|
||||
|
||||
const selectedVariableDefinition = useMemo(() =>
|
||||
{
|
||||
if(!isCustomVariableToken(variableToken)) return null;
|
||||
|
||||
const itemId = getCustomVariableItemId(variableToken);
|
||||
|
||||
return (targetDefinitions.find(definition => (definition.itemId === itemId)) ?? null) as IVariableDefinition | null;
|
||||
}, [ targetDefinitions, variableToken ]);
|
||||
|
||||
const canUseTextDisplay = !!selectedVariableDefinition?.isTextConnected;
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!trigger) return;
|
||||
|
||||
const [ nextVariableToken, nextCapturerName ] = splitStringData(trigger.stringData);
|
||||
|
||||
setVariableToken(normalizeVariableTokenFromWire(nextVariableToken));
|
||||
setCapturerName(normalizeCapturerName(nextCapturerName));
|
||||
setDisplayType(normalizeDisplayType((trigger.intData.length > 0) ? trigger.intData[0] : DISPLAY_NUMERIC));
|
||||
}, [ trigger ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(canUseTextDisplay || (displayType !== DISPLAY_TEXTUAL)) return;
|
||||
|
||||
setDisplayType(DISPLAY_NUMERIC);
|
||||
}, [ canUseTextDisplay, displayType ]);
|
||||
|
||||
const previewToken = useMemo(() =>
|
||||
{
|
||||
const effectiveName = normalizeCapturerName(capturerName) || 'capturer';
|
||||
|
||||
return `#${ effectiveName }#`;
|
||||
}, [ capturerName ]);
|
||||
|
||||
const previewHtml = useMemo(() => LocalizeText('wiredfurni.params.texts.placeholder_preview', [ 'placeholder' ], [ escapeHtml(previewToken) ]), [ previewToken ]);
|
||||
|
||||
const save = () =>
|
||||
{
|
||||
const variableItemId = getCustomVariableItemId(variableToken);
|
||||
|
||||
setIntParams([ canUseTextDisplay ? normalizeDisplayType(displayType) : DISPLAY_NUMERIC ]);
|
||||
setStringParam(`${ variableItemId ? String(variableItemId) : '' }\t${ normalizeCapturerName(capturerName) }`);
|
||||
};
|
||||
|
||||
const validate = () => !!normalizeCapturerName(capturerName).length && (getCustomVariableItemId(variableToken) > 0);
|
||||
|
||||
return (
|
||||
<WiredExtraBaseView hasSpecialInput={ true } requiresFurni={ WiredFurniType.STUFF_SELECTION_OPTION_NONE } save={ save } validate={ validate } cardStyle={ { width: 400 } }>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<Text>{ LocalizeText('wiredfurni.params.texts.capturer_name') }</Text>
|
||||
<NitroInput maxLength={ MAX_CAPTURER_NAME_LENGTH } type="text" value={ capturerName } onChange={ event => setCapturerName(normalizeCapturerName(event.target.value)) } />
|
||||
</div>
|
||||
|
||||
<WiredPlaceholderPreview previewHtml={ previewHtml } previewToken={ previewToken } />
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<Text>{ LocalizeText('wiredfurni.params.variables.variable_selection') }</Text>
|
||||
<WiredVariablePicker entries={ resolvedVariableEntries } recentScope="variable-text-input" selectedToken={ variableToken } onSelect={ entry => setVariableToken(entry.token) } />
|
||||
{ !targetDefinitions.length && <Text small>{ 'No wf_var_context variables with value found in this room.' }</Text> }
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<Text>{ LocalizeText('wiredfurni.params.texts.variable_input_type') }</Text>
|
||||
<label className="flex items-center gap-1 cursor-pointer">
|
||||
<input checked={ (displayType === DISPLAY_NUMERIC) } className="form-check-input" name="wiredTextInputVariableType" type="radio" onChange={ () => setDisplayType(DISPLAY_NUMERIC) } />
|
||||
<Text>{ LocalizeText('wiredfurni.params.texts.variable_display_type.1') }</Text>
|
||||
</label>
|
||||
<label className="flex items-center gap-1 cursor-pointer">
|
||||
<input checked={ (displayType === DISPLAY_TEXTUAL) } className="form-check-input" disabled={ !canUseTextDisplay } name="wiredTextInputVariableType" type="radio" onChange={ () => setDisplayType(DISPLAY_TEXTUAL) } />
|
||||
<Text>{ LocalizeText('wiredfurni.params.texts.variable_display_type.2') }</Text>
|
||||
</label>
|
||||
<Text small>{ LocalizeText('wiredfurni.params.texts.variable_display_type.2.info') }</Text>
|
||||
</div>
|
||||
</div>
|
||||
</WiredExtraBaseView>
|
||||
);
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import { useWired } from '../../../../hooks';
|
||||
import { NitroInput } from '../../../../layout';
|
||||
import { WiredSourcesSelector } from '../WiredSourcesSelector';
|
||||
import { WiredExtraBaseView } from './WiredExtraBaseView';
|
||||
import { WiredPlaceholderPreview } from './WiredPlaceholderPreview';
|
||||
|
||||
const TYPE_SINGLE = 1;
|
||||
const TYPE_MULTIPLE = 2;
|
||||
@@ -100,7 +101,7 @@ export const WiredExtraTextOutputFurniNameView: FC<{}> = () =>
|
||||
<Text>{ LocalizeText('wiredfurni.params.texts.placeholder_name') }</Text>
|
||||
<NitroInput maxLength={ MAX_PLACEHOLDER_NAME_LENGTH } type="text" value={ placeholderName } onChange={ event => setPlaceholderName(normalizePlaceholderName(event.target.value)) } />
|
||||
</div>
|
||||
<Text dangerouslySetInnerHTML={ { __html: previewHtml } } />
|
||||
<WiredPlaceholderPreview previewHtml={ previewHtml } previewToken={ previewToken } />
|
||||
<div className="flex flex-col gap-1">
|
||||
<Text>{ LocalizeText('wiredfurni.params.texts.placeholder_type') }</Text>
|
||||
<label className="flex items-center gap-1 cursor-pointer">
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useWired } from '../../../../hooks';
|
||||
import { NitroInput } from '../../../../layout';
|
||||
import { WiredSourcesSelector, CLICKED_USER_SOURCE_VALUE } from '../WiredSourcesSelector';
|
||||
import { WiredExtraBaseView } from './WiredExtraBaseView';
|
||||
import { WiredPlaceholderPreview } from './WiredPlaceholderPreview';
|
||||
|
||||
const TYPE_SINGLE = 1;
|
||||
const TYPE_MULTIPLE = 2;
|
||||
@@ -100,7 +101,7 @@ export const WiredExtraTextOutputUsernameView: FC<{}> = () =>
|
||||
<Text>{ LocalizeText('wiredfurni.params.texts.placeholder_name') }</Text>
|
||||
<NitroInput maxLength={ MAX_PLACEHOLDER_NAME_LENGTH } type="text" value={ placeholderName } onChange={ event => setPlaceholderName(normalizePlaceholderName(event.target.value)) } />
|
||||
</div>
|
||||
<Text dangerouslySetInnerHTML={ { __html: previewHtml } } />
|
||||
<WiredPlaceholderPreview previewHtml={ previewHtml } previewToken={ previewToken } />
|
||||
<div className="flex flex-col gap-1">
|
||||
<Text>{ LocalizeText('wiredfurni.params.texts.placeholder_type') }</Text>
|
||||
<label className="flex items-center gap-1 cursor-pointer">
|
||||
|
||||
@@ -0,0 +1,318 @@
|
||||
import { FC, useEffect, useMemo, useState } from 'react';
|
||||
import { LocalizeText, 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 globalVariableIcon from '../../../../assets/images/wired/var/icon_source_global.png';
|
||||
import userVariableIcon from '../../../../assets/images/wired/var/icon_source_user.png';
|
||||
import { Text } from '../../../../common';
|
||||
import { useWired, useWiredTools } from '../../../../hooks';
|
||||
import { NitroInput } from '../../../../layout';
|
||||
import { WiredVariablePicker } from '../WiredVariablePicker';
|
||||
import { buildWiredVariablePickerEntries, createFallbackVariableEntry, flattenWiredVariablePickerEntries, getCustomVariableItemId, isCustomVariableToken, normalizeVariableTokenFromWire } from '../WiredVariablePickerData';
|
||||
import { WiredFurniSelectionSourceRow } from '../WiredFurniSelectionSourceRow';
|
||||
import { CLICKED_USER_SOURCE_VALUE, WiredSourcesSelector } from '../WiredSourcesSelector';
|
||||
import { WiredExtraBaseView } from './WiredExtraBaseView';
|
||||
import { WiredPlaceholderPreview } from './WiredPlaceholderPreview';
|
||||
|
||||
type VariableTargetType = 'user' | 'furni' | 'global' | 'context';
|
||||
|
||||
interface IVariableDefinition
|
||||
{
|
||||
availability: number;
|
||||
hasValue: boolean;
|
||||
isTextConnected: boolean;
|
||||
itemId: number;
|
||||
isReadOnly?: boolean;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const TARGET_USER = 0;
|
||||
const TARGET_FURNI = 1;
|
||||
const TARGET_CONTEXT = 2;
|
||||
const TARGET_GLOBAL = 3;
|
||||
const DISPLAY_NUMERIC = 1;
|
||||
const DISPLAY_TEXTUAL = 2;
|
||||
const TYPE_SINGLE = 1;
|
||||
const TYPE_MULTIPLE = 2;
|
||||
const DEFAULT_PLACEHOLDER_NAME = '';
|
||||
const DEFAULT_DELIMITER = ', ';
|
||||
const MAX_PLACEHOLDER_NAME_LENGTH = 32;
|
||||
const MAX_DELIMITER_LENGTH = 16;
|
||||
const PLACEHOLDER_WRAPPER_PATTERN = /^\$\((.*)\)$/;
|
||||
|
||||
const TARGET_BUTTONS: Array<{ key: VariableTargetType; icon: string; disabled?: boolean; }> = [
|
||||
{ key: 'furni', icon: furniVariableIcon },
|
||||
{ key: 'user', icon: userVariableIcon },
|
||||
{ key: 'global', icon: globalVariableIcon },
|
||||
{ key: 'context', icon: contextVariableIcon }
|
||||
];
|
||||
|
||||
const normalizeTargetType = (value: number): VariableTargetType =>
|
||||
{
|
||||
switch(value)
|
||||
{
|
||||
case TARGET_FURNI: return 'furni';
|
||||
case TARGET_GLOBAL: return 'global';
|
||||
case TARGET_CONTEXT: return 'context';
|
||||
default: return 'user';
|
||||
}
|
||||
};
|
||||
|
||||
const getTargetValue = (value: VariableTargetType) =>
|
||||
{
|
||||
switch(value)
|
||||
{
|
||||
case 'furni': return TARGET_FURNI;
|
||||
case 'global': return TARGET_GLOBAL;
|
||||
case 'context': return TARGET_CONTEXT;
|
||||
default: return TARGET_USER;
|
||||
}
|
||||
};
|
||||
|
||||
const normalizeDisplayType = (value: number) => ((value === DISPLAY_TEXTUAL) ? DISPLAY_TEXTUAL : DISPLAY_NUMERIC);
|
||||
const normalizePlaceholderType = (value: number) => ((value === TYPE_MULTIPLE) ? TYPE_MULTIPLE : TYPE_SINGLE);
|
||||
const normalizeUserSource = (value: number) => ((value === 0) || (value === 200) || (value === 201) || (value === CLICKED_USER_SOURCE_VALUE) ? value : 0);
|
||||
const normalizeFurniSource = (value: number) => ((value === 0) || (value === 100) || (value === 200) || (value === 201) ? value : 0);
|
||||
const normalizePlaceholderName = (value: string) =>
|
||||
{
|
||||
let normalizedValue = (value ?? '').trim().replace(/[\t\r\n]/g, '');
|
||||
|
||||
if(PLACEHOLDER_WRAPPER_PATTERN.test(normalizedValue))
|
||||
{
|
||||
normalizedValue = normalizedValue.substring(2, normalizedValue.length - 1).trim();
|
||||
}
|
||||
|
||||
return normalizedValue.slice(0, MAX_PLACEHOLDER_NAME_LENGTH);
|
||||
};
|
||||
|
||||
const normalizeDelimiter = (value: string) =>
|
||||
{
|
||||
if(value === undefined || value === null) return DEFAULT_DELIMITER;
|
||||
|
||||
return value.replace(/[\t\r\n]/g, '').slice(0, MAX_DELIMITER_LENGTH);
|
||||
};
|
||||
|
||||
const splitStringData = (value: string) =>
|
||||
{
|
||||
if(!value?.length) return [ '', DEFAULT_PLACEHOLDER_NAME, DEFAULT_DELIMITER ];
|
||||
|
||||
const parts = value.split('\t');
|
||||
|
||||
if(parts.length === 1) return [ parts[0], DEFAULT_PLACEHOLDER_NAME, DEFAULT_DELIMITER ];
|
||||
if(parts.length === 2) return [ parts[0], parts[1], DEFAULT_DELIMITER ];
|
||||
|
||||
return [ parts[0], parts[1], parts[2] ];
|
||||
};
|
||||
|
||||
const escapeHtml = (value: string) => value
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
|
||||
const getTargetDefinitions = (targetType: VariableTargetType, userDefinitions: IVariableDefinition[], furniDefinitions: IVariableDefinition[], roomDefinitions: IVariableDefinition[], contextDefinitions: IVariableDefinition[]) =>
|
||||
{
|
||||
switch(targetType)
|
||||
{
|
||||
case 'furni': return furniDefinitions;
|
||||
case 'global': return roomDefinitions;
|
||||
case 'context': return contextDefinitions;
|
||||
default: return userDefinitions;
|
||||
}
|
||||
};
|
||||
|
||||
const serializeStringData = (variableToken: string, placeholderName: string, delimiter: string) => `${ variableToken || '' }\t${ normalizePlaceholderName(placeholderName) }\t${ normalizeDelimiter(delimiter) }`;
|
||||
|
||||
export const WiredExtraTextOutputVariableView: FC<{}> = () =>
|
||||
{
|
||||
const { trigger = null, furniIds = [], setFurniIds = null, setIntParams = null, setStringParam = null } = useWired();
|
||||
const { userVariableDefinitions = [], furniVariableDefinitions = [], roomVariableDefinitions = [], contextVariableDefinitions = [] } = useWiredTools();
|
||||
const [ targetType, setTargetType ] = useState<VariableTargetType>('user');
|
||||
const [ variableToken, setVariableToken ] = useState('');
|
||||
const [ displayType, setDisplayType ] = useState(DISPLAY_NUMERIC);
|
||||
const [ placeholderType, setPlaceholderType ] = useState(TYPE_SINGLE);
|
||||
const [ placeholderName, setPlaceholderName ] = useState(DEFAULT_PLACEHOLDER_NAME);
|
||||
const [ delimiter, setDelimiter ] = useState(DEFAULT_DELIMITER);
|
||||
const [ userSource, setUserSource ] = useState(0);
|
||||
const [ furniSource, setFurniSource ] = useState(0);
|
||||
|
||||
const targetDefinitions = useMemo(() => getTargetDefinitions(targetType, userVariableDefinitions, furniVariableDefinitions, roomVariableDefinitions, contextVariableDefinitions), [ contextVariableDefinitions, furniVariableDefinitions, roomVariableDefinitions, targetType, userVariableDefinitions ]);
|
||||
const variableEntries = useMemo(() => buildWiredVariablePickerEntries(targetType, 'change-reference', targetDefinitions), [ targetDefinitions, targetType ]);
|
||||
const resolvedVariableEntries = useMemo(() =>
|
||||
{
|
||||
if(!variableToken) return variableEntries;
|
||||
if(flattenWiredVariablePickerEntries(variableEntries).some(entry => (entry.token === variableToken))) return variableEntries;
|
||||
|
||||
const fallbackEntry = createFallbackVariableEntry(targetType, variableToken);
|
||||
|
||||
return fallbackEntry ? [ fallbackEntry, ...variableEntries ] : variableEntries;
|
||||
}, [ targetType, variableEntries, variableToken ]);
|
||||
|
||||
const selectedCustomDefinition = useMemo(() =>
|
||||
{
|
||||
if(!isCustomVariableToken(variableToken)) return null;
|
||||
|
||||
const itemId = getCustomVariableItemId(variableToken);
|
||||
|
||||
return (targetDefinitions.find(definition => (definition.itemId === itemId)) ?? null);
|
||||
}, [ targetDefinitions, variableToken ]);
|
||||
|
||||
const canUseTextDisplay = !!selectedCustomDefinition?.isTextConnected;
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!trigger) return;
|
||||
|
||||
const [ nextVariableToken, nextPlaceholderName, nextDelimiter ] = splitStringData(trigger.stringData);
|
||||
|
||||
setTargetType(normalizeTargetType((trigger.intData.length > 0) ? trigger.intData[0] : TARGET_USER));
|
||||
setVariableToken(normalizeVariableTokenFromWire(nextVariableToken));
|
||||
setDisplayType(normalizeDisplayType((trigger.intData.length > 1) ? trigger.intData[1] : DISPLAY_NUMERIC));
|
||||
setPlaceholderType(normalizePlaceholderType((trigger.intData.length > 2) ? trigger.intData[2] : TYPE_SINGLE));
|
||||
setUserSource(normalizeUserSource((trigger.intData.length > 3) ? trigger.intData[3] : 0));
|
||||
setFurniSource(normalizeFurniSource((trigger.intData.length > 4) ? trigger.intData[4] : 0));
|
||||
setPlaceholderName(normalizePlaceholderName(nextPlaceholderName));
|
||||
setDelimiter(normalizeDelimiter(nextDelimiter));
|
||||
setFurniIds([ ...(trigger.selectedItems ?? []) ]);
|
||||
}, [ setFurniIds, trigger ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(canUseTextDisplay || (displayType !== DISPLAY_TEXTUAL)) return;
|
||||
|
||||
setDisplayType(DISPLAY_NUMERIC);
|
||||
}, [ canUseTextDisplay, displayType ]);
|
||||
|
||||
const previewToken = useMemo(() =>
|
||||
{
|
||||
const effectiveName = normalizePlaceholderName(placeholderName) || 'placeholder';
|
||||
|
||||
return `$(${ effectiveName })`;
|
||||
}, [ placeholderName ]);
|
||||
|
||||
const previewHtml = useMemo(() => LocalizeText('wiredfurni.params.texts.placeholder_preview', [ 'placeholder' ], [ escapeHtml(previewToken) ]), [ previewToken ]);
|
||||
|
||||
const save = () =>
|
||||
{
|
||||
setIntParams([
|
||||
getTargetValue(targetType),
|
||||
(canUseTextDisplay ? normalizeDisplayType(displayType) : DISPLAY_NUMERIC),
|
||||
normalizePlaceholderType(placeholderType),
|
||||
normalizeUserSource(userSource),
|
||||
normalizeFurniSource(furniSource)
|
||||
]);
|
||||
setStringParam(serializeStringData(variableToken, placeholderName, delimiter));
|
||||
setFurniIds(((targetType === 'furni') && (furniSource === 100)) ? [ ...furniIds ] : []);
|
||||
};
|
||||
|
||||
const validate = () =>
|
||||
{
|
||||
return !!variableToken;
|
||||
};
|
||||
|
||||
const footer = useMemo(() =>
|
||||
{
|
||||
if(targetType === 'global')
|
||||
{
|
||||
return <WiredFurniSelectionSourceRow title="wiredfurni.params.sources.merged.title.variables" options={ [ { value: 0, label: 'wiredfurni.params.sources.global' } ] } value={ 0 } selectionKind="primary" selectionActive={ false } selectionCount={ 0 } selectionLimit={ 0 } selectionEnabledValues={ [] } showSelectionToggle={ false } onChange={ () => null } />;
|
||||
}
|
||||
|
||||
if(targetType === 'context')
|
||||
{
|
||||
return <WiredFurniSelectionSourceRow title="wiredfurni.params.sources.merged.title.variables" options={ [ { value: 0, label: 'Current execution' } ] } value={ 0 } selectionKind="primary" selectionActive={ false } selectionCount={ 0 } selectionLimit={ 0 } selectionEnabledValues={ [] } showSelectionToggle={ false } onChange={ () => null } />;
|
||||
}
|
||||
|
||||
return (
|
||||
<WiredSourcesSelector
|
||||
showFurni={ targetType === 'furni' }
|
||||
showUsers={ targetType === 'user' }
|
||||
furniSource={ furniSource }
|
||||
userSource={ userSource }
|
||||
furniTitle="wiredfurni.params.sources.merged.title.variables"
|
||||
usersTitle="wiredfurni.params.sources.merged.title.variables"
|
||||
onChangeFurni={ value => setFurniSource(normalizeFurniSource(value)) }
|
||||
onChangeUsers={ value => setUserSource(normalizeUserSource(value)) } />
|
||||
);
|
||||
}, [ furniSource, targetType, userSource ]);
|
||||
|
||||
const handleTargetChange = (nextTargetType: VariableTargetType) =>
|
||||
{
|
||||
if(nextTargetType === targetType) return;
|
||||
|
||||
setTargetType(nextTargetType);
|
||||
setVariableToken('');
|
||||
};
|
||||
|
||||
return (
|
||||
<WiredExtraBaseView
|
||||
hasSpecialInput={ true }
|
||||
requiresFurni={ (targetType === 'furni') ? WiredFurniType.STUFF_SELECTION_OPTION_BY_ID_BY_TYPE_OR_FROM_CONTEXT : WiredFurniType.STUFF_SELECTION_OPTION_NONE }
|
||||
save={ save }
|
||||
validate={ validate }
|
||||
cardStyle={ { width: 400 } }
|
||||
footer={ footer }>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<Text>{ LocalizeText('wiredfurni.params.texts.placeholder_name') }</Text>
|
||||
<NitroInput maxLength={ MAX_PLACEHOLDER_NAME_LENGTH } type="text" value={ placeholderName } onChange={ event => setPlaceholderName(normalizePlaceholderName(event.target.value)) } />
|
||||
</div>
|
||||
|
||||
<WiredPlaceholderPreview previewHtml={ previewHtml } previewToken={ previewToken } />
|
||||
|
||||
<div className="nitro-wired__give-var-heading">
|
||||
<Text>{ LocalizeText('wiredfurni.params.variables.variable_selection') }</Text>
|
||||
<div className="nitro-wired__give-var-targets">
|
||||
{ TARGET_BUTTONS.map(button => (
|
||||
<button
|
||||
key={ button.key }
|
||||
type="button"
|
||||
disabled={ button.disabled }
|
||||
className={ `nitro-wired__give-var-target nitro-wired__give-var-target--${ button.key } ${ targetType === button.key ? 'is-active' : '' }` }
|
||||
onClick={ () => handleTargetChange(button.key) }>
|
||||
<img src={ button.icon } alt={ button.key } />
|
||||
</button>
|
||||
)) }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<WiredVariablePicker
|
||||
entries={ resolvedVariableEntries }
|
||||
recentScope="variable-text-output"
|
||||
selectedToken={ variableToken }
|
||||
onSelect={ entry => setVariableToken(entry.token) } />
|
||||
|
||||
<div className="nitro-wired__give-var-section">
|
||||
<Text>{ LocalizeText('wiredfurni.params.texts.variable_display_type') }</Text>
|
||||
<label className="flex items-center gap-1 cursor-pointer">
|
||||
<input checked={ (displayType === DISPLAY_NUMERIC) } className="form-check-input" name="wiredTextOutputVariableDisplayType" type="radio" onChange={ () => setDisplayType(DISPLAY_NUMERIC) } />
|
||||
<Text>{ LocalizeText('wiredfurni.params.texts.variable_display_type.1') }</Text>
|
||||
</label>
|
||||
<label className="flex items-center gap-1 cursor-pointer">
|
||||
<input checked={ (displayType === DISPLAY_TEXTUAL) } className="form-check-input" disabled={ !canUseTextDisplay } name="wiredTextOutputVariableDisplayType" type="radio" onChange={ () => setDisplayType(DISPLAY_TEXTUAL) } />
|
||||
<Text>{ LocalizeText('wiredfurni.params.texts.variable_display_type.2') }</Text>
|
||||
</label>
|
||||
<Text small>{ LocalizeText('wiredfurni.params.texts.variable_display_type.2.info') }</Text>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<Text>{ LocalizeText('wiredfurni.params.texts.placeholder_type') }</Text>
|
||||
<label className="flex items-center gap-1 cursor-pointer">
|
||||
<input checked={ (placeholderType === TYPE_SINGLE) } className="form-check-input" name="wiredTextOutputVariablePlaceholderType" type="radio" onChange={ () => setPlaceholderType(TYPE_SINGLE) } />
|
||||
<Text>{ LocalizeText('wiredfurni.params.texts.placeholder_type.1') }</Text>
|
||||
</label>
|
||||
<label className="flex items-center gap-1 cursor-pointer">
|
||||
<input checked={ (placeholderType === TYPE_MULTIPLE) } className="form-check-input" name="wiredTextOutputVariablePlaceholderType" type="radio" onChange={ () => setPlaceholderType(TYPE_MULTIPLE) } />
|
||||
<Text>{ LocalizeText('wiredfurni.params.texts.placeholder_type.2') }</Text>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{ placeholderType === TYPE_MULTIPLE &&
|
||||
<div className="flex flex-col gap-1">
|
||||
<Text>{ LocalizeText('wiredfurni.params.texts.select_delimiter') }</Text>
|
||||
<NitroInput maxLength={ MAX_DELIMITER_LENGTH } type="text" value={ delimiter } onChange={ event => setDelimiter(normalizeDelimiter(event.target.value)) } />
|
||||
</div> }
|
||||
</div>
|
||||
</WiredExtraBaseView>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,116 @@
|
||||
import { FC, useEffect, useMemo, useState } from 'react';
|
||||
import { LocalizeText, WiredFurniType } from '../../../../api';
|
||||
import { Text } from '../../../../common';
|
||||
import { useWired } from '../../../../hooks';
|
||||
import { NitroInput } from '../../../../layout';
|
||||
import { WiredExtraBaseView } from './WiredExtraBaseView';
|
||||
|
||||
const AVAILABILITY_ROOM = 0;
|
||||
const AVAILABILITY_PERMANENT = 10;
|
||||
const AVAILABILITY_SHARED = 11;
|
||||
const MAX_NAME_LENGTH = 40;
|
||||
|
||||
const normalizeVariableName = (value: string) =>
|
||||
{
|
||||
let normalizedValue = (value ?? '').trim().replace(/[\t\r\n]/g, '');
|
||||
|
||||
if(normalizedValue.includes('=')) normalizedValue = normalizedValue.substring(0, normalizedValue.indexOf('=')).trim();
|
||||
|
||||
while(normalizedValue.startsWith('@') || normalizedValue.startsWith('~'))
|
||||
{
|
||||
normalizedValue = normalizedValue.substring(1).trim();
|
||||
}
|
||||
|
||||
normalizedValue = normalizedValue.replace(/[^A-Za-z0-9_]/g, '');
|
||||
|
||||
return normalizedValue.slice(0, MAX_NAME_LENGTH);
|
||||
};
|
||||
|
||||
interface WiredExtraVariableViewProps
|
||||
{
|
||||
availabilityRoomValue: number;
|
||||
availabilityRoomText: string;
|
||||
availabilityRadioName: string;
|
||||
showSharedAvailability?: boolean;
|
||||
}
|
||||
|
||||
export const WiredExtraVariableView: FC<WiredExtraVariableViewProps> = props =>
|
||||
{
|
||||
const { trigger = null, setIntParams = null, setStringParam = null } = useWired();
|
||||
const [ variableName, setVariableName ] = useState('');
|
||||
const [ hasValue, setHasValue ] = useState(false);
|
||||
const [ availability, setAvailability ] = useState(props.availabilityRoomValue);
|
||||
const roomAvailabilityText = useMemo(() =>
|
||||
{
|
||||
const localizedText = props.availabilityRoomText;
|
||||
|
||||
if(localizedText && (localizedText !== 'wiredfurni.params.variables.availability.1')) return localizedText;
|
||||
|
||||
return 'Mentre la stanza è attiva';
|
||||
}, [ props.availabilityRoomText ]);
|
||||
const normalizeAvailability = useMemo(() => (value: number) =>
|
||||
{
|
||||
if(props.showSharedAvailability && (value === AVAILABILITY_SHARED)) return AVAILABILITY_SHARED;
|
||||
if(value === AVAILABILITY_PERMANENT) return AVAILABILITY_PERMANENT;
|
||||
|
||||
return props.availabilityRoomValue;
|
||||
}, [ props.availabilityRoomValue, props.showSharedAvailability ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!trigger) return;
|
||||
|
||||
setVariableName(normalizeVariableName(trigger.stringData));
|
||||
setHasValue((trigger.intData.length > 0) ? (trigger.intData[0] === 1) : false);
|
||||
setAvailability(normalizeAvailability((trigger.intData.length > 1) ? trigger.intData[1] : props.availabilityRoomValue));
|
||||
}, [ normalizeAvailability, props.availabilityRoomValue, trigger ]);
|
||||
|
||||
const save = () =>
|
||||
{
|
||||
setStringParam(normalizeVariableName(variableName));
|
||||
setIntParams([ hasValue ? 1 : 0, normalizeAvailability(availability) ]);
|
||||
};
|
||||
|
||||
return (
|
||||
<WiredExtraBaseView hasSpecialInput={ true } requiresFurni={ WiredFurniType.STUFF_SELECTION_OPTION_NONE } save={ save } cardStyle={ { width: 400 } }>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<Text>{ LocalizeText('wiredfurni.params.variables.variable_name') }</Text>
|
||||
<NitroInput maxLength={ MAX_NAME_LENGTH } type="text" value={ variableName } onChange={ event => setVariableName(normalizeVariableName(event.target.value)) } />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<Text>{ LocalizeText('wiredfurni.params.variables.settings') }</Text>
|
||||
<label className="flex items-center gap-1 cursor-pointer">
|
||||
<input checked={ hasValue } className="form-check-input" type="checkbox" onChange={ event => setHasValue(event.target.checked) } />
|
||||
<Text>{ LocalizeText('wiredfurni.params.variables.settings.has_value') }</Text>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<Text>{ LocalizeText('wiredfurni.params.variables.availability') }</Text>
|
||||
<label className="flex items-center gap-1 cursor-pointer">
|
||||
<input checked={ (availability === props.availabilityRoomValue) } className="form-check-input" name={ props.availabilityRadioName } type="radio" onChange={ () => setAvailability(props.availabilityRoomValue) } />
|
||||
<Text>{ roomAvailabilityText }</Text>
|
||||
</label>
|
||||
<label className="flex items-center gap-1 cursor-pointer">
|
||||
<input checked={ (availability === AVAILABILITY_PERMANENT) } className="form-check-input" name={ props.availabilityRadioName } type="radio" onChange={ () => setAvailability(AVAILABILITY_PERMANENT) } />
|
||||
<Text>{ LocalizeText('wiredfurni.params.variables.availability.10') }</Text>
|
||||
</label>
|
||||
{ !!props.showSharedAvailability &&
|
||||
<label className="flex items-center gap-1 cursor-pointer">
|
||||
<input checked={ (availability === AVAILABILITY_SHARED) } className="form-check-input" name={ props.availabilityRadioName } type="radio" onChange={ () => setAvailability(AVAILABILITY_SHARED) } />
|
||||
<Text>{ LocalizeText('wiredfurni.params.variables.availability.11') }</Text>
|
||||
</label> }
|
||||
</div>
|
||||
</div>
|
||||
</WiredExtraBaseView>
|
||||
);
|
||||
};
|
||||
|
||||
export const WiredExtraUserVariableView: FC<{}> = () =>
|
||||
{
|
||||
return <WiredExtraVariableView availabilityRadioName="wiredUserVariableAvailability" availabilityRoomText={ LocalizeText('wiredfurni.params.variables.availability.0') } availabilityRoomValue={ AVAILABILITY_ROOM } showSharedAvailability={ true } />;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
import { FC, useEffect, useMemo, useState } from 'react';
|
||||
import { LocalizeText, WiredFurniType } from '../../../../api';
|
||||
import furniVariableIcon from '../../../../assets/images/wired/var/icon_source_furni.png';
|
||||
import globalVariableIcon from '../../../../assets/images/wired/var/icon_source_global.png';
|
||||
import userVariableIcon from '../../../../assets/images/wired/var/icon_source_user.png';
|
||||
import { Text } from '../../../../common';
|
||||
import { useWired, useWiredTools } from '../../../../hooks';
|
||||
import { NitroInput } from '../../../../layout';
|
||||
import { WiredVariablePicker } from '../WiredVariablePicker';
|
||||
import { buildWiredVariablePickerEntries, createFallbackVariableEntry, flattenWiredVariablePickerEntries, getCustomVariableItemId, IWiredVariablePickerEntry } from '../WiredVariablePickerData';
|
||||
import { WiredExtraBaseView } from './WiredExtraBaseView';
|
||||
|
||||
const TARGET_USER = 0;
|
||||
const TARGET_FURNI = 1;
|
||||
const TARGET_ROOM = 3;
|
||||
const MAX_NAME_LENGTH = 40;
|
||||
|
||||
type EchoSourceTarget = 'user' | 'furni' | 'global';
|
||||
|
||||
interface IEchoEditorData
|
||||
{
|
||||
sourceTargetType?: number;
|
||||
sourceVariableItemId?: number;
|
||||
sourceVariableName?: string;
|
||||
sourceVariableToken?: string;
|
||||
variableName?: string;
|
||||
}
|
||||
|
||||
const TARGET_BUTTONS: Array<{ key: EchoSourceTarget; icon: string; }> = [
|
||||
{ key: 'furni', icon: furniVariableIcon },
|
||||
{ key: 'user', icon: userVariableIcon },
|
||||
{ key: 'global', icon: globalVariableIcon }
|
||||
];
|
||||
|
||||
const normalizeVariableName = (value: string) =>
|
||||
{
|
||||
let normalizedValue = (value ?? '').trim().replace(/[\t\r\n]/g, '');
|
||||
|
||||
if(normalizedValue.includes('=')) normalizedValue = normalizedValue.substring(0, normalizedValue.indexOf('=')).trim();
|
||||
|
||||
while(normalizedValue.startsWith('@') || normalizedValue.startsWith('~'))
|
||||
{
|
||||
normalizedValue = normalizedValue.substring(1).trim();
|
||||
}
|
||||
|
||||
normalizedValue = normalizedValue.replace(/[^A-Za-z0-9_]/g, '');
|
||||
|
||||
return normalizedValue.slice(0, MAX_NAME_LENGTH);
|
||||
};
|
||||
|
||||
const parseEditorData = (value: string): IEchoEditorData =>
|
||||
{
|
||||
if(!value?.trim().startsWith('{')) return {};
|
||||
|
||||
try
|
||||
{
|
||||
return (JSON.parse(value) as IEchoEditorData) || {};
|
||||
}
|
||||
catch
|
||||
{
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
const normalizeTargetType = (value: number): EchoSourceTarget =>
|
||||
{
|
||||
switch(value)
|
||||
{
|
||||
case TARGET_FURNI: return 'furni';
|
||||
case TARGET_ROOM: return 'global';
|
||||
default: return 'user';
|
||||
}
|
||||
};
|
||||
|
||||
const getTargetValue = (targetType: EchoSourceTarget) =>
|
||||
{
|
||||
switch(targetType)
|
||||
{
|
||||
case 'furni': return TARGET_FURNI;
|
||||
case 'global': return TARGET_ROOM;
|
||||
default: return TARGET_USER;
|
||||
}
|
||||
};
|
||||
|
||||
export const WiredExtraVariableEchoView: FC<{}> = () =>
|
||||
{
|
||||
const { trigger = null, setIntParams = null, setStringParam = null } = useWired();
|
||||
const { userVariableDefinitions = [], furniVariableDefinitions = [], roomVariableDefinitions = [] } = useWiredTools();
|
||||
const [ variableName, setVariableName ] = useState('');
|
||||
const [ sourceTargetType, setSourceTargetType ] = useState<EchoSourceTarget>('user');
|
||||
const [ sourceVariableToken, setSourceVariableToken ] = useState('');
|
||||
const [ fallbackSourceName, setFallbackSourceName ] = useState('');
|
||||
|
||||
const targetDefinitions = useMemo(() =>
|
||||
{
|
||||
switch(sourceTargetType)
|
||||
{
|
||||
case 'furni': return furniVariableDefinitions;
|
||||
case 'global': return roomVariableDefinitions;
|
||||
default: return userVariableDefinitions;
|
||||
}
|
||||
}, [ furniVariableDefinitions, roomVariableDefinitions, sourceTargetType, userVariableDefinitions ]);
|
||||
|
||||
const variableEntries = useMemo(() => buildWiredVariablePickerEntries(sourceTargetType, 'echo', targetDefinitions), [ sourceTargetType, targetDefinitions ]);
|
||||
const resolvedVariableEntries = useMemo(() =>
|
||||
{
|
||||
if(!sourceVariableToken) return variableEntries;
|
||||
if(flattenWiredVariablePickerEntries(variableEntries).some(entry => (entry.token === sourceVariableToken))) return variableEntries;
|
||||
|
||||
const fallbackEntry = createFallbackVariableEntry(sourceTargetType, sourceVariableToken);
|
||||
|
||||
if(fallbackEntry) return [ fallbackEntry, ...variableEntries ];
|
||||
if(!fallbackSourceName) return variableEntries;
|
||||
|
||||
return [ {
|
||||
id: sourceVariableToken,
|
||||
token: sourceVariableToken,
|
||||
label: fallbackSourceName,
|
||||
displayLabel: fallbackSourceName,
|
||||
searchableText: fallbackSourceName,
|
||||
selectable: true,
|
||||
hasValue: true,
|
||||
kind: 'custom',
|
||||
target: sourceTargetType
|
||||
}, ...variableEntries ];
|
||||
}, [ fallbackSourceName, sourceTargetType, sourceVariableToken, variableEntries ]);
|
||||
|
||||
const selectedEntry = useMemo(() => flattenWiredVariablePickerEntries(resolvedVariableEntries).find(entry => (entry.token === sourceVariableToken)) ?? null, [ resolvedVariableEntries, sourceVariableToken ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!trigger)
|
||||
{
|
||||
setVariableName('');
|
||||
setSourceTargetType('user');
|
||||
setSourceVariableToken('');
|
||||
setFallbackSourceName('');
|
||||
return;
|
||||
}
|
||||
|
||||
const editorData = parseEditorData(trigger.stringData);
|
||||
|
||||
setVariableName(normalizeVariableName(editorData.variableName || ''));
|
||||
setSourceTargetType(normalizeTargetType(editorData.sourceTargetType ?? TARGET_USER));
|
||||
setSourceVariableToken((editorData.sourceVariableToken || '').trim());
|
||||
setFallbackSourceName((editorData.sourceVariableName || '').trim());
|
||||
}, [ trigger ]);
|
||||
|
||||
const save = () =>
|
||||
{
|
||||
setIntParams([]);
|
||||
setStringParam(JSON.stringify({
|
||||
variableName: normalizeVariableName(variableName),
|
||||
sourceTargetType: getTargetValue(sourceTargetType),
|
||||
sourceVariableToken,
|
||||
sourceVariableItemId: getCustomVariableItemId(sourceVariableToken),
|
||||
sourceVariableName: selectedEntry?.displayLabel || fallbackSourceName || ''
|
||||
}));
|
||||
};
|
||||
|
||||
const validate = () => !!sourceVariableToken;
|
||||
|
||||
const handleTargetTypeChange = (nextValue: EchoSourceTarget) =>
|
||||
{
|
||||
if(nextValue === sourceTargetType) return;
|
||||
|
||||
setSourceTargetType(nextValue);
|
||||
setSourceVariableToken('');
|
||||
};
|
||||
|
||||
return (
|
||||
<WiredExtraBaseView hasSpecialInput={ true } requiresFurni={ WiredFurniType.STUFF_SELECTION_OPTION_NONE } save={ save } validate={ validate } cardStyle={ { width: 244 } }>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<Text>{ LocalizeText('wiredfurni.params.variables.variable_name') }</Text>
|
||||
<NitroInput maxLength={ MAX_NAME_LENGTH } type="text" value={ variableName } onChange={ event => setVariableName(normalizeVariableName(event.target.value)) } />
|
||||
</div>
|
||||
|
||||
<div className="nitro-wired__give-var-heading">
|
||||
<Text>{ LocalizeText('wiredfurni.params.variables.variable_selection') }</Text>
|
||||
<div className="nitro-wired__give-var-targets">
|
||||
{ TARGET_BUTTONS.map(button => (
|
||||
<button
|
||||
key={ button.key }
|
||||
type="button"
|
||||
className={ `nitro-wired__give-var-target nitro-wired__give-var-target--${ button.key } ${ sourceTargetType === button.key ? 'is-active' : '' }` }
|
||||
onClick={ () => handleTargetTypeChange(button.key) }>
|
||||
<img src={ button.icon } alt={ button.key } />
|
||||
</button>
|
||||
)) }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<WiredVariablePicker
|
||||
entries={ resolvedVariableEntries as IWiredVariablePickerEntry[] }
|
||||
recentScope="variable-echo"
|
||||
selectedToken={ sourceVariableToken }
|
||||
onSelect={ entry => setSourceVariableToken(entry.token) } />
|
||||
</div>
|
||||
</WiredExtraBaseView>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,437 @@
|
||||
import { FC, useEffect, useMemo, useState } from 'react';
|
||||
import { LocalizeText, WiredFurniType } from '../../../../api';
|
||||
import { Text } from '../../../../common';
|
||||
import { useWired } from '../../../../hooks';
|
||||
import { NitroInput } from '../../../../layout';
|
||||
import { WiredExtraBaseView } from './WiredExtraBaseView';
|
||||
|
||||
const MODE_LINEAR = 1;
|
||||
const MODE_EXPONENTIAL = 2;
|
||||
const MODE_MANUAL = 3;
|
||||
|
||||
const SUB_CURRENT_LEVEL = 0;
|
||||
const SUB_CURRENT_XP = 1;
|
||||
const SUB_LEVEL_PROGRESS = 2;
|
||||
const SUB_LEVEL_PROGRESS_PERCENT = 3;
|
||||
const SUB_TOTAL_XP_REQUIRED = 4;
|
||||
const SUB_XP_REMAINING = 5;
|
||||
const SUB_IS_AT_MAX = 6;
|
||||
const SUB_MAX_LEVEL = 7;
|
||||
|
||||
const DEFAULT_STEP_SIZE = 100;
|
||||
const DEFAULT_MAX_LEVEL = 10;
|
||||
const DEFAULT_FIRST_LEVEL_XP = 100;
|
||||
const DEFAULT_INCREASE_FACTOR = 100;
|
||||
const DEFAULT_INTERPOLATION_TEXT = '';
|
||||
const DEFAULT_SUBVARIABLES = [ SUB_CURRENT_LEVEL, SUB_CURRENT_XP ];
|
||||
const DEFAULT_PLACEHOLDER = '5=100 (Level 5 = 100 XP)\n10=500\n20=4000\n...';
|
||||
|
||||
interface IVariableLevelUpEditorData
|
||||
{
|
||||
mode?: number;
|
||||
stepSize?: number;
|
||||
maxLevel?: number;
|
||||
firstLevelXp?: number;
|
||||
increaseFactor?: number;
|
||||
interpolationText?: string;
|
||||
subvariables?: number[] | null;
|
||||
}
|
||||
|
||||
interface ILevelEntry
|
||||
{
|
||||
level: number;
|
||||
requiredXp: number;
|
||||
}
|
||||
|
||||
const localizeOrFallback = (key: string, fallback: string, params?: string[], values?: string[]) =>
|
||||
{
|
||||
const localized = params?.length ? LocalizeText(key, params, values ?? []) : LocalizeText(key);
|
||||
|
||||
return (!localized || (localized === key)) ? fallback : localized;
|
||||
};
|
||||
|
||||
const normalizeMode = (value: number) =>
|
||||
{
|
||||
switch(value)
|
||||
{
|
||||
case MODE_EXPONENTIAL:
|
||||
case MODE_MANUAL:
|
||||
return value;
|
||||
default:
|
||||
return MODE_LINEAR;
|
||||
}
|
||||
};
|
||||
|
||||
const normalizeNonNegativeInt = (value: number, fallback: number) =>
|
||||
{
|
||||
if(!Number.isFinite(value)) return fallback;
|
||||
|
||||
return Math.max(0, Math.trunc(value));
|
||||
};
|
||||
|
||||
const normalizePositiveInt = (value: number, fallback: number) =>
|
||||
{
|
||||
if(!Number.isFinite(value)) return fallback;
|
||||
|
||||
return Math.max(1, Math.trunc(value));
|
||||
};
|
||||
|
||||
const normalizeInterpolationText = (value: string) => (value ?? '').replace(/\r/g, '');
|
||||
|
||||
const normalizeSubvariables = (value?: number[] | null) =>
|
||||
{
|
||||
if(value === null) return [ ...DEFAULT_SUBVARIABLES ];
|
||||
if(!Array.isArray(value)) return [ ...DEFAULT_SUBVARIABLES ];
|
||||
|
||||
return [ ...new Set(value.filter(subvariable => Number.isInteger(subvariable) && (subvariable >= SUB_CURRENT_LEVEL) && (subvariable <= SUB_MAX_LEVEL))) ];
|
||||
};
|
||||
|
||||
const parseEditorData = (value: string): IVariableLevelUpEditorData =>
|
||||
{
|
||||
if(!value?.trim()) return {};
|
||||
|
||||
if(!value.trim().startsWith('{'))
|
||||
{
|
||||
return {
|
||||
mode: MODE_MANUAL,
|
||||
interpolationText: normalizeInterpolationText(value)
|
||||
};
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return (JSON.parse(value) as IVariableLevelUpEditorData) || {};
|
||||
}
|
||||
catch
|
||||
{
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
const parseIntInput = (value: string, fallback: number) =>
|
||||
{
|
||||
const parsedValue = parseInt((value ?? '').trim(), 10);
|
||||
|
||||
return Number.isFinite(parsedValue) ? parsedValue : fallback;
|
||||
};
|
||||
|
||||
const parseManualAnchors = (value: string) =>
|
||||
{
|
||||
const anchors = new Map<number, number>();
|
||||
const lines = normalizeInterpolationText(value).split('\n');
|
||||
|
||||
for(const rawLine of lines)
|
||||
{
|
||||
const trimmedLine = rawLine.trim();
|
||||
|
||||
if(!trimmedLine.length) continue;
|
||||
|
||||
const separator = trimmedLine.includes('=') ? '=' : (trimmedLine.includes(',') ? ',' : '');
|
||||
|
||||
if(!separator.length) continue;
|
||||
|
||||
const [ rawLevel, rawXp ] = trimmedLine.split(separator, 2).map(part => part.trim());
|
||||
const level = parseInt(rawLevel, 10);
|
||||
const xp = parseInt(rawXp, 10);
|
||||
|
||||
if(!Number.isFinite(level) || !Number.isFinite(xp) || (level <= 0)) continue;
|
||||
|
||||
anchors.set(level, Math.max(0, xp));
|
||||
}
|
||||
|
||||
if(!anchors.has(1)) anchors.set(1, 0);
|
||||
|
||||
return [ ...anchors.entries() ].sort((left, right) => left[0] - right[0]);
|
||||
};
|
||||
|
||||
const buildLinearEntries = (stepSize: number, maxLevel: number) =>
|
||||
{
|
||||
const entries: ILevelEntry[] = [];
|
||||
|
||||
for(let level = 1; level <= maxLevel; level++)
|
||||
{
|
||||
entries.push({
|
||||
level,
|
||||
requiredXp: Math.max(0, (level - 1) * stepSize)
|
||||
});
|
||||
}
|
||||
|
||||
return entries;
|
||||
};
|
||||
|
||||
const buildExponentialEntries = (firstLevelXp: number, increaseFactor: number, maxLevel: number) =>
|
||||
{
|
||||
const entries: ILevelEntry[] = [ { level: 1, requiredXp: 0 } ];
|
||||
let nextIncrement = Math.max(0, firstLevelXp);
|
||||
let threshold = 0;
|
||||
|
||||
for(let level = 2; level <= maxLevel; level++)
|
||||
{
|
||||
threshold += nextIncrement;
|
||||
|
||||
entries.push({
|
||||
level,
|
||||
requiredXp: Math.max(0, Math.round(threshold))
|
||||
});
|
||||
|
||||
nextIncrement = Math.max(0, Math.round(nextIncrement * ((100 + Math.max(0, increaseFactor)) / 100)));
|
||||
}
|
||||
|
||||
return entries;
|
||||
};
|
||||
|
||||
const buildManualEntries = (value: string) =>
|
||||
{
|
||||
const anchors = parseManualAnchors(value);
|
||||
|
||||
if(!anchors.length) return [ { level: 1, requiredXp: 0 } ];
|
||||
|
||||
const entries = new Map<number, number>();
|
||||
|
||||
for(let index = 0; index < anchors.length; index++)
|
||||
{
|
||||
const [ currentLevel, currentXp ] = anchors[index];
|
||||
|
||||
entries.set(currentLevel, currentXp);
|
||||
|
||||
if(index >= (anchors.length - 1)) continue;
|
||||
|
||||
const [ nextLevel, nextXp ] = anchors[index + 1];
|
||||
|
||||
if(nextLevel <= currentLevel) continue;
|
||||
|
||||
const deltaLevel = nextLevel - currentLevel;
|
||||
const deltaXp = nextXp - currentXp;
|
||||
|
||||
for(let level = currentLevel + 1; level < nextLevel; level++)
|
||||
{
|
||||
const progress = (level - currentLevel) / deltaLevel;
|
||||
entries.set(level, Math.max(0, Math.round(currentXp + (deltaXp * progress))));
|
||||
}
|
||||
}
|
||||
|
||||
return [ ...entries.entries() ]
|
||||
.sort((left, right) => left[0] - right[0])
|
||||
.map(([ level, requiredXp ]) => ({ level, requiredXp }));
|
||||
};
|
||||
|
||||
const buildPreviewEntries = (mode: number, stepSize: number, maxLevel: number, firstLevelXp: number, increaseFactor: number, interpolationText: string) =>
|
||||
{
|
||||
switch(mode)
|
||||
{
|
||||
case MODE_EXPONENTIAL:
|
||||
return buildExponentialEntries(firstLevelXp, increaseFactor, maxLevel);
|
||||
case MODE_MANUAL:
|
||||
return buildManualEntries(interpolationText);
|
||||
default:
|
||||
return buildLinearEntries(stepSize, maxLevel);
|
||||
}
|
||||
};
|
||||
|
||||
export const WiredExtraVariableLevelUpSystemView: FC<{}> = () =>
|
||||
{
|
||||
const { trigger = null, setIntParams = null, setStringParam = null } = useWired();
|
||||
const [ mode, setMode ] = useState(MODE_LINEAR);
|
||||
const [ stepSizeInput, setStepSizeInput ] = useState(DEFAULT_STEP_SIZE.toString());
|
||||
const [ maxLevelInput, setMaxLevelInput ] = useState(DEFAULT_MAX_LEVEL.toString());
|
||||
const [ firstLevelXpInput, setFirstLevelXpInput ] = useState(DEFAULT_FIRST_LEVEL_XP.toString());
|
||||
const [ increaseFactorInput, setIncreaseFactorInput ] = useState(DEFAULT_INCREASE_FACTOR.toString());
|
||||
const [ interpolationText, setInterpolationText ] = useState(DEFAULT_INTERPOLATION_TEXT);
|
||||
const [ selectedSubvariables, setSelectedSubvariables ] = useState<number[]>(DEFAULT_SUBVARIABLES);
|
||||
const [ isModeSectionOpen, setIsModeSectionOpen ] = useState(true);
|
||||
const [ isPreviewSectionOpen, setIsPreviewSectionOpen ] = useState(true);
|
||||
const [ isSubvariablesSectionOpen, setIsSubvariablesSectionOpen ] = useState(true);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!trigger)
|
||||
{
|
||||
setMode(MODE_LINEAR);
|
||||
setStepSizeInput(DEFAULT_STEP_SIZE.toString());
|
||||
setMaxLevelInput(DEFAULT_MAX_LEVEL.toString());
|
||||
setFirstLevelXpInput(DEFAULT_FIRST_LEVEL_XP.toString());
|
||||
setIncreaseFactorInput(DEFAULT_INCREASE_FACTOR.toString());
|
||||
setInterpolationText(DEFAULT_INTERPOLATION_TEXT);
|
||||
setSelectedSubvariables([ ...DEFAULT_SUBVARIABLES ]);
|
||||
return;
|
||||
}
|
||||
|
||||
const editorData = parseEditorData(trigger.stringData);
|
||||
|
||||
setMode(normalizeMode(editorData.mode ?? MODE_LINEAR));
|
||||
setStepSizeInput(normalizeNonNegativeInt(editorData.stepSize ?? DEFAULT_STEP_SIZE, DEFAULT_STEP_SIZE).toString());
|
||||
setMaxLevelInput(normalizePositiveInt(editorData.maxLevel ?? DEFAULT_MAX_LEVEL, DEFAULT_MAX_LEVEL).toString());
|
||||
setFirstLevelXpInput(normalizeNonNegativeInt(editorData.firstLevelXp ?? DEFAULT_FIRST_LEVEL_XP, DEFAULT_FIRST_LEVEL_XP).toString());
|
||||
setIncreaseFactorInput(normalizeNonNegativeInt(editorData.increaseFactor ?? DEFAULT_INCREASE_FACTOR, DEFAULT_INCREASE_FACTOR).toString());
|
||||
setInterpolationText(normalizeInterpolationText(editorData.interpolationText ?? DEFAULT_INTERPOLATION_TEXT));
|
||||
setSelectedSubvariables(normalizeSubvariables(editorData.subvariables));
|
||||
}, [ trigger ]);
|
||||
|
||||
const normalizedStepSize = useMemo(() => normalizeNonNegativeInt(parseIntInput(stepSizeInput, DEFAULT_STEP_SIZE), DEFAULT_STEP_SIZE), [ stepSizeInput ]);
|
||||
const normalizedMaxLevel = useMemo(() => normalizePositiveInt(parseIntInput(maxLevelInput, DEFAULT_MAX_LEVEL), DEFAULT_MAX_LEVEL), [ maxLevelInput ]);
|
||||
const normalizedFirstLevelXp = useMemo(() => normalizeNonNegativeInt(parseIntInput(firstLevelXpInput, DEFAULT_FIRST_LEVEL_XP), DEFAULT_FIRST_LEVEL_XP), [ firstLevelXpInput ]);
|
||||
const normalizedIncreaseFactor = useMemo(() => normalizeNonNegativeInt(parseIntInput(increaseFactorInput, DEFAULT_INCREASE_FACTOR), DEFAULT_INCREASE_FACTOR), [ increaseFactorInput ]);
|
||||
const normalizedInterpolation = useMemo(() => normalizeInterpolationText(interpolationText), [ interpolationText ]);
|
||||
|
||||
const previewEntries = useMemo(() => buildPreviewEntries(mode, normalizedStepSize, normalizedMaxLevel, normalizedFirstLevelXp, normalizedIncreaseFactor, normalizedInterpolation), [ mode, normalizedFirstLevelXp, normalizedIncreaseFactor, normalizedInterpolation, normalizedMaxLevel, normalizedStepSize ]);
|
||||
|
||||
const interpolationPlaceholder = useMemo(() =>
|
||||
{
|
||||
const localizedText = LocalizeText('wiredfurni.params.levelup.interpolation_placeholder');
|
||||
|
||||
if(!localizedText || (localizedText === 'wiredfurni.params.levelup.interpolation_placeholder')) return DEFAULT_PLACEHOLDER;
|
||||
|
||||
return localizedText.includes('5,100') ? localizedText.replace(/,/g, '=') : localizedText;
|
||||
}, []);
|
||||
|
||||
const save = () =>
|
||||
{
|
||||
setIntParams([]);
|
||||
setStringParam(JSON.stringify({
|
||||
mode,
|
||||
stepSize: normalizedStepSize,
|
||||
maxLevel: normalizedMaxLevel,
|
||||
firstLevelXp: normalizedFirstLevelXp,
|
||||
increaseFactor: normalizedIncreaseFactor,
|
||||
interpolationText: normalizedInterpolation,
|
||||
subvariables: [ ...selectedSubvariables ].sort((left, right) => left - right)
|
||||
}));
|
||||
};
|
||||
|
||||
const toggleSubvariable = (subvariable: number) =>
|
||||
{
|
||||
setSelectedSubvariables(previousValue =>
|
||||
{
|
||||
if(previousValue.includes(subvariable))
|
||||
{
|
||||
return previousValue.filter(value => value !== subvariable);
|
||||
}
|
||||
|
||||
return [ ...previousValue, subvariable ].sort((left, right) => left - right);
|
||||
});
|
||||
};
|
||||
|
||||
const modeOptions = [
|
||||
{ value: MODE_LINEAR, label: localizeOrFallback('wiredfurni.params.levelup.mode.1', 'Lineare') },
|
||||
{ value: MODE_EXPONENTIAL, label: localizeOrFallback('wiredfurni.params.levelup.mode.2', 'Esponenziale') },
|
||||
{ value: MODE_MANUAL, label: localizeOrFallback('wiredfurni.params.levelup.mode.3', 'Manuale') }
|
||||
];
|
||||
|
||||
const subvariableOptions = [
|
||||
{ key: SUB_CURRENT_LEVEL, suffix: 'current_level' },
|
||||
{ key: SUB_CURRENT_XP, suffix: 'current_xp' },
|
||||
{ key: SUB_LEVEL_PROGRESS, suffix: 'level_progress' },
|
||||
{ key: SUB_LEVEL_PROGRESS_PERCENT, suffix: 'level_progress_percent' },
|
||||
{ key: SUB_TOTAL_XP_REQUIRED, suffix: 'total_xp_required' },
|
||||
{ key: SUB_XP_REMAINING, suffix: 'xp_remaining' },
|
||||
{ key: SUB_IS_AT_MAX, suffix: 'is_at_max' },
|
||||
{ key: SUB_MAX_LEVEL, suffix: 'max_level' }
|
||||
];
|
||||
|
||||
return (
|
||||
<WiredExtraBaseView hasSpecialInput={ true } requiresFurni={ WiredFurniType.STUFF_SELECTION_OPTION_NONE } save={ save } cardStyle={ { width: 260 } }>
|
||||
<div className="nitro-wired__levelup">
|
||||
<div className="nitro-wired__levelup-section">
|
||||
<button type="button" className="nitro-wired__levelup-section-header" onClick={ () => setIsModeSectionOpen(value => !value) }>
|
||||
<Text bold>{ LocalizeText('wiredfurni.params.levelup.mode') }</Text>
|
||||
<span className={ `nitro-wired__levelup-chevron ${ isModeSectionOpen ? 'is-open' : '' }` }>^</span>
|
||||
</button>
|
||||
|
||||
{ isModeSectionOpen &&
|
||||
<div className="nitro-wired__levelup-section-body">
|
||||
<div className={ `nitro-wired__levelup-mode-block ${ mode === MODE_LINEAR ? 'is-active' : 'is-inactive' }` }>
|
||||
<label className="nitro-wired__levelup-mode-label">
|
||||
<input checked={ mode === MODE_LINEAR } className="form-check-input" name="wiredVariableLevelUpMode" type="radio" onChange={ () => setMode(MODE_LINEAR) } />
|
||||
<Text>{ localizeOrFallback('wiredfurni.params.levelup.mode.1', 'Lineare') }</Text>
|
||||
</label>
|
||||
<div className="nitro-wired__levelup-fields">
|
||||
<div className="nitro-wired__levelup-field-row">
|
||||
<Text>{ LocalizeText('wiredfurni.params.levelup.step_size') }</Text>
|
||||
<NitroInput className="nitro-wired__levelup-number" disabled={ mode !== MODE_LINEAR } type="number" value={ stepSizeInput } onChange={ event => setStepSizeInput(event.target.value) } />
|
||||
</div>
|
||||
<div className="nitro-wired__levelup-field-row">
|
||||
<Text>{ LocalizeText('wiredfurni.params.levelup.max_level') }</Text>
|
||||
<NitroInput className="nitro-wired__levelup-number" disabled={ mode !== MODE_LINEAR } type="number" value={ maxLevelInput } onChange={ event => setMaxLevelInput(event.target.value) } />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={ `nitro-wired__levelup-mode-block ${ mode === MODE_EXPONENTIAL ? 'is-active' : 'is-inactive' }` }>
|
||||
<label className="nitro-wired__levelup-mode-label">
|
||||
<input checked={ mode === MODE_EXPONENTIAL } className="form-check-input" name="wiredVariableLevelUpMode" type="radio" onChange={ () => setMode(MODE_EXPONENTIAL) } />
|
||||
<Text>{ localizeOrFallback('wiredfurni.params.levelup.mode.2', 'Esponenziale') }</Text>
|
||||
</label>
|
||||
<div className="nitro-wired__levelup-fields">
|
||||
<div className="nitro-wired__levelup-field-row">
|
||||
<Text>{ LocalizeText('wiredfurni.params.levelup.first_level_xp') }</Text>
|
||||
<NitroInput className="nitro-wired__levelup-number" disabled={ mode !== MODE_EXPONENTIAL } type="number" value={ firstLevelXpInput } onChange={ event => setFirstLevelXpInput(event.target.value) } />
|
||||
</div>
|
||||
<div className="nitro-wired__levelup-field-row">
|
||||
<Text>{ LocalizeText('wiredfurni.params.levelup.increase_factor') }</Text>
|
||||
<NitroInput className="nitro-wired__levelup-number" disabled={ mode !== MODE_EXPONENTIAL } type="number" value={ increaseFactorInput } onChange={ event => setIncreaseFactorInput(event.target.value) } />
|
||||
</div>
|
||||
<div className="nitro-wired__levelup-field-row">
|
||||
<Text>{ LocalizeText('wiredfurni.params.levelup.max_level') }</Text>
|
||||
<NitroInput className="nitro-wired__levelup-number" disabled={ mode !== MODE_EXPONENTIAL } type="number" value={ maxLevelInput } onChange={ event => setMaxLevelInput(event.target.value) } />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={ `nitro-wired__levelup-mode-block ${ mode === MODE_MANUAL ? 'is-active' : 'is-inactive' }` }>
|
||||
<label className="nitro-wired__levelup-mode-label">
|
||||
<input checked={ mode === MODE_MANUAL } className="form-check-input" name="wiredVariableLevelUpMode" type="radio" onChange={ () => setMode(MODE_MANUAL) } />
|
||||
<Text>{ localizeOrFallback('wiredfurni.params.levelup.mode.3', 'Inserimento manuale') }</Text>
|
||||
</label>
|
||||
<textarea
|
||||
className="form-control form-control-sm nitro-wired__levelup-textarea"
|
||||
disabled={ mode !== MODE_MANUAL }
|
||||
placeholder={ interpolationPlaceholder }
|
||||
value={ interpolationText }
|
||||
onChange={ event => setInterpolationText(event.target.value) } />
|
||||
</div>
|
||||
</div> }
|
||||
</div>
|
||||
|
||||
<div className="nitro-wired__divider" />
|
||||
|
||||
<div className="nitro-wired__levelup-section">
|
||||
<button type="button" className="nitro-wired__levelup-section-header" onClick={ () => setIsPreviewSectionOpen(value => !value) }>
|
||||
<Text bold>{ LocalizeText('wiredfurni.params.levelup.preview') }</Text>
|
||||
<span className={ `nitro-wired__levelup-chevron ${ isPreviewSectionOpen ? 'is-open' : '' }` }>^</span>
|
||||
</button>
|
||||
|
||||
{ isPreviewSectionOpen &&
|
||||
<div className="nitro-wired__levelup-preview">
|
||||
{ previewEntries.map(entry => (
|
||||
<div key={ entry.level } className="nitro-wired__levelup-preview-entry">
|
||||
{ localizeOrFallback('wiredfurni.params.levelup.preview.entry', `Livello: ${ entry.level } - XP: ${ entry.requiredXp }`, [ 'lvl', 'xp' ], [ entry.level.toString(), entry.requiredXp.toString() ]) }
|
||||
</div>
|
||||
)) }
|
||||
</div> }
|
||||
</div>
|
||||
|
||||
<div className="nitro-wired__divider" />
|
||||
|
||||
<div className="nitro-wired__levelup-section">
|
||||
<button type="button" className="nitro-wired__levelup-section-header" onClick={ () => setIsSubvariablesSectionOpen(value => !value) }>
|
||||
<Text bold>{ LocalizeText('wiredfurni.params.create_subvariables') }</Text>
|
||||
<span className={ `nitro-wired__levelup-chevron ${ isSubvariablesSectionOpen ? 'is-open' : '' }` }>^</span>
|
||||
</button>
|
||||
|
||||
{ isSubvariablesSectionOpen &&
|
||||
<div className="nitro-wired__levelup-subvariables">
|
||||
{ subvariableOptions.map(subvariable => (
|
||||
<div key={ subvariable.key } className="nitro-wired__levelup-subvariable-row">
|
||||
<label className="nitro-wired__levelup-subvariable-label">
|
||||
<input checked={ selectedSubvariables.includes(subvariable.key) } className="form-check-input" type="checkbox" onChange={ () => toggleSubvariable(subvariable.key) } />
|
||||
<Text>{ LocalizeText(`wiredfurni.params.levelup.subvariable.${ subvariable.key }`) }</Text>
|
||||
</label>
|
||||
<input className="nitro-wired__levelup-subvariable-token" readOnly tabIndex={ -1 } type="text" value={ subvariable.suffix } />
|
||||
</div>
|
||||
)) }
|
||||
</div> }
|
||||
</div>
|
||||
</div>
|
||||
</WiredExtraBaseView>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,232 @@
|
||||
import { FC, useEffect, useMemo, useState } from 'react';
|
||||
import { LocalizeText, WiredFurniType } from '../../../../api';
|
||||
import { Text } from '../../../../common';
|
||||
import { useWired } from '../../../../hooks';
|
||||
import { NitroInput } from '../../../../layout';
|
||||
import { WiredExtraBaseView } from './WiredExtraBaseView';
|
||||
|
||||
const TARGET_USER = 0;
|
||||
const TARGET_ROOM = 3;
|
||||
const MAX_NAME_LENGTH = 40;
|
||||
|
||||
interface IVariableReferenceEditorVariable
|
||||
{
|
||||
hasValue: boolean;
|
||||
itemId: number;
|
||||
name: string;
|
||||
targetType: number;
|
||||
}
|
||||
|
||||
interface IVariableReferenceEditorRoom
|
||||
{
|
||||
roomId: number;
|
||||
roomName: string;
|
||||
variables: IVariableReferenceEditorVariable[];
|
||||
}
|
||||
|
||||
interface IVariableReferenceEditorData
|
||||
{
|
||||
readOnly?: boolean;
|
||||
rooms?: IVariableReferenceEditorRoom[];
|
||||
sourceRoomId?: number;
|
||||
sourceRoomName?: string;
|
||||
sourceTargetType?: number;
|
||||
sourceVariableItemId?: number;
|
||||
sourceVariableName?: string;
|
||||
variableName?: string;
|
||||
}
|
||||
|
||||
const normalizeVariableName = (value: string) =>
|
||||
{
|
||||
let normalizedValue = (value ?? '').trim().replace(/[\t\r\n]/g, '');
|
||||
|
||||
if(normalizedValue.includes('=')) normalizedValue = normalizedValue.substring(0, normalizedValue.indexOf('=')).trim();
|
||||
|
||||
while(normalizedValue.startsWith('@') || normalizedValue.startsWith('~'))
|
||||
{
|
||||
normalizedValue = normalizedValue.substring(1).trim();
|
||||
}
|
||||
|
||||
normalizedValue = normalizedValue.replace(/[^A-Za-z0-9_]/g, '');
|
||||
|
||||
return normalizedValue.slice(0, MAX_NAME_LENGTH);
|
||||
};
|
||||
|
||||
const parseEditorData = (value: string): IVariableReferenceEditorData =>
|
||||
{
|
||||
if(!value?.trim().startsWith('{')) return {};
|
||||
|
||||
try
|
||||
{
|
||||
return (JSON.parse(value) as IVariableReferenceEditorData) || {};
|
||||
}
|
||||
catch
|
||||
{
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
export const WiredExtraVariableReferenceView: FC<{}> = () =>
|
||||
{
|
||||
const { trigger = null, setIntParams = null, setStringParam = null } = useWired();
|
||||
const [ variableName, setVariableName ] = useState('');
|
||||
const [ sourceRoomId, setSourceRoomId ] = useState(0);
|
||||
const [ sourceVariableItemId, setSourceVariableItemId ] = useState(0);
|
||||
const [ sourceTargetType, setSourceTargetType ] = useState(TARGET_USER);
|
||||
const [ readOnly, setReadOnly ] = useState(true);
|
||||
const [ roomOptions, setRoomOptions ] = useState<IVariableReferenceEditorRoom[]>([]);
|
||||
const [ fallbackRoomName, setFallbackRoomName ] = useState('');
|
||||
const [ fallbackVariableName, setFallbackVariableName ] = useState('');
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!trigger)
|
||||
{
|
||||
setVariableName('');
|
||||
setSourceRoomId(0);
|
||||
setSourceVariableItemId(0);
|
||||
setSourceTargetType(TARGET_USER);
|
||||
setReadOnly(true);
|
||||
setRoomOptions([]);
|
||||
setFallbackRoomName('');
|
||||
setFallbackVariableName('');
|
||||
return;
|
||||
}
|
||||
|
||||
const editorData = parseEditorData(trigger.stringData);
|
||||
|
||||
setVariableName(normalizeVariableName(editorData.variableName || ''));
|
||||
setSourceRoomId(editorData.sourceRoomId || 0);
|
||||
setSourceVariableItemId(editorData.sourceVariableItemId || 0);
|
||||
setSourceTargetType((editorData.sourceTargetType === TARGET_ROOM) ? TARGET_ROOM : TARGET_USER);
|
||||
setReadOnly(editorData.readOnly !== false);
|
||||
setRoomOptions([ ...(editorData.rooms || []) ]);
|
||||
setFallbackRoomName((editorData.sourceRoomName || '').trim());
|
||||
setFallbackVariableName((editorData.sourceVariableName || '').trim());
|
||||
}, [ trigger ]);
|
||||
|
||||
const mergedRoomOptions = useMemo(() =>
|
||||
{
|
||||
const nextValue = [ ...roomOptions ];
|
||||
|
||||
if(sourceRoomId <= 0) return nextValue;
|
||||
if(nextValue.some(room => (room.roomId === sourceRoomId))) return nextValue;
|
||||
|
||||
nextValue.push({
|
||||
roomId: sourceRoomId,
|
||||
roomName: (fallbackRoomName || `#${ sourceRoomId }`),
|
||||
variables: sourceVariableItemId > 0
|
||||
? [ {
|
||||
itemId: sourceVariableItemId,
|
||||
name: (fallbackVariableName || `#${ sourceVariableItemId }`),
|
||||
targetType: sourceTargetType,
|
||||
hasValue: true
|
||||
} ]
|
||||
: []
|
||||
});
|
||||
|
||||
return nextValue;
|
||||
}, [ fallbackRoomName, fallbackVariableName, roomOptions, sourceRoomId, sourceTargetType, sourceVariableItemId ]);
|
||||
|
||||
const selectedRoom = useMemo(() => mergedRoomOptions.find(option => (option.roomId === sourceRoomId)) ?? null, [ mergedRoomOptions, sourceRoomId ]);
|
||||
const selectedRoomVariables = (selectedRoom?.variables || []);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!selectedRoom)
|
||||
{
|
||||
if(!sourceRoomId && mergedRoomOptions.length) setSourceRoomId(mergedRoomOptions[0].roomId);
|
||||
return;
|
||||
}
|
||||
|
||||
const hasSelectedVariable = selectedRoomVariables.some(variable => (variable.itemId === sourceVariableItemId) && (variable.targetType === sourceTargetType));
|
||||
|
||||
if(hasSelectedVariable) return;
|
||||
|
||||
const fallbackVariable = selectedRoomVariables[0];
|
||||
|
||||
if(!fallbackVariable)
|
||||
{
|
||||
setSourceVariableItemId(0);
|
||||
setSourceTargetType(TARGET_USER);
|
||||
return;
|
||||
}
|
||||
|
||||
setSourceVariableItemId(fallbackVariable.itemId);
|
||||
setSourceTargetType(fallbackVariable.targetType);
|
||||
}, [ mergedRoomOptions, selectedRoom, selectedRoomVariables, sourceRoomId, sourceTargetType, sourceVariableItemId ]);
|
||||
|
||||
const save = () =>
|
||||
{
|
||||
setIntParams([]);
|
||||
setStringParam(JSON.stringify({
|
||||
variableName: normalizeVariableName(variableName),
|
||||
sourceRoomId,
|
||||
sourceVariableItemId,
|
||||
sourceTargetType,
|
||||
readOnly
|
||||
}));
|
||||
};
|
||||
|
||||
const validate = () => !!normalizeVariableName(variableName).length && (sourceRoomId > 0) && (sourceVariableItemId > 0);
|
||||
|
||||
const getTargetLabel = (targetType: number) =>
|
||||
{
|
||||
if(targetType === TARGET_ROOM)
|
||||
{
|
||||
const globalLabel = LocalizeText('wiredfurni.params.sources.global');
|
||||
|
||||
return ((globalLabel && (globalLabel !== 'wiredfurni.params.sources.global')) ? globalLabel : 'Global');
|
||||
}
|
||||
|
||||
return 'User';
|
||||
};
|
||||
|
||||
return (
|
||||
<WiredExtraBaseView hasSpecialInput={ true } requiresFurni={ WiredFurniType.STUFF_SELECTION_OPTION_NONE } save={ save } validate={ validate } cardStyle={ { width: 400 } }>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<Text>{ LocalizeText('wiredfurni.params.variables.variable_name') }</Text>
|
||||
<NitroInput maxLength={ MAX_NAME_LENGTH } type="text" value={ variableName } onChange={ event => setVariableName(normalizeVariableName(event.target.value)) } />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<Text>{ LocalizeText('wiredfurni.params.variables.room_selection') }</Text>
|
||||
<select className="form-select form-select-sm" value={ sourceRoomId } onChange={ event => setSourceRoomId(parseInt(event.target.value, 10) || 0) }>
|
||||
<option value={ 0 }>{ LocalizeText('wiredfurni.variable_picker.search') }</option>
|
||||
{ mergedRoomOptions.map(option => <option key={ option.roomId } value={ option.roomId }>{ option.roomName }</option>) }
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<Text>{ LocalizeText('wiredfurni.params.variables.variable_ref_selection') }</Text>
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
value={ `${ sourceVariableItemId }:${ sourceTargetType }` }
|
||||
onChange={ event =>
|
||||
{
|
||||
const [ nextItemId, nextTargetType ] = event.target.value.split(':').map(value => parseInt(value, 10) || 0);
|
||||
|
||||
setSourceVariableItemId(nextItemId);
|
||||
setSourceTargetType((nextTargetType === TARGET_ROOM) ? TARGET_ROOM : TARGET_USER);
|
||||
} }>
|
||||
<option value="0:0">{ LocalizeText('wiredfurni.variable_picker.search') }</option>
|
||||
{ selectedRoomVariables.map(variable => (
|
||||
<option key={ `${ variable.itemId }:${ variable.targetType }` } value={ `${ variable.itemId }:${ variable.targetType }` }>
|
||||
{ `${ variable.name } (${ getTargetLabel(variable.targetType) })` }
|
||||
</option>
|
||||
)) }
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<Text>{ LocalizeText('wiredfurni.params.variables.settings') }</Text>
|
||||
<label className="flex items-center gap-1 cursor-pointer">
|
||||
<input checked={ readOnly } className="form-check-input" type="checkbox" onChange={ event => setReadOnly(event.target.checked) } />
|
||||
<Text>{ LocalizeText('wiredfurni.params.variables.settings.read_only') }</Text>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</WiredExtraBaseView>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { LocalizeText, WiredFurniType } from '../../../../api';
|
||||
import { Text } from '../../../../common';
|
||||
import { useWired } from '../../../../hooks';
|
||||
import { WiredExtraBaseView } from './WiredExtraBaseView';
|
||||
|
||||
const DEFAULT_CONNECTOR_PLACEHOLDER = '0=text 1\n1=text 2\n2 = text 3';
|
||||
|
||||
export const WiredExtraVariableTextConnectorView: FC<{}> = () =>
|
||||
{
|
||||
const { trigger = null, setIntParams = null, setStringParam = null } = useWired();
|
||||
const [ mappingsText, setMappingsText ] = useState('');
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!trigger) return;
|
||||
|
||||
setMappingsText(trigger.stringData || '');
|
||||
}, [ trigger ]);
|
||||
|
||||
const save = () =>
|
||||
{
|
||||
setIntParams([]);
|
||||
setStringParam(mappingsText ?? '');
|
||||
};
|
||||
|
||||
const placeholderText = (() =>
|
||||
{
|
||||
const localizedText = LocalizeText('wiredfurni.params.variables.connect_text.caption');
|
||||
|
||||
if(!localizedText || (localizedText === 'wiredfurni.params.variables.connect_text.caption')) return DEFAULT_CONNECTOR_PLACEHOLDER;
|
||||
if(localizedText.includes('0,text0') || localizedText.includes('1,text1')) return DEFAULT_CONNECTOR_PLACEHOLDER;
|
||||
|
||||
return localizedText;
|
||||
})();
|
||||
|
||||
return (
|
||||
<WiredExtraBaseView hasSpecialInput={ true } requiresFurni={ WiredFurniType.STUFF_SELECTION_OPTION_NONE } save={ save } cardStyle={ { width: 400 } }>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Text bold>{ LocalizeText('wiredfurni.params.variables.connect_text.title') }</Text>
|
||||
<textarea
|
||||
className="form-control form-control-sm nitro-wired__resizable-textarea"
|
||||
placeholder={ placeholderText }
|
||||
value={ mappingsText }
|
||||
onChange={ event => setMappingsText(event.target.value) } />
|
||||
</div>
|
||||
</WiredExtraBaseView>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { Text } from '../../../../common';
|
||||
|
||||
interface WiredPlaceholderPreviewProps
|
||||
{
|
||||
previewHtml: string;
|
||||
previewToken: string;
|
||||
}
|
||||
|
||||
const copyToClipboard = async (value: string) =>
|
||||
{
|
||||
if(!value) return false;
|
||||
|
||||
try
|
||||
{
|
||||
if(navigator?.clipboard?.writeText)
|
||||
{
|
||||
await navigator.clipboard.writeText(value);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
const textArea = document.createElement('textarea');
|
||||
|
||||
textArea.value = value;
|
||||
textArea.setAttribute('readonly', '');
|
||||
textArea.style.position = 'fixed';
|
||||
textArea.style.opacity = '0';
|
||||
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
|
||||
const copied = document.execCommand('copy');
|
||||
|
||||
document.body.removeChild(textArea);
|
||||
|
||||
return copied;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const WiredPlaceholderPreview: FC<WiredPlaceholderPreviewProps> = props =>
|
||||
{
|
||||
const { previewHtml, previewToken } = props;
|
||||
const [ copied, setCopied ] = useState(false);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!copied) return;
|
||||
|
||||
const timeout = window.setTimeout(() => setCopied(false), 1200);
|
||||
|
||||
return () => window.clearTimeout(timeout);
|
||||
}, [ copied ]);
|
||||
|
||||
const handleCopy = async () =>
|
||||
{
|
||||
const didCopy = await copyToClipboard(previewToken);
|
||||
|
||||
setCopied(didCopy);
|
||||
};
|
||||
|
||||
return (
|
||||
<button type="button" className={ `nitro-wired__placeholder-preview ${ copied ? 'is-copied' : '' }` } onClick={ handleCopy }>
|
||||
<Text dangerouslySetInnerHTML={ { __html: previewHtml } } />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user