From 39dc37dda3391aabce5e427de7fe610340648445 Mon Sep 17 00:00:00 2001 From: duckietm Date: Tue, 3 Jun 2025 09:26:55 +0200 Subject: [PATCH] :new: Added Build Tool to the UI --- src/components/room/widgets/RoomWidgets.scss | 114 ++++++- .../infostand/InfoStandWidgetFurniView.tsx | 277 +++++++++++++++++- 2 files changed, 378 insertions(+), 13 deletions(-) diff --git a/src/components/room/widgets/RoomWidgets.scss b/src/components/room/widgets/RoomWidgets.scss index 6dc55ad..96dcb45 100644 --- a/src/components/room/widgets/RoomWidgets.scss +++ b/src/components/room/widgets/RoomWidgets.scss @@ -2,8 +2,8 @@ position: absolute; bottom: $toolbar-height + 25px; left: 0px; - display: flex; // Lay out children side by side - align-items: flex-start; // Align items to the top + display: flex; + align-items: flex-start; .nitro-room-tools { background: #212131; @@ -47,12 +47,12 @@ } } } - - .nitro-room-tools-side-container { + + .nitro-room-tools-side-container { display: flex; - flex-direction: column; // Stack children vertically - margin-left: 10px; // Space between nitro-room-tools and side container - gap: 10px; // Space between history and info + flex-direction: column; + margin-left: 10px; + gap: 10px; } .nitro-room-history { @@ -61,7 +61,7 @@ transition: all .2s ease; width: 150px; overflow: hidden; - z-index: 1; + z-index: 5; } .nitro-room-tools-info { @@ -170,6 +170,104 @@ } } +.buildtool-box { + background-color: 3D5D63; + align-items: center; + width: 100%; + border-radius: 6px; + border: 1px solid #fff; + padding: 2px; +} + +.buildtool-box-left { + background-color: #3D5D63; +} + +.buildtool-box-right { + background-color: #3D5D63; +} + +.buildtool-movefurni { + background-color: #E55959; + color: #fff; + width: 25px; + height: 25px; + border: 1px solid #fff; + cursor: pointer; + border-radius: 3px; + display: flex; + justify-content: center; + align-items: center; + transition: filter 0.3s ease; + + &:hover { + filter: brightness(1.5); + } +} + +.buildtool-rotatefurni { + background-color: #D1A245; + color: #000; + width: 28px; + height: 28px; + border: 2px solid #eee; + cursor: pointer; + border-radius: 14px; + display: flex; + justify-content: center; + align-items: center; + transition: filter 0.3s ease; + + &:hover { + filter: brightness(1.5); + } +} + +.buildtool-setheight { + color: #fff; + width: 24px; + height: 24px; + border: 1px solid #fff; + cursor: pointer; + border-radius: 3px; + line-height: 1.0; + display: flex; + justify-content: center; + align-items: center; + transition: filter 0.3s ease; + + &:hover { + filter: brightness(1.5); + } +} + +.buildtool-setheightup { + background-color: #247FD1; +} + +.buildtool-setheightdown { + background-color: #44A750; +} + +.button-leftdown { + transform: rotate(135deg); +} + +.button-leftup { + transform: rotate(225deg); +} + +.button-rightdown { + transform: rotate(45deg); +} + +.button-rightup { + transform: rotate(315deg); +} + +.floor-spaceing { + gap: 0.6em!important; +} @import './avatar-info/AvatarInfoWidgetView'; @import './chat/ChatWidgetView'; @import './chat-input/ChatInputView'; diff --git a/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetFurniView.tsx b/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetFurniView.tsx index 33ea96f..732818c 100644 --- a/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetFurniView.tsx +++ b/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetFurniView.tsx @@ -1,5 +1,7 @@ -import { CrackableDataType, GroupInformationComposer, GroupInformationEvent, NowPlayingEvent, RoomControllerLevel, RoomObjectCategory, RoomObjectOperationType, RoomObjectVariable, RoomWidgetEnumItemExtradataParameter, RoomWidgetFurniInfoUsagePolicyEnum, SetObjectDataMessageComposer, SongInfoReceivedEvent, StringDataType } from '@nitrots/nitro-renderer'; +import { CrackableDataType, FurnitureFloorUpdateEvent, 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 { FaTimes } from 'react-icons/fa'; +import { GrFormNextLink, GrRotateLeft, GrRotateRight } from 'react-icons/gr'; import { AvatarInfoFurni, CreateLinkEvent, GetGroupInformation, GetNitroInstance, GetRoomEngine, LocalizeText, SendMessageComposer } from '../../../../../api'; import { Base, Button, Column, Flex, LayoutBadgeImageView, LayoutLimitedEditionCompactPlateView, LayoutRarityLevelView, Text, UserProfileIconView } from '../../../../../common'; import { useMessageEvent, useRoom, useSoundEvent } from '../../../../../hooks'; @@ -39,6 +41,217 @@ 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 [ furniLocationX, setFurniLocationX ] = useState(null); + const [ furniLocationY, setFurniLocationY ] = useState(null); + const [ furniLocationZ, setFurniLocationZ ] = useState(null); + const [ furniDirection, setFurniDirection ] = useState(null); + const [ furniState, setFurniState ] = useState(null); + + const sendUpdate = useCallback((deltaX: number, deltaY: number, deltaZ: 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 newZ = deltaZ * 10000; + + const currentDirection = roomObject.getDirection().x; + + const newDirection = (deltaDirection !== 0) + ? getValidRoomObjectDirection(roomObject, deltaDirection > 0) / 45 + : currentDirection / 45; + + SendMessageComposer(new UpdateFurniturePositionComposer(avatarInfo.id, newX, newY, newZ, newDirection)); + }, [ avatarInfo ]); + + function getRotationIndex(directionVector) + { + const angle = directionVector.x; + + switch(angle) + { + case 0: return 0; + case 45: return 1; + case 90: return 2; + case 135: return 3; + case 180: return 4; + case 225: return 5; + case 270: return 6; + case 315: return 7; + default: return null; // Handle unexpected angles + } + } + + useEffect(() => + { + const roomId = roomSession.roomId; + const objectId = avatarInfo.id; + const isWallItem = avatarInfo.isWallItem; + + const locationString = GetRoomEngine().getFurniLocation(roomId, objectId, isWallItem); + const locationVector = parseVector3d(locationString); + + if (locationVector) + { + setFurniLocationX(locationVector.x); + setFurniLocationY(locationVector.y); + setFurniLocationZ(locationVector.z); + } + + const directionString = GetRoomEngine().getFurniDirection(roomId, objectId, isWallItem); + const directionVector = parseVector3d(directionString); + const rotationIndex = directionVector ? getRotationIndex(directionVector) : null; + + const state = GetRoomEngine().getFurniState(roomId, objectId, isWallItem); + + setFurniDirection(rotationIndex); + setFurniState(state); + }, [ avatarInfo, roomSession ]); + + function parseVector3d(vectorString: string) + { + if (!vectorString) return null; + + const matches = vectorString.match(/\[Vector3d: ([\d.]+), ([\d.]+), ([\d.]+)/); + if (matches && matches.length === 4) + { + return { + x: parseFloat(matches[1]), + y: parseFloat(matches[2]), + z: parseFloat(matches[3]) + }; + } + return null; + } + + useMessageEvent(FurnitureFloorUpdateEvent, event => + { + const parser = event.getParser(); + const item = parser.item; + + if (item.itemId !== avatarInfo.id) return; + + const locationVector = { + x: item.x, + y: item.y, + z: item.z + }; + + if (locationVector) + { + setFurniLocationX(locationVector.x); + setFurniLocationY(locationVector.y); + setFurniLocationZ(locationVector.z); + } + + const directionVector = { x: item.direction }; + const rotationIndex = directionVector ? getRotationIndex(directionVector) : null; + + const state = item.state; + + setFurniDirection(rotationIndex); + setFurniState(state); + }); + + const handleHeightChange = useCallback((event) => + { + 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) => + { + 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) => + { + let newZ = furniLocationZ + 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 ]); + + function getValidRoomObjectDirection(roomObject, isPositive) + { + if (!roomObject || !roomObject.model) return 0; + + let allowedDirections = []; + + 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; + } useSoundEvent(NowPlayingEvent.NPE_SONG_CHANGED, event => { @@ -241,7 +454,6 @@ export const InfoStandWidgetFurniView: FC = props if(furniKeys.length === 0 || furniValues.length === 0) return ''; let data = ''; - let i = 0; while(i < furniKeys.length) @@ -250,7 +462,6 @@ export const InfoStandWidgetFurniView: FC = props const value = furniValues[i]; data = (data + (key + '=' + value + '\t')); - i++; } @@ -296,7 +507,6 @@ export const InfoStandWidgetFurniView: FC = props for(const part of dataParts) { const [ key, value ] = part.split('=', 2); - mapData.set(key, value); } } @@ -425,6 +635,63 @@ export const InfoStandWidgetFurniView: FC = props { godMode && <>
+ { (!avatarInfo.isWallItem && canMove) && + <> + + { dropdownOpen && + <> + + + + {LocalizeText('group.edit.badge.position')} + + + sendUpdate(-1, 0, furniLocationZ, 0) }> + sendUpdate(0, -1, furniLocationZ, 0) }> + + + sendUpdate(0, 1, furniLocationZ, 0) }> + sendUpdate(1, 0, furniLocationZ, 0) }> + + + {LocalizeText('infostand.button.rotate')} + + sendUpdate(0, 0, furniLocationZ, -1) }> + sendUpdate(0, 0, furniLocationZ, 1) }> + + + + + + {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) && <>
@@ -485,4 +752,4 @@ export const InfoStandWidgetFurniView: FC = props ); -} \ No newline at end of file +}