nitro-v36-with-renderer-v36
/
src /
components /
variables-explorer /
VariablesExplorerHolderView.tsx
VariablesExplorerHolderView.tsx 11 Ko
Télécharger le fichier
import { GetRoomEngine, GetSessionDataManager, RoomObjectCategory, RoomObjectVariable } from '@octane/renderer';
import { FC, KeyboardEvent, ReactNode, useMemo, useState } from 'react';
import { GetRoomSession, giveableVariables, isWallTargetKind, parseInt32, profileToEntries, VariablesWebApiClient, WebApiVariable } from '../../api';
import wiredGlobalPlaceholderImage from '../../assets/images/wiredtools/wired_global_placeholder.png';
import { Button, LayoutAvatarImageView, LayoutPetImageView, LayoutRoomObjectImageView } from '../../common';
import { useVariablesExplorerProfile } from '../../hooks/variables-explorer/useVariablesExplorerProfile';
import { WiredVariableHolderPanelView } from '../wired-tools/WiredVariableHolderPanelView';
import { EXPLORER_VARIABLE_ELEMENTS, ExplorerHolderTarget, holderInfoLines, holderPanelTitle, resolvedEntityId } from './VariablesExplorer.helpers';
import { NumberInput } from '../../common/NumberInput';
import { NativeTextTree } from '../../common/native-text/NativeTextTree';
export interface VariablesExplorerHolderViewProps {
client: VariablesWebApiClient;
roomId: number;
target: ExplorerHolderTarget;
variables: WebApiVariable[];
onClose: () => void;
}
const WARNING_TEXT: Record<ExplorerHolderTarget['scope'], string> = {
user: 'Changes go through the Variables Web API and fire the room wired like any other variable change.',
furni: 'Only furni placed in this room can hold furni variables.',
global: 'Global variables are room-scoped. Values can be changed, not removed.'
};
interface LocalHolder {
name: string;
owner?: string;
preview: ReactNode;
}
/** What the room you stand in knows about the holder: its name and picture. Null outside that room. */
const describeLocalHolder = (target: ExplorerHolderTarget, entityId: number | null, roomId: number): LocalHolder | null => {
const session = GetRoomSession();
if (target.scope === 'global' || entityId === null || !session || session.roomId !== roomId) return null;
if (target.scope === 'furni') {
const wall = isWallTargetKind(target.kind);
const category = wall ? RoomObjectCategory.WALL : RoomObjectCategory.FLOOR;
const roomObject = GetRoomEngine().getRoomObject(roomId, entityId, category);
if (!roomObject) return null;
const typeId = roomObject.model?.getValue<number>(RoomObjectVariable.FURNITURE_TYPE_ID);
const data = wall ? GetSessionDataManager().getWallItemData(typeId) : GetSessionDataManager().getFloorItemData(typeId);
return {
name: data?.name || data?.className || '',
owner: roomObject.model?.getValue<string>(RoomObjectVariable.FURNITURE_OWNER_NAME) || '',
preview: <LayoutRoomObjectImageView category={category} objectId={entityId} roomId={roomId} />
};
}
const users = session.userDataManager;
const userData =
target.kind === 'pets'
? users.getPetData(entityId)
: target.kind === 'bots'
? (users.getBotData(-entityId) ?? users.getRentableBotData(-entityId) ?? users.getBotData(entityId))
: users.getUserData(entityId);
if (!userData) return null;
return {
name: userData.name ?? '',
owner: target.kind === 'users' ? '' : (userData.ownerName ?? ''),
preview:
target.kind === 'pets' ? (
<LayoutPetImageView direction={2} figure={userData.figure} />
) : (
<LayoutAvatarImageView direction={2} figure={userData.figure} />
)
};
};
/** The creator tools' holder window, fed from a Variables Web API profile. */
export const VariablesExplorerHolderView: FC<VariablesExplorerHolderViewProps> = ({ client, roomId, target, variables, onClose }) => {
const { profile, status, setStatus, busy, reload, write } = useVariablesExplorerProfile(client, target);
const [selectedName, setSelectedName] = useState<string | null>(null);
const [editingName, setEditingName] = useState<string | null>(null);
const [editingValue, setEditingValue] = useState('');
const [isGiveOpen, setIsGiveOpen] = useState(false);
const [giveName, setGiveName] = useState('');
const [giveValue, setGiveValue] = useState('0');
const [addValue, setAddValue] = useState('1');
const canWrite = client.hasWriteAccess;
const entityId = resolvedEntityId(target, profile);
const entries = useMemo(() => profileToEntries(profile, variables), [profile, variables]);
const giveOptions = useMemo(() => (target.scope === 'global' ? [] : giveableVariables(variables, target.scope, entries)), [target, variables, entries]);
const selectedEntry = entries.find((entry) => entry.name === selectedName) ?? entries[0] ?? null;
const selectedGive = giveOptions.find((option) => option.name === giveName) ?? giveOptions[0] ?? null;
const holderReady = target.scope === 'global' || entityId !== null;
const setValues = (values: Record<string, number | null | true>) => {
if (target.scope === 'global') {
const numbers = Object.fromEntries(Object.entries(values).filter((entry): entry is [string, number] => typeof entry[1] === 'number'));
return write(() => client.patchGlobalProfile(numbers));
}
if (entityId === null) return Promise.resolve(false);
return write(() => client.patchProfile(target.scope, target.kind, entityId, values));
};
const commitEdit = () => {
if (!editingName) return;
const value = parseInt32(editingValue);
setEditingName(null);
if (value === null) {
setStatus('Enter a whole number.');
return;
}
void setValues({ [editingName]: value });
};
const onEditKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
event.stopPropagation();
if (event.key === 'Enter' || event.key === 'NumpadEnter') {
event.preventDefault();
commitEdit();
} else if (event.key === 'Escape') {
event.preventDefault();
setEditingName(null);
}
};
const give = () => {
if (!selectedGive) return;
const value = selectedGive.hasValue ? parseInt32(giveValue) : null;
if (selectedGive.hasValue && value === null) {
setStatus('Enter a whole number.');
return;
}
void setValues({ [selectedGive.name]: selectedGive.hasValue ? value : true }).then((ok) => {
if (ok) {
setIsGiveOpen(false);
setSelectedName(selectedGive.name);
}
});
};
const addToSelected = () => {
if (!selectedEntry?.hasValue) return;
const amount = parseInt32(addValue);
if (amount === null) {
setStatus('Enter a whole number.');
return;
}
if (target.scope === 'global') {
void write(() => client.patchGlobal(selectedEntry.name, { add: amount }));
} else if (entityId !== null) {
void write(() => client.patchEntry(target.scope, selectedEntry.name, target.kind, entityId, { add: amount }));
}
};
const icon = EXPLORER_VARIABLE_ELEMENTS.find((element) => element.key === target.scope)?.icon;
const local = describeLocalHolder(target, entityId, roomId);
return (
<NativeTextTree>
<WiredVariableHolderPanelView
title={holderPanelTitle(target)}
warningText={WARNING_TEXT[target.scope]}
onRefresh={() => void reload()}
onClose={onClose}
preview={
local?.preview ?? (
<img
alt=""
className={
target.scope === 'global' ? 'max-w-full max-h-full object-contain p-3' : 'w-auto h-auto max-w-[48px] max-h-[48px] object-contain'
}
src={target.scope === 'global' ? wiredGlobalPlaceholderImage : icon}
/>
)
}
infoLines={holderInfoLines(target, profile, roomId, local?.name, local?.owner)}
variablesTitle={target.scope === 'global' ? 'Room variables:' : 'Assigned variables:'}
entries={entries.map((entry) => ({
id: entry.name,
name: entry.name,
availability: 'Permanent',
hasValue: entry.hasValue,
value: entry.value
}))}
selectedId={selectedEntry?.name ?? null}
onSelect={setSelectedName}
canEdit={canWrite && holderReady && !busy}
editingId={editingName}
editingValue={editingValue}
onBeginEdit={(entry) => {
setSelectedName(entry.id);
setEditingName(entry.id);
setEditingValue(String(entry.value ?? 0));
}}
onEditingValueChange={setEditingValue}
onEditBlur={() => setEditingName(null)}
onEditKeyDown={onEditKeyDown}
isGiveOpen={isGiveOpen}
onToggleGive={() => setIsGiveOpen((value) => !value)}
giveOptions={giveOptions.map((option) => ({ id: option.name, name: option.name, hasValue: option.hasValue }))}
giveSelectedId={selectedGive?.name ?? ''}
onGiveSelect={setGiveName}
giveValue={giveValue}
onGiveValueChange={setGiveValue}
canGive={canWrite && holderReady && !busy && target.scope !== 'global' && !!giveOptions.length}
onGive={give}
canRemove={canWrite && holderReady && !busy && target.scope !== 'global' && !!selectedEntry}
onRemove={() => selectedEntry && void setValues({ [selectedEntry.name]: null })}
status={
status ? (
<div className="rounded border border-[#d9a3a3] bg-[#fbeaea] px-2 py-1 text-[12px] text-[#8c2424]" role="alert">
{status}
</div>
) : null
}
extraActions={
canWrite && selectedEntry?.hasValue ? (
<div className="flex items-center justify-end gap-2 text-[12px]">
<span>Add to {selectedEntry.name}:</span>
<NumberInput aria-label="Amount to add" className="w-[80px] rounded border border-[#b8b2a4] bg-white px-2 py-[2px] text-[12px]" value={addValue} onChange={(event) => setAddValue(event.target.value)} />
<Button disabled={busy || !holderReady} variant="secondary" onClick={addToSelected}>
Add
</Button>
</div>
) : null
}
uniqueKey="variables-explorer-holder"
offsetLeft={760}
offsetTop={120}
/>
</NativeTextTree>
);
};
