From 865ed3028d1746ee553915ee07b20aceaa04ebeb Mon Sep 17 00:00:00 2001 From: Tom MOUSSET <62646888+pytomprog@users.noreply.github.com> Date: Tue, 11 Aug 2026 18:18:47 +0200 Subject: [PATCH 1/2] Add radial tool selection menu Add a new `ToolsMenu` widget composed of 6 sectors, each has a label and a hovered texture. Modify the Tab key pressing behavior so now it opens the menu while the key is pressed. Mouse movements are sent to the tools menu when it is opened so the hovered tool can be updated. When the Tab key is released, the selected tool changes. --- assets/assets.json | 30 +++++++++++ include/ClientLib/ToolsMenu.hpp | 58 +++++++++++++++++++++ include/ClientLib/ToolsMenu.inl | 11 ++++ src/ClientLib/ToolsMenu.cpp | 85 +++++++++++++++++++++++++++++++ src/Game/States/GameState.cpp | 90 +++++++++++++++++++++++++-------- src/Game/States/GameState.hpp | 2 + 6 files changed, 256 insertions(+), 20 deletions(-) create mode 100644 include/ClientLib/ToolsMenu.hpp create mode 100644 include/ClientLib/ToolsMenu.inl create mode 100644 src/ClientLib/ToolsMenu.cpp diff --git a/assets/assets.json b/assets/assets.json index 1eeb57c3..cce23c08 100644 --- a/assets/assets.json +++ b/assets/assets.json @@ -340,6 +340,36 @@ "channel1": "InvRed", "channelSourceTexture0": 0, "channelSourceTexture1": 1 + }, + { + "input": "UI/ToolsMenu/ToolsMenu_Tool0.png", + "output": "UI/ToolsMenu/ToolsMenu_Tool0.dds", + "method": "Texture" + }, + { + "input": "UI/ToolsMenu/ToolsMenu_Tool1.png", + "output": "UI/ToolsMenu/ToolsMenu_Tool1.dds", + "method": "Texture" + }, + { + "input": "UI/ToolsMenu/ToolsMenu_Tool2.png", + "output": "UI/ToolsMenu/ToolsMenu_Tool2.dds", + "method": "Texture" + }, + { + "input": "UI/ToolsMenu/ToolsMenu_Tool3.png", + "output": "UI/ToolsMenu/ToolsMenu_Tool3.dds", + "method": "Texture" + }, + { + "input": "UI/ToolsMenu/ToolsMenu_Tool4.png", + "output": "UI/ToolsMenu/ToolsMenu_Tool4.dds", + "method": "Texture" + }, + { + "input": "UI/ToolsMenu/ToolsMenu_Tool5.png", + "output": "UI/ToolsMenu/ToolsMenu_Tool5.dds", + "method": "Texture" } ] } \ No newline at end of file diff --git a/include/ClientLib/ToolsMenu.hpp b/include/ClientLib/ToolsMenu.hpp new file mode 100644 index 00000000..964daea1 --- /dev/null +++ b/include/ClientLib/ToolsMenu.hpp @@ -0,0 +1,58 @@ +// Copyright (C) 2026 Jérôme "SirLynix" Leclercq (lynix680@gmail.com) +// This file is part of the "This Space Of Mine" project +// For conditions of distribution and use, see copyright notice in LICENSE + +#pragma once + +#ifndef TSOM_CLIENTLIB_TOOLSMENU_HPP +#define TSOM_CLIENTLIB_TOOLSMENU_HPP + +#include +#include + +#include + +namespace Nz +{ + class FilesystemAppComponent; + class ImageWidget; + class LabelWidget; +} + +namespace tsom +{ + class TSOM_CLIENTLIB_API ToolsMenu : public Nz::BaseWidget + { + public: + ToolsMenu(Nz::BaseWidget* parent, Nz::FilesystemAppComponent& filesystem, uint8_t selectedTool); + ToolsMenu(const ToolsMenu&) = delete; + ToolsMenu(ToolsMenu&&) = delete; + ~ToolsMenu() = default; + + ToolsMenu& operator=(const ToolsMenu&) = delete; + ToolsMenu& operator=(ToolsMenu&&) = delete; + + void HandleMouseMoved(float mouseX, float mouseY, uint8_t selectedTool); + + inline const uint8_t GetHoveredTool() const; + + private: + void Layout() override; + + void HoverTool(uint8_t toolIndex, uint8_t oldToolIndex); + + uint8_t m_hoveredTool; + + Nz::FilesystemAppComponent& m_filesystem; + + std::array, 6> m_imageMaterials; + Nz::ImageWidget* m_image; + + std::array m_toolTexts = { "None", "Block", "Place\nEntity", "Remove\nEntity", "Connect", "Grab" }; + std::array m_toolLabelWidgets; + }; +} + +#include + +#endif // TSOM_CLIENTLIB_TOOLSMENU_HPP diff --git a/include/ClientLib/ToolsMenu.inl b/include/ClientLib/ToolsMenu.inl new file mode 100644 index 00000000..a1a4f3e4 --- /dev/null +++ b/include/ClientLib/ToolsMenu.inl @@ -0,0 +1,11 @@ +// Copyright (C) 2026 Jérôme "SirLynix" Leclercq (lynix680@gmail.com) +// This file is part of the "This Space Of Mine" project +// For conditions of distribution and use, see copyright notice in LICENSE + +namespace tsom +{ + inline const uint8_t ToolsMenu::GetHoveredTool() const + { + return m_hoveredTool; + } +} diff --git a/src/ClientLib/ToolsMenu.cpp b/src/ClientLib/ToolsMenu.cpp new file mode 100644 index 00000000..007c4c31 --- /dev/null +++ b/src/ClientLib/ToolsMenu.cpp @@ -0,0 +1,85 @@ +// Copyright (C) 2026 Jérôme "SirLynix" Leclercq (lynix680@gmail.com) +// This file is part of the "This Space Of Mine" project +// For conditions of distribution and use, see copyright notice in LICENSE + +#include + +#include +#include +#include +#include + +#include + +namespace tsom +{ + ToolsMenu::ToolsMenu(Nz::BaseWidget* parent, Nz::FilesystemAppComponent& filesystem, uint8_t selectedTool) : + BaseWidget(parent), + m_hoveredTool(selectedTool), + m_filesystem(filesystem) + { + //EnableBackground(true); + //SetBackgroundColor(Nz::Color(0, 0, 0, 0.6f)); + + for (size_t i = 0; i < m_imageMaterials.size(); i++) { + std::shared_ptr& imageMaterial = m_imageMaterials[i]; + imageMaterial = Nz::MaterialInstance::Instantiate(Nz::MaterialType::Basic, Nz::MaterialInstancePreset::UI); + imageMaterial->SetTextureProperty("BaseColorMap", m_filesystem.Open(fmt::format("CookedAssets/UI/ToolsMenu/ToolsMenu_Tool{0}.dds", i), { .sRGB = true })); + } + + m_image = Add(m_imageMaterials[m_hoveredTool]); + m_image->Resize({ 600.f, 600.f }); + + for (size_t i = 0; i < m_toolLabelWidgets.size(); i++) { + Nz::LabelWidget*& toolLabelWidget = m_toolLabelWidgets[i]; + toolLabelWidget = Add(); + toolLabelWidget->UpdateText(Nz::SimpleTextDrawer::Draw(m_toolTexts[i], 30, Nz::TextStyle_Regular, (i == m_hoveredTool ? Nz::Color::Black() : Nz::Color::White()))); + } + + Resize(m_image->GetSize()); + } + + void ToolsMenu::HandleMouseMoved(float mouseX, float mouseY, uint8_t selectedTool) + { + Nz::Vector3f centerPosition = m_image->GetGlobalPosition() + m_image->GetSize() / 2.f; + Nz::Vector2f centeredMousePosition = { mouseX - centerPosition.x , mouseY - centerPosition.y }; + + uint8_t previousHoveredTool = m_hoveredTool; + if (centeredMousePosition.GetLength() >= m_image->GetSize()[0] / 4.f) + { + float angle = std::atan2(-centeredMousePosition.x, centeredMousePosition.y); + m_hoveredTool = static_cast(round(m_imageMaterials.size() * (angle / Nz::Pi() + 1.f) / 2.f)) % m_imageMaterials.size(); + } + else + { + m_hoveredTool = selectedTool; + } + + HoverTool(m_hoveredTool, previousHoveredTool); + Layout(); + } + + void ToolsMenu::HoverTool(uint8_t newToolIndex, uint8_t oldToolIndex) + { + m_image->SetMaterial(m_imageMaterials[newToolIndex % m_imageMaterials.size()]); + m_toolLabelWidgets[oldToolIndex]->UpdateText(Nz::SimpleTextDrawer::Draw(m_toolTexts[oldToolIndex], 30, Nz::TextStyle_Regular, Nz::Color::White())); + m_toolLabelWidgets[newToolIndex]->UpdateText(Nz::SimpleTextDrawer::Draw(m_toolTexts[newToolIndex], 30, Nz::TextStyle_Regular, Nz::Color::Black())); + } + + void ToolsMenu::Layout() + { + BaseWidget::Layout(); + + m_image->Center(); + + float distFromCenter = m_image->GetSize()[0] * 3.f / 8.f; + for (size_t i = 0; i < m_toolLabelWidgets.size(); i++) { + Nz::LabelWidget*& toolLabelWidget = m_toolLabelWidgets[i]; + + float angle = i * 2.f * Nz::Pi() / m_toolLabelWidgets.size(); + + toolLabelWidget->Center(); + toolLabelWidget->Move(Nz::Vector2f({ distFromCenter * sinf(angle), distFromCenter * cosf(angle) })); + } + } +} diff --git a/src/Game/States/GameState.cpp b/src/Game/States/GameState.cpp index 3fdfeebe..04bef78b 100644 --- a/src/Game/States/GameState.cpp +++ b/src/Game/States/GameState.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -317,6 +318,12 @@ namespace tsom UpdateMouseLock(); }); + m_toolsMenu = CreateWidget(filesystem, m_toolIndex); + m_toolsMenu->OnWidgetVisibilityUpdated.Connect([&](const Nz::BaseWidget* /*widget*/, bool /*isVisible*/) + { + UpdateMouseLock(); + }); + m_chatBox = CreateWidget(); m_chatBox->OnChatMessage.Connect([&](const std::string& message) { @@ -402,19 +409,13 @@ namespace tsom case Nz::Keyboard::Scancode::Tab: { - m_tools[m_toolIndex]->OnDeactivate(); - - m_toolIndex++; - if (m_toolIndex >= m_tools.size()) - m_toolIndex = 0; - - m_tools[m_toolIndex]->OnActivate(); - spdlog::info("New tool: {}", m_tools[m_toolIndex]->GetName()); - - UpdateMouseLock(); + if (!m_toolsMenu->IsVisible() && !m_escapeMenu->IsVisible()) + { + Nz::Mouse::SetPosition(static_cast(stateData.renderTarget->GetSize()) / 2.f, *stateData.window); + m_toolsMenu->Show(); + UpdateMouseLock(); + } - m_blockSelectionBar->Show(m_toolIndex == 1 /*BlockTool*/ && !m_pilotedShip.has_value()); - LayoutWidgets(Nz::Vector2f(GetStateData().renderTarget->GetSize())); break; } @@ -431,7 +432,12 @@ namespace tsom else if (m_pilotedShip) stateData.networkSession->SendPacket(Packets::C_ExitShipControl{}); else + { + if (m_toolsMenu->IsVisible()) + m_toolsMenu->Hide(); + m_escapeMenu->Show(); + } UpdateMouseLock(); break; @@ -560,6 +566,41 @@ namespace tsom } }); + m_onUnhandledKeyReleased.Connect(stateData.canvas->OnUnhandledKeyReleased, [this](const Nz::WindowEventHandler*, const Nz::WindowEvent::KeyEvent& event) + { + auto& stateData = GetStateData(); + + switch (event.scancode) + { + case Nz::Keyboard::Scancode::Tab: + { + if (m_toolsMenu->IsVisible()) + { + m_toolsMenu->Hide(); + + m_tools[m_toolIndex]->OnDeactivate(); + + size_t newTool = static_cast(m_toolsMenu->GetHoveredTool()); + if (newTool != m_toolIndex) + { + m_toolIndex = newTool; + m_tools[m_toolIndex]->OnActivate(); + spdlog::info("New tool: {}", m_tools[m_toolIndex]->GetName()); + + UpdateMouseLock(); + + m_blockSelectionBar->Show(m_toolIndex == 1 /*BlockTool*/ && !m_pilotedShip.has_value()); + LayoutWidgets(Nz::Vector2f(stateData.renderTarget->GetSize())); + } + } + break; + } + + default: + break; + } + }); + m_onChatMessage.Connect(stateData.sessionHandler->OnChatMessage, [this](const std::string& message) { m_chatBox->PrintMessage({ @@ -780,7 +821,7 @@ namespace tsom void GameState::UpdateMouseLock() { - m_isMouseLocked = !m_chatBox->IsTyping() && !m_escapeMenu->IsVisible() && !m_localConsole->IsVisible() && !m_remoteConsole->IsVisible() && !m_tools[m_toolIndex]->IsCursorUnlocked(); + m_isMouseLocked = !m_chatBox->IsTyping() && !m_escapeMenu->IsVisible() && !m_toolsMenu->IsVisible() && !m_localConsole->IsVisible() && !m_remoteConsole->IsVisible() && !m_tools[m_toolIndex]->IsCursorUnlocked(); m_chatBox->EnableMouseInput(!m_isMouseLocked); m_localConsole->EnableMouseInput(!m_isMouseLocked); m_remoteConsole->EnableMouseInput(!m_isMouseLocked); @@ -799,6 +840,7 @@ namespace tsom m_chatBox->Close(); m_escapeMenu->Hide(); + m_toolsMenu->Hide(); m_localConsole->Hide(); m_remoteConsole->Hide(); @@ -814,16 +856,23 @@ namespace tsom float mouseSensitivity = config.GetFloatValue(Config::Input_MouseSensitivity); m_mouseMovedSlot.Connect(stateData.canvas->OnUnhandledMouseMoved, [&, mouseSensitivity](const Nz::WindowEventHandler*, const Nz::WindowEvent::MouseMoveEvent& event) { - if (!m_isMouseLocked) - return; - auto& stateData = GetStateData(); - float pitchMod = -event.deltaY * mouseSensitivity; - float yawMod = -event.deltaX * mouseSensitivity; + if (m_isMouseLocked) + { + float pitchMod = -event.deltaY * mouseSensitivity; + float yawMod = -event.deltaX * mouseSensitivity; - m_incomingCameraRotation.pitch += pitchMod; - m_incomingCameraRotation.yaw += yawMod; + m_incomingCameraRotation.pitch += pitchMod; + m_incomingCameraRotation.yaw += yawMod; + } + else + { + if (m_toolsMenu->IsVisible()) + { + m_toolsMenu->HandleMouseMoved(event.x, event.y, m_toolIndex); + } + } }); m_mouseButtonReleasedSlot.Connect(stateData.canvas->OnUnhandledMouseButtonReleased, [&](const Nz::WindowEventHandler*, const Nz::WindowEvent::MouseButtonEvent& event) @@ -1255,6 +1304,7 @@ namespace tsom m_healthOxygen.entity.get().SetPosition({ newSize.x * 0.5f - m_healthOxygen.entity.get().GetAABB().x / 2.f, hudNextHeight }); m_escapeMenu->Center(); + m_toolsMenu->Center(); } Nz::Vector3f GameState::RaycastCamera(const Nz::Vector3f& from, const Nz::Vector3f& to) diff --git a/src/Game/States/GameState.hpp b/src/Game/States/GameState.hpp index 4cb7f3f5..e4007ab5 100644 --- a/src/Game/States/GameState.hpp +++ b/src/Game/States/GameState.hpp @@ -44,6 +44,7 @@ namespace tsom class EscapeMenu; struct StateData; class ToolBase; + class ToolsMenu; class GameState : public WidgetState, public GameInterface { @@ -192,6 +193,7 @@ namespace tsom Console* m_localConsole; Console* m_remoteConsole; EscapeMenu* m_escapeMenu; + ToolsMenu* m_toolsMenu; bool m_isMouseLocked; float m_currentCameraDistance; float m_defaultCameraDistance; From 6f1b8608729c975d4fb7caf62fc13564b5721462 Mon Sep 17 00:00:00 2001 From: Tom MOUSSET <62646888+pytomprog@users.noreply.github.com> Date: Tue, 11 Aug 2026 18:19:51 +0200 Subject: [PATCH 2/2] Fix block selection bar shown at startup even if block tool isn't selected --- src/Game/States/GameState.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Game/States/GameState.cpp b/src/Game/States/GameState.cpp index 04bef78b..eca32a7a 100644 --- a/src/Game/States/GameState.cpp +++ b/src/Game/States/GameState.cpp @@ -843,6 +843,7 @@ namespace tsom m_toolsMenu->Hide(); m_localConsole->Hide(); m_remoteConsole->Hide(); + m_blockSelectionBar->Show(m_toolIndex == 1 /*BlockTool*/ && !m_pilotedShip.has_value()); auto& stateData = GetStateData(); LayoutWidgets(Nz::Vector2f(stateData.renderTarget->GetSize()));