diff --git a/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetFurniView.tsx b/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetFurniView.tsx index d60e3c2..d58a720 100644 --- a/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetFurniView.tsx +++ b/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetFurniView.tsx @@ -1,6 +1,7 @@ -import { CrackableDataType, CreateLinkEvent, FurnitureFloorUpdateEvent, GetRoomEngine, GetSoundManager, GroupInformationComposer, GroupInformationEvent, NowPlayingEvent, RoomControllerLevel, RoomObjectCategory, RoomObjectOperationType, RoomObjectVariable, RoomWidgetEnumItemExtradataParameter, RoomWidgetFurniInfoUsagePolicyEnum, SetObjectDataMessageComposer, SongInfoReceivedEvent, StringDataType } from '@nitrots/nitro-renderer'; +import { CrackableDataType, CreateLinkEvent, FurnitureFloorUpdateEvent, GetRoomEngine, GetSoundManager, GroupInformationComposer, GroupInformationEvent, NowPlayingEvent, RoomControllerLevel, RoomObjectCategory, RoomObjectOperationType, RoomObjectVariable, RoomWidgetEnumItemExtradataParameter, RoomWidgetFurniInfoUsagePolicyEnum, SetObjectDataMessageComposer, SongInfoReceivedEvent, StringDataType, UpdateFurniturePositionComposer } from '@nitrots/nitro-renderer'; import { FC, useCallback, useEffect, useState } from 'react'; import { FaCrosshairs, FaRulerVertical, FaTimes } from 'react-icons/fa'; +import { GrFormNextLink, GrRotateLeft, GrRotateRight } from 'react-icons/gr'; import { AvatarInfoFurni, GetGroupInformation, LocalizeText, SendMessageComposer } from '../../../../../api'; import { Button, Column, Flex, LayoutBadgeImageView, LayoutLimitedEditionCompactPlateView, LayoutRarityLevelView, LayoutRoomObjectImageView, Text, UserProfileIconView } from '../../../../../common'; import { useMessageEvent, useNitroEvent, useRoom } from '../../../../../hooks'; @@ -41,6 +42,113 @@ export const InfoStandWidgetFurniView: FC = props const [ songName, setSongName ] = useState(''); const [ songCreator, setSongCreator ] = useState(''); const [ itemLocation, setItemLocation ] = useState<{ x: number; y: number; z: number }>({ x: -1, y: -1, z: -1 }); + const [ dropdownOpen, setDropdownOpen ] = useState(sessionStorage.getItem('dropdownOpen') === 'true'); + const [ furniLocationZ, setFurniLocationZ ] = useState(null); + + const sendUpdate = useCallback((deltaX: number, deltaY: number, newZ: number = 0, deltaDirection: number = 0) => + { + if(!avatarInfo) return; + + const roomId = GetRoomEngine().activeRoomId; + const roomObject = GetRoomEngine().getRoomObject(roomId, avatarInfo.id, avatarInfo.category); + + if(!roomObject) return; + + const newX = roomObject.getLocation().x + deltaX; + const newY = roomObject.getLocation().y + deltaY; + const currentDirection = roomObject.getDirection().x; + + const newDirection = (deltaDirection !== 0) + ? getValidRoomObjectDirection(roomObject, deltaDirection > 0) / 45 + : currentDirection / 45; + + SendMessageComposer(new UpdateFurniturePositionComposer(avatarInfo.id, newX, newY, Math.round(newZ * 10000), newDirection)); + }, [ avatarInfo ]); + + function getValidRoomObjectDirection(roomObject: any, isPositive: boolean) + { + if(!roomObject || !roomObject.model) return 0; + + let allowedDirections: number[] = []; + + if(roomObject.type === 'monster_plant') + { + allowedDirections = roomObject.model.getValue('pet_allowed_directions'); + } + else + { + allowedDirections = roomObject.model.getValue('furniture_allowed_directions'); + } + + let direction = roomObject.getDirection().x; + + if(allowedDirections && allowedDirections.length) + { + let index = allowedDirections.indexOf(direction); + + if(index < 0) + { + index = 0; + + for(let i = 0; i < allowedDirections.length; i++) + { + if(direction <= allowedDirections[i]) break; + + index++; + } + + index = index % allowedDirections.length; + } + + if(isPositive) + { + index = (index + 1) % allowedDirections.length; + } + else + { + index = (index - 1 + allowedDirections.length) % allowedDirections.length; + } + + direction = allowedDirections[index]; + } + + return direction; + } + + const handleHeightChange = useCallback((event: React.ChangeEvent) => + { + let newZ = parseFloat(event.target.value); + + if(isNaN(newZ) || newZ < 0) newZ = 0; + else if(newZ > 40) newZ = 40; + + setFurniLocationZ(newZ); + sendUpdate(0, 0, newZ, 0); + }, [ sendUpdate ]); + + const handleHeightBlur = useCallback((event: React.FocusEvent) => + { + let newZ = parseFloat(event.target.value); + + if(isNaN(newZ) || newZ < 0) newZ = 0; + else if(newZ > 40) newZ = 40; + + newZ = parseFloat(newZ.toFixed(4)); + setFurniLocationZ(newZ); + sendUpdate(0, 0, newZ, 0); + }, [ sendUpdate ]); + + const adjustHeight = useCallback((amount: number) => + { + let newZ = (furniLocationZ ?? 0) + amount; + + if(newZ < 0) newZ = 0; + else if(newZ > 40) newZ = 40; + + newZ = parseFloat(newZ.toFixed(4)); + setFurniLocationZ(newZ); + sendUpdate(0, 0, newZ, 0); + }, [ furniLocationZ, sendUpdate ]); useNitroEvent(NowPlayingEvent.NPE_SONG_CHANGED, event => { @@ -80,7 +188,12 @@ export const InfoStandWidgetFurniView: FC = props const roomObjForLocation = GetRoomEngine().getRoomObject(roomSession.roomId, avatarInfo.id, avatarInfo.isWallItem ? RoomObjectCategory.WALL : RoomObjectCategory.FLOOR); const location = roomObjForLocation?.getLocation(); - if(location) setItemLocation({ x: location.x, y: location.y, z: location.z }); + + if(location) + { + setItemLocation({ x: location.x, y: location.y, z: location.z }); + setFurniLocationZ(location.z); + } const isValidController = (avatarInfo.roomControllerLevel >= RoomControllerLevel.GUEST); @@ -218,6 +331,7 @@ export const InfoStandWidgetFurniView: FC = props if(!avatarInfo || item.itemId !== avatarInfo.id) return; setItemLocation({ x: item.x, y: item.y, z: item.z }); + setFurniLocationZ(item.z); }); useEffect(() => @@ -439,6 +553,91 @@ export const InfoStandWidgetFurniView: FC = props <>
{ canSeeFurniId && ID: { avatarInfo.id } } + { (!avatarInfo.isWallItem && canMove) && + <> + + { dropdownOpen && +
+ { /* Left panel: position + rotation */ } +
+ { LocalizeText('group.edit.badge.position') } +
+
+
sendUpdate(-1, 0, furniLocationZ ?? 0, 0) }> + +
+
sendUpdate(0, -1, furniLocationZ ?? 0, 0) }> + +
+
+
+
sendUpdate(0, 1, furniLocationZ ?? 0, 0) }> + +
+
sendUpdate(1, 0, furniLocationZ ?? 0, 0) }> + +
+
+
+ { LocalizeText('infostand.button.rotate') } +
+
sendUpdate(0, 0, furniLocationZ ?? 0, -1) }> + +
+
sendUpdate(0, 0, furniLocationZ ?? 0, 1) }> + +
+
+
+ { /* Right panel: height */ } +
+ { LocalizeText('stack.magic.tile.height.label') } + +
+
+
adjustHeight(1) }>↑
+ +
adjustHeight(-1) }>↓
+
+
+
adjustHeight(0.1) }>↑
+ +
adjustHeight(-0.1) }>↓
+
+
+
adjustHeight(0.01) }>↑
+ _ +
adjustHeight(-0.01) }>↓
+
+
+
+
} + } { (furniKeys.length > 0) && <>
diff --git a/src/css/room/RoomWidgets.css b/src/css/room/RoomWidgets.css index 9723ff4..093fa67 100644 --- a/src/css/room/RoomWidgets.css +++ b/src/css/room/RoomWidgets.css @@ -46,6 +46,13 @@ } } + .nitro-room-tools-side-container { + display: flex; + flex-direction: column; + margin-left: 10px; + gap: 10px; + } + .nitro-room-history { background: #212131; box-shadow: inset 0px 5px lighten(rgba(#000, .6), 2.5), inset 0 -4px darken(rgba(#000, .6), 4);