feat: add advanced wired variable tools UI

This commit is contained in:
Lorenzune
2026-04-02 04:44:04 +02:00
parent 0a23bfaee4
commit 83540ff329
69 changed files with 10040 additions and 434 deletions
@@ -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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
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>
);
};