mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 15:06:20 +00:00
Polish wired editor UI and variable handling
This commit is contained in:
@@ -84,4 +84,6 @@ export class WiredActionLayoutCode
|
||||
public static VARIABLE_ECHO_EXTRA: number = 83;
|
||||
public static CONTEXT_VARIABLE_EXTRA: number = 84;
|
||||
public static TEXT_INPUT_VARIABLE_EXTRA: number = 85;
|
||||
public static NEG_CALL_ANOTHER_STACK: number = 86;
|
||||
public static NEG_SEND_SIGNAL: number = 87;
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 236 B |
Binary file not shown.
|
After Width: | Height: | Size: 218 B |
Binary file not shown.
|
After Width: | Height: | Size: 737 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
@@ -1,5 +1,5 @@
|
||||
import { ConditionDefinition, TriggerDefinition, WiredActionDefinition } from '@nitrots/nitro-renderer';
|
||||
import { FC } from 'react';
|
||||
import { FC, Fragment } from 'react';
|
||||
import { useWired } from '../../hooks';
|
||||
import { WiredActionLayoutView } from './views/actions/WiredActionLayoutView';
|
||||
import { WiredConditionLayoutView } from './views/conditions/WiredConditionLayoutView';
|
||||
@@ -11,11 +11,20 @@ export const WiredView: FC<{}> = props =>
|
||||
|
||||
if(!trigger) return null;
|
||||
|
||||
if(trigger instanceof WiredActionDefinition) return WiredActionLayoutView(trigger.code);
|
||||
if(trigger instanceof WiredActionDefinition)
|
||||
{
|
||||
return <Fragment key={ `wired-action-${ trigger.id }-${ trigger.code }` }>{ WiredActionLayoutView(trigger.code) }</Fragment>;
|
||||
}
|
||||
|
||||
if(trigger instanceof TriggerDefinition) return WiredTriggerLayoutView(trigger.code);
|
||||
if(trigger instanceof TriggerDefinition)
|
||||
{
|
||||
return <Fragment key={ `wired-trigger-${ trigger.id }-${ trigger.code }` }>{ WiredTriggerLayoutView(trigger.code) }</Fragment>;
|
||||
}
|
||||
|
||||
if(trigger instanceof ConditionDefinition) return WiredConditionLayoutView(trigger.code);
|
||||
if(trigger instanceof ConditionDefinition)
|
||||
{
|
||||
return <Fragment key={ `wired-condition-${ trigger.id }-${ trigger.code }` }>{ WiredConditionLayoutView(trigger.code) }</Fragment>;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { GetRoomEngine, GetSessionDataManager } from '@nitrots/nitro-renderer';
|
||||
import { CSSProperties, FC, PropsWithChildren, ReactNode, useEffect, useState } from 'react';
|
||||
import { LocalizeText, WiredFurniType, WiredSelectionVisualizer } from '../../../api';
|
||||
import wiredBgLeft from '../../../assets/images/wired/wired_bg_left.png';
|
||||
import wiredBgRight from '../../../assets/images/wired/wired_bg_right.png';
|
||||
import { Button, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../common';
|
||||
import { useWired, useWiredTools } from '../../../hooks';
|
||||
import { WiredFurniSelectorView } from './WiredFurniSelectorView';
|
||||
@@ -20,6 +22,7 @@ export interface WiredBaseViewProps
|
||||
|
||||
export const WiredBaseView: FC<PropsWithChildren<WiredBaseViewProps>> = props =>
|
||||
{
|
||||
const WIRED_CARD_WIDTH = 244;
|
||||
const { wiredType = '', requiresFurni = WiredFurniType.STUFF_SELECTION_OPTION_NONE, save = null, validate = null, children = null, hasSpecialInput = false, cardStyle = undefined, footer = null, footerCollapsible = true, selectionPreview = null } = props;
|
||||
const [ wiredName, setWiredName ] = useState<string>(null);
|
||||
const [ needsSave, setNeedsSave ] = useState<boolean>(false);
|
||||
@@ -122,64 +125,53 @@ export const WiredBaseView: FC<PropsWithChildren<WiredBaseViewProps>> = props =>
|
||||
|
||||
const resolvedCardStyle: CSSProperties = { ...cardStyle };
|
||||
|
||||
if(resolvedCardStyle.width !== undefined)
|
||||
{
|
||||
if(typeof resolvedCardStyle.width === 'number')
|
||||
{
|
||||
resolvedCardStyle.maxWidth = Math.min(resolvedCardStyle.width, 324);
|
||||
}
|
||||
else if(typeof resolvedCardStyle.width === 'string')
|
||||
{
|
||||
const match = resolvedCardStyle.width.trim().match(/^(\d+(?:\.\d+)?)px$/i);
|
||||
|
||||
resolvedCardStyle.maxWidth = match ? `${ Math.min(parseFloat(match[1]), 324) }px` : resolvedCardStyle.width;
|
||||
}
|
||||
|
||||
delete resolvedCardStyle.width;
|
||||
}
|
||||
|
||||
if(resolvedCardStyle.minWidth === undefined) resolvedCardStyle.minWidth = 216;
|
||||
if(resolvedCardStyle.maxWidth === undefined) resolvedCardStyle.maxWidth = 'min(90vw, 324px)';
|
||||
resolvedCardStyle.width = WIRED_CARD_WIDTH;
|
||||
resolvedCardStyle.minWidth = WIRED_CARD_WIDTH;
|
||||
resolvedCardStyle.maxWidth = WIRED_CARD_WIDTH;
|
||||
resolvedCardStyle.resize = 'none';
|
||||
|
||||
return (
|
||||
<NitroCardView className="nitro-wired" theme="primary-slim" uniqueKey="nitro-wired" isResizable={ false } style={ resolvedCardStyle }>
|
||||
<NitroCardHeaderView classNames={ [ 'nitro-wired__header' ] } headerText={ LocalizeText('wiredfurni.title') } onCloseClick={ onClose } />
|
||||
<NitroCardContentView classNames={ [ 'nitro-wired__content' ] } gap={ 2 }>
|
||||
<NitroCardContentView classNames={ [ 'nitro-wired__content' ] } gap={ 0 }>
|
||||
<div className="nitro-wired__section nitro-wired__summary">
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<i className={ `icon icon-wired-${ wiredType }` } />
|
||||
<img className="nitro-wired__summary-bg nitro-wired__summary-bg--left" src={ wiredBgLeft } alt="" />
|
||||
<img className="nitro-wired__summary-bg nitro-wired__summary-bg--right" src={ wiredBgRight } alt="" />
|
||||
<div className="nitro-wired__summary-copy">
|
||||
<Text bold className="nitro-wired__summary-title">{ wiredName }</Text>
|
||||
</div>
|
||||
</div>
|
||||
{ !!children && <div className="nitro-wired__divider" /> }
|
||||
{ !!children && <div className="nitro-wired__section nitro-wired__section--body">{ children }</div> }
|
||||
{ (requiresFurni > WiredFurniType.STUFF_SELECTION_OPTION_NONE) &&
|
||||
<>
|
||||
<div className="nitro-wired__divider" />
|
||||
<div className="nitro-wired__section nitro-wired__section--selector">
|
||||
{ selectionPreview || <WiredFurniSelectorView /> }
|
||||
</div>
|
||||
</> }
|
||||
{ footer &&
|
||||
<>
|
||||
<div className="nitro-wired__divider" />
|
||||
<div className="nitro-wired__section nitro-wired__section--footer">
|
||||
{ footerCollapsible
|
||||
? (
|
||||
<>
|
||||
<button className="nitro-wired__advanced-toggle" type="button" onClick={ () => setShowFooter(value => !value) }>
|
||||
{ LocalizeText(showFooter ? 'wiredfurni.params.sources.collapse' : 'wiredfurni.params.sources.expand') }
|
||||
</button>
|
||||
{ showFooter && <div className="nitro-wired__advanced-body">{ footer }</div> }
|
||||
</>
|
||||
)
|
||||
: footer }
|
||||
</div>
|
||||
</> }
|
||||
<div className="nitro-wired__divider" />
|
||||
<div className="flex items-center gap-1 nitro-wired__actions">
|
||||
<Button disabled={ !roomSettings.canModify } fullWidth variant="success" classNames={ [ 'nitro-wired__button', 'nitro-wired__button--primary' ] } onClick={ onSave }>{ LocalizeText('wiredfurni.ready') }</Button>
|
||||
<Button fullWidth variant="secondary" classNames={ [ 'nitro-wired__button', 'nitro-wired__button--secondary' ] } onClick={ onClose }>{ LocalizeText('cancel') }</Button>
|
||||
<div className="nitro-wired__body">
|
||||
{ !!children && <div className="nitro-wired__divider" /> }
|
||||
{ !!children && <div className="nitro-wired__section nitro-wired__section--body">{ children }</div> }
|
||||
{ (requiresFurni > WiredFurniType.STUFF_SELECTION_OPTION_NONE) &&
|
||||
<>
|
||||
<div className="nitro-wired__divider" />
|
||||
<div className="nitro-wired__section nitro-wired__section--selector">
|
||||
{ selectionPreview || <WiredFurniSelectorView /> }
|
||||
</div>
|
||||
</> }
|
||||
{ footer &&
|
||||
<>
|
||||
<div className="nitro-wired__divider" />
|
||||
<div className="nitro-wired__section nitro-wired__section--footer">
|
||||
{ footerCollapsible
|
||||
? (
|
||||
<>
|
||||
<button className="nitro-wired__advanced-toggle" type="button" onClick={ () => setShowFooter(value => !value) }>
|
||||
{ LocalizeText(showFooter ? 'wiredfurni.params.sources.collapse' : 'wiredfurni.params.sources.expand') }
|
||||
</button>
|
||||
{ showFooter && <div className="nitro-wired__advanced-body">{ footer }</div> }
|
||||
</>
|
||||
)
|
||||
: footer }
|
||||
</div>
|
||||
</> }
|
||||
<div className="nitro-wired__divider" />
|
||||
<div className="flex items-center gap-1 nitro-wired__actions">
|
||||
<Button disabled={ !roomSettings.canModify } fullWidth variant="success" classNames={ [ 'nitro-wired__button', 'nitro-wired__button--primary' ] } onClick={ onSave }>{ LocalizeText('wiredfurni.ready') }</Button>
|
||||
<Button fullWidth variant="secondary" classNames={ [ 'nitro-wired__button', 'nitro-wired__button--secondary' ] } onClick={ onClose }>{ LocalizeText('cancel') }</Button>
|
||||
</div>
|
||||
</div>
|
||||
</NitroCardContentView>
|
||||
</NitroCardView>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FC } from 'react';
|
||||
import { FC, ReactNode } from 'react';
|
||||
import { FaChevronLeft, FaChevronRight, FaMousePointer } from 'react-icons/fa';
|
||||
import { LocalizeText } from '../../../api';
|
||||
import { Button, Text } from '../../../common';
|
||||
@@ -16,13 +16,14 @@ interface WiredFurniSelectionSourceRowProps
|
||||
selectionLimit: number;
|
||||
selectionEnabledValues: number[];
|
||||
showSelectionToggle?: boolean;
|
||||
headerContent?: ReactNode;
|
||||
onChange: (value: number) => void;
|
||||
onSelectionActivate?: () => void;
|
||||
}
|
||||
|
||||
export const WiredFurniSelectionSourceRow: FC<WiredFurniSelectionSourceRowProps> = props =>
|
||||
{
|
||||
const { title = '', titleIsLiteral = false, options = [], value = 0, selectionKind = 'primary', selectionActive = false, selectionCount = 0, selectionLimit = 0, selectionEnabledValues = [], showSelectionToggle = true, onChange = null, onSelectionActivate = null } = props;
|
||||
const { title = '', titleIsLiteral = false, options = [], value = 0, selectionKind = 'primary', selectionActive = false, selectionCount = 0, selectionLimit = 0, selectionEnabledValues = [], showSelectionToggle = true, headerContent = null, onChange = null, onSelectionActivate = null } = props;
|
||||
const currentIndex = Math.max(0, options.findIndex(option => (option.value === value)));
|
||||
const currentOption = options[currentIndex] ?? options[0];
|
||||
const canActivateSelection = !!onSelectionActivate && selectionEnabledValues.includes(currentOption?.value);
|
||||
@@ -45,6 +46,7 @@ export const WiredFurniSelectionSourceRow: FC<WiredFurniSelectionSourceRowProps>
|
||||
<div className="nitro-wired__source-row">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<Text>{ resolvedTitle }</Text>
|
||||
{ headerContent }
|
||||
{ showSelectionToggle &&
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -46,10 +46,10 @@ const FURNI_SOURCE_LABEL_ORDER = [
|
||||
const USER_SOURCE_LABEL_ORDER = [
|
||||
'wiredfurni.params.sources.users.0',
|
||||
'wiredfurni.params.sources.users.11',
|
||||
'wiredfurni.params.sources.users.100',
|
||||
'wiredfurni.params.sources.users.101',
|
||||
'wiredfurni.params.sources.users.200',
|
||||
'wiredfurni.params.sources.users.201',
|
||||
'wiredfurni.params.sources.users.100',
|
||||
'wiredfurni.params.sources.users.101',
|
||||
'wiredfurni.params.sources.users.900'
|
||||
];
|
||||
|
||||
@@ -219,7 +219,12 @@ export const WiredSourcesSelector: FC<WiredSourcesSelectorProps> = props =>
|
||||
const { trigger = null } = useWired();
|
||||
const availableUserSources = useAvailableUserSources(trigger, userSources, usersTitle, allowClickedUserSource);
|
||||
const orderedFurniSources = useMemo(() => sortWiredSourceOptions(furniSources, 'furni'), [ furniSources ]);
|
||||
const orderedUserSources = useMemo(() => sortWiredSourceOptions(availableUserSources, 'users'), [ availableUserSources ]);
|
||||
const orderedUserSources = useMemo(() =>
|
||||
{
|
||||
if(usersTitle === BOT_SOURCE_TITLE) return [ ...availableUserSources ];
|
||||
|
||||
return sortWiredSourceOptions(availableUserSources, 'users');
|
||||
}, [ availableUserSources, usersTitle ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
|
||||
@@ -55,6 +55,7 @@ import { WiredActionSetFurniStateToView } from './WiredActionSetFurniStateToView
|
||||
import { WiredActionTeleportView } from './WiredActionTeleportView';
|
||||
import { WiredActionToggleFurniStateView } from './WiredActionToggleFurniStateView';
|
||||
import { WiredActionUnfreezeView } from './WiredActionUnfreezeView';
|
||||
import { WiredActionNegativeCallAnotherStackView } from './WiredActionNegativeCallAnotherStackView';
|
||||
import { WiredExtraFilterFurniView } from '../extras/WiredExtraFilterFurniView';
|
||||
import { WiredExtraFilterFurniByVariableView } from '../extras/WiredExtraFilterFurniByVariableView';
|
||||
import { WiredExtraFilterUserView } from '../extras/WiredExtraFilterUserView';
|
||||
@@ -103,6 +104,8 @@ export const WiredActionLayoutView = (code: number) =>
|
||||
return <WiredActionBotTeleportView />;
|
||||
case WiredActionLayoutCode.CALL_ANOTHER_STACK:
|
||||
return <WiredActionCallAnotherStackView />;
|
||||
case WiredActionLayoutCode.NEG_CALL_ANOTHER_STACK:
|
||||
return <WiredActionNegativeCallAnotherStackView />;
|
||||
case WiredActionLayoutCode.CHASE:
|
||||
return <WiredActionChaseView />;
|
||||
case WiredActionLayoutCode.CHAT:
|
||||
@@ -251,6 +254,8 @@ export const WiredActionLayoutView = (code: number) =>
|
||||
return <WiredExtraTextInputVariableView />;
|
||||
case WiredActionLayoutCode.SEND_SIGNAL:
|
||||
return <WiredActionSendSignalView />;
|
||||
case WiredActionLayoutCode.NEG_SEND_SIGNAL:
|
||||
return <WiredActionSendSignalView />;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { WiredFurniType } from '../../../../api';
|
||||
import { useWired } from '../../../../hooks';
|
||||
import { WiredSourcesSelector } from '../WiredSourcesSelector';
|
||||
import { WiredActionBaseView } from './WiredActionBaseView';
|
||||
|
||||
export const WiredActionNegativeCallAnotherStackView: FC<{}> = () =>
|
||||
{
|
||||
const { trigger = null, setIntParams = null } = useWired();
|
||||
const [ furniSource, setFurniSource ] = useState<number>(() =>
|
||||
{
|
||||
if(trigger?.intData?.length >= 1) return trigger.intData[0];
|
||||
|
||||
return (trigger?.selectedItems?.length ?? 0) > 0 ? 100 : 0;
|
||||
});
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!trigger) return;
|
||||
|
||||
if(trigger.intData.length >= 1) setFurniSource(trigger.intData[0]);
|
||||
else setFurniSource((trigger.selectedItems?.length ?? 0) > 0 ? 100 : 0);
|
||||
}, [ trigger ]);
|
||||
|
||||
const onChangeFurniSource = (next: number) => setFurniSource(next);
|
||||
|
||||
const save = () => setIntParams([ furniSource ]);
|
||||
|
||||
return (
|
||||
<WiredActionBaseView
|
||||
hasSpecialInput={ true }
|
||||
requiresFurni={ WiredFurniType.STUFF_SELECTION_OPTION_BY_ID_BY_TYPE_OR_FROM_CONTEXT }
|
||||
save={ save }
|
||||
footer={ <WiredSourcesSelector showFurni={ true } furniSource={ furniSource } onChangeFurni={ onChangeFurniSource } /> } />
|
||||
);
|
||||
};
|
||||
@@ -9,20 +9,37 @@ const MAX_NAME_LENGTH = 40;
|
||||
|
||||
const normalizeVariableName = (value: string) =>
|
||||
{
|
||||
let normalizedValue = (value ?? '').trim().replace(/[\t\r\n]/g, '');
|
||||
let normalizedValue = (value ?? '').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.substring(1);
|
||||
}
|
||||
|
||||
normalizedValue = normalizedValue.replace(/\s+/g, '_');
|
||||
normalizedValue = normalizedValue.replace(/[^A-Za-z0-9_]/g, '');
|
||||
|
||||
return normalizedValue.slice(0, MAX_NAME_LENGTH);
|
||||
};
|
||||
|
||||
const handleVariableNameKeyDown = (event: React.KeyboardEvent<HTMLInputElement>, setValue: (value: string) => void) =>
|
||||
{
|
||||
if(event.key !== ' ') return;
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const input = event.currentTarget;
|
||||
const start = (input.selectionStart ?? input.value.length);
|
||||
const end = (input.selectionEnd ?? start);
|
||||
const nextValue = `${ input.value.substring(0, start) }_${ input.value.substring(end) }`;
|
||||
|
||||
setValue(normalizeVariableName(nextValue));
|
||||
|
||||
window.requestAnimationFrame(() => input.setSelectionRange(Math.min(start + 1, input.value.length + 1), Math.min(start + 1, input.value.length + 1)));
|
||||
};
|
||||
|
||||
export const WiredExtraContextVariableView: FC<{}> = () =>
|
||||
{
|
||||
const { trigger = null, setIntParams = null, setStringParam = null } = useWired();
|
||||
@@ -48,7 +65,7 @@ export const WiredExtraContextVariableView: FC<{}> = () =>
|
||||
<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)) } />
|
||||
<NitroInput maxLength={ MAX_NAME_LENGTH } type="text" value={ variableName } onChange={ event => setVariableName(normalizeVariableName(event.target.value)) } onKeyDown={ event => handleVariableNameKeyDown(event, setVariableName) } />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
|
||||
@@ -12,20 +12,37 @@ const MAX_NAME_LENGTH = 40;
|
||||
|
||||
const normalizeVariableName = (value: string) =>
|
||||
{
|
||||
let normalizedValue = (value ?? '').trim().replace(/[\t\r\n]/g, '');
|
||||
let normalizedValue = (value ?? '').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.substring(1);
|
||||
}
|
||||
|
||||
normalizedValue = normalizedValue.replace(/\s+/g, '_');
|
||||
normalizedValue = normalizedValue.replace(/[^A-Za-z0-9_]/g, '');
|
||||
|
||||
return normalizedValue.slice(0, MAX_NAME_LENGTH);
|
||||
};
|
||||
|
||||
const handleVariableNameKeyDown = (event: React.KeyboardEvent<HTMLInputElement>, setValue: (value: string) => void) =>
|
||||
{
|
||||
if(event.key !== ' ') return;
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const input = event.currentTarget;
|
||||
const start = (input.selectionStart ?? input.value.length);
|
||||
const end = (input.selectionEnd ?? start);
|
||||
const nextValue = `${ input.value.substring(0, start) }_${ input.value.substring(end) }`;
|
||||
|
||||
setValue(normalizeVariableName(nextValue));
|
||||
|
||||
window.requestAnimationFrame(() => input.setSelectionRange(Math.min(start + 1, input.value.length + 1), Math.min(start + 1, input.value.length + 1)));
|
||||
};
|
||||
|
||||
export const WiredExtraRoomVariableView: FC<{}> = () =>
|
||||
{
|
||||
const { trigger = null, setIntParams = null, setStringParam = null } = useWired();
|
||||
@@ -57,7 +74,7 @@ export const WiredExtraRoomVariableView: FC<{}> = () =>
|
||||
<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)) } />
|
||||
<NitroInput maxLength={ MAX_NAME_LENGTH } type="text" value={ variableName } onChange={ event => setVariableName(normalizeVariableName(event.target.value)) } onKeyDown={ event => handleVariableNameKeyDown(event, setVariableName) } />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
|
||||
@@ -12,20 +12,37 @@ const MAX_NAME_LENGTH = 40;
|
||||
|
||||
const normalizeVariableName = (value: string) =>
|
||||
{
|
||||
let normalizedValue = (value ?? '').trim().replace(/[\t\r\n]/g, '');
|
||||
let normalizedValue = (value ?? '').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.substring(1);
|
||||
}
|
||||
|
||||
normalizedValue = normalizedValue.replace(/\s+/g, '_');
|
||||
normalizedValue = normalizedValue.replace(/[^A-Za-z0-9_]/g, '');
|
||||
|
||||
return normalizedValue.slice(0, MAX_NAME_LENGTH);
|
||||
};
|
||||
|
||||
const handleVariableNameKeyDown = (event: React.KeyboardEvent<HTMLInputElement>, setValue: (value: string) => void) =>
|
||||
{
|
||||
if(event.key !== ' ') return;
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const input = event.currentTarget;
|
||||
const start = (input.selectionStart ?? input.value.length);
|
||||
const end = (input.selectionEnd ?? start);
|
||||
const nextValue = `${ input.value.substring(0, start) }_${ input.value.substring(end) }`;
|
||||
|
||||
setValue(normalizeVariableName(nextValue));
|
||||
|
||||
window.requestAnimationFrame(() => input.setSelectionRange(Math.min(start + 1, input.value.length + 1), Math.min(start + 1, input.value.length + 1)));
|
||||
};
|
||||
|
||||
interface WiredExtraVariableViewProps
|
||||
{
|
||||
availabilityRoomValue: number;
|
||||
@@ -76,7 +93,7 @@ export const WiredExtraVariableView: FC<WiredExtraVariableViewProps> = props =>
|
||||
<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)) } />
|
||||
<NitroInput maxLength={ MAX_NAME_LENGTH } type="text" value={ variableName } onChange={ event => setVariableName(normalizeVariableName(event.target.value)) } onKeyDown={ event => handleVariableNameKeyDown(event, setVariableName) } />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
|
||||
@@ -34,20 +34,37 @@ const TARGET_BUTTONS: Array<{ key: EchoSourceTarget; icon: string; }> = [
|
||||
|
||||
const normalizeVariableName = (value: string) =>
|
||||
{
|
||||
let normalizedValue = (value ?? '').trim().replace(/[\t\r\n]/g, '');
|
||||
let normalizedValue = (value ?? '').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.substring(1);
|
||||
}
|
||||
|
||||
normalizedValue = normalizedValue.replace(/\s+/g, '_');
|
||||
normalizedValue = normalizedValue.replace(/[^A-Za-z0-9_]/g, '');
|
||||
|
||||
return normalizedValue.slice(0, MAX_NAME_LENGTH);
|
||||
};
|
||||
|
||||
const handleVariableNameKeyDown = (event: React.KeyboardEvent<HTMLInputElement>, setValue: (value: string) => void) =>
|
||||
{
|
||||
if(event.key !== ' ') return;
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const input = event.currentTarget;
|
||||
const start = (input.selectionStart ?? input.value.length);
|
||||
const end = (input.selectionEnd ?? start);
|
||||
const nextValue = `${ input.value.substring(0, start) }_${ input.value.substring(end) }`;
|
||||
|
||||
setValue(normalizeVariableName(nextValue));
|
||||
|
||||
window.requestAnimationFrame(() => input.setSelectionRange(Math.min(start + 1, input.value.length + 1), Math.min(start + 1, input.value.length + 1)));
|
||||
};
|
||||
|
||||
const parseEditorData = (value: string): IEchoEditorData =>
|
||||
{
|
||||
if(!value?.trim().startsWith('{')) return {};
|
||||
@@ -173,7 +190,7 @@ export const WiredExtraVariableEchoView: FC<{}> = () =>
|
||||
<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)) } />
|
||||
<NitroInput maxLength={ MAX_NAME_LENGTH } type="text" value={ variableName } onChange={ event => setVariableName(normalizeVariableName(event.target.value)) } onKeyDown={ event => handleVariableNameKeyDown(event, setVariableName) } />
|
||||
</div>
|
||||
|
||||
<div className="nitro-wired__give-var-heading">
|
||||
|
||||
@@ -38,20 +38,37 @@ interface IVariableReferenceEditorData
|
||||
|
||||
const normalizeVariableName = (value: string) =>
|
||||
{
|
||||
let normalizedValue = (value ?? '').trim().replace(/[\t\r\n]/g, '');
|
||||
let normalizedValue = (value ?? '').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.substring(1);
|
||||
}
|
||||
|
||||
normalizedValue = normalizedValue.replace(/\s+/g, '_');
|
||||
normalizedValue = normalizedValue.replace(/[^A-Za-z0-9_]/g, '');
|
||||
|
||||
return normalizedValue.slice(0, MAX_NAME_LENGTH);
|
||||
};
|
||||
|
||||
const handleVariableNameKeyDown = (event: React.KeyboardEvent<HTMLInputElement>, setValue: (value: string) => void) =>
|
||||
{
|
||||
if(event.key !== ' ') return;
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const input = event.currentTarget;
|
||||
const start = (input.selectionStart ?? input.value.length);
|
||||
const end = (input.selectionEnd ?? start);
|
||||
const nextValue = `${ input.value.substring(0, start) }_${ input.value.substring(end) }`;
|
||||
|
||||
setValue(normalizeVariableName(nextValue));
|
||||
|
||||
window.requestAnimationFrame(() => input.setSelectionRange(Math.min(start + 1, input.value.length + 1), Math.min(start + 1, input.value.length + 1)));
|
||||
};
|
||||
|
||||
const parseEditorData = (value: string): IVariableReferenceEditorData =>
|
||||
{
|
||||
if(!value?.trim().startsWith('{')) return {};
|
||||
@@ -187,7 +204,7 @@ export const WiredExtraVariableReferenceView: FC<{}> = () =>
|
||||
<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)) } />
|
||||
<NitroInput maxLength={ MAX_NAME_LENGTH } type="text" value={ variableName } onChange={ event => setVariableName(normalizeVariableName(event.target.value)) } onKeyDown={ event => handleVariableNameKeyDown(event, setVariableName) } />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
|
||||
@@ -5,6 +5,26 @@ import { useWired } from '../../../../hooks';
|
||||
import { WiredExtraBaseView } from './WiredExtraBaseView';
|
||||
|
||||
const DEFAULT_CONNECTOR_PLACEHOLDER = '0=text 1\n1=text 2\n2 = text 3';
|
||||
const MAX_CONNECTOR_LINES = 30;
|
||||
const MAX_CONNECTOR_CHARACTERS = 1000;
|
||||
|
||||
const truncateMappingsText = (value: string) =>
|
||||
{
|
||||
const normalizedValue = (value ?? '').replace(/\r/g, '');
|
||||
const lines = normalizedValue.split('\n');
|
||||
const limitedByLines = lines.slice(0, MAX_CONNECTOR_LINES).join('\n');
|
||||
|
||||
return (limitedByLines.length > MAX_CONNECTOR_CHARACTERS)
|
||||
? limitedByLines.slice(0, MAX_CONNECTOR_CHARACTERS)
|
||||
: limitedByLines;
|
||||
};
|
||||
|
||||
const getLineCount = (value: string) =>
|
||||
{
|
||||
if(!value.length) return 0;
|
||||
|
||||
return value.split('\n').length;
|
||||
};
|
||||
|
||||
export const WiredExtraVariableTextConnectorView: FC<{}> = () =>
|
||||
{
|
||||
@@ -15,7 +35,7 @@ export const WiredExtraVariableTextConnectorView: FC<{}> = () =>
|
||||
{
|
||||
if(!trigger) return;
|
||||
|
||||
setMappingsText(trigger.stringData || '');
|
||||
setMappingsText(truncateMappingsText(trigger.stringData || ''));
|
||||
}, [ trigger ]);
|
||||
|
||||
const save = () =>
|
||||
@@ -24,6 +44,8 @@ export const WiredExtraVariableTextConnectorView: FC<{}> = () =>
|
||||
setStringParam(mappingsText ?? '');
|
||||
};
|
||||
|
||||
const handleTextChange = (value: string) => setMappingsText(truncateMappingsText(value));
|
||||
|
||||
const placeholderText = (() =>
|
||||
{
|
||||
const localizedText = LocalizeText('wiredfurni.params.variables.connect_text.caption');
|
||||
@@ -34,15 +56,20 @@ export const WiredExtraVariableTextConnectorView: FC<{}> = () =>
|
||||
return localizedText;
|
||||
})();
|
||||
|
||||
const lineCount = getLineCount(mappingsText);
|
||||
const characterCount = mappingsText.length;
|
||||
|
||||
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"
|
||||
maxLength={ MAX_CONNECTOR_CHARACTERS }
|
||||
placeholder={ placeholderText }
|
||||
value={ mappingsText }
|
||||
onChange={ event => setMappingsText(event.target.value) } />
|
||||
onChange={ event => handleTextChange(event.target.value) } />
|
||||
<Text small>{ `${ lineCount }/${ MAX_CONNECTOR_LINES } righe - ${ characterCount }/${ MAX_CONNECTOR_CHARACTERS } caratteri` }</Text>
|
||||
</div>
|
||||
</WiredExtraBaseView>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,456 @@
|
||||
import { GetRoomEngine } from '@nitrots/nitro-renderer';
|
||||
import { CSSProperties, FC, MouseEvent as ReactMouseEvent, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { FaMinus, FaPlus, FaTimes } from 'react-icons/fa';
|
||||
import { MdGridOn } from 'react-icons/md';
|
||||
import { LocalizeText, WiredFurniType } from '../../../../api';
|
||||
import sourceFurniIcon from '../../../../assets/images/wired/source_furni.png';
|
||||
import sourceUserIcon from '../../../../assets/images/wired/source_user.png';
|
||||
import { Button, Text } from '../../../../common';
|
||||
import { useWired } from '../../../../hooks';
|
||||
import { WiredFurniSelectionSourceRow } from '../WiredFurniSelectionSourceRow';
|
||||
import { sortWiredSourceOptions, useAvailableUserSources } from '../WiredSourcesSelector';
|
||||
import { WiredSelectorBaseView } from './WiredSelectorBaseView';
|
||||
|
||||
const SOURCE_USER_TRIGGER = 0;
|
||||
const SOURCE_USER_SIGNAL = 1;
|
||||
const SOURCE_USER_CLICKED = 2;
|
||||
const SOURCE_FURNI_TRIGGER = 3;
|
||||
const SOURCE_FURNI_PICKED = 4;
|
||||
const SOURCE_FURNI_SIGNAL = 5;
|
||||
|
||||
const USER_SOURCES = sortWiredSourceOptions([
|
||||
{ value: SOURCE_USER_TRIGGER, label: 'wiredfurni.params.sources.users.0' },
|
||||
{ value: SOURCE_USER_SIGNAL, label: 'wiredfurni.params.sources.users.201' },
|
||||
{ value: SOURCE_USER_CLICKED, label: 'wiredfurni.params.sources.users.11' }
|
||||
], 'users');
|
||||
|
||||
const FURNI_SOURCES = sortWiredSourceOptions([
|
||||
{ value: SOURCE_FURNI_TRIGGER, label: 'wiredfurni.params.sources.furni.0' },
|
||||
{ value: SOURCE_FURNI_PICKED, label: 'wiredfurni.params.sources.furni.100' },
|
||||
{ value: SOURCE_FURNI_SIGNAL, label: 'wiredfurni.params.sources.furni.201' }
|
||||
], 'furni');
|
||||
|
||||
const SOURCE_GROUP_BUTTONS = [
|
||||
{ key: 'user', icon: sourceUserIcon, isUserGroup: true },
|
||||
{ key: 'furni', icon: sourceFurniIcon, isUserGroup: false }
|
||||
] as const;
|
||||
|
||||
const TILE_W = 22;
|
||||
const TILE_H = 11;
|
||||
const GRID_RANGE = 4;
|
||||
const CX = GRID_RANGE * TILE_W + TILE_W / 2;
|
||||
const CY = GRID_RANGE * TILE_H + TILE_H / 2;
|
||||
const GRID_PX_W = (GRID_RANGE * 2 + 1) * TILE_W;
|
||||
const GRID_PX_H = (GRID_RANGE * 2 + 1) * TILE_H;
|
||||
|
||||
type Tile = { x: number; y: number };
|
||||
|
||||
const tileIncluded = (tiles: Tile[], x: number, y: number) => tiles.some(tile => (tile.x === x && tile.y === y));
|
||||
const tileLeft = (rx: number, ry: number) => CX + (rx - ry) * (TILE_W / 2) - TILE_W / 2;
|
||||
const tileTop = (rx: number, ry: number) => CY + (rx + ry) * (TILE_H / 2) - TILE_H / 2;
|
||||
|
||||
interface NeighborhoodGridProps
|
||||
{
|
||||
selectedTiles: Tile[];
|
||||
targetTile: Tile;
|
||||
invert: boolean;
|
||||
onSetTile: (x: number, y: number, selected: boolean) => void;
|
||||
onMoveTarget: (x: number, y: number) => void;
|
||||
targetPlacementMode: boolean;
|
||||
}
|
||||
|
||||
const NeighborhoodGrid: FC<NeighborhoodGridProps> = props =>
|
||||
{
|
||||
const { selectedTiles = [], targetTile, invert = false, onSetTile = null, onMoveTarget = null, targetPlacementMode = false } = props;
|
||||
const [ dragMode, setDragMode ] = useState<'add' | 'remove' | 'target' | null>(null);
|
||||
const tiles: JSX.Element[] = [];
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const stopDragging = () => setDragMode(null);
|
||||
|
||||
window.addEventListener('mouseup', stopDragging);
|
||||
|
||||
return () => window.removeEventListener('mouseup', stopDragging);
|
||||
}, []);
|
||||
|
||||
const beginTileDrag = (event: ReactMouseEvent<HTMLDivElement>, rx: number, ry: number, isSelected: boolean) =>
|
||||
{
|
||||
event.preventDefault();
|
||||
|
||||
if(targetPlacementMode)
|
||||
{
|
||||
setDragMode('target');
|
||||
onMoveTarget && onMoveTarget(rx, ry);
|
||||
return;
|
||||
}
|
||||
|
||||
const nextDragMode = isSelected ? 'remove' : 'add';
|
||||
|
||||
setDragMode(nextDragMode);
|
||||
onSetTile && onSetTile(rx, ry, nextDragMode === 'add');
|
||||
};
|
||||
|
||||
const continueTileDrag = (event: ReactMouseEvent<HTMLDivElement>, rx: number, ry: number) =>
|
||||
{
|
||||
if(!(event.buttons & 1) || !dragMode) return;
|
||||
|
||||
if(dragMode === 'target')
|
||||
{
|
||||
onMoveTarget && onMoveTarget(rx, ry);
|
||||
return;
|
||||
}
|
||||
|
||||
onSetTile && onSetTile(rx, ry, dragMode === 'add');
|
||||
};
|
||||
|
||||
for(let ry = -GRID_RANGE; ry <= GRID_RANGE; ry++)
|
||||
{
|
||||
for(let rx = -GRID_RANGE; rx <= GRID_RANGE; rx++)
|
||||
{
|
||||
const isTarget = (rx === targetTile.x && ry === targetTile.y);
|
||||
const isSelected = tileIncluded(selectedTiles, rx, ry);
|
||||
const isActive = invert ? !isSelected : isSelected;
|
||||
const left = tileLeft(rx, ry);
|
||||
const top = tileTop(rx, ry);
|
||||
const zIndex = rx + ry + GRID_RANGE * 2 + 10;
|
||||
|
||||
const diamondStyle: CSSProperties = {
|
||||
position: 'absolute',
|
||||
width: TILE_W,
|
||||
height: TILE_H,
|
||||
left,
|
||||
top,
|
||||
clipPath: 'polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)',
|
||||
backgroundColor: isActive ? '#3399ff' : '#2a3042',
|
||||
cursor: 'pointer',
|
||||
zIndex,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: isTarget ? '#ffffff' : 'transparent',
|
||||
fontSize: 10
|
||||
};
|
||||
|
||||
const borderStyle: CSSProperties = {
|
||||
position: 'absolute',
|
||||
width: TILE_W + (isTarget ? 6 : 2),
|
||||
height: TILE_H + (isTarget ? 6 : 2),
|
||||
left: left - (isTarget ? 3 : 1),
|
||||
top: top - (isTarget ? 3 : 1),
|
||||
clipPath: 'polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)',
|
||||
backgroundColor: isTarget ? '#ffffff' : (isActive ? '#1166cc' : '#1a2032'),
|
||||
zIndex: zIndex - 1,
|
||||
pointerEvents: 'none'
|
||||
};
|
||||
|
||||
const outlineStyle: CSSProperties = {
|
||||
position: 'absolute',
|
||||
width: TILE_W + 4,
|
||||
height: TILE_H + 4,
|
||||
left: left - 2,
|
||||
top: top - 2,
|
||||
zIndex: zIndex + 1,
|
||||
pointerEvents: 'none',
|
||||
overflow: 'visible'
|
||||
};
|
||||
|
||||
tiles.push(
|
||||
<div key={ `border-${ rx }-${ ry }` } style={ borderStyle } />,
|
||||
isTarget && (
|
||||
<svg key={ `outline-${ rx }-${ ry }` } style={ outlineStyle } viewBox={ `0 0 ${ TILE_W + 4 } ${ TILE_H + 4 }` }>
|
||||
<polygon
|
||||
points={ `${ (TILE_W + 4) / 2 },2 ${ TILE_W + 2 },${ (TILE_H + 4) / 2 } ${ (TILE_W + 4) / 2 },${ TILE_H + 2 } 2,${ (TILE_H + 4) / 2 }` }
|
||||
fill="none"
|
||||
stroke="#ffffff"
|
||||
strokeWidth="1" />
|
||||
</svg>
|
||||
),
|
||||
<div
|
||||
key={ `tile-${ rx }-${ ry }` }
|
||||
style={ diamondStyle }
|
||||
title={ `(${ rx }, ${ ry })` }
|
||||
onMouseDown={ event => beginTileDrag(event, rx, ry, isSelected) }
|
||||
onMouseEnter={ event => continueTileDrag(event, rx, ry) } />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={ { position: 'relative', width: GRID_PX_W, height: GRID_PX_H, flexShrink: 0 } } onContextMenu={ event => event.preventDefault() }>
|
||||
{ tiles }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const WiredNeighborhoodSelectorView: FC<{}> = () =>
|
||||
{
|
||||
const [ selectedTiles, setSelectedTiles ] = useState<Tile[]>([]);
|
||||
const [ filterExisting, setFilterExisting ] = useState(false);
|
||||
const [ invert, setInvert ] = useState(false);
|
||||
const [ sourceType, setSourceType ] = useState(SOURCE_USER_TRIGGER);
|
||||
const [ targetTile, setTargetTile ] = useState<Tile>({ x: 0, y: 0 });
|
||||
const [ targetPlacementMode, setTargetPlacementMode ] = useState(false);
|
||||
const [ curX, setCurX ] = useState(0);
|
||||
const [ curY, setCurY ] = useState(0);
|
||||
|
||||
const { trigger = null, furniIds = [], setIntParams } = useWired();
|
||||
const availableUserSources = useAvailableUserSources(trigger, USER_SOURCES);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
GetRoomEngine().areaSelectionManager.clearHighlight();
|
||||
GetRoomEngine().areaSelectionManager.deactivate();
|
||||
}, []);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!trigger) return;
|
||||
|
||||
const params = trigger.intData;
|
||||
|
||||
if(params.length >= 1) setSourceType(params[0]);
|
||||
if(params.length >= 2) setFilterExisting(params[1] === 1);
|
||||
if(params.length >= 3) setInvert(params[2] === 1);
|
||||
if(params.length >= 5) setTargetTile({ x: params[3], y: params[4] });
|
||||
else setTargetTile({ x: 0, y: 0 });
|
||||
|
||||
if(params.length < 6)
|
||||
{
|
||||
setSelectedTiles([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const tileCount = params[5];
|
||||
const nextTiles: Tile[] = [];
|
||||
|
||||
for(let index = 0; index < tileCount; index++)
|
||||
{
|
||||
const tileIndex = 6 + index * 2;
|
||||
|
||||
if((tileIndex + 1) >= params.length) break;
|
||||
|
||||
nextTiles.push({ x: params[tileIndex], y: params[tileIndex + 1] });
|
||||
}
|
||||
|
||||
setSelectedTiles(nextTiles);
|
||||
}, [ trigger ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(sourceType !== SOURCE_USER_CLICKED) return;
|
||||
if(availableUserSources.some(option => option.value === SOURCE_USER_CLICKED)) return;
|
||||
|
||||
setSourceType(SOURCE_USER_TRIGGER);
|
||||
}, [ availableUserSources, sourceType ]);
|
||||
|
||||
const save = useCallback(() =>
|
||||
{
|
||||
setIntParams([
|
||||
sourceType,
|
||||
filterExisting ? 1 : 0,
|
||||
invert ? 1 : 0,
|
||||
targetTile.x,
|
||||
targetTile.y,
|
||||
selectedTiles.length,
|
||||
...selectedTiles.flatMap(tile => [ tile.x, tile.y ])
|
||||
]);
|
||||
}, [ filterExisting, invert, selectedTiles, setIntParams, sourceType, targetTile.x, targetTile.y ]);
|
||||
|
||||
const setTileSelection = useCallback((x: number, y: number, selected: boolean) =>
|
||||
{
|
||||
setSelectedTiles(previous =>
|
||||
{
|
||||
const alreadySelected = tileIncluded(previous, x, y);
|
||||
|
||||
if(selected)
|
||||
{
|
||||
if(alreadySelected) return previous;
|
||||
|
||||
return [ ...previous, { x, y } ];
|
||||
}
|
||||
|
||||
if(!alreadySelected) return previous;
|
||||
|
||||
return previous.filter(tile => !(tile.x === x && tile.y === y));
|
||||
});
|
||||
}, []);
|
||||
|
||||
const activeSources = useMemo(() => ((sourceType <= SOURCE_USER_CLICKED) ? availableUserSources : FURNI_SOURCES), [ availableUserSources, sourceType ]);
|
||||
const isUserGroup = sourceType <= SOURCE_USER_CLICKED;
|
||||
const currentIndex = Math.max(0, activeSources.findIndex(option => (option.value === sourceType)));
|
||||
const currentSourceType = activeSources[currentIndex]?.value ?? sourceType;
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(currentSourceType === sourceType) return;
|
||||
|
||||
setSourceType(currentSourceType);
|
||||
}, [ currentSourceType, sourceType ]);
|
||||
|
||||
const changeGroup = useCallback((nextIsUserGroup: boolean) =>
|
||||
{
|
||||
if(nextIsUserGroup === isUserGroup) return;
|
||||
|
||||
const nextOptions = nextIsUserGroup ? availableUserSources : FURNI_SOURCES;
|
||||
const nextIndex = Math.min(currentIndex, Math.max(0, nextOptions.length - 1));
|
||||
const nextOption = nextOptions[nextIndex] ?? nextOptions[0];
|
||||
|
||||
if(nextOption) setSourceType(nextOption.value);
|
||||
}, [ availableUserSources, currentIndex, isUserGroup ]);
|
||||
|
||||
const addTile = useCallback(() =>
|
||||
{
|
||||
setSelectedTiles(previous =>
|
||||
{
|
||||
if(tileIncluded(previous, curX, curY)) return previous;
|
||||
|
||||
return [ ...previous, { x: curX, y: curY } ];
|
||||
});
|
||||
}, [ curX, curY ]);
|
||||
|
||||
const removeTile = useCallback(() =>
|
||||
{
|
||||
setSelectedTiles(previous => previous.filter(tile => !(tile.x === curX && tile.y === curY)));
|
||||
}, [ curX, curY ]);
|
||||
|
||||
const loadDefaultPattern = useCallback(() =>
|
||||
{
|
||||
const nextTiles: Tile[] = [];
|
||||
|
||||
for(let y = -2; y <= 2; y++)
|
||||
{
|
||||
for(let x = -2; x <= 2; x++)
|
||||
{
|
||||
if(x === 0 && y === 0) continue;
|
||||
|
||||
nextTiles.push({ x, y });
|
||||
}
|
||||
}
|
||||
|
||||
setSelectedTiles(nextTiles);
|
||||
}, []);
|
||||
|
||||
const requiresFurni = (sourceType === SOURCE_FURNI_PICKED)
|
||||
? WiredFurniType.STUFF_SELECTION_OPTION_BY_ID
|
||||
: WiredFurniType.STUFF_SELECTION_OPTION_NONE;
|
||||
|
||||
return (
|
||||
<WiredSelectorBaseView hasSpecialInput={ true } requiresFurni={ requiresFurni } save={ save } hideDelay={ true } cardStyle={ { width: '400px' } }>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Text bold>{ LocalizeText('wiredfurni.params.neighborhood_selection') }</Text>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant={ targetPlacementMode ? 'success' : 'secondary' }
|
||||
className="px-2 py-1"
|
||||
onClick={ () => setTargetPlacementMode(value => !value) }
|
||||
title="Sposta target">
|
||||
<span aria-hidden className="relative inline-block h-[14px] w-[14px]">
|
||||
<span className="absolute left-1/2 top-0 h-full w-px -translate-x-1/2 bg-current" />
|
||||
<span className="absolute left-0 top-1/2 h-px w-full -translate-y-1/2 bg-current" />
|
||||
<span className="absolute left-1/2 top-1/2 h-[8px] w-[8px] -translate-x-1/2 -translate-y-1/2 rounded-full border border-current" />
|
||||
</span>
|
||||
</Button>
|
||||
<Button variant="success" className="px-2 py-1" onClick={ addTile } title={ LocalizeText('wiredfurni.tooltip.select.tile') }>
|
||||
<FaPlus />
|
||||
</Button>
|
||||
<Button variant="danger" className="px-2 py-1" onClick={ removeTile } title={ LocalizeText('wiredfurni.tooltip.remove.tile') }>
|
||||
<FaMinus />
|
||||
</Button>
|
||||
<Button variant="primary" className="px-2 py-1" onClick={ loadDefaultPattern } title={ LocalizeText('wiredfurni.tooltip.remove.5x5_tile') }>
|
||||
<MdGridOn />
|
||||
</Button>
|
||||
<Button variant="secondary" className="px-2 py-1" onClick={ () => setSelectedTiles([]) } title={ LocalizeText('wiredfurni.tooltip.remove.clear_tile') }>
|
||||
<FaTimes />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center">
|
||||
<NeighborhoodGrid
|
||||
selectedTiles={ selectedTiles }
|
||||
targetTile={ targetTile }
|
||||
invert={ invert }
|
||||
onSetTile={ setTileSelection }
|
||||
onMoveTarget={ (x, y) => setTargetTile({ x, y }) }
|
||||
targetPlacementMode={ targetPlacementMode } />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Text small>X:</Text>
|
||||
<input
|
||||
type="number"
|
||||
className="form-control form-control-sm"
|
||||
style={ { width: 56 } }
|
||||
value={ curX }
|
||||
min={ -GRID_RANGE }
|
||||
max={ GRID_RANGE }
|
||||
onChange={ event => setCurX(parseInt(event.target.value) || 0) } />
|
||||
<Text small>Y:</Text>
|
||||
<input
|
||||
type="number"
|
||||
className="form-control form-control-sm"
|
||||
style={ { width: 56 } }
|
||||
value={ curY }
|
||||
min={ -GRID_RANGE }
|
||||
max={ GRID_RANGE }
|
||||
onChange={ event => setCurY(parseInt(event.target.value) || 0) } />
|
||||
</div>
|
||||
|
||||
<hr className="m-0 bg-dark" />
|
||||
|
||||
<Text bold>{ LocalizeText('wiredfurni.params.selector_options_selector') }</Text>
|
||||
|
||||
<label className="flex items-center gap-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-check-input"
|
||||
checked={ filterExisting }
|
||||
onChange={ event => setFilterExisting(event.target.checked) } />
|
||||
<Text small>{ LocalizeText('wiredfurni.params.selector_option.0') }</Text>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center gap-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-check-input"
|
||||
checked={ invert }
|
||||
onChange={ event => setInvert(event.target.checked) } />
|
||||
<Text small>{ LocalizeText('wiredfurni.params.selector_option.1') }</Text>
|
||||
</label>
|
||||
|
||||
<hr className="m-0 bg-dark" />
|
||||
|
||||
<WiredFurniSelectionSourceRow
|
||||
title="wiredfurni.params.sources.merged.title.neighborhood"
|
||||
options={ activeSources }
|
||||
value={ sourceType }
|
||||
selectionKind={ isUserGroup ? 'primary' : 'secondary' }
|
||||
selectionActive={ sourceType === SOURCE_FURNI_PICKED }
|
||||
selectionCount={ furniIds.length }
|
||||
selectionLimit={ trigger?.maximumItemSelectionCount ?? 20 }
|
||||
selectionEnabledValues={ [ SOURCE_FURNI_PICKED ] }
|
||||
showSelectionToggle={ false }
|
||||
headerContent={
|
||||
<div className="nitro-wired__give-var-targets">
|
||||
{ SOURCE_GROUP_BUTTONS.map(button => (
|
||||
<button
|
||||
key={ button.key }
|
||||
type="button"
|
||||
className={ `nitro-wired__give-var-target nitro-wired__give-var-target--${ button.key } ${ isUserGroup === button.isUserGroup ? 'is-active' : '' }` }
|
||||
onClick={ () => changeGroup(button.isUserGroup) }>
|
||||
<img src={ button.icon } alt={ button.key } />
|
||||
</button>
|
||||
)) }
|
||||
</div>
|
||||
}
|
||||
onChange={ value => setSourceType(value) } />
|
||||
|
||||
{ sourceType === SOURCE_FURNI_PICKED &&
|
||||
<Text small className="text-center">
|
||||
{ LocalizeText('wiredfurni.pickfurnis.caption', [ 'count', 'limit' ], [ furniIds.length.toString(), (trigger?.maximumItemSelectionCount ?? 20).toString() ]) }
|
||||
</Text> }
|
||||
</div>
|
||||
</WiredSelectorBaseView>
|
||||
);
|
||||
};
|
||||
@@ -1,453 +1,4 @@
|
||||
import { GetRoomEngine } from '@nitrots/nitro-renderer';
|
||||
import { CSSProperties, FC, MouseEvent as ReactMouseEvent, useCallback, useEffect, useState } from 'react';
|
||||
import { FaMinus, FaPlus, FaTimes } from 'react-icons/fa';
|
||||
import { MdGridOn } from 'react-icons/md';
|
||||
import { LocalizeText, WiredFurniType } from '../../../../api';
|
||||
import { Button, Text } from '../../../../common';
|
||||
import { useWired } from '../../../../hooks';
|
||||
import { sortWiredSourceOptions } from '../WiredSourcesSelector';
|
||||
import { WiredSelectorBaseView } from './WiredSelectorBaseView';
|
||||
import { FC } from 'react';
|
||||
import { WiredNeighborhoodSelectorView } from './WiredNeighborhoodSelectorView';
|
||||
|
||||
const SOURCE_USER_TRIGGER = 0;
|
||||
const SOURCE_USER_SIGNAL = 1;
|
||||
const SOURCE_USER_CLICKED = 2;
|
||||
const SOURCE_FURNI_TRIGGER = 3;
|
||||
const SOURCE_FURNI_PICKED = 4;
|
||||
const SOURCE_FURNI_SIGNAL = 5;
|
||||
|
||||
const USER_SOURCES = sortWiredSourceOptions([
|
||||
{ value: SOURCE_USER_TRIGGER, label: 'wiredfurni.params.sources.users.0' },
|
||||
{ value: SOURCE_USER_SIGNAL, label: 'wiredfurni.params.sources.users.201' },
|
||||
{ value: SOURCE_USER_CLICKED, label: 'wiredfurni.params.sources.users.11' },
|
||||
], 'users');
|
||||
|
||||
const FURNI_SOURCES = sortWiredSourceOptions([
|
||||
{ value: SOURCE_FURNI_TRIGGER, label: 'wiredfurni.params.sources.furni.0' },
|
||||
{ value: SOURCE_FURNI_PICKED, label: 'wiredfurni.params.sources.furni.100' },
|
||||
{ value: SOURCE_FURNI_SIGNAL, label: 'wiredfurni.params.sources.furni.201' },
|
||||
], 'furni');
|
||||
|
||||
const TILE_W = 22;
|
||||
const TILE_H = 11;
|
||||
const GRID_RANGE = 4; // -4 … +4
|
||||
const CX = GRID_RANGE * TILE_W + TILE_W / 2;
|
||||
const CY = GRID_RANGE * TILE_H + TILE_H / 2;
|
||||
const GRID_PX_W = (GRID_RANGE * 2 + 1) * TILE_W;
|
||||
const GRID_PX_H = (GRID_RANGE * 2 + 1) * TILE_H;
|
||||
|
||||
type Tile = { x: number; y: number };
|
||||
|
||||
const tileIncluded = (tiles: Tile[], x: number, y: number) =>
|
||||
tiles.some(t => t.x === x && t.y === y);
|
||||
|
||||
const tileLeft = (rx: number, ry: number) =>
|
||||
CX + (rx - ry) * (TILE_W / 2) - TILE_W / 2;
|
||||
|
||||
const tileTop = (rx: number, ry: number) =>
|
||||
CY + (rx + ry) * (TILE_H / 2) - TILE_H / 2;
|
||||
|
||||
interface GridProps {
|
||||
selectedTiles: Tile[];
|
||||
targetTile: Tile;
|
||||
invert: boolean;
|
||||
onSetTile: (x: number, y: number, selected: boolean) => void;
|
||||
onMoveTarget: (x: number, y: number) => void;
|
||||
targetPlacementMode: boolean;
|
||||
}
|
||||
|
||||
const NeighborhoodGrid: FC<GridProps> = ({ selectedTiles, targetTile, invert, onSetTile, onMoveTarget, targetPlacementMode }) =>
|
||||
{
|
||||
const [ dragMode, setDragMode ] = useState<'add' | 'remove' | 'target' | null>(null);
|
||||
const tiles: JSX.Element[] = [];
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const stopDragging = () => setDragMode(null);
|
||||
|
||||
window.addEventListener('mouseup', stopDragging);
|
||||
|
||||
return () => window.removeEventListener('mouseup', stopDragging);
|
||||
}, []);
|
||||
|
||||
const beginTileDrag = (event: ReactMouseEvent<HTMLDivElement>, rx: number, ry: number, isTarget: boolean, isSelected: boolean) =>
|
||||
{
|
||||
event.preventDefault();
|
||||
|
||||
if(targetPlacementMode)
|
||||
{
|
||||
setDragMode('target');
|
||||
onMoveTarget(rx, ry);
|
||||
return;
|
||||
}
|
||||
|
||||
const nextMode = isSelected ? 'remove' : 'add';
|
||||
|
||||
setDragMode(nextMode);
|
||||
onSetTile(rx, ry, nextMode === 'add');
|
||||
};
|
||||
|
||||
const continueTileDrag = (event: ReactMouseEvent<HTMLDivElement>, rx: number, ry: number, isTarget: boolean) =>
|
||||
{
|
||||
if(!(event.buttons & 1) || !dragMode) return;
|
||||
|
||||
if(dragMode === 'target')
|
||||
{
|
||||
onMoveTarget(rx, ry);
|
||||
return;
|
||||
}
|
||||
|
||||
onSetTile(rx, ry, dragMode === 'add');
|
||||
};
|
||||
|
||||
for (let ry = -GRID_RANGE; ry <= GRID_RANGE; ry++)
|
||||
{
|
||||
for (let rx = -GRID_RANGE; rx <= GRID_RANGE; rx++)
|
||||
{
|
||||
const isTarget = rx === targetTile.x && ry === targetTile.y;
|
||||
const isSelected = tileIncluded(selectedTiles, rx, ry);
|
||||
const isActive = invert ? !isSelected : isSelected;
|
||||
const left = tileLeft(rx, ry);
|
||||
const top_ = tileTop(rx, ry);
|
||||
const zIdx = rx + ry + GRID_RANGE * 2 + 10;
|
||||
|
||||
const bgColor = isActive
|
||||
? '#3399ff'
|
||||
: '#2a3042';
|
||||
|
||||
const borderColor = isTarget
|
||||
? '#ffffff'
|
||||
: isActive
|
||||
? '#1166cc'
|
||||
: '#1a2032';
|
||||
|
||||
const diamond: CSSProperties = {
|
||||
position: 'absolute',
|
||||
width: TILE_W,
|
||||
height: TILE_H,
|
||||
left,
|
||||
top: top_,
|
||||
clipPath: 'polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)',
|
||||
backgroundColor: bgColor,
|
||||
cursor: 'pointer',
|
||||
zIndex: zIdx,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: isTarget ? '#ffffff' : 'transparent',
|
||||
fontSize: 10
|
||||
};
|
||||
|
||||
const border: CSSProperties = {
|
||||
position: 'absolute',
|
||||
width: TILE_W + (isTarget ? 6 : 2),
|
||||
height: TILE_H + (isTarget ? 6 : 2),
|
||||
left: left - (isTarget ? 3 : 1),
|
||||
top: top_ - (isTarget ? 3 : 1),
|
||||
clipPath: 'polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)',
|
||||
backgroundColor: borderColor,
|
||||
zIndex: zIdx - 1,
|
||||
pointerEvents: 'none',
|
||||
};
|
||||
|
||||
const targetOutline: CSSProperties = {
|
||||
position: 'absolute',
|
||||
width: TILE_W + 4,
|
||||
height: TILE_H + 4,
|
||||
left: left - 2,
|
||||
top: top_ - 2,
|
||||
zIndex: zIdx + 1,
|
||||
pointerEvents: 'none',
|
||||
overflow: 'visible'
|
||||
};
|
||||
|
||||
tiles.push(
|
||||
<div key={ `b-${ rx }-${ ry }` } style={ border } />,
|
||||
isTarget && (
|
||||
<svg key={ `o-${ rx }-${ ry }` } style={ targetOutline } viewBox={ `0 0 ${ TILE_W + 4 } ${ TILE_H + 4 }` }>
|
||||
<polygon
|
||||
points={ `${ (TILE_W + 4) / 2 },2 ${ TILE_W + 2 },${ (TILE_H + 4) / 2 } ${ (TILE_W + 4) / 2 },${ TILE_H + 2 } 2,${ (TILE_H + 4) / 2 }` }
|
||||
fill="none"
|
||||
stroke="#ffffff"
|
||||
strokeWidth="1" />
|
||||
</svg>
|
||||
),
|
||||
<div
|
||||
key={ `t-${ rx }-${ ry }` }
|
||||
style={ diamond }
|
||||
title={ `(${ rx }, ${ ry })` }
|
||||
onMouseDown={ event => beginTileDrag(event, rx, ry, isTarget, isSelected) }
|
||||
onMouseEnter={ event => continueTileDrag(event, rx, ry, isTarget) } />,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={ { position: 'relative', width: GRID_PX_W, height: GRID_PX_H, flexShrink: 0 } } onContextMenu={ event => event.preventDefault() }>
|
||||
{ tiles }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const WiredSelectorFurniNeighborhoodView: FC<{}> = () =>
|
||||
{
|
||||
const [ selectedTiles, setSelectedTiles ] = useState<Tile[]>([]);
|
||||
const [ filterExisting, setFilterExisting ] = useState(false);
|
||||
const [ invert, setInvert ] = useState(false);
|
||||
const [ sourceType, setSourceType ] = useState(SOURCE_USER_TRIGGER);
|
||||
const [ targetTile, setTargetTile ] = useState<Tile>({ x: 0, y: 0 });
|
||||
const [ targetPlacementMode, setTargetPlacementMode ] = useState(false);
|
||||
const [ curX, setCurX ] = useState(0);
|
||||
const [ curY, setCurY ] = useState(0);
|
||||
|
||||
const { trigger = null, furniIds = [], setIntParams } = useWired();
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
GetRoomEngine().areaSelectionManager.clearHighlight();
|
||||
GetRoomEngine().areaSelectionManager.deactivate();
|
||||
}, []);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!trigger) return;
|
||||
|
||||
const p = trigger.intData;
|
||||
if(p.length >= 1) setSourceType(p[0]);
|
||||
if(p.length >= 2) setFilterExisting(p[1] === 1);
|
||||
if(p.length >= 3) setInvert(p[2] === 1);
|
||||
if(p.length >= 5) setTargetTile({ x: p[3], y: p[4] });
|
||||
else setTargetTile({ x: 0, y: 0 });
|
||||
|
||||
if(p.length >= 6)
|
||||
{
|
||||
const n = p[5];
|
||||
const tiles: Tile[] = [];
|
||||
|
||||
for(let i = 0; i < n; i++)
|
||||
{
|
||||
const xi = 6 + i * 2;
|
||||
if(xi + 1 < p.length) tiles.push({ x: p[xi], y: p[xi + 1] });
|
||||
}
|
||||
|
||||
setSelectedTiles(tiles);
|
||||
}
|
||||
else
|
||||
{
|
||||
setSelectedTiles([]);
|
||||
}
|
||||
}, [ trigger ]);
|
||||
|
||||
const save = useCallback(() =>
|
||||
{
|
||||
const params: number[] = [
|
||||
sourceType,
|
||||
filterExisting ? 1 : 0,
|
||||
invert ? 1 : 0,
|
||||
targetTile.x,
|
||||
targetTile.y,
|
||||
selectedTiles.length,
|
||||
...selectedTiles.flatMap(t => [ t.x, t.y ]),
|
||||
];
|
||||
|
||||
setIntParams(params);
|
||||
}, [ sourceType, filterExisting, invert, selectedTiles, targetTile.x, targetTile.y, setIntParams ]);
|
||||
|
||||
const setTileSelection = useCallback((x: number, y: number, selected: boolean) =>
|
||||
{
|
||||
setSelectedTiles(prev =>
|
||||
{
|
||||
const alreadySelected = tileIncluded(prev, x, y);
|
||||
|
||||
if(selected)
|
||||
{
|
||||
if(alreadySelected) return prev;
|
||||
|
||||
return [ ...prev, { x, y } ];
|
||||
}
|
||||
|
||||
if(!alreadySelected) return prev;
|
||||
|
||||
return prev.filter(t => !(t.x === x && t.y === y));
|
||||
});
|
||||
}, []);
|
||||
|
||||
const moveTargetTile = useCallback((x: number, y: number) =>
|
||||
{
|
||||
setTargetTile({ x, y });
|
||||
}, []);
|
||||
|
||||
const addTile = useCallback(() =>
|
||||
{
|
||||
if(!tileIncluded(selectedTiles, curX, curY))
|
||||
setSelectedTiles(prev => [ ...prev, { x: curX, y: curY } ]);
|
||||
}, [ curX, curY, selectedTiles ]);
|
||||
|
||||
const removeTile = useCallback(() =>
|
||||
{
|
||||
setSelectedTiles(prev => prev.filter(t => !(t.x === curX && t.y === curY)));
|
||||
}, [ curX, curY ]);
|
||||
|
||||
const clearTiles = useCallback(() => setSelectedTiles([]), []);
|
||||
|
||||
const loadDefaultPattern = useCallback(() =>
|
||||
{
|
||||
const tiles: Tile[] = [];
|
||||
|
||||
for(let y = -2; y <= 2; y++)
|
||||
{
|
||||
for(let x = -2; x <= 2; x++)
|
||||
{
|
||||
if(x === 0 && y === 0) continue;
|
||||
tiles.push({ x, y });
|
||||
}
|
||||
}
|
||||
|
||||
setSelectedTiles(tiles);
|
||||
}, []);
|
||||
|
||||
const isUserGroup = sourceType <= SOURCE_USER_CLICKED;
|
||||
const activeSources = isUserGroup ? USER_SOURCES : FURNI_SOURCES;
|
||||
const groupOffset = isUserGroup ? 0 : SOURCE_FURNI_TRIGGER;
|
||||
const groupIndex = sourceType - groupOffset;
|
||||
|
||||
const prevInGroup = () =>
|
||||
setSourceType(groupOffset + (groupIndex - 1 + activeSources.length) % activeSources.length);
|
||||
|
||||
const nextInGroup = () =>
|
||||
setSourceType(groupOffset + (groupIndex + 1) % activeSources.length);
|
||||
|
||||
const switchGroup = (toUser: boolean) =>
|
||||
{
|
||||
if(toUser === isUserGroup) return;
|
||||
const newOffset = toUser ? 0 : SOURCE_FURNI_TRIGGER;
|
||||
setSourceType(newOffset + groupIndex);
|
||||
};
|
||||
|
||||
const requiresFurni = sourceType === SOURCE_FURNI_PICKED
|
||||
? WiredFurniType.STUFF_SELECTION_OPTION_BY_ID
|
||||
: WiredFurniType.STUFF_SELECTION_OPTION_NONE;
|
||||
|
||||
const pickedCount = furniIds.length;
|
||||
const pickedLimit = trigger?.maximumItemSelectionCount ?? 20;
|
||||
|
||||
return (
|
||||
<WiredSelectorBaseView hasSpecialInput={ true } requiresFurni={ requiresFurni } save={ save } hideDelay={ true } cardStyle={ { width: '400px' } }>
|
||||
<div className="flex flex-col gap-2">
|
||||
|
||||
<Text bold>{ LocalizeText('wiredfurni.params.neighborhood_selection') }</Text>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant={ targetPlacementMode ? 'success' : 'secondary' }
|
||||
className="px-2 py-1"
|
||||
onClick={ () => setTargetPlacementMode(value => !value) }
|
||||
title="Sposta target">
|
||||
<span aria-hidden className="relative inline-block h-[14px] w-[14px]">
|
||||
<span className="absolute left-1/2 top-0 h-full w-px -translate-x-1/2 bg-current" />
|
||||
<span className="absolute left-0 top-1/2 h-px w-full -translate-y-1/2 bg-current" />
|
||||
<span className="absolute left-1/2 top-1/2 h-[8px] w-[8px] -translate-x-1/2 -translate-y-1/2 rounded-full border border-current" />
|
||||
</span>
|
||||
</Button>
|
||||
<Button variant="success" className="px-2 py-1" onClick={ addTile } title={ LocalizeText('wiredfurni.tooltip.select.tile') }>
|
||||
<FaPlus />
|
||||
</Button>
|
||||
<Button variant="danger" className="px-2 py-1" onClick={ removeTile } title={ LocalizeText('wiredfurni.tooltip.remove.tile') }>
|
||||
<FaMinus />
|
||||
</Button>
|
||||
<Button variant="primary" className="px-2 py-1" onClick={ loadDefaultPattern } title={ LocalizeText('wiredfurni.tooltip.remove.5x5_tile') }>
|
||||
<MdGridOn />
|
||||
</Button>
|
||||
<Button variant="secondary" className="px-2 py-1" onClick={ clearTiles } title={ LocalizeText('wiredfurni.tooltip.remove.clear_tile') }>
|
||||
<FaTimes />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center">
|
||||
<NeighborhoodGrid
|
||||
selectedTiles={ selectedTiles }
|
||||
targetTile={ targetTile }
|
||||
invert={ invert }
|
||||
onSetTile={ setTileSelection }
|
||||
onMoveTarget={ moveTargetTile }
|
||||
targetPlacementMode={ targetPlacementMode } />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Text small>X:</Text>
|
||||
<input
|
||||
type="number"
|
||||
className="form-control form-control-sm"
|
||||
style={ { width: 56 } }
|
||||
value={ curX }
|
||||
min={ -GRID_RANGE }
|
||||
max={ GRID_RANGE }
|
||||
onChange={ e => setCurX(parseInt(e.target.value) || 0) } />
|
||||
<Text small>Y:</Text>
|
||||
<input
|
||||
type="number"
|
||||
className="form-control form-control-sm"
|
||||
style={ { width: 56 } }
|
||||
value={ curY }
|
||||
min={ -GRID_RANGE }
|
||||
max={ GRID_RANGE }
|
||||
onChange={ e => setCurY(parseInt(e.target.value) || 0) } />
|
||||
</div>
|
||||
|
||||
<hr className="m-0 bg-dark" />
|
||||
|
||||
<Text bold>{ LocalizeText('wiredfurni.params.selector_options_selector') }</Text>
|
||||
|
||||
<label className="flex items-center gap-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-check-input"
|
||||
checked={ filterExisting }
|
||||
onChange={ e => setFilterExisting(e.target.checked) } />
|
||||
<Text small>{ LocalizeText('wiredfurni.params.selector_option.0') }</Text>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center gap-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-check-input"
|
||||
checked={ invert }
|
||||
onChange={ e => setInvert(e.target.checked) } />
|
||||
<Text small>{ LocalizeText('wiredfurni.params.selector_option.1') }</Text>
|
||||
</label>
|
||||
|
||||
<hr className="m-0 bg-dark" />
|
||||
|
||||
<Text bold>{ LocalizeText('wiredfurni.params.sources.merged.title.neighborhood') }</Text>
|
||||
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
fullWidth
|
||||
variant={ isUserGroup ? 'primary' : 'secondary' }
|
||||
onClick={ () => switchGroup(true) }>
|
||||
{ LocalizeText('wiredfurni.params.furni_neighborhood.group.user') }
|
||||
</Button>
|
||||
<Button
|
||||
fullWidth
|
||||
variant={ !isUserGroup ? 'primary' : 'secondary' }
|
||||
onClick={ () => switchGroup(false) }>
|
||||
{ LocalizeText('wiredfurni.params.furni_neighborhood.group.furni') }
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="primary" className="px-2 py-1" onClick={ prevInGroup }>‹</Button>
|
||||
<div className="flex flex-1 items-center justify-center">
|
||||
<Text small>{ LocalizeText(activeSources[groupIndex].label) }</Text>
|
||||
</div>
|
||||
<Button variant="primary" className="px-2 py-1" onClick={ nextInGroup }>›</Button>
|
||||
</div>
|
||||
|
||||
{ sourceType === SOURCE_FURNI_PICKED &&
|
||||
<Text small className="text-center">
|
||||
{ LocalizeText('wiredfurni.pickfurnis.caption', [ 'count', 'limit' ], [ pickedCount.toString(), pickedLimit.toString() ]) }
|
||||
</Text> }
|
||||
|
||||
</div>
|
||||
</WiredSelectorBaseView>
|
||||
);
|
||||
};
|
||||
export const WiredSelectorFurniNeighborhoodView: FC<{}> = () => <WiredNeighborhoodSelectorView />;
|
||||
|
||||
@@ -1,454 +1,4 @@
|
||||
import { GetRoomEngine } from '@nitrots/nitro-renderer';
|
||||
import { CSSProperties, FC, MouseEvent as ReactMouseEvent, useCallback, useEffect, useState } from 'react';
|
||||
import { FaMinus, FaPlus, FaTimes } from 'react-icons/fa';
|
||||
import { MdGridOn } from 'react-icons/md';
|
||||
import { LocalizeText, WiredFurniType } from '../../../../api';
|
||||
import { Button, Text } from '../../../../common';
|
||||
import { useWired } from '../../../../hooks';
|
||||
import { sortWiredSourceOptions } from '../WiredSourcesSelector';
|
||||
import { WiredSelectorBaseView } from './WiredSelectorBaseView';
|
||||
import { FC } from 'react';
|
||||
import { WiredNeighborhoodSelectorView } from './WiredNeighborhoodSelectorView';
|
||||
|
||||
const SOURCE_USER_TRIGGER = 0;
|
||||
const SOURCE_USER_SIGNAL = 1;
|
||||
const SOURCE_USER_CLICKED = 2;
|
||||
const SOURCE_FURNI_TRIGGER = 3;
|
||||
const SOURCE_FURNI_PICKED = 4;
|
||||
const SOURCE_FURNI_SIGNAL = 5;
|
||||
|
||||
const USER_SOURCES = sortWiredSourceOptions([
|
||||
{ value: SOURCE_USER_TRIGGER, label: 'wiredfurni.params.sources.users.0' },
|
||||
{ value: SOURCE_USER_SIGNAL, label: 'wiredfurni.params.sources.users.201' },
|
||||
{ value: SOURCE_USER_CLICKED, label: 'wiredfurni.params.sources.users.11' },
|
||||
], 'users');
|
||||
|
||||
const FURNI_SOURCES = sortWiredSourceOptions([
|
||||
{ value: SOURCE_FURNI_TRIGGER, label: 'wiredfurni.params.sources.furni.0' },
|
||||
{ value: SOURCE_FURNI_PICKED, label: 'wiredfurni.params.sources.furni.100' },
|
||||
{ value: SOURCE_FURNI_SIGNAL, label: 'wiredfurni.params.sources.furni.201' },
|
||||
], 'furni');
|
||||
|
||||
const TILE_W = 22;
|
||||
const TILE_H = 11;
|
||||
const GRID_RANGE = 4;
|
||||
const CX = GRID_RANGE * TILE_W + TILE_W / 2;
|
||||
const CY = GRID_RANGE * TILE_H + TILE_H / 2;
|
||||
const GRID_PX_W = (GRID_RANGE * 2 + 1) * TILE_W;
|
||||
const GRID_PX_H = (GRID_RANGE * 2 + 1) * TILE_H;
|
||||
|
||||
type Tile = { x: number; y: number };
|
||||
|
||||
const tileIncluded = (tiles: Tile[], x: number, y: number) =>
|
||||
tiles.some(t => t.x === x && t.y === y);
|
||||
|
||||
const tileLeft = (rx: number, ry: number) =>
|
||||
CX + (rx - ry) * (TILE_W / 2) - TILE_W / 2;
|
||||
|
||||
const tileTop = (rx: number, ry: number) =>
|
||||
CY + (rx + ry) * (TILE_H / 2) - TILE_H / 2;
|
||||
|
||||
interface GridProps {
|
||||
selectedTiles: Tile[];
|
||||
targetTile: Tile;
|
||||
invert: boolean;
|
||||
onSetTile: (x: number, y: number, selected: boolean) => void;
|
||||
onMoveTarget: (x: number, y: number) => void;
|
||||
targetPlacementMode: boolean;
|
||||
}
|
||||
|
||||
const NeighborhoodGrid: FC<GridProps> = ({ selectedTiles, targetTile, invert, onSetTile, onMoveTarget, targetPlacementMode }) =>
|
||||
{
|
||||
const [ dragMode, setDragMode ] = useState<'add' | 'remove' | 'target' | null>(null);
|
||||
const tiles: JSX.Element[] = [];
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const stopDragging = () => setDragMode(null);
|
||||
|
||||
window.addEventListener('mouseup', stopDragging);
|
||||
|
||||
return () => window.removeEventListener('mouseup', stopDragging);
|
||||
}, []);
|
||||
|
||||
const beginTileDrag = (event: ReactMouseEvent<HTMLDivElement>, rx: number, ry: number, isTarget: boolean, isSelected: boolean) =>
|
||||
{
|
||||
event.preventDefault();
|
||||
|
||||
if(targetPlacementMode)
|
||||
{
|
||||
setDragMode('target');
|
||||
onMoveTarget(rx, ry);
|
||||
return;
|
||||
}
|
||||
|
||||
const nextMode = isSelected ? 'remove' : 'add';
|
||||
|
||||
setDragMode(nextMode);
|
||||
onSetTile(rx, ry, nextMode === 'add');
|
||||
};
|
||||
|
||||
const continueTileDrag = (event: ReactMouseEvent<HTMLDivElement>, rx: number, ry: number, isTarget: boolean) =>
|
||||
{
|
||||
if(!(event.buttons & 1) || !dragMode) return;
|
||||
|
||||
if(dragMode === 'target')
|
||||
{
|
||||
onMoveTarget(rx, ry);
|
||||
return;
|
||||
}
|
||||
|
||||
onSetTile(rx, ry, dragMode === 'add');
|
||||
};
|
||||
|
||||
for (let ry = -GRID_RANGE; ry <= GRID_RANGE; ry++)
|
||||
{
|
||||
for (let rx = -GRID_RANGE; rx <= GRID_RANGE; rx++)
|
||||
{
|
||||
const isTarget = rx === targetTile.x && ry === targetTile.y;
|
||||
const isSelected = tileIncluded(selectedTiles, rx, ry);
|
||||
const isActive = invert ? !isSelected : isSelected;
|
||||
const left = tileLeft(rx, ry);
|
||||
const top_ = tileTop(rx, ry);
|
||||
const zIdx = rx + ry + GRID_RANGE * 2 + 10;
|
||||
|
||||
const bgColor = isActive
|
||||
? '#3399ff'
|
||||
: '#2a3042';
|
||||
|
||||
const borderColor = isTarget
|
||||
? '#ffffff'
|
||||
: isActive
|
||||
? '#1166cc'
|
||||
: '#1a2032';
|
||||
|
||||
const diamond: CSSProperties = {
|
||||
position: 'absolute',
|
||||
width: TILE_W,
|
||||
height: TILE_H,
|
||||
left,
|
||||
top: top_,
|
||||
clipPath: 'polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)',
|
||||
backgroundColor: bgColor,
|
||||
cursor: 'pointer',
|
||||
zIndex: zIdx,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: isTarget ? '#ffffff' : 'transparent',
|
||||
fontSize: 10
|
||||
};
|
||||
|
||||
const border: CSSProperties = {
|
||||
position: 'absolute',
|
||||
width: TILE_W + (isTarget ? 6 : 2),
|
||||
height: TILE_H + (isTarget ? 6 : 2),
|
||||
left: left - (isTarget ? 3 : 1),
|
||||
top: top_ - (isTarget ? 3 : 1),
|
||||
clipPath: 'polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)',
|
||||
backgroundColor: borderColor,
|
||||
zIndex: zIdx - 1,
|
||||
pointerEvents: 'none',
|
||||
};
|
||||
|
||||
const targetOutline: CSSProperties = {
|
||||
position: 'absolute',
|
||||
width: TILE_W + 4,
|
||||
height: TILE_H + 4,
|
||||
left: left - 2,
|
||||
top: top_ - 2,
|
||||
zIndex: zIdx + 1,
|
||||
pointerEvents: 'none',
|
||||
overflow: 'visible'
|
||||
};
|
||||
|
||||
tiles.push(
|
||||
<div key={ `b-${ rx }-${ ry }` } style={ border } />,
|
||||
isTarget && (
|
||||
<svg key={ `o-${ rx }-${ ry }` } style={ targetOutline } viewBox={ `0 0 ${ TILE_W + 4 } ${ TILE_H + 4 }` }>
|
||||
<polygon
|
||||
points={ `${ (TILE_W + 4) / 2 },2 ${ TILE_W + 2 },${ (TILE_H + 4) / 2 } ${ (TILE_W + 4) / 2 },${ TILE_H + 2 } 2,${ (TILE_H + 4) / 2 }` }
|
||||
fill="none"
|
||||
stroke="#ffffff"
|
||||
strokeWidth="1" />
|
||||
</svg>
|
||||
),
|
||||
<div
|
||||
key={ `t-${ rx }-${ ry }` }
|
||||
style={ diamond }
|
||||
title={ `(${ rx }, ${ ry })` }
|
||||
onMouseDown={ event => beginTileDrag(event, rx, ry, isTarget, isSelected) }
|
||||
onMouseEnter={ event => continueTileDrag(event, rx, ry, isTarget) } />,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={ { position: 'relative', width: GRID_PX_W, height: GRID_PX_H, flexShrink: 0 } } onContextMenu={ event => event.preventDefault() }>
|
||||
{ tiles }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const WiredSelectorUsersNeighborhoodView: FC<{}> = () =>
|
||||
{
|
||||
const [ selectedTiles, setSelectedTiles ] = useState<Tile[]>([]);
|
||||
const [ filterExisting, setFilterExisting ] = useState(false);
|
||||
const [ invert, setInvert ] = useState(false);
|
||||
const [ sourceType, setSourceType ] = useState(SOURCE_USER_TRIGGER);
|
||||
const [ targetTile, setTargetTile ] = useState<Tile>({ x: 0, y: 0 });
|
||||
const [ targetPlacementMode, setTargetPlacementMode ] = useState(false);
|
||||
const [ curX, setCurX ] = useState(0);
|
||||
const [ curY, setCurY ] = useState(0);
|
||||
|
||||
const { trigger = null, furniIds = [], setIntParams } = useWired();
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
GetRoomEngine().areaSelectionManager.clearHighlight();
|
||||
GetRoomEngine().areaSelectionManager.deactivate();
|
||||
}, []);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!trigger) return;
|
||||
|
||||
const p = trigger.intData;
|
||||
if(p.length >= 1) setSourceType(p[0]);
|
||||
if(p.length >= 2) setFilterExisting(p[1] === 1);
|
||||
if(p.length >= 3) setInvert(p[2] === 1);
|
||||
if(p.length >= 5) setTargetTile({ x: p[3], y: p[4] });
|
||||
else setTargetTile({ x: 0, y: 0 });
|
||||
|
||||
if(p.length >= 6)
|
||||
{
|
||||
const n = p[5];
|
||||
const tiles: Tile[] = [];
|
||||
|
||||
for(let i = 0; i < n; i++)
|
||||
{
|
||||
const xi = 6 + i * 2;
|
||||
if(xi + 1 < p.length) tiles.push({ x: p[xi], y: p[xi + 1] });
|
||||
}
|
||||
|
||||
setSelectedTiles(tiles);
|
||||
}
|
||||
else
|
||||
{
|
||||
setSelectedTiles([]);
|
||||
}
|
||||
}, [ trigger ]);
|
||||
|
||||
const save = useCallback(() =>
|
||||
{
|
||||
const params: number[] = [
|
||||
sourceType,
|
||||
filterExisting ? 1 : 0,
|
||||
invert ? 1 : 0,
|
||||
targetTile.x,
|
||||
targetTile.y,
|
||||
selectedTiles.length,
|
||||
...selectedTiles.flatMap(t => [ t.x, t.y ]),
|
||||
];
|
||||
|
||||
setIntParams(params);
|
||||
}, [ sourceType, filterExisting, invert, selectedTiles, targetTile.x, targetTile.y, setIntParams ]);
|
||||
|
||||
const setTileSelection = useCallback((x: number, y: number, selected: boolean) =>
|
||||
{
|
||||
setSelectedTiles(prev =>
|
||||
{
|
||||
const alreadySelected = tileIncluded(prev, x, y);
|
||||
|
||||
if(selected)
|
||||
{
|
||||
if(alreadySelected) return prev;
|
||||
|
||||
return [ ...prev, { x, y } ];
|
||||
}
|
||||
|
||||
if(!alreadySelected) return prev;
|
||||
|
||||
return prev.filter(t => !(t.x === x && t.y === y));
|
||||
});
|
||||
}, []);
|
||||
|
||||
const moveTargetTile = useCallback((x: number, y: number) =>
|
||||
{
|
||||
setTargetTile({ x, y });
|
||||
}, []);
|
||||
|
||||
const addTile = useCallback(() =>
|
||||
{
|
||||
if(!tileIncluded(selectedTiles, curX, curY))
|
||||
setSelectedTiles(prev => [ ...prev, { x: curX, y: curY } ]);
|
||||
}, [ curX, curY, selectedTiles ]);
|
||||
|
||||
const removeTile = useCallback(() =>
|
||||
{
|
||||
setSelectedTiles(prev => prev.filter(t => !(t.x === curX && t.y === curY)));
|
||||
}, [ curX, curY ]);
|
||||
|
||||
const clearTiles = useCallback(() => setSelectedTiles([]), []);
|
||||
|
||||
const loadDefaultPattern = useCallback(() =>
|
||||
{
|
||||
const tiles: Tile[] = [];
|
||||
|
||||
for(let y = -2; y <= 2; y++)
|
||||
{
|
||||
for(let x = -2; x <= 2; x++)
|
||||
{
|
||||
if(x === 0 && y === 0) continue;
|
||||
tiles.push({ x, y });
|
||||
}
|
||||
}
|
||||
|
||||
setSelectedTiles(tiles);
|
||||
}, []);
|
||||
|
||||
const isUserGroup = sourceType <= SOURCE_USER_CLICKED;
|
||||
const activeSources = isUserGroup ? USER_SOURCES : FURNI_SOURCES;
|
||||
const groupOffset = isUserGroup ? 0 : SOURCE_FURNI_TRIGGER;
|
||||
const groupIndex = sourceType - groupOffset;
|
||||
|
||||
const prevSource = () =>
|
||||
setSourceType(groupOffset + ((groupIndex - 1 + activeSources.length) % activeSources.length));
|
||||
|
||||
const nextSource = () =>
|
||||
setSourceType(groupOffset + ((groupIndex + 1) % activeSources.length));
|
||||
|
||||
const switchGroup = (toUser: boolean) =>
|
||||
{
|
||||
if(toUser === isUserGroup) return;
|
||||
|
||||
const newOffset = toUser ? 0 : SOURCE_FURNI_TRIGGER;
|
||||
setSourceType(newOffset + groupIndex);
|
||||
};
|
||||
|
||||
const requiresFurni = sourceType === SOURCE_FURNI_PICKED
|
||||
? WiredFurniType.STUFF_SELECTION_OPTION_BY_ID
|
||||
: WiredFurniType.STUFF_SELECTION_OPTION_NONE;
|
||||
|
||||
const pickedCount = furniIds.length;
|
||||
const pickedLimit = trigger?.maximumItemSelectionCount ?? 20;
|
||||
|
||||
return (
|
||||
<WiredSelectorBaseView hasSpecialInput={ true } requiresFurni={ requiresFurni } save={ save } hideDelay={ true } cardStyle={ { width: '400px' } }>
|
||||
<div className="flex flex-col gap-2">
|
||||
|
||||
<Text bold>{ LocalizeText('wiredfurni.params.neighborhood_selection') }</Text>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant={ targetPlacementMode ? 'success' : 'secondary' }
|
||||
className="px-2 py-1"
|
||||
onClick={ () => setTargetPlacementMode(value => !value) }
|
||||
title="Sposta target">
|
||||
<span aria-hidden className="relative inline-block h-[14px] w-[14px]">
|
||||
<span className="absolute left-1/2 top-0 h-full w-px -translate-x-1/2 bg-current" />
|
||||
<span className="absolute left-0 top-1/2 h-px w-full -translate-y-1/2 bg-current" />
|
||||
<span className="absolute left-1/2 top-1/2 h-[8px] w-[8px] -translate-x-1/2 -translate-y-1/2 rounded-full border border-current" />
|
||||
</span>
|
||||
</Button>
|
||||
<Button variant="success" className="px-2 py-1" onClick={ addTile } title={ LocalizeText('wiredfurni.tooltip.select.tile') }>
|
||||
<FaPlus />
|
||||
</Button>
|
||||
<Button variant="danger" className="px-2 py-1" onClick={ removeTile } title={ LocalizeText('wiredfurni.tooltip.remove.tile') }>
|
||||
<FaMinus />
|
||||
</Button>
|
||||
<Button variant="primary" className="px-2 py-1" onClick={ loadDefaultPattern } title={ LocalizeText('wiredfurni.tooltip.remove.5x5_tile') }>
|
||||
<MdGridOn />
|
||||
</Button>
|
||||
<Button variant="secondary" className="px-2 py-1" onClick={ clearTiles } title={ LocalizeText('wiredfurni.tooltip.remove.clear_tile') }>
|
||||
<FaTimes />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center">
|
||||
<NeighborhoodGrid
|
||||
selectedTiles={ selectedTiles }
|
||||
targetTile={ targetTile }
|
||||
invert={ invert }
|
||||
onSetTile={ setTileSelection }
|
||||
onMoveTarget={ moveTargetTile }
|
||||
targetPlacementMode={ targetPlacementMode } />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Text small>X:</Text>
|
||||
<input
|
||||
type="number"
|
||||
className="form-control form-control-sm"
|
||||
style={ { width: 56 } }
|
||||
value={ curX }
|
||||
min={ -GRID_RANGE }
|
||||
max={ GRID_RANGE }
|
||||
onChange={ e => setCurX(parseInt(e.target.value) || 0) } />
|
||||
<Text small>Y:</Text>
|
||||
<input
|
||||
type="number"
|
||||
className="form-control form-control-sm"
|
||||
style={ { width: 56 } }
|
||||
value={ curY }
|
||||
min={ -GRID_RANGE }
|
||||
max={ GRID_RANGE }
|
||||
onChange={ e => setCurY(parseInt(e.target.value) || 0) } />
|
||||
</div>
|
||||
|
||||
<hr className="m-0 bg-dark" />
|
||||
|
||||
<Text bold>{ LocalizeText('wiredfurni.params.selector_options_selector') }</Text>
|
||||
|
||||
<label className="flex items-center gap-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-check-input"
|
||||
checked={ filterExisting }
|
||||
onChange={ e => setFilterExisting(e.target.checked) } />
|
||||
<Text small>{ LocalizeText('wiredfurni.params.selector_option.0') }</Text>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center gap-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-check-input"
|
||||
checked={ invert }
|
||||
onChange={ e => setInvert(e.target.checked) } />
|
||||
<Text small>{ LocalizeText('wiredfurni.params.selector_option.1') }</Text>
|
||||
</label>
|
||||
|
||||
<hr className="m-0 bg-dark" />
|
||||
|
||||
<Text bold>{ LocalizeText('wiredfurni.params.sources.merged.title.neighborhood') }</Text>
|
||||
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
fullWidth
|
||||
variant={ isUserGroup ? 'primary' : 'secondary' }
|
||||
onClick={ () => switchGroup(true) }>
|
||||
{ LocalizeText('wiredfurni.params.furni_neighborhood.group.user') }
|
||||
</Button>
|
||||
<Button
|
||||
fullWidth
|
||||
variant={ !isUserGroup ? 'primary' : 'secondary' }
|
||||
onClick={ () => switchGroup(false) }>
|
||||
{ LocalizeText('wiredfurni.params.furni_neighborhood.group.furni') }
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="primary" className="px-2 py-1" onClick={ prevSource }>‹</Button>
|
||||
<div className="flex flex-1 items-center justify-center">
|
||||
<Text small>{ LocalizeText(activeSources[groupIndex].label) }</Text>
|
||||
</div>
|
||||
<Button variant="primary" className="px-2 py-1" onClick={ nextSource }>›</Button>
|
||||
</div>
|
||||
|
||||
{ sourceType === SOURCE_FURNI_PICKED &&
|
||||
<Text small className="text-center">
|
||||
{ LocalizeText('wiredfurni.pickfurnis.caption', [ 'count', 'limit' ], [ pickedCount.toString(), pickedLimit.toString() ]) }
|
||||
</Text> }
|
||||
|
||||
</div>
|
||||
</WiredSelectorBaseView>
|
||||
);
|
||||
};
|
||||
export const WiredSelectorUsersNeighborhoodView: FC<{}> = () => <WiredNeighborhoodSelectorView />;
|
||||
|
||||
+222
-48
@@ -182,7 +182,7 @@ body {
|
||||
@apply pointer-events-none relative h-[130px] w-[90px] bg-position-[center_-8px] bg-no-repeat;
|
||||
}
|
||||
|
||||
.nitro-card-shell {
|
||||
.nitro-card-shell:not(.nitro-wired) {
|
||||
border: 2px solid #000 !important;
|
||||
border-radius: 10px;
|
||||
background: #f2f2eb;
|
||||
@@ -192,7 +192,7 @@ body {
|
||||
max-height: calc(100vh - 16px);
|
||||
}
|
||||
|
||||
.nitro-card-header-shell {
|
||||
.nitro-card-shell:not(.nitro-wired) .nitro-card-header-shell {
|
||||
border: 2px solid #3c88a6;
|
||||
border-bottom-color: #000;
|
||||
border-radius: 8px 8px 0 0;
|
||||
@@ -200,7 +200,7 @@ body {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.nitro-card-header-shell.builders-club-card-header {
|
||||
.nitro-card-shell:not(.nitro-wired) .nitro-card-header-shell.builders-club-card-header {
|
||||
border-color: #d79d2e;
|
||||
border-bottom-color: #000;
|
||||
background: linear-gradient(180deg, #d89f2d 0%, #c68515 100%);
|
||||
@@ -274,7 +274,7 @@ body {
|
||||
filter: brightness(0.96);
|
||||
}
|
||||
|
||||
.nitro-card-content-shell {
|
||||
.nitro-card-shell:not(.nitro-wired) .nitro-card-content-shell {
|
||||
border: 0;
|
||||
border-top: 0;
|
||||
border-radius: 0 0 8px 8px;
|
||||
@@ -283,8 +283,8 @@ body {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.nitro-card-shell,
|
||||
.nitro-card-content-shell {
|
||||
.nitro-card-shell:not(.nitro-wired),
|
||||
.nitro-card-shell:not(.nitro-wired) .nitro-card-content-shell {
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
@@ -385,13 +385,13 @@ body {
|
||||
}
|
||||
|
||||
@media (max-width: 991.98px) {
|
||||
.nitro-card-shell {
|
||||
.nitro-card-shell:not(.nitro-wired) {
|
||||
width: min(calc(100vw - 16px), 100%) !important;
|
||||
max-width: calc(100vw - 16px) !important;
|
||||
max-height: calc(100vh - 16px) !important;
|
||||
}
|
||||
|
||||
.nitro-card-header-shell {
|
||||
.nitro-card-shell:not(.nitro-wired) .nitro-card-header-shell {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
@@ -403,7 +403,7 @@ body {
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.nitro-card-content-shell {
|
||||
.nitro-card-shell:not(.nitro-wired) .nitro-card-content-shell {
|
||||
padding: 8px !important;
|
||||
}
|
||||
|
||||
@@ -417,8 +417,8 @@ body {
|
||||
padding: 4px 9px;
|
||||
}
|
||||
|
||||
.nitro-card-shell,
|
||||
.nitro-card-content-shell {
|
||||
.nitro-card-shell:not(.nitro-wired),
|
||||
.nitro-card-shell:not(.nitro-wired) .nitro-card-content-shell {
|
||||
p,
|
||||
label,
|
||||
li,
|
||||
@@ -814,70 +814,190 @@ body {
|
||||
}
|
||||
|
||||
.nitro-wired {
|
||||
background: #d5d5d5;
|
||||
border: 1px solid #8f8f8f !important;
|
||||
background: #efefef;
|
||||
border: 1px solid #8d8d8d !important;
|
||||
border-radius: 6px !important;
|
||||
box-shadow: inset 0 1px 0 #ffffff, 0 2px 0 rgba(0, 0, 0, 0.18) !important;
|
||||
box-shadow: inset 0 0 0 1px #fff !important;
|
||||
color: #000;
|
||||
outline: none !important;
|
||||
overflow: hidden;
|
||||
|
||||
.nitro-wired__header {
|
||||
min-height: 22px !important;
|
||||
max-height: 22px !important;
|
||||
background: linear-gradient(180deg, #f1f1f1 0%, #dfdfdf 45%, #c8c8c8 46%, #d7d7d7 100%) !important;
|
||||
border-bottom: 1px solid #9b9b9b;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.95) !important;
|
||||
padding-top: 0 !important;
|
||||
min-height: 21px !important;
|
||||
max-height: 21px !important;
|
||||
position: absolute !important;
|
||||
inset: 0 0 auto 0;
|
||||
z-index: 3;
|
||||
border: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
padding: 0 !important;
|
||||
pointer-events: auto;
|
||||
|
||||
span {
|
||||
.nitro-card-title {
|
||||
display: block;
|
||||
color: #000 !important;
|
||||
font-size: 12px !important;
|
||||
font-weight: 700;
|
||||
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.85) !important;
|
||||
letter-spacing: 0.01em;
|
||||
font-size: 11px !important;
|
||||
font-weight: 700 !important;
|
||||
line-height: 21px;
|
||||
text-align: center;
|
||||
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.75);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
> .flex::before {
|
||||
content: none;
|
||||
}
|
||||
|
||||
> .flex::after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
.nitro-card-close-button {
|
||||
right: 6px !important;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
min-width: 18px;
|
||||
border: 1px solid #777;
|
||||
border-radius: 5px;
|
||||
background: linear-gradient(180deg, #f9f9f9 0%, #cacaca 100%);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.nitro-card-close-button::before,
|
||||
.nitro-card-close-button::after {
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: 10px;
|
||||
height: 2px;
|
||||
background: #6a6a6a;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ubuntu-close-button {
|
||||
transform: scale(0.82);
|
||||
transform-origin: center;
|
||||
.nitro-wired__content {
|
||||
position: relative;
|
||||
background: #efefef !important;
|
||||
border: 0 !important;
|
||||
box-shadow: none !important;
|
||||
padding: 0 !important;
|
||||
gap: 0 !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.nitro-wired__content {
|
||||
background: #d9d9d9 !important;
|
||||
padding: 4px !important;
|
||||
gap: 4px !important;
|
||||
overflow: visible !important;
|
||||
.nitro-wired__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 8px 10px 10px;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.nitro-wired__section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.nitro-wired__summary {
|
||||
position: relative;
|
||||
min-height: 56px;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 22px 10px 7px 12px;
|
||||
background: linear-gradient(180deg, #e5e5e5 0%, #d7d7d7 100%);
|
||||
overflow: hidden;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.nitro-wired__summary-bg {
|
||||
position: absolute;
|
||||
width: auto;
|
||||
height: auto;
|
||||
top: 50%;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
|
||||
.nitro-wired__summary-bg--left {
|
||||
left: 50%;
|
||||
transform: translate(-150%, -50%);
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.nitro-wired__summary-bg--right {
|
||||
left: 50%;
|
||||
top: 72%;
|
||||
transform: translate(-46%, -50%);
|
||||
opacity: 0.24;
|
||||
}
|
||||
|
||||
.nitro-wired__summary-copy {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.nitro-wired__summary-copy {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.nitro-wired__summary-kind {
|
||||
color: #000;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
line-height: 1;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.nitro-wired__summary-title {
|
||||
color: #4a4a4a !important;
|
||||
font-size: 15px !important;
|
||||
font-weight: 700 !important;
|
||||
line-height: 1.02;
|
||||
text-align: left !important;
|
||||
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.75);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.nitro-wired__section--body {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.nitro-wired__section .font-bold {
|
||||
.nitro-wired__section .font-bold:not(.nitro-wired__summary-title) {
|
||||
width: 100%;
|
||||
font-weight: 400 !important;
|
||||
text-align: center;
|
||||
font-size: 13px !important;
|
||||
font-weight: 700 !important;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.nitro-wired__summary-description,
|
||||
.nitro-wired__furni-selector-description,
|
||||
.nitro-wired__picker-label {
|
||||
font-size: 12px;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.nitro-wired__source-row,
|
||||
.nitro-wired__give-var-section-title,
|
||||
.nitro-wired__advanced-toggle {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.nitro-wired__divider,
|
||||
hr {
|
||||
height: 1px;
|
||||
border: 0;
|
||||
background: #a6a6a6;
|
||||
box-shadow: 0 1px 0 #f8f8f8;
|
||||
background: #bababa;
|
||||
box-shadow: 0 1px 0 #fff;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
@@ -918,14 +1038,47 @@ body {
|
||||
|
||||
.form-check-input[type='checkbox']:checked,
|
||||
input[type='checkbox']:checked {
|
||||
background:
|
||||
linear-gradient(135deg, transparent 0 46%, #ffffff 46% 54%, transparent 54% 100%),
|
||||
linear-gradient(45deg, transparent 0 30%, #4a4a4a 30% 42%, transparent 42% 100%),
|
||||
linear-gradient(180deg, #d9d9d9 0%, #c8c8c8 100%);
|
||||
background: linear-gradient(180deg, #d9d9d9 0%, #c8c8c8 100%);
|
||||
border-color: #767676;
|
||||
}
|
||||
|
||||
.form-check-input[type='checkbox']:checked::before,
|
||||
.form-check-input[type='checkbox']:checked::after,
|
||||
input[type='checkbox']:checked::before,
|
||||
input[type='checkbox']:checked::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: 12px;
|
||||
height: 2px;
|
||||
border-radius: 2px;
|
||||
background: #111111;
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
.form-check-input[type='checkbox']:checked::before,
|
||||
input[type='checkbox']:checked::before {
|
||||
transform: translate(-50%, -50%) rotate(45deg);
|
||||
}
|
||||
|
||||
.form-check-input[type='checkbox']:checked::after,
|
||||
input[type='checkbox']:checked::after {
|
||||
transform: translate(-50%, -50%) rotate(-45deg);
|
||||
}
|
||||
|
||||
.form-check {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.form-check-label {
|
||||
margin-left: 1px;
|
||||
}
|
||||
|
||||
.form-control,
|
||||
.form-select,
|
||||
input[type='text'],
|
||||
input[type='number'],
|
||||
textarea {
|
||||
@@ -935,7 +1088,7 @@ body {
|
||||
background: #f6f6f6 !important;
|
||||
color: #000 !important;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.95);
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
padding: 3px 6px;
|
||||
}
|
||||
|
||||
@@ -945,10 +1098,10 @@ body {
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
min-height: 20px !important;
|
||||
min-height: 22px !important;
|
||||
border: 1px solid #8e8e8e !important;
|
||||
border-radius: 5px !important;
|
||||
background: linear-gradient(180deg, #f6f6f6 0%, #e4e4e4 49%, #d0d0d0 50%, #ececec 100%) !important;
|
||||
border-radius: 6px !important;
|
||||
background: linear-gradient(180deg, #fcfcfc 0%, #e8e8e8 46%, #d5d5d5 47%, #efefef 100%) !important;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.92) !important;
|
||||
color: #000 !important;
|
||||
padding-top: 0 !important;
|
||||
@@ -966,9 +1119,17 @@ body {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
&.nitro-card-shell {
|
||||
resize: none !important;
|
||||
}
|
||||
|
||||
.nitro-wired__actions {
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.nitro-slider-wrapper {
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.nitro-wired__advanced-toggle {
|
||||
@@ -1591,6 +1752,19 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
.nitro-card-shell.nitro-wired {
|
||||
border: 1px solid #8d8d8d !important;
|
||||
border-color: #8d8d8d !important;
|
||||
box-shadow: inset 0 0 0 1px #fff !important;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.nitro-card-shell.nitro-wired .nitro-card-header-shell,
|
||||
.nitro-card-shell.nitro-wired .nitro-card-content-shell {
|
||||
border: 0 !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* ── Avatar Editor ─────────────────────────────────────────────────────── */
|
||||
|
||||
.color-picker-frame {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useBetween } from 'use-between';
|
||||
import { GetRoomSession, IsOwnerOfFloorFurniture, LocalizeText, SendMessageComposer, WiredFurniType, WiredSelectionVisualizer } from '../../api';
|
||||
import { useMessageEvent } from '../events';
|
||||
import { useNotification } from '../notification';
|
||||
import { useWiredTools } from '../wired-tools/useWiredTools';
|
||||
|
||||
const useWiredState = () =>
|
||||
{
|
||||
@@ -19,6 +20,7 @@ const useWiredState = () =>
|
||||
const [ allowedInteractionTypes, setAllowedInteractionTypes ] = useState<string[] | null>(null);
|
||||
const [ allowedInteractionErrorKey, setAllowedInteractionErrorKey ] = useState<string | null>(null);
|
||||
const { showConfirm = null, simpleAlert = null } = useNotification();
|
||||
const { requestUserVariables = null, roomSettings = null } = useWiredTools();
|
||||
|
||||
const saveWired = () =>
|
||||
{
|
||||
@@ -273,6 +275,7 @@ const useWiredState = () =>
|
||||
const parser = event.getParser();
|
||||
|
||||
WiredSelectionVisualizer.clearAllSelectionShaders();
|
||||
if(roomSettings?.canInspect && requestUserVariables) requestUserVariables();
|
||||
setTrigger(null);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user