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..eca32a7a 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,8 +840,10 @@ namespace tsom m_chatBox->Close(); m_escapeMenu->Hide(); + 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())); @@ -814,16 +857,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 +1305,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;