-
- { prefixes.map(prefix => (
-
setSelectedPrefix(prefix) } />
- )) }
+
+
+
+
+
- { (!prefixes || prefixes.length === 0) &&
-
- { LocalizeText('inventory.empty.title') }
-
}
-
- { activePrefix &&
-
-
Active prefix
-
-
+
+ { activeTab === 'prefixes' &&
+
+
+
+ { prefixes.map(prefix => (
+
setSelectedPrefix(prefix) } />
+ )) }
-
}
- { !activePrefix &&
-
-
Active prefix
-
-
No active prefix
+ { !hasPrefixes &&
+
+ { LocalizeText('inventory.empty.title') }
+
}
+
+
+ { activePrefix &&
+
}
+ { !activePrefix &&
+
+
Active prefix
+
+ No active prefix
+
+
}
+ { !!selectedPrefix &&
+
+
+
+ selectedPrefix.active ? deactivatePrefix() : activatePrefix(selectedPrefix.id) }>
+ { selectedPrefix.active ? 'Deactivate' : 'Activate' }
+
+ { !selectedPrefix.active &&
+
+
+ }
+
+
}
+
+
}
+
+ { activeTab === 'icons' &&
+
+
+
+ { nickIcons.map(icon => (
+ setSelectedNickIcon(icon) } />
+ )) }
-
}
- { !!selectedPrefix &&
-
-
-
+ { !hasNickIcons &&
+
+ No purchased icons yet
+
}
+
+
+
+
Active icon
+
+ { activeNickIcon &&

}
+ { !activeNickIcon &&
No active icon }
+
-
- selectedPrefix.active ? deactivatePrefix() : activatePrefix(selectedPrefix.id) }>
- { selectedPrefix.active ? 'Deactivate' : 'Activate' }
-
- { !selectedPrefix.active &&
-
-
- }
-
-
}
-
+ { !!selectedNickIcon &&
+
+
+

+
{ selectedNickIcon.displayName || selectedNickIcon.iconKey }
+
+
+
}
+
+
}
);
};
diff --git a/src/components/purse/PurseView.tsx b/src/components/purse/PurseView.tsx
index 4f6bb8b..6bda43e 100644
--- a/src/components/purse/PurseView.tsx
+++ b/src/components/purse/PurseView.tsx
@@ -1,6 +1,6 @@
import { CreateLinkEvent, HabboClubLevelEnum } from '@nitrots/nitro-renderer';
import { FC, useEffect, useMemo, useState } from 'react';
-import { FaChevronDown, FaQuestionCircle } from 'react-icons/fa';
+import { FaChevronDown, FaLanguage, FaQuestionCircle } from 'react-icons/fa';
import { FriendlyTime, GetConfigurationValue, LocalizeText } from '../../api';
import { Column, Flex, LayoutCurrencyIcon, Text } from '../../common';
import { usePurse } from '../../hooks';
@@ -91,6 +91,9 @@ export const PurseView: FC<{}> = props => {
}
+
diff --git a/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx b/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx
index 12aca46..24a7f7e 100644
--- a/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx
+++ b/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx
@@ -2,7 +2,7 @@ import { GetSessionDataManager, RelationshipStatusInfoEvent, RelationshipStatusI
import { Dispatch, FC, FocusEvent, KeyboardEvent, SetStateAction, useCallback, useEffect, useState } from 'react';
import { FaPencilAlt, FaTimes } from 'react-icons/fa';
import { AvatarInfoUser, CloneObject, GetConfigurationValue, GetGroupInformation, GetUserProfile, LocalizeText, SendMessageComposer } from '../../../../../api';
-import { Base, Column, Flex, LayoutAvatarImageView, LayoutBadgeImageView, Text, UserProfileIconView } from '../../../../../common';
+import { Base, Column, Flex, LayoutAvatarImageView, LayoutBadgeImageView, Text, UserIdentityView, UserProfileIconView } from '../../../../../common';
import { useMessageEvent, useNitroEvent, useRoom } from '../../../../../hooks';
import { InfoStandBadgeSlotView } from './InfoStandBadgeSlotView';
import { InfoStandWidgetUserRelationshipsView } from './InfoStandWidgetUserRelationshipsView';
@@ -29,7 +29,6 @@ export const InfoStandWidgetUserView: FC
= props =
const infostandBackgroundClass = `background-${backgroundId ?? 'default'}`;
const infostandStandClass = `stand-${standId ?? 'default'}`;
const infostandOverlayClass = `overlay-${overlayId ?? 'default'}`;
-
const handleProfileClick = useCallback(() => { GetUserProfile(avatarInfo.webID); }, [avatarInfo.webID]);
const handleEditClick = useCallback((event: React.MouseEvent) => { event.stopPropagation(); setIsVisible(prev => !prev); }, []);
@@ -79,6 +78,12 @@ export const InfoStandWidgetUserView: FC = props =
newValue.figure = event.figure;
newValue.motto = event.customInfo;
newValue.achievementScore = event.activityPoints;
+ newValue.nickIcon = event.nickIcon;
+ newValue.prefixText = event.prefixText;
+ newValue.prefixColor = event.prefixColor;
+ newValue.prefixIcon = event.prefixIcon;
+ newValue.prefixEffect = event.prefixEffect;
+ newValue.displayOrder = event.displayOrder;
newValue.backgroundId = event.backgroundId;
newValue.standId = event.standId;
newValue.overlayId = event.overlayId;
@@ -139,7 +144,17 @@ export const InfoStandWidgetUserView: FC = props =
diff --git a/src/components/room/widgets/chat/ChatWidgetMessageView.tsx b/src/components/room/widgets/chat/ChatWidgetMessageView.tsx
index 1dba1a9..c791599 100644
--- a/src/components/room/widgets/chat/ChatWidgetMessageView.tsx
+++ b/src/components/room/widgets/chat/ChatWidgetMessageView.tsx
@@ -1,6 +1,7 @@
import { GetRoomEngine, RoomChatSettings, RoomObjectCategory } from '@nitrots/nitro-renderer';
import { FC, useEffect, useMemo, useRef, useState } from 'react';
-import { ChatBubbleMessage, parsePrefixColors, getPrefixEffectStyle, PREFIX_EFFECT_KEYFRAMES } from '../../../../api';
+import { ChatBubbleMessage } from '../../../../api';
+import { UserIdentityView } from '../../../../common';
import { useOnClickChat } from '../../../../hooks';
interface ChatWidgetMessageViewProps
@@ -38,11 +39,11 @@ export const ChatWidgetMessageView: FC = ({
useEffect(() =>
{
- setIsVisible(false);
-
const element = elementRef.current;
if(!element) return;
+ const previousWidth = chat.width;
+ const previousHeight = chat.height;
const { offsetWidth: width, offsetHeight: height } = element;
chat.width = width;
@@ -62,10 +63,14 @@ export const ChatWidgetMessageView: FC = ({
setIsReady(true);
+ if(isVisible && ((previousWidth !== width) || (previousHeight !== height)) && makeRoom) makeRoom(chat);
+ }, [ chat, chat.formattedText, chat.originalFormattedText, chat.showTranslation, chat.translatedFormattedText, isVisible, makeRoom ]);
+
+ useEffect(() =>
+ {
return () =>
{
chat.elementRef = null;
- setIsReady(false);
};
}, [ chat ]);
@@ -77,6 +82,8 @@ export const ChatWidgetMessageView: FC = ({
setIsVisible(true);
}, [ chat, isReady, isVisible, makeRoom ]);
+ const messageClassName = `message [overflow-wrap:anywhere] break-words${ chat.type === 1 ? ' italic text-[#595959]' : '' }${ chat.type === 2 ? ' font-bold' : '' }`;
+
return (
GetRoomEngine().selectRoomObject(chat.roomId, chat.senderId, RoomObjectCategory.UNIT) }>
@@ -90,29 +97,33 @@ export const ChatWidgetMessageView: FC = ({
) }
- { chat.prefixEffect === 'pulse' && }
- { chat.prefixText && (() => {
- const colors = parsePrefixColors(chat.prefixText, chat.prefixColor);
- const hasMultiColor = colors.length > 1 && new Set(colors).size > 1;
- const fxStyle = getPrefixEffectStyle(chat.prefixEffect, colors[0] || '#FFFFFF');
- return (
-
- { chat.prefixIcon && { chat.prefixIcon } }
-
- {'{'}
- { hasMultiColor
- ? [ ...chat.prefixText ].map((char, i) => (
- { char }
- ))
- : chat.prefixText
- }
- {'}'}
-
-
- );
- })() }
-
-
+
+ { !chat.showTranslation &&
+
}
+ { chat.showTranslation &&
+
+
+ original:
+
+
+
+ translate:
+
+
+
}
diff --git a/src/components/room/widgets/chat/ChatWidgetWindowView.tsx b/src/components/room/widgets/chat/ChatWidgetWindowView.tsx
index 7a6118a..7734bd8 100644
--- a/src/components/room/widgets/chat/ChatWidgetWindowView.tsx
+++ b/src/components/room/widgets/chat/ChatWidgetWindowView.tsx
@@ -2,7 +2,7 @@ import { GetSessionDataManager, RoomObjectType } from '@nitrots/nitro-renderer';
import { FC, UIEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { ChatEntryType, LocalizeText } from '../../../../api';
import { DraggableWindowPosition, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common';
-import { useChatHistory, useChatWindow } from '../../../../hooks';
+import { useChatHistory, useChatWindow, useOnClickChat } from '../../../../hooks';
import { useRoom } from '../../../../hooks/rooms';
const BOTTOM_SCROLL_THRESHOLD = 20;
@@ -19,6 +19,7 @@ export const ChatWidgetWindowView: FC<{}> = () =>
const { chatHistory = [], clearChatHistory = null } = useChatHistory();
const [ , setChatWindowEnabled ] = useChatWindow();
const { roomSession = null } = useRoom();
+ const { onClickChat } = useOnClickChat();
const ownUserId = (GetSessionDataManager()?.userId || -1);
const roomChatHistory = useMemo(() =>
@@ -33,7 +34,7 @@ export const ChatWidgetWindowView: FC<{}> = () =>
if(!normalizedSearch.length) return true;
- return (`${ chat.name } ${ chat.message }`.toLowerCase().includes(normalizedSearch));
+ return (`${ chat.name } ${ chat.message || '' } ${ chat.originalMessage || '' } ${ chat.translatedMessage || '' }`.toLowerCase().includes(normalizedSearch));
});
}, [ chatHistory, roomSession?.roomId, hidePets, search ]);
@@ -125,14 +126,27 @@ export const ChatWidgetWindowView: FC<{}> = () =>
{
const isOwnMessage = (chat.webId === ownUserId);
const rowClassName = `mb-1 flex items-start gap-1 break-words ${ isOwnMessage ? 'justify-end' : '' }`;
+ const messageClassName = `message${ chat.chatType === 1 ? ' italic text-[#595959]' : '' }${ chat.chatType === 2 ? ' font-bold' : '' }`;
return (
{ hideBalloons && !hideAvatars &&
}
{ hideBalloons && (
-
+
-
+ { !chat.showTranslation &&
+
}
+ { chat.showTranslation &&
+
+
+ original:
+
+
+
+ translate:
+
+
+
}
) }
{ !hideBalloons && (
@@ -148,7 +162,19 @@ export const ChatWidgetWindowView: FC<{}> = () =>
-
+ { !chat.showTranslation &&
+
}
+ { chat.showTranslation &&
+
+
+ original:
+
+
+
+ translate:
+
+
+
}
diff --git a/src/components/translation/TranslationBootstrap.tsx b/src/components/translation/TranslationBootstrap.tsx
new file mode 100644
index 0000000..389f566
--- /dev/null
+++ b/src/components/translation/TranslationBootstrap.tsx
@@ -0,0 +1,9 @@
+import { FC } from 'react';
+import { useTranslation } from '../../hooks';
+
+export const TranslationBootstrap: FC<{}> = () =>
+{
+ useTranslation();
+
+ return null;
+};
diff --git a/src/components/translation/TranslationSettingsView.tsx b/src/components/translation/TranslationSettingsView.tsx
new file mode 100644
index 0000000..efe4571
--- /dev/null
+++ b/src/components/translation/TranslationSettingsView.tsx
@@ -0,0 +1,138 @@
+import { AddLinkEventTracker, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer';
+import { FC, useEffect, useState } from 'react';
+import { NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../common';
+import { useTranslation } from '../../hooks';
+
+export const TranslationSettingsView: FC<{}> = () =>
+{
+ const [ isVisible, setIsVisible ] = useState(false);
+ const {
+ settings,
+ supportedLanguages = [],
+ availableTextLocales = [],
+ languagesLoading = false,
+ localizationTextsLoading = false,
+ lastIncomingLanguage = '',
+ lastOutgoingLanguage = '',
+ lastError = '',
+ updateSettings,
+ ensureSupportedLanguagesLoaded,
+ getLanguageName
+ } = useTranslation();
+
+ useEffect(() =>
+ {
+ const linkTracker: ILinkEventTracker = {
+ linkReceived: (url: string) =>
+ {
+ const parts = url.split('/');
+
+ if(parts.length < 2) return;
+
+ switch(parts[1])
+ {
+ case 'show':
+ setIsVisible(true);
+ return;
+ case 'hide':
+ setIsVisible(false);
+ return;
+ case 'toggle':
+ setIsVisible(prevValue => !prevValue);
+ return;
+ }
+ },
+ eventUrlPrefix: 'translation-settings/'
+ };
+
+ AddLinkEventTracker(linkTracker);
+
+ return () => RemoveLinkEventTracker(linkTracker);
+ }, []);
+
+ useEffect(() =>
+ {
+ if(!isVisible) return;
+
+ ensureSupportedLanguagesLoaded();
+ }, [ ensureSupportedLanguagesLoaded, isVisible ]);
+
+ if(!isVisible) return null;
+
+ return (
+
+ setIsVisible(false) } />
+
+
+ updateSettings({ enabled: event.target.checked }) } />
+ Enable automatic translation
+
+
+ When enabled, chat bubbles always show two lines: original: and translate:.
+
+
+
Interface texts
+
+
+
+
+
+
Incoming messages
+
+ Detected language (auto): { getLanguageName(lastIncomingLanguage) }
+
+
+
+
+
Outgoing messages
+
+ Detected writing language (auto): { getLanguageName(lastOutgoingLanguage) }
+
+
+
+
+ { languagesLoading ? 'Loading languages...' : `${ supportedLanguages.length } languages available` }
+
+
+ { localizationTextsLoading &&
+
+ Loading localized interface texts...
+
}
+ { lastError.length > 0 &&
+
+ { lastError }
+
}
+
+
+ );
+};
diff --git a/src/components/user-profile/UserContainerView.tsx b/src/components/user-profile/UserContainerView.tsx
index 88d3150..0425e2f 100644
--- a/src/components/user-profile/UserContainerView.tsx
+++ b/src/components/user-profile/UserContainerView.tsx
@@ -1,7 +1,7 @@
import { GetSessionDataManager, RequestFriendComposer, UserProfileParser } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react';
import { FriendlyTime, LocalizeText, SendMessageComposer } from '../../api';
-import { LayoutAvatarImageView, Text } from '../../common';
+import { LayoutAvatarImageView, Text, UserIdentityView } from '../../common';
export const UserContainerView: FC<{
userProfile: UserProfileParser;
@@ -18,7 +18,6 @@ export const UserContainerView: FC<{
const infostandBackgroundClass = `background-${userProfile.backgroundId ?? 'default'}`;
const infostandStandClass = `stand-${userProfile.standId ?? 'default'}`;
const infostandOverlayClass = `overlay-${userProfile.overlayId ?? 'default'}`;
-
const addFriend = () =>
{
setRequestSent(true);
@@ -41,7 +40,16 @@ export const UserContainerView: FC<{
-
{ userProfile.username }
+
{ userProfile.motto }
@@ -115,4 +123,4 @@ export const UserContainerView: FC<{
);
-};
\ No newline at end of file
+};
diff --git a/src/components/wired-tools/WiredCreatorToolsView.tsx b/src/components/wired-tools/WiredCreatorToolsView.tsx
index 7d1f539..b2279db 100644
--- a/src/components/wired-tools/WiredCreatorToolsView.tsx
+++ b/src/components/wired-tools/WiredCreatorToolsView.tsx
@@ -1,13 +1,13 @@
-import { AddLinkEventTracker, AvatarExpressionEnum, FigureUpdateEvent, FurnitureFloorUpdateEvent, FurnitureMultiStateComposer, FurnitureWallMultiStateComposer, FurnitureWallUpdateComposer, FurnitureWallUpdateEvent, GetLocalizationManager, GetRoomEngine, GetSessionDataManager, ILinkEventTracker, RemoveLinkEventTracker, RoomControllerLevel, RoomObjectCategory, RoomObjectType, RoomObjectVariable, RoomUnitDanceEvent, RoomUnitEffectEvent, RoomUnitExpressionEvent, RoomUnitHandItemEvent, RoomUnitInfoEvent, RoomUnitStatusEvent, UpdateFurniturePositionComposer, Vector3d, WiredUserInspectMoveComposer } from '@nitrots/nitro-renderer';
+import { AddLinkEventTracker, AvatarExpressionEnum, FigureUpdateEvent, FurnitureFloorUpdateEvent, FurnitureMultiStateComposer, FurnitureWallMultiStateComposer, FurnitureWallUpdateComposer, FurnitureWallUpdateEvent, GetLocalizationManager, GetRoomEngine, GetSessionDataManager, GetStage, GetTicker, ILinkEventTracker, RemoveLinkEventTracker, RoomControllerLevel, RoomObjectCategory, RoomObjectType, RoomObjectVariable, RoomUnitDanceEvent, RoomUnitEffectEvent, RoomUnitExpressionEvent, RoomUnitHandItemEvent, RoomUnitInfoEvent, RoomUnitStatusEvent, UpdateFurniturePositionComposer, Vector3d, WiredUserInspectMoveComposer } from '@nitrots/nitro-renderer';
import { WiredMonitorDataEvent, WiredMonitorRequestComposer } from '@nitrots/nitro-renderer';
-import { FC, KeyboardEvent, useCallback, useEffect, useMemo, useState } from 'react';
+import { FC, KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import furniInspectionIcon from '../../assets/images/wiredtools/furni.png';
import globalInspectionIcon from '../../assets/images/wiredtools/global.png';
import userInspectionIcon from '../../assets/images/wiredtools/user.png';
import contextInspectionIcon from '../../assets/images/wiredtools/context.png';
import wiredGlobalPlaceholderImage from '../../assets/images/wiredtools/wired_global_placeholder.png';
import wiredMonitorImage from '../../assets/images/wiredtools/wired_monitor.png';
-import { AvatarInfoFurni, AvatarInfoUtilities, LocalizeText, NotificationAlertType, SendMessageComposer } from '../../api';
+import { AvatarInfoFurni, AvatarInfoUtilities, GetRoomObjectBounds, GetRoomObjectScreenLocation, LocalizeText, NotificationAlertType, SendMessageComposer, WiredSelectionVisualizer } from '../../api';
import { Button, DraggableWindowPosition, LayoutAvatarImageView, LayoutPetImageView, LayoutRoomObjectImageView, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView, Text } from '../../common';
import { useInventoryTrade, useMessageEvent, useNotification, useObjectSelectedEvent, useRoom, useWiredTools } from '../../hooks';
import { WiredToolsSettingsTabView } from './WiredToolsSettingsTabView';
@@ -184,6 +184,21 @@ interface VariableManageEntry
value: number | null;
}
+interface VariableHighlightTarget
+{
+ category: number;
+ hasValue: boolean;
+ objectId: number;
+ value: number | null;
+}
+
+interface VariableHighlightOverlay extends VariableHighlightTarget
+{
+ key: string;
+ x: number;
+ y: number;
+}
+
interface ManagedHolderVariableEntry
{
availability: string;
@@ -631,6 +646,9 @@ export const WiredCreatorToolsView: FC<{}> = () =>
const [ isManagedGiveOpen, setIsManagedGiveOpen ] = useState(false);
const [ managedGiveVariableItemId, setManagedGiveVariableItemId ] = useState(0);
const [ managedGiveValue, setManagedGiveValue ] = useState('0');
+ const [ isVariableHighlightActive, setIsVariableHighlightActive ] = useState(false);
+ const [ variableHighlightOverlays, setVariableHighlightOverlays ] = useState
([]);
+ const variableHighlightObjectsRef = useRef>([]);
const shouldPauseVariableSnapshotRefresh = (!!editingVariable || !!editingManagedHolderVariableId || isInspectionGiveOpen || isManagedGiveOpen);
const [ selectedVariableKeys, setSelectedVariableKeys ] = useState>({
furni: VARIABLE_DEFINITIONS.furni[0].key,
@@ -2400,6 +2418,155 @@ export const WiredCreatorToolsView: FC<{}> = () =>
manageLabel: 'Manage'
} ];
}, [ selectedVariableDefinition, variablesType, roomSession, userVariableAssignments, furniVariableAssignments, roomVariableAssignmentMap ]);
+ const canVariableHighlight = !!selectedVariableDefinition?.itemId
+ && (selectedVariableDefinition.type === 'Custom')
+ && ((variablesType === 'user') || (variablesType === 'furni'))
+ && !!roomSession;
+ const variableHighlightTargets = useMemo((): VariableHighlightTarget[] =>
+ {
+ if(!isVariableHighlightActive || !canVariableHighlight || !roomSession || !selectedVariableDefinition?.itemId) return [];
+
+ if(variablesType === 'user')
+ {
+ const targets: VariableHighlightTarget[] = [];
+
+ for(const [ userIdString, assignments ] of Object.entries(userVariableAssignments))
+ {
+ const assignment = assignments.find(entry => (entry.variableItemId === selectedVariableDefinition.itemId));
+
+ if(!assignment) continue;
+
+ const userId = Number(userIdString);
+ const userData = roomSession.userDataManager.getUserData(userId)
+ ?? roomSession.userDataManager.getBotData(userId)
+ ?? roomSession.userDataManager.getRentableBotData(userId)
+ ?? roomSession.userDataManager.getPetData(userId);
+ const roomIndex = Number(userData?.roomIndex ?? -1);
+
+ if(roomIndex < 0) continue;
+
+ targets.push({
+ category: RoomObjectCategory.UNIT,
+ objectId: roomIndex,
+ hasValue: !!assignment.hasValue && !!selectedVariableDefinition.hasValue && (assignment.value !== null) && (assignment.value !== undefined),
+ value: assignment.value
+ });
+ }
+
+ return targets;
+ }
+
+ if(variablesType === 'furni')
+ {
+ const targets: VariableHighlightTarget[] = [];
+
+ for(const [ furniIdString, assignments ] of Object.entries(furniVariableAssignments))
+ {
+ const assignment = assignments.find(entry => (entry.variableItemId === selectedVariableDefinition.itemId));
+
+ if(!assignment) continue;
+
+ const furniId = Number(furniIdString);
+ const floorObject = GetRoomEngine().getRoomObject(roomSession.roomId, furniId, RoomObjectCategory.FLOOR);
+ const wallObject = floorObject ? null : GetRoomEngine().getRoomObject(roomSession.roomId, furniId, RoomObjectCategory.WALL);
+ const category = floorObject ? RoomObjectCategory.FLOOR : (wallObject ? RoomObjectCategory.WALL : -1);
+
+ if(category < 0) continue;
+
+ targets.push({
+ category,
+ objectId: furniId,
+ hasValue: !!assignment.hasValue && !!selectedVariableDefinition.hasValue && (assignment.value !== null) && (assignment.value !== undefined),
+ value: assignment.value
+ });
+ }
+
+ return targets;
+ }
+
+ return [];
+ }, [ canVariableHighlight, furniVariableAssignments, isVariableHighlightActive, roomSession, selectedVariableDefinition, userVariableAssignments, variablesType ]);
+ useEffect(() =>
+ {
+ if(isVisible && (activeTab === 'variables') && canVariableHighlight) return;
+
+ setIsVariableHighlightActive(false);
+ }, [ activeTab, canVariableHighlight, isVisible ]);
+ useEffect(() =>
+ {
+ if(variableHighlightObjectsRef.current.length)
+ {
+ WiredSelectionVisualizer.clearVariableHighlightFromObjects(variableHighlightObjectsRef.current);
+ variableHighlightObjectsRef.current = [];
+ }
+
+ if(!isVariableHighlightActive || !variableHighlightTargets.length)
+ {
+
+ setVariableHighlightOverlays([]);
+
+ return;
+ }
+
+ const objects = variableHighlightTargets.map(target => ({
+ category: target.category,
+ objectId: target.objectId
+ }));
+
+ WiredSelectionVisualizer.applyVariableHighlightToObjects(objects);
+ variableHighlightObjectsRef.current = objects;
+
+ return () =>
+ {
+ if(!variableHighlightObjectsRef.current.length) return;
+
+ WiredSelectionVisualizer.clearVariableHighlightFromObjects(variableHighlightObjectsRef.current);
+ variableHighlightObjectsRef.current = [];
+ };
+ }, [ isVariableHighlightActive, variableHighlightTargets ]);
+ useEffect(() =>
+ {
+ if(!isVariableHighlightActive || !roomSession?.roomId || !variableHighlightTargets.length)
+ {
+ setVariableHighlightOverlays([]);
+
+ return;
+ }
+
+ const updateOverlays = () =>
+ {
+ const stage = GetStage();
+ const nextOverlays: VariableHighlightOverlay[] = [];
+
+ for(const target of variableHighlightTargets)
+ {
+ const bounds = GetRoomObjectBounds(roomSession.roomId, target.objectId, target.category);
+ const location = GetRoomObjectScreenLocation(roomSession.roomId, target.objectId, target.category);
+
+ if(!bounds || !location) continue;
+
+ const x = Math.max(8, Math.min(Math.round(location.x), (stage.width - 8)));
+ const y = Math.max(8, Math.min(Math.round(bounds.top), (stage.height - 40)));
+
+ nextOverlays.push({
+ ...target,
+ key: `${ target.category }:${ target.objectId }`,
+ x,
+ y
+ });
+ }
+
+ setVariableHighlightOverlays(nextOverlays);
+ };
+
+ updateOverlays();
+
+ const ticker = GetTicker();
+
+ ticker.add(updateOverlays);
+
+ return () => ticker.remove(updateOverlays);
+ }, [ isVariableHighlightActive, roomSession?.roomId, variableHighlightTargets ]);
const variableManageTypeOptions = useMemo(() =>
{
switch(variablesType)
@@ -3465,6 +3632,27 @@ export const WiredCreatorToolsView: FC<{}> = () =>
return (
<>
+ { isVariableHighlightActive && !!variableHighlightOverlays.length &&
+
+ { variableHighlightOverlays.map(overlay => (
+
+ { overlay.hasValue &&
+
+
+ { overlay.value ?? 0 }
+
+
+
}
+
+ )) }
+
}
setIsVisible(false) } />
@@ -3830,7 +4018,12 @@ export const WiredCreatorToolsView: FC<{}> = () =>