From 6d7fcb1157f254561465c318baa8be71b3a944bc Mon Sep 17 00:00:00 2001 From: Sergey Lapin Date: Mon, 20 Apr 2026 20:21:27 +0300 Subject: [PATCH] Not so well working game mode --- src/features/editScene/CMakeLists.txt | 12 + src/features/editScene/EditorApp.cpp | 233 ++++++++++- src/features/editScene/EditorApp.hpp | 72 +++- .../editScene/components/PlayerController.hpp | 26 ++ .../components/PlayerControllerModule.cpp | 24 ++ .../editScene/components/StartupMenu.hpp | 20 + .../components/StartupMenuModule.cpp | 23 ++ src/features/editScene/main.cpp | 29 +- src/features/editScene/resources.cfg | 1 + .../editScene/systems/CharacterSlotSystem.cpp | 22 + .../editScene/systems/CharacterSlotSystem.hpp | 6 + .../editScene/systems/EditorUISystem.cpp | 22 + .../editScene/systems/EditorUISystem.hpp | 16 + .../systems/PlayerControllerSystem.cpp | 382 ++++++++++++++++++ .../systems/PlayerControllerSystem.hpp | 61 +++ .../editScene/systems/SceneSerializer.cpp | 82 ++++ .../editScene/systems/SceneSerializer.hpp | 4 + .../editScene/systems/StartupMenuSystem.cpp | 217 ++++++++++ .../editScene/systems/StartupMenuSystem.hpp | 47 +++ .../editScene/ui/PlayerControllerEditor.cpp | 85 ++++ .../editScene/ui/PlayerControllerEditor.hpp | 18 + .../editScene/ui/StartupMenuEditor.cpp | 48 +++ .../editScene/ui/StartupMenuEditor.hpp | 18 + 23 files changed, 1451 insertions(+), 17 deletions(-) create mode 100644 src/features/editScene/components/PlayerController.hpp create mode 100644 src/features/editScene/components/PlayerControllerModule.cpp create mode 100644 src/features/editScene/components/StartupMenu.hpp create mode 100644 src/features/editScene/components/StartupMenuModule.cpp create mode 100644 src/features/editScene/systems/PlayerControllerSystem.cpp create mode 100644 src/features/editScene/systems/PlayerControllerSystem.hpp create mode 100644 src/features/editScene/systems/StartupMenuSystem.cpp create mode 100644 src/features/editScene/systems/StartupMenuSystem.hpp create mode 100644 src/features/editScene/ui/PlayerControllerEditor.cpp create mode 100644 src/features/editScene/ui/PlayerControllerEditor.hpp create mode 100644 src/features/editScene/ui/StartupMenuEditor.cpp create mode 100644 src/features/editScene/ui/StartupMenuEditor.hpp diff --git a/src/features/editScene/CMakeLists.txt b/src/features/editScene/CMakeLists.txt index cda13cb..e0dbf15 100644 --- a/src/features/editScene/CMakeLists.txt +++ b/src/features/editScene/CMakeLists.txt @@ -25,6 +25,8 @@ set(EDITSCENE_SOURCES systems/CellGridSystem.cpp systems/RoomLayoutSystem.cpp systems/FurnitureLibrary.cpp + systems/StartupMenuSystem.cpp + systems/PlayerControllerSystem.cpp systems/CharacterSlotSystem.cpp systems/AnimationTreeSystem.cpp systems/CharacterSystem.cpp @@ -55,6 +57,8 @@ set(EDITSCENE_SOURCES ui/ClearAreaEditor.cpp ui/FurnitureTemplateEditor.cpp + ui/StartupMenuEditor.cpp + ui/PlayerControllerEditor.cpp ui/ComponentRegistration.cpp components/LightModule.cpp components/CameraModule.cpp @@ -71,6 +75,8 @@ set(EDITSCENE_SOURCES components/CellGridModule.cpp components/CellGridEditorsModule.cpp components/CellGrid.cpp + components/StartupMenuModule.cpp + components/PlayerControllerModule.cpp camera/EditorCamera.cpp gizmo/Gizmo.cpp physics/physics.cpp @@ -98,6 +104,10 @@ set(EDITSCENE_HEADERS components/AnimationTree.hpp components/Character.hpp components/CellGrid.hpp + components/StartupMenu.hpp + components/PlayerController.hpp + systems/StartupMenuSystem.hpp + systems/PlayerControllerSystem.hpp systems/EditorUISystem.hpp systems/CellGridSystem.hpp systems/RoomLayoutSystem.hpp @@ -144,6 +154,8 @@ set(EDITSCENE_HEADERS ui/ClearAreaEditor.hpp ui/FurnitureTemplateEditor.hpp + ui/StartupMenuEditor.hpp + ui/PlayerControllerEditor.hpp camera/EditorCamera.hpp gizmo/Gizmo.hpp physics/physics.h diff --git a/src/features/editScene/EditorApp.cpp b/src/features/editScene/EditorApp.cpp index 4613240..c0822fb 100644 --- a/src/features/editScene/EditorApp.cpp +++ b/src/features/editScene/EditorApp.cpp @@ -14,6 +14,9 @@ #include "systems/CharacterSystem.hpp" #include "systems/CellGridSystem.hpp" #include "systems/RoomLayoutSystem.hpp" +#include "systems/StartupMenuSystem.hpp" +#include "systems/PlayerControllerSystem.hpp" +#include "systems/SceneSerializer.hpp" #include "camera/EditorCamera.hpp" #include "components/EntityName.hpp" #include "components/Transform.hpp" @@ -35,6 +38,8 @@ #include "components/CharacterSlots.hpp" #include "components/AnimationTree.hpp" #include "components/Character.hpp" +#include "components/StartupMenu.hpp" +#include "components/PlayerController.hpp" #include "components/CellGrid.hpp" #include "components/CellGridModule.hpp" #include @@ -46,10 +51,12 @@ ImGuiRenderListener::ImGuiRenderListener(Ogre::ImGuiOverlay *imguiOverlay, EditorUISystem *uiSystem, - Ogre::RenderWindow *renderWindow) + Ogre::RenderWindow *renderWindow, + EditorApp *editorApp) : m_imguiOverlay(imguiOverlay) , m_uiSystem(uiSystem) , m_renderWindow(renderWindow) + , m_editorApp(editorApp) { m_lastTime = m_timer.getMilliseconds(); } @@ -71,6 +78,14 @@ void ImGuiRenderListener::preViewportUpdate( if (m_uiSystem) { m_uiSystem->update(m_deltaTime); } + + // Render startup menu in game mode (inside ImGui frame scope) + if (m_editorApp && m_editorApp->getGameMode() == EditorApp::GameMode::Game && + m_editorApp->getGamePlayState() == EditorApp::GamePlayState::Menu) { + StartupMenuSystem *sms = m_editorApp->getStartupMenuSystem(); + if (sms) + sms->update(m_deltaTime); + } } void ImGuiRenderListener::postViewportUpdate( @@ -100,6 +115,8 @@ EditorApp::EditorApp() , m_overlaySystem(nullptr) , m_imguiOverlay(nullptr) , m_currentModifiers(0) + , m_gameMode(GameMode::Editor) + , m_gamePlayState(GamePlayState::Menu) { } @@ -125,6 +142,8 @@ EditorApp::~EditorApp() } // Release all systems + m_playerControllerSystem.reset(); + m_startupMenuSystem.reset(); m_characterSlotSystem.reset(); m_animationTreeSystem.reset(); m_characterSystem.reset(); @@ -172,7 +191,6 @@ void EditorApp::setup() m_imguiOverlay = initialiseImGui(); if (m_imguiOverlay) { m_imguiOverlay->setZOrder(300); - m_imguiOverlay->show(); ImGui::StyleColorsDark(); } @@ -218,13 +236,13 @@ void EditorApp::setup() // Setup ProceduralTexture system m_proceduralTextureSystem = std::make_unique(m_world, - m_sceneMgr); + m_sceneMgr); m_proceduralTextureSystem->initialize(); // Setup ProceduralMaterial system m_proceduralMaterialSystem = std::make_unique(m_world, - m_sceneMgr); + m_sceneMgr); m_proceduralMaterialSystem->initialize(); // Setup ProceduralMesh system @@ -257,6 +275,34 @@ void EditorApp::setup() std::make_unique(m_world, m_sceneMgr); m_roomLayoutSystem->initialize(); + // Setup game systems + m_startupMenuSystem = + std::make_unique(m_world, m_sceneMgr, + this); + m_playerControllerSystem = + std::make_unique( + m_world, m_sceneMgr, this); + + if (m_gameMode == GameMode::Game) { + // Load startup menu scene configured in editor + SceneSerializer serializer(m_world, m_sceneMgr); + if (!serializer.loadFromFile("startup_menu.json", + m_uiSystem.get())) { + Ogre::LogManager::getSingleton().logMessage( + "Game mode: Failed to load startup_menu.json: " + + serializer.getLastError()); + } + } + + // Pre-load menu font before showing overlay + // (OGRE builds the atlas in createFontTexture() during show()) + if (m_startupMenuSystem) + m_startupMenuSystem->prepareFont(); + + // Now show the overlay — font atlas will be built with our font + if (m_imguiOverlay) + m_imguiOverlay->show(); + // Add default entities to UI cache for (auto &e : m_defaultEntities) { m_uiSystem->addEntity(e); @@ -264,13 +310,16 @@ void EditorApp::setup() // Create and register ImGui render listener m_imguiListener = std::make_unique( - m_imguiOverlay, m_uiSystem.get(), getRenderWindow()); + m_imguiOverlay, m_uiSystem.get(), getRenderWindow(), this); getRenderWindow()->addListener(m_imguiListener.get()); // Register input listeners addInputListener(this); addInputListener(getImGuiInputListener()); + // Game mode can be set externally before setup() is called + m_setupComplete = true; + } catch (const std::exception &e) { Ogre::LogManager::getSingleton().logMessage( "Setup failed: " + Ogre::String(e.what())); @@ -278,6 +327,71 @@ void EditorApp::setup() } } +void EditorApp::setGameMode(GameMode mode) +{ + if (m_setupComplete) { + Ogre::LogManager::getSingleton().logMessage( + "setGameMode ignored: cannot change mode after setup"); + return; + } + m_gameMode = mode; + if (m_gameMode == GameMode::Game) { + m_gamePlayState = GamePlayState::Menu; + if (m_uiSystem) + m_uiSystem->setEditorUIEnabled(false); + } else { + m_gamePlayState = GamePlayState::Menu; + if (m_uiSystem) + m_uiSystem->setEditorUIEnabled(true); + } +} + +void EditorApp::setGamePlayState(GamePlayState state) +{ + m_gamePlayState = state; + + // Grab/ungrab mouse based on gameplay state + if (m_gameMode == GameMode::Game) { + if (state == GamePlayState::Playing) { + setWindowGrab(true); + } else if (state == GamePlayState::Menu) { + setWindowGrab(false); + } + } +} + +void EditorApp::clearScene() +{ + // Destroy all entities with EditorMarkerComponent + std::vector entitiesToDelete; + m_world.query().each( + [&](flecs::entity e, EditorMarkerComponent) { + entitiesToDelete.push_back(e); + }); + for (auto &e : entitiesToDelete) { + if (e.is_alive()) { + e.destruct(); + } + } + if (m_uiSystem) { + m_uiSystem->clearEntityCache(); + } +} + +void EditorApp::startNewGame(const Ogre::String &scenePath) +{ + clearScene(); + SceneSerializer serializer(m_world, m_sceneMgr); + if (serializer.loadFromFile(scenePath, m_uiSystem.get())) { + m_gamePlayState = GamePlayState::Playing; + Ogre::LogManager::getSingleton().logMessage( + "Game started: loaded scene " + scenePath); + } else { + Ogre::LogManager::getSingleton().logMessage( + "Failed to load scene: " + serializer.getLastError()); + } +} + void EditorApp::setupECS() { // Register components @@ -322,6 +436,10 @@ void EditorApp::setupECS() // Register Character component m_world.component(); + // Register game components + m_world.component(); + m_world.component(); + // Register CellGrid/Town components CellGridModule::registerComponents(m_world); } @@ -442,9 +560,18 @@ void EditorApp::createAxes() bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt) { - // Update camera - if (m_camera) { - m_camera->update(evt.timeSinceLastFrame); + if (m_gameMode == GameMode::Editor) { + // Update camera + if (m_camera) { + m_camera->update(evt.timeSinceLastFrame); + } + } else if (m_gameMode == GameMode::Game) { + if (m_gamePlayState == GamePlayState::Playing) { + if (m_playerControllerSystem) { + m_playerControllerSystem->update( + evt.timeSinceLastFrame); + } + } } /* --- Visual mesh setup (must run before animation) --- */ @@ -498,12 +625,22 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt) m_proceduralMaterialSystem->update(); } + // Reset per-frame input state + m_gameInput.resetPerFrame(); + // Don't call base class - it crashes when iterating input listeners return true; } bool EditorApp::mouseMoved(const OgreBites::MouseMotionEvent &evt) { + if (m_gameMode == GameMode::Game) { + m_gameInput.mouseMoved = true; + m_gameInput.mouseDeltaX += evt.xrel; + m_gameInput.mouseDeltaY += evt.yrel; + return true; + } + // Skip if ImGui wants to capture mouse (for gizmo, we still want to process even if over UI) // But we need to update hover state if (m_camera && m_uiSystem) { @@ -526,6 +663,10 @@ bool EditorApp::mouseMoved(const OgreBites::MouseMotionEvent &evt) bool EditorApp::mousePressed(const OgreBites::MouseButtonEvent &evt) { + if (m_gameMode == GameMode::Game) { + return true; + } + // Get mouse ray for gizmo interaction FIRST (before ImGui check) // This allows clicking on 3D gizmo even when mouse is over empty UI areas if (m_camera && m_uiSystem) { @@ -552,6 +693,10 @@ bool EditorApp::mousePressed(const OgreBites::MouseButtonEvent &evt) bool EditorApp::mouseReleased(const OgreBites::MouseButtonEvent &evt) { + if (m_gameMode == GameMode::Game) { + return true; + } + // Handle gizmo mouse release (always process to end dragging) if (m_uiSystem) { if (m_uiSystem->onMouseReleased()) { @@ -571,6 +716,42 @@ bool EditorApp::keyPressed(const OgreBites::KeyboardEvent &evt) { m_currentModifiers = evt.keysym.mod; + if (m_gameMode == GameMode::Game) { + bool pressed = true; + switch (evt.keysym.sym) { + case 'w': + case 'W': + m_gameInput.w = pressed; + break; + case 's': + case 'S': + m_gameInput.s = pressed; + break; + case 'a': + case 'A': + m_gameInput.a = pressed; + break; + case 'd': + case 'D': + m_gameInput.d = pressed; + break; + case OgreBites::SDLK_LSHIFT: + m_gameInput.shift = pressed; + break; + case 'e': + case 'E': + m_gameInput.e = pressed; + m_gameInput.ePressed = true; + break; + case 'f': + case 'F': + m_gameInput.f = pressed; + m_gameInput.fPressed = true; + break; + } + return true; + } + // Forward to camera for FPS movement if (m_camera) { m_camera->handleKeyboard(evt); @@ -601,6 +782,40 @@ bool EditorApp::keyReleased(const OgreBites::KeyboardEvent &evt) { m_currentModifiers = evt.keysym.mod; + if (m_gameMode == GameMode::Game) { + bool pressed = false; + switch (evt.keysym.sym) { + case 'w': + case 'W': + m_gameInput.w = pressed; + break; + case 's': + case 'S': + m_gameInput.s = pressed; + break; + case 'a': + case 'A': + m_gameInput.a = pressed; + break; + case 'd': + case 'D': + m_gameInput.d = pressed; + break; + case OgreBites::SDLK_LSHIFT: + m_gameInput.shift = pressed; + break; + case 'e': + case 'E': + m_gameInput.e = pressed; + break; + case 'f': + case 'F': + m_gameInput.f = pressed; + break; + } + return true; + } + // Forward to camera for FPS movement if (m_camera) { m_camera->handleKeyboard(evt); @@ -632,4 +847,4 @@ void EditorApp::locateResources() Ogre::ResourceGroupManager::getSingleton().addResourceLocation( "./characters/female", "FileSystem", "Characters", false, true); OgreBites::ApplicationContext::locateResources(); -} \ No newline at end of file +} diff --git a/src/features/editScene/EditorApp.hpp b/src/features/editScene/EditorApp.hpp index a0a5743..0858e6a 100644 --- a/src/features/editScene/EditorApp.hpp +++ b/src/features/editScene/EditorApp.hpp @@ -26,6 +26,36 @@ class AnimationTreeSystem; class CharacterSystem; class CellGridSystem; class RoomLayoutSystem; +class StartupMenuSystem; +class PlayerControllerSystem; +class EditorApp; + +/** + * Shared input state for game mode + */ +struct GameInputState { + bool w = false; + bool a = false; + bool s = false; + bool d = false; + bool shift = false; + bool e = false; + bool f = false; + bool ePressed = false; + bool fPressed = false; + float mouseDeltaX = 0.0f; + float mouseDeltaY = 0.0f; + bool mouseMoved = false; + + void resetPerFrame() + { + mouseMoved = false; + mouseDeltaX = 0.0f; + mouseDeltaY = 0.0f; + ePressed = false; + fPressed = false; + } +}; /** * RenderTargetListener for ImGui frame management @@ -35,7 +65,8 @@ class ImGuiRenderListener : public Ogre::RenderTargetListener { public: ImGuiRenderListener(Ogre::ImGuiOverlay *imguiOverlay, EditorUISystem *uiSystem, - Ogre::RenderWindow *renderWindow); + Ogre::RenderWindow *renderWindow, + EditorApp *editorApp); void preViewportUpdate(const Ogre::RenderTargetViewportEvent &evt) override; @@ -46,6 +77,7 @@ private: Ogre::ImGuiOverlay *m_imguiOverlay; EditorUISystem *m_uiSystem; Ogre::RenderWindow *m_renderWindow; + EditorApp *m_editorApp; // Timer for delta time calculation Ogre::Timer m_timer; @@ -57,11 +89,14 @@ private: }; /** - * Main application class for the scene editor + * Main application class for the scene editor / game */ class EditorApp : public OgreBites::ApplicationContext, public OgreBites::InputListener { public: + enum class GameMode { Editor, Game }; + enum class GamePlayState { Menu, Playing, Paused }; + EditorApp(); virtual ~EditorApp(); @@ -86,6 +121,17 @@ public: void setupECS(); void createDefaultEntities(); + // Game mode management + void setGameMode(GameMode mode); + GameMode getGameMode() const { return m_gameMode; } + GamePlayState getGamePlayState() const { return m_gamePlayState; } + void setGamePlayState(GamePlayState state); + void startNewGame(const Ogre::String &scenePath); + void clearScene(); + + // Input access + GameInputState &getGameInputState() { return m_gameInput; } + // Getters flecs::entity getSelectedEntity() const; Ogre::SceneManager *getSceneManager() const @@ -96,6 +142,20 @@ public: { return &m_world; } + EditorCamera *getEditorCamera() const { return m_camera.get(); } + AnimationTreeSystem *getAnimationTreeSystem() const + { + return m_animationTreeSystem.get(); + } + CharacterSlotSystem *getCharacterSlotSystem() const + { + return m_characterSlotSystem.get(); + } + StartupMenuSystem *getStartupMenuSystem() const + { + return m_startupMenuSystem.get(); + } + Ogre::ImGuiOverlay *getImGuiOverlay() const { return m_imguiOverlay; } private: // Ogre objects @@ -125,8 +185,16 @@ private: std::unique_ptr m_cellGridSystem; std::unique_ptr m_roomLayoutSystem; + // Game systems + std::unique_ptr m_startupMenuSystem; + std::unique_ptr m_playerControllerSystem; + // State uint16_t m_currentModifiers; + GameMode m_gameMode = GameMode::Editor; + GamePlayState m_gamePlayState = GamePlayState::Menu; + GameInputState m_gameInput; + bool m_setupComplete = false; }; #endif // EDITSCENE_EDITORAPP_HPP diff --git a/src/features/editScene/components/PlayerController.hpp b/src/features/editScene/components/PlayerController.hpp new file mode 100644 index 0000000..2e81eee --- /dev/null +++ b/src/features/editScene/components/PlayerController.hpp @@ -0,0 +1,26 @@ +#ifndef EDITSCENE_PLAYERCONTROLLER_HPP +#define EDITSCENE_PLAYERCONTROLLER_HPP +#pragma once + +#include + +/** + * Player controller component. + * Only active in game mode. Editable in editor mode. + */ +struct PlayerControllerComponent { + enum CameraMode { TPS = 0, FPS = 1 }; + int cameraMode = TPS; + Ogre::String targetCharacterName = ""; + Ogre::String fpsBoneName = "Head"; + float tpsDistance = 3.0f; + float tpsHeight = 2.0f; + float mouseSensitivity = 0.2f; + /* Animation state machine configuration */ + Ogre::String locomotionStateMachine = "locomotion"; + Ogre::String idleState = "idle"; + Ogre::String walkState = "walking"; + Ogre::String runState = "running"; +}; + +#endif // EDITSCENE_PLAYERCONTROLLER_HPP diff --git a/src/features/editScene/components/PlayerControllerModule.cpp b/src/features/editScene/components/PlayerControllerModule.cpp new file mode 100644 index 0000000..7e1ef64 --- /dev/null +++ b/src/features/editScene/components/PlayerControllerModule.cpp @@ -0,0 +1,24 @@ +#include "../ui/ComponentRegistration.hpp" +#include "../ui/PlayerControllerEditor.hpp" +#include "PlayerController.hpp" + +REGISTER_COMPONENT_GROUP("Player Controller", "Game", + PlayerControllerComponent, PlayerControllerEditor) +{ + registry.registerComponent( + PlayerControllerComponent_name, + PlayerControllerComponent_group, + std::make_unique(), + // Adder + [](flecs::entity e) { + if (!e.has()) { + e.set({}); + } + }, + // Remover + [](flecs::entity e) { + if (e.has()) { + e.remove(); + } + }); +} diff --git a/src/features/editScene/components/StartupMenu.hpp b/src/features/editScene/components/StartupMenu.hpp new file mode 100644 index 0000000..937f8b1 --- /dev/null +++ b/src/features/editScene/components/StartupMenu.hpp @@ -0,0 +1,20 @@ +#ifndef EDITSCENE_STARTUPMENU_HPP +#define EDITSCENE_STARTUPMENU_HPP +#pragma once + +#include + +/** + * Configurable startup menu component. + * Only active in game mode. Editable in editor mode. + */ +struct StartupMenuComponent { + Ogre::String fontName = "Kenney Bold.ttf"; + float fontSize = 36.0f; + Ogre::String newGameScene = "scene.json"; + bool showLoadGame = true; + bool showOptions = true; + bool showQuit = true; +}; + +#endif // EDITSCENE_STARTUPMENU_HPP diff --git a/src/features/editScene/components/StartupMenuModule.cpp b/src/features/editScene/components/StartupMenuModule.cpp new file mode 100644 index 0000000..9e355b4 --- /dev/null +++ b/src/features/editScene/components/StartupMenuModule.cpp @@ -0,0 +1,23 @@ +#include "../ui/ComponentRegistration.hpp" +#include "../ui/StartupMenuEditor.hpp" +#include "StartupMenu.hpp" + +REGISTER_COMPONENT_GROUP("Startup Menu", "Game", StartupMenuComponent, + StartupMenuEditor) +{ + registry.registerComponent( + StartupMenuComponent_name, StartupMenuComponent_group, + std::make_unique(), + // Adder + [](flecs::entity e) { + if (!e.has()) { + e.set({}); + } + }, + // Remover + [](flecs::entity e) { + if (e.has()) { + e.remove(); + } + }); +} diff --git a/src/features/editScene/main.cpp b/src/features/editScene/main.cpp index 8df4d5b..b162e2c 100644 --- a/src/features/editScene/main.cpp +++ b/src/features/editScene/main.cpp @@ -6,17 +6,34 @@ int main(int argc, char *argv[]) { try { EditorApp app; + + // Parse command line arguments + bool gameMode = false; + Ogre::String sceneFile; + for (int i = 1; i < argc; i++) { + Ogre::String arg = argv[i]; + if (arg == "--game") { + gameMode = true; + } else if (arg.length() > 0 && arg[0] != '-') { + sceneFile = arg; + } + } + + if (gameMode) { + app.setGameMode(EditorApp::GameMode::Game); + } + app.initApp(); - - // Auto-load scene if provided as argument - if (argc > 1) { - std::cout << "Auto-loading scene: " << argv[1] << std::endl; + + // Auto-load scene if provided as argument (editor mode only) + if (!sceneFile.empty() && app.getGameMode() == EditorApp::GameMode::Editor) { + std::cout << "Auto-loading scene: " << sceneFile << std::endl; SceneSerializer serializer(*app.getWorld(), app.getSceneManager()); - if (!serializer.loadFromFile(argv[1])) { + if (!serializer.loadFromFile(sceneFile, nullptr)) { std::cerr << "Failed to load scene: " << serializer.getLastError() << std::endl; } } - + app.getRoot()->startRendering(); app.closeApp(); } catch (const std::exception &e) { diff --git a/src/features/editScene/resources.cfg b/src/features/editScene/resources.cfg index 03be0fe..20cb5c5 100644 --- a/src/features/editScene/resources.cfg +++ b/src/features/editScene/resources.cfg @@ -10,6 +10,7 @@ FileSystem=resources/buildings FileSystem=resources/buildings/parts/pier FileSystem=resources/buildings/parts/furniture FileSystem=resources/vehicles +FileSystem=resources/fonts [Popular] FileSystem=resources/materials/programs diff --git a/src/features/editScene/systems/CharacterSlotSystem.cpp b/src/features/editScene/systems/CharacterSlotSystem.cpp index 6a6c3c4..10b2924 100644 --- a/src/features/editScene/systems/CharacterSlotSystem.cpp +++ b/src/features/editScene/systems/CharacterSlotSystem.cpp @@ -281,3 +281,25 @@ void CharacterSlotSystem::destroyCharacterParts(flecs::entity e) it->second.parts.clear(); m_entities.erase(it); } + +Ogre::Entity *CharacterSlotSystem::getSlotEntity(flecs::entity e, + const Ogre::String &slot) +{ + auto it = m_entities.find(e.id()); + if (it == m_entities.end()) + return nullptr; + auto pit = it->second.parts.find(slot); + if (pit == it->second.parts.end()) + return nullptr; + return pit->second; +} + +void CharacterSlotSystem::setSlotVisible(flecs::entity e, + const Ogre::String &slot, + bool visible) +{ + Ogre::Entity *ent = getSlotEntity(e, slot); + if (ent) { + ent->setVisible(visible); + } +} diff --git a/src/features/editScene/systems/CharacterSlotSystem.hpp b/src/features/editScene/systems/CharacterSlotSystem.hpp index 6b98848..1b7f216 100644 --- a/src/features/editScene/systems/CharacterSlotSystem.hpp +++ b/src/features/editScene/systems/CharacterSlotSystem.hpp @@ -35,6 +35,12 @@ public: const Ogre::String &sex, const Ogre::String &slot); + /* Slot visibility helpers */ + Ogre::Entity *getSlotEntity(flecs::entity e, + const Ogre::String &slot); + void setSlotVisible(flecs::entity e, const Ogre::String &slot, + bool visible); + private: static bool s_catalogLoaded; static nlohmann::json s_bodyParts; diff --git a/src/features/editScene/systems/EditorUISystem.cpp b/src/features/editScene/systems/EditorUISystem.cpp index bd18ec7..14d9ef0 100644 --- a/src/features/editScene/systems/EditorUISystem.cpp +++ b/src/features/editScene/systems/EditorUISystem.cpp @@ -19,6 +19,8 @@ #include "../components/CharacterSlots.hpp" #include "../components/Character.hpp" #include "../components/AnimationTree.hpp" +#include "../components/StartupMenu.hpp" +#include "../components/PlayerController.hpp" #include "../components/CellGrid.hpp" #include "../ui/TransformEditor.hpp" #include "../ui/RenderableEditor.hpp" @@ -182,6 +184,12 @@ void EditorUISystem::registerModularComponents() void EditorUISystem::update(float deltaTime) { + if (!m_editorUIEnabled) { + // Only render FPS overlay when editor UI is disabled + renderFPSOverlay(deltaTime); + return; + } + // Render UI windows // Note: NewFrame() is called by ImGuiRenderListener::preViewportUpdate renderHierarchyWindow(); @@ -651,6 +659,20 @@ void EditorUISystem::renderComponentList(flecs::entity entity) componentCount++; } + // Render StartupMenu if present + if (entity.has()) { + auto &sm = entity.get_mut(); + m_componentRegistry.render(entity, sm); + componentCount++; + } + + // Render PlayerController if present + if (entity.has()) { + auto &pc = entity.get_mut(); + m_componentRegistry.render(entity, pc); + componentCount++; + } + // Render CellGrid if present if (entity.has()) { auto &grid = entity.get_mut(); diff --git a/src/features/editScene/systems/EditorUISystem.hpp b/src/features/editScene/systems/EditorUISystem.hpp index 310bc1d..b36a6bc 100644 --- a/src/features/editScene/systems/EditorUISystem.hpp +++ b/src/features/editScene/systems/EditorUISystem.hpp @@ -52,6 +52,14 @@ public: */ void addEntity(flecs::entity entity) { m_allEntities.push_back(entity); } + /** + * Clear entity cache and deselect + */ + void clearEntityCache() { + m_allEntities.clear(); + setSelectedEntity(flecs::entity::null()); + } + /** * Get the currently selected entity */ @@ -88,6 +96,11 @@ public: * Set physics system for debug toggle */ void setPhysicsSystem(EditorPhysicsSystem* physics) { m_physicsSystem = physics; } + + /** + * Enable/disable editor UI rendering + */ + void setEditorUIEnabled(bool enabled) { m_editorUIEnabled = enabled; } /** * Set last frame batch count (called from render listener) @@ -167,6 +180,9 @@ private: // Physics system reference (for debug toggle) EditorPhysicsSystem* m_physicsSystem = nullptr; + // Editor UI enabled flag + bool m_editorUIEnabled = true; + // Render window reference (for viewport access) Ogre::RenderWindow* m_renderWindow = nullptr; diff --git a/src/features/editScene/systems/PlayerControllerSystem.cpp b/src/features/editScene/systems/PlayerControllerSystem.cpp new file mode 100644 index 0000000..20e895d --- /dev/null +++ b/src/features/editScene/systems/PlayerControllerSystem.cpp @@ -0,0 +1,382 @@ +#include "PlayerControllerSystem.hpp" +#include "../EditorApp.hpp" +#include "../components/PlayerController.hpp" +#include "../components/Character.hpp" +#include "../components/Transform.hpp" +#include "../components/EntityName.hpp" +#include "AnimationTreeSystem.hpp" +#include "CharacterSlotSystem.hpp" +#include "../camera/EditorCamera.hpp" +#include +#include +#include +#include +#include +#include + +PlayerControllerSystem::PlayerControllerSystem(flecs::world &world, + Ogre::SceneManager *sceneMgr, + EditorApp *editorApp) + : m_world(world) + , m_sceneMgr(sceneMgr) + , m_editorApp(editorApp) +{ +} + +PlayerControllerSystem::~PlayerControllerSystem() +{ + for (auto &pair : m_states) { + shutdownController(pair.second); + } + m_states.clear(); +} + +void PlayerControllerSystem::shutdownController(ControllerState &state) +{ + if (state.pivotNode) { + m_sceneMgr->destroySceneNode(state.pivotNode); + state.pivotNode = nullptr; + } + if (state.goalNode) { + m_sceneMgr->destroySceneNode(state.goalNode); + state.goalNode = nullptr; + } + state.initialized = false; +} + +flecs::entity PlayerControllerSystem::findTargetEntity(const Ogre::String &name) +{ + if (name.empty()) + return flecs::entity::null(); + + flecs::entity result = flecs::entity::null(); + m_world.query().each( + [&](flecs::entity e, EntityNameComponent &en) { + if (result.is_alive()) + return; + if (en.name == name) + result = e; + }); + return result; +} + +void PlayerControllerSystem::initController(flecs::entity controllerEntity, + PlayerControllerComponent &pc, + ControllerState &state) +{ + (void)controllerEntity; + state.targetEntity = findTargetEntity(pc.targetCharacterName); + if (!state.targetEntity.is_alive()) + return; + + if (!state.pivotNode) { + state.pivotNode = + m_sceneMgr->getRootSceneNode()->createChildSceneNode(); + } + if (!state.goalNode) { + state.goalNode = + m_sceneMgr->getRootSceneNode()->createChildSceneNode(); + } + + state.initialized = true; + state.faceHidden = false; +} + +void PlayerControllerSystem::update(float deltaTime) +{ + if (!m_editorApp || + m_editorApp->getGameMode() != EditorApp::GameMode::Game || + m_editorApp->getGamePlayState() != EditorApp::GamePlayState::Playing) + return; + + m_world.query().each( + [&](flecs::entity e, PlayerControllerComponent &pc) { + auto it = m_states.find(e.id()); + if (it == m_states.end()) { + ControllerState newState; + initController(e, pc, newState); + it = m_states.insert({ e.id(), newState }).first; + } + + ControllerState &state = it->second; + + // Re-resolve target if name changed or entity invalid + if (!state.targetEntity.is_alive()) { + initController(e, pc, state); + } + if (!state.targetEntity.is_alive()) + return; + + if (!state.targetEntity.has()) + return; + + auto &transform = + state.targetEntity.get(); + if (!transform.node) + return; + + Ogre::Vector3 charPos = + transform.node->_getDerivedPosition(); + + // Update camera + if (pc.cameraMode == PlayerControllerComponent::TPS) { + updateTPSCamera(pc, state, charPos, deltaTime); + } else { + updateFPSCamera(pc, state, charPos); + } + + // Update locomotion + updateLocomotion(pc, state, deltaTime); + }); +} + +void PlayerControllerSystem::updateTPSCamera(PlayerControllerComponent &pc, + ControllerState &state, + const Ogre::Vector3 &charPos, + float deltaTime) +{ + if (!state.pivotNode || !state.goalNode) + return; + + EditorCamera *editorCam = m_editorApp->getEditorCamera(); + if (!editorCam) + return; + Ogre::Camera *cam = editorCam->getCamera(); + if (!cam) + return; + Ogre::SceneNode *camNode = cam->getParentSceneNode(); + if (!camNode) + return; + + // Restore face visibility when switching from FPS + if (state.faceHidden) { + CharacterSlotSystem *css = + m_editorApp->getCharacterSlotSystem(); + if (css) { + css->setSlotVisible(state.targetEntity, "face", + true); + } + state.faceHidden = false; + } + + // Read mouse input + GameInputState &input = m_editorApp->getGameInputState(); + if (input.mouseMoved) { + state.yaw -= input.mouseDeltaX * pc.mouseSensitivity; + state.pitch -= input.mouseDeltaY * pc.mouseSensitivity; + // Clamp pitch + if (state.pitch > 25.0f) + state.pitch = 25.0f; + if (state.pitch < -60.0f) + state.pitch = -60.0f; + } + + // Position pivot at character shoulder height + Ogre::Vector3 pivotPos = charPos + Ogre::Vector3(0, pc.tpsHeight, 0); + state.pivotNode->setPosition(pivotPos); + + // Compute goal position based on yaw/pitch/distance + Ogre::Quaternion yawRot(Ogre::Degree(state.yaw), + Ogre::Vector3::UNIT_Y); + Ogre::Quaternion pitchRot(Ogre::Degree(state.pitch), + Ogre::Vector3::UNIT_X); + Ogre::Vector3 offset = yawRot * pitchRot * + Ogre::Vector3(0, 0, pc.tpsDistance); + Ogre::Vector3 goalPos = pivotPos + offset; + + state.goalNode->setPosition(goalPos); + + // Smoothly interpolate camera to goal + Ogre::Vector3 currentPos = camNode->getPosition(); + Ogre::Vector3 newPos = Ogre::Math::lerp(currentPos, goalPos, + deltaTime * 9.0f); + camNode->setPosition(newPos); + camNode->lookAt(pivotPos, Ogre::Node::TS_WORLD); +} + +void PlayerControllerSystem::updateFPSCamera(PlayerControllerComponent &pc, + ControllerState &state, + const Ogre::Vector3 &charPos) +{ + (void)charPos; + + EditorCamera *editorCam = m_editorApp->getEditorCamera(); + if (!editorCam) + return; + Ogre::Camera *cam = editorCam->getCamera(); + if (!cam) + return; + Ogre::SceneNode *camNode = cam->getParentSceneNode(); + if (!camNode) + return; + + // Find animated entity for bone access + AnimationTreeSystem *ats = + m_editorApp->getAnimationTreeSystem(); + if (!ats) + return; + + Ogre::Entity *ent = ats->findAnimatedEntity(state.targetEntity); + if (!ent || !ent->hasSkeleton()) { + // Fallback to TPS if no skeleton + pc.cameraMode = PlayerControllerComponent::TPS; + return; + } + + Ogre::SkeletonInstance *skel = ent->getSkeleton(); + Ogre::Bone *bone = nullptr; + try { + bone = skel->getBone(pc.fpsBoneName); + } catch (...) { + // Try common alternatives + const char *alternatives[] = { "Head", "head", "Neck", "neck", + "Camera", "camera" }; + for (const char *name : alternatives) { + try { + bone = skel->getBone(name); + break; + } catch (...) { + } + } + } + + if (!bone) { + Ogre::LogManager::getSingleton().logMessage( + "PlayerControllerSystem: Bone " + pc.fpsBoneName + + " not found, falling back to TPS"); + pc.cameraMode = PlayerControllerComponent::TPS; + return; + } + + // Hide face slot + if (!state.faceHidden) { + CharacterSlotSystem *css = + m_editorApp->getCharacterSlotSystem(); + if (css) { + css->setSlotVisible(state.targetEntity, "face", + false); + } + state.faceHidden = true; + } + + // Get character scene node + auto &transform = + state.targetEntity.get(); + Ogre::SceneNode *charNode = transform.node; + + // Compute bone world transform + Ogre::Vector3 boneWorldPos = + charNode->_getDerivedOrientation() * + bone->_getDerivedPosition() + + charNode->_getDerivedPosition(); + Ogre::Quaternion boneWorldRot = + charNode->_getDerivedOrientation() * + bone->_getDerivedOrientation(); + + // Offset slightly forward + Ogre::Vector3 offset = boneWorldRot * Ogre::Vector3(0, 0, 0.15f); + + camNode->setPosition(boneWorldPos + offset); + + // Apply mouse look + GameInputState &input = m_editorApp->getGameInputState(); + if (input.mouseMoved) { + state.yaw -= input.mouseDeltaX * pc.mouseSensitivity; + state.pitch -= input.mouseDeltaY * pc.mouseSensitivity; + if (state.pitch > 89.0f) + state.pitch = 89.0f; + if (state.pitch < -89.0f) + state.pitch = -89.0f; + } + + Ogre::Quaternion yawRot(Ogre::Degree(state.yaw), + Ogre::Vector3::UNIT_Y); + Ogre::Quaternion pitchRot(Ogre::Degree(state.pitch), + Ogre::Vector3::UNIT_X); + camNode->setOrientation(yawRot * pitchRot); +} + +void PlayerControllerSystem::updateLocomotion(PlayerControllerComponent &pc, + ControllerState &state, + float deltaTime) +{ + (void)deltaTime; + if (!state.targetEntity.has()) + return; + + GameInputState &input = m_editorApp->getGameInputState(); + auto &cc = state.targetEntity.get_mut(); + + // Get camera yaw for relative movement + Ogre::Quaternion yawRot(Ogre::Degree(state.yaw), + Ogre::Vector3::UNIT_Y); + Ogre::Vector3 forward = yawRot * Ogre::Vector3::NEGATIVE_UNIT_Z; + Ogre::Vector3 right = yawRot * Ogre::Vector3::UNIT_X; + + // Flatten to horizontal plane + forward.y = 0; + right.y = 0; + if (forward.squaredLength() > 0.0001f) + forward.normalise(); + if (right.squaredLength() > 0.0001f) + right.normalise(); + + Ogre::Vector3 desiredVel = Ogre::Vector3::ZERO; + if (input.w) + desiredVel += forward; + if (input.s) + desiredVel -= forward; + if (input.a) + desiredVel -= right; + if (input.d) + desiredVel += right; + + bool isMoving = desiredVel.squaredLength() > 0.0001f; + float speed = input.shift ? 5.0f : 2.5f; + + if (isMoving) { + desiredVel.normalise(); + cc.linearVelocity = desiredVel * speed; + + // Rotate character to face movement direction + auto &transform = + state.targetEntity.get_mut(); + if (transform.node) { + Ogre::Vector3 flatForward = desiredVel; + flatForward.y = 0; + if (flatForward.squaredLength() > 0.0001f) { + flatForward.normalise(); + Ogre::Quaternion targetRot = Ogre::Vector3::NEGATIVE_UNIT_Z.getRotationTo( + flatForward); + Ogre::Quaternion currentRot = transform.node->getOrientation(); + transform.node->setOrientation( + Ogre::Quaternion::Slerp(deltaTime * 10.0f, + currentRot, + targetRot, + true)); + } + } + } else { + cc.linearVelocity = Ogre::Vector3::ZERO; + } + + // Update animation state + AnimationTreeSystem *ats = + m_editorApp->getAnimationTreeSystem(); + if (!ats) + return; + + Ogre::String animState; + if (!isMoving) { + animState = pc.idleState; + } else if (input.shift) { + animState = pc.runState; + } else { + animState = pc.walkState; + } + + if (!animState.empty()) { + ats->setState(state.targetEntity, + pc.locomotionStateMachine, animState); + } +} diff --git a/src/features/editScene/systems/PlayerControllerSystem.hpp b/src/features/editScene/systems/PlayerControllerSystem.hpp new file mode 100644 index 0000000..86ddc9a --- /dev/null +++ b/src/features/editScene/systems/PlayerControllerSystem.hpp @@ -0,0 +1,61 @@ +#ifndef EDITSCENE_PLAYERCONTROLLERSYSTEM_HPP +#define EDITSCENE_PLAYERCONTROLLERSYSTEM_HPP +#pragma once + +#include +#include +#include + +#include "../components/PlayerController.hpp" + +class EditorApp; +class AnimationTreeSystem; +class CharacterSlotSystem; + +/** + * System that handles player input, camera control (FPS/TPS), + * character locomotion, and animation state setting in game mode. + * Only active when EditorApp is in GameMode::Game and GamePlayState::Playing. + */ +class PlayerControllerSystem { +public: + PlayerControllerSystem(flecs::world &world, + Ogre::SceneManager *sceneMgr, + EditorApp *editorApp); + ~PlayerControllerSystem(); + + void update(float deltaTime); + +private: + struct ControllerState { + flecs::entity targetEntity = flecs::entity::null(); + float yaw = 0.0f; + float pitch = 0.0f; + Ogre::SceneNode *pivotNode = nullptr; + Ogre::SceneNode *goalNode = nullptr; + bool initialized = false; + bool faceHidden = false; + }; + + void initController(flecs::entity controllerEntity, + PlayerControllerComponent &pc, + ControllerState &state); + void shutdownController(ControllerState &state); + flecs::entity findTargetEntity(const Ogre::String &name); + void updateTPSCamera(PlayerControllerComponent &pc, + ControllerState &state, + const Ogre::Vector3 &charPos, float deltaTime); + void updateFPSCamera(PlayerControllerComponent &pc, + ControllerState &state, + const Ogre::Vector3 &charPos); + void updateLocomotion(PlayerControllerComponent &pc, + ControllerState &state, float deltaTime); + + flecs::world &m_world; + Ogre::SceneManager *m_sceneMgr; + EditorApp *m_editorApp; + + std::unordered_map m_states; +}; + +#endif // EDITSCENE_PLAYERCONTROLLERSYSTEM_HPP diff --git a/src/features/editScene/systems/SceneSerializer.cpp b/src/features/editScene/systems/SceneSerializer.cpp index 8d9b804..fc9182b 100644 --- a/src/features/editScene/systems/SceneSerializer.cpp +++ b/src/features/editScene/systems/SceneSerializer.cpp @@ -18,6 +18,8 @@ #include "../components/Character.hpp" #include "../components/CharacterSlots.hpp" #include "../components/AnimationTree.hpp" +#include "../components/StartupMenu.hpp" +#include "../components/PlayerController.hpp" #include "../components/CellGrid.hpp" #include "../components/GeneratedPhysicsTag.hpp" #include "EditorUISystem.hpp" @@ -192,6 +194,14 @@ nlohmann::json SceneSerializer::serializeEntity(flecs::entity entity) if (entity.has()) { json["animationTree"] = serializeAnimationTree(entity); } + + if (entity.has()) { + json["startupMenu"] = serializeStartupMenu(entity); + } + + if (entity.has()) { + json["playerController"] = serializePlayerController(entity); + } // CellGrid/Town components if (entity.has()) { @@ -321,6 +331,14 @@ void SceneSerializer::deserializeEntity(const nlohmann::json& json, flecs::entit deserializeAnimationTree(entity, json["animationTree"]); } + if (json.contains("startupMenu")) { + deserializeStartupMenu(entity, json["startupMenu"]); + } + + if (json.contains("playerController")) { + deserializePlayerController(entity, json["playerController"]); + } + if (json.contains("triangleBuffer")) { deserializeTriangleBuffer(entity, json["triangleBuffer"]); } @@ -1960,3 +1978,67 @@ void SceneSerializer::deserializeClearArea(flecs::entity entity, const nlohmann: clearArea.markDirty(); entity.set(clearArea); } + + +nlohmann::json SceneSerializer::serializeStartupMenu(flecs::entity entity) +{ + auto &sm = entity.get(); + nlohmann::json json; + json["fontName"] = sm.fontName; + json["fontSize"] = sm.fontSize; + json["newGameScene"] = sm.newGameScene; + json["showLoadGame"] = sm.showLoadGame; + json["showOptions"] = sm.showOptions; + json["showQuit"] = sm.showQuit; + return json; +} + +nlohmann::json SceneSerializer::serializePlayerController(flecs::entity entity) +{ + auto &pc = entity.get(); + nlohmann::json json; + json["cameraMode"] = pc.cameraMode; + json["targetCharacterName"] = pc.targetCharacterName; + json["fpsBoneName"] = pc.fpsBoneName; + json["tpsDistance"] = pc.tpsDistance; + json["tpsHeight"] = pc.tpsHeight; + json["mouseSensitivity"] = pc.mouseSensitivity; + json["locomotionStateMachine"] = pc.locomotionStateMachine; + json["idleState"] = pc.idleState; + json["walkState"] = pc.walkState; + json["runState"] = pc.runState; + return json; +} + +void SceneSerializer::deserializeStartupMenu(flecs::entity entity, + const nlohmann::json &json) +{ + StartupMenuComponent sm; + sm.fontName = json.value("fontName", sm.fontName); + sm.fontSize = json.value("fontSize", sm.fontSize); + sm.newGameScene = json.value("newGameScene", sm.newGameScene); + sm.showLoadGame = json.value("showLoadGame", sm.showLoadGame); + sm.showOptions = json.value("showOptions", sm.showOptions); + sm.showQuit = json.value("showQuit", sm.showQuit); + entity.set(sm); +} + +void SceneSerializer::deserializePlayerController(flecs::entity entity, + const nlohmann::json &json) +{ + PlayerControllerComponent pc; + pc.cameraMode = json.value("cameraMode", pc.cameraMode); + pc.targetCharacterName = json.value("targetCharacterName", + pc.targetCharacterName); + pc.fpsBoneName = json.value("fpsBoneName", pc.fpsBoneName); + pc.tpsDistance = json.value("tpsDistance", pc.tpsDistance); + pc.tpsHeight = json.value("tpsHeight", pc.tpsHeight); + pc.mouseSensitivity = json.value("mouseSensitivity", + pc.mouseSensitivity); + pc.locomotionStateMachine = json.value("locomotionStateMachine", + pc.locomotionStateMachine); + pc.idleState = json.value("idleState", pc.idleState); + pc.walkState = json.value("walkState", pc.walkState); + pc.runState = json.value("runState", pc.runState); + entity.set(pc); +} diff --git a/src/features/editScene/systems/SceneSerializer.hpp b/src/features/editScene/systems/SceneSerializer.hpp index 9134e44..eafa7e3 100644 --- a/src/features/editScene/systems/SceneSerializer.hpp +++ b/src/features/editScene/systems/SceneSerializer.hpp @@ -57,6 +57,8 @@ private: nlohmann::json serializeCharacter(flecs::entity entity); nlohmann::json serializeCharacterSlots(flecs::entity entity); nlohmann::json serializeAnimationTree(flecs::entity entity); + nlohmann::json serializeStartupMenu(flecs::entity entity); + nlohmann::json serializePlayerController(flecs::entity entity); // CellGrid/Town component serialization nlohmann::json serializeCellGrid(flecs::entity entity); @@ -87,6 +89,8 @@ private: void deserializeCharacter(flecs::entity entity, const nlohmann::json& json); void deserializeCharacterSlots(flecs::entity entity, const nlohmann::json& json); void deserializeAnimationTree(flecs::entity entity, const nlohmann::json& json); + void deserializeStartupMenu(flecs::entity entity, const nlohmann::json& json); + void deserializePlayerController(flecs::entity entity, const nlohmann::json& json); // CellGrid/Town component deserialization void deserializeCellGrid(flecs::entity entity, const nlohmann::json& json); diff --git a/src/features/editScene/systems/StartupMenuSystem.cpp b/src/features/editScene/systems/StartupMenuSystem.cpp new file mode 100644 index 0000000..a93d980 --- /dev/null +++ b/src/features/editScene/systems/StartupMenuSystem.cpp @@ -0,0 +1,217 @@ +#include "StartupMenuSystem.hpp" +#include "../EditorApp.hpp" +#include "../components/StartupMenu.hpp" +#include +#include +#include +#include +#include + +StartupMenuSystem::StartupMenuSystem(flecs::world &world, + Ogre::SceneManager *sceneMgr, + EditorApp *editorApp) + : m_world(world) + , m_sceneMgr(sceneMgr) + , m_editorApp(editorApp) +{ +} + +StartupMenuSystem::~StartupMenuSystem() = default; + +void StartupMenuSystem::ensureFontLoaded(const Ogre::String &fontName, + float fontSize) +{ + if (m_fontLoaded && m_currentFontName == fontName && + m_currentFontSize == fontSize) + return; + + Ogre::ImGuiOverlay *overlay = m_editorApp->getImGuiOverlay(); + if (!overlay) + return; + + // Try to load via Ogre font manager + Ogre::FontPtr font; + try { + if (Ogre::FontManager::getSingleton().resourceExists( + "StartupMenuFont", "General")) { + Ogre::FontManager::getSingleton().remove( + "StartupMenuFont", "General"); + } + font = Ogre::FontManager::getSingleton().create( + "StartupMenuFont", "General"); + font->setType(Ogre::FontType::FT_TRUETYPE); + font->setSource(fontName); + font->setTrueTypeSize(fontSize); + font->setTrueTypeResolution(75); + font->addCodePointRange(Ogre::Font::CodePointRange(32, 255)); + font->load(); + } catch (...) { + Ogre::LogManager::getSingleton().logMessage( + "StartupMenuSystem: Failed to load font " + fontName); + m_menuFont = nullptr; + m_fontLoaded = false; + return; + } + + m_menuFont = overlay->addFont("StartupMenuFont", "General"); + m_currentFontName = fontName; + m_currentFontSize = fontSize; + m_fontLoaded = true; +} + +void StartupMenuSystem::prepareFont() +{ + // Must be called BEFORE Ogre::ImGuiOverlay::show() so that the font + // is added to the atlas before OGRE builds it in createFontTexture(). + if (!m_editorApp) + return; + + // Find an entity with StartupMenuComponent + flecs::entity menuEntity = flecs::entity::null(); + m_world.query().each( + [&](flecs::entity e, StartupMenuComponent &) { + if (!menuEntity.is_alive()) + menuEntity = e; + }); + + if (menuEntity.is_alive()) { + auto &sm = menuEntity.get_mut(); + ensureFontLoaded(sm.fontName, sm.fontSize); + } +} + +void StartupMenuSystem::update(float deltaTime) +{ + (void)deltaTime; + + if (!m_editorApp || + m_editorApp->getGamePlayState() != EditorApp::GamePlayState::Menu) + return; + + // Find an entity with StartupMenuComponent + flecs::entity menuEntity = flecs::entity::null(); + m_world.query().each( + [&](flecs::entity e, StartupMenuComponent &) { + if (!menuEntity.is_alive()) + menuEntity = e; + }); + + if (!menuEntity.is_alive()) { + // No startup menu entity configured + renderMissingMenuError(); + return; + } + + auto &sm = menuEntity.get_mut(); + renderMenu(sm); +} + +void StartupMenuSystem::renderMenu(StartupMenuComponent &sm) +{ + + ImVec2 size = ImGui::GetMainViewport()->Size; + ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(size.x, size.y), ImGuiCond_Always); + + ImVec4 solidColor = ImVec4(0.0f, 0.0f, 0.0f, 1.0f); + ImGui::PushStyleColor(ImGuiCol_WindowBg, solidColor); + + ImGui::Begin("StartupMenu", nullptr, + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoDecoration | + ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoCollapse | + ImGuiWindowFlags_NoFocusOnAppearing | + ImGuiWindowFlags_NoInputs); + + if (m_menuFont) + ImGui::PushFont(m_menuFont); + + struct ButtonData { + const char *label; + std::function action; + }; + + std::vector buttons; + buttons.push_back({ "NEW GAME", [&]() { + m_editorApp->startNewGame(sm.newGameScene); + } }); + + if (sm.showLoadGame) + buttons.push_back({ "LOAD GAME", [&]() { + Ogre::LogManager::getSingleton().logMessage( + "Load game not implemented"); + } }); + + if (sm.showOptions) + buttons.push_back({ "OPTIONS", [&]() { + Ogre::LogManager::getSingleton().logMessage( + "Options not implemented"); + } }); + + if (sm.showQuit) + buttons.push_back({ "QUIT", [&]() { + Ogre::Root::getSingleton().queueEndRendering(); + } }); + + // Calculate button dimensions + float buttonWidth = 0.0f; + float buttonsHeight = 0.0f; + float extraPixels = 20.0f; + for (const auto &b : buttons) { + ImVec2 textSize = ImGui::CalcTextSize(b.label); + float bwidth = textSize.x + + (ImGui::GetStyle().FramePadding.x * 2.0f) + + extraPixels; + float bheight = textSize.y + + (ImGui::GetStyle().FramePadding.y * 2.0f); + if (buttonWidth < bwidth) + buttonWidth = bwidth; + buttonsHeight += bheight + + ImGui::GetStyle().ItemSpacing.y; + } + if (!buttons.empty()) + buttonsHeight -= ImGui::GetStyle().ItemSpacing.y; + + ImGui::SetCursorPosY((size.y - buttonsHeight) * 0.5f); + for (const auto &b : buttons) { + ImGui::SetCursorPosX((size.x - buttonWidth) * 0.5f); + if (ImGui::Button(b.label, ImVec2(buttonWidth, 0))) + b.action(); + } + + if (m_menuFont) + ImGui::PopFont(); + + ImGui::End(); + ImGui::PopStyleColor(); +} + +void StartupMenuSystem::renderMissingMenuError() +{ + ImVec2 size = ImGui::GetMainViewport()->Size; + ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(size.x, size.y), ImGuiCond_Always); + + ImVec4 solidColor = ImVec4(0.0f, 0.0f, 0.0f, 1.0f); + ImGui::PushStyleColor(ImGuiCol_WindowBg, solidColor); + + ImGui::Begin("StartupMenuError", nullptr, + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoDecoration | + ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoCollapse | + ImGuiWindowFlags_NoFocusOnAppearing | + ImGuiWindowFlags_NoInputs); + + const char *msg = "Error: No StartupMenuComponent entity found.\n" + "Make sure startup_menu.json is loaded and contains\n" + "an entity with a StartupMenuComponent."; + ImVec2 textSize = ImGui::CalcTextSize(msg); + ImGui::SetCursorPosX((size.x - textSize.x) * 0.5f); + ImGui::SetCursorPosY((size.y - textSize.y) * 0.5f); + ImGui::TextUnformatted(msg); + + ImGui::End(); + ImGui::PopStyleColor(); +} diff --git a/src/features/editScene/systems/StartupMenuSystem.hpp b/src/features/editScene/systems/StartupMenuSystem.hpp new file mode 100644 index 0000000..6b1a0ec --- /dev/null +++ b/src/features/editScene/systems/StartupMenuSystem.hpp @@ -0,0 +1,47 @@ +#ifndef EDITSCENE_STARTUPMENUSYSTEM_HPP +#define EDITSCENE_STARTUPMENUSYSTEM_HPP +#pragma once + +#include +#include +#include +#include + +#include "../components/StartupMenu.hpp" + +class EditorApp; + +/** + * System that renders the full-screen startup menu in game mode. + * Only active when EditorApp is in GameMode::Game and GamePlayState::Menu. + */ +class StartupMenuSystem { +public: + StartupMenuSystem(flecs::world &world, Ogre::SceneManager *sceneMgr, + EditorApp *editorApp); + ~StartupMenuSystem(); + + void update(float deltaTime); + + /** + * Pre-load the menu font before ImGui NewFrame(). + * Must be called outside an active ImGui frame (before NewFrame). + */ + void prepareFont(); + +private: + void renderMenu(StartupMenuComponent &sm); + void renderMissingMenuError(); + void ensureFontLoaded(const Ogre::String &fontName, float fontSize); + + flecs::world &m_world; + Ogre::SceneManager *m_sceneMgr; + EditorApp *m_editorApp; + + bool m_fontLoaded = false; + Ogre::String m_currentFontName; + float m_currentFontSize = 0.0f; + ImFont *m_menuFont = nullptr; +}; + +#endif // EDITSCENE_STARTUPMENUSYSTEM_HPP diff --git a/src/features/editScene/ui/PlayerControllerEditor.cpp b/src/features/editScene/ui/PlayerControllerEditor.cpp new file mode 100644 index 0000000..e150a9a --- /dev/null +++ b/src/features/editScene/ui/PlayerControllerEditor.cpp @@ -0,0 +1,85 @@ +#include "PlayerControllerEditor.hpp" +#include + +bool PlayerControllerEditor::renderComponent(flecs::entity entity, + PlayerControllerComponent &pc) +{ + (void)entity; + bool modified = false; + + if (ImGui::CollapsingHeader("Player Controller", + ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Indent(); + + const char *modes[] = { "Third Person", "First Person" }; + int mode = pc.cameraMode; + if (ImGui::Combo("Camera Mode", &mode, modes, 2)) { + pc.cameraMode = mode; + modified = true; + } + + char nameBuf[256]; + snprintf(nameBuf, sizeof(nameBuf), "%s", + pc.targetCharacterName.c_str()); + if (ImGui::InputText("Target Character Name", nameBuf, + sizeof(nameBuf))) { + pc.targetCharacterName = nameBuf; + modified = true; + } + + char boneBuf[256]; + snprintf(boneBuf, sizeof(boneBuf), "%s", + pc.fpsBoneName.c_str()); + if (ImGui::InputText("FPS Bone Name", boneBuf, + sizeof(boneBuf))) { + pc.fpsBoneName = boneBuf; + modified = true; + } + + if (ImGui::DragFloat("TPS Distance", &pc.tpsDistance, 0.1f, + 0.5f, 20.0f)) + modified = true; + if (ImGui::DragFloat("TPS Height", &pc.tpsHeight, 0.1f, + 0.0f, 10.0f)) + modified = true; + if (ImGui::DragFloat("Mouse Sensitivity", &pc.mouseSensitivity, + 0.01f, 0.01f, 2.0f)) + modified = true; + + char smBuf[256]; + snprintf(smBuf, sizeof(smBuf), "%s", + pc.locomotionStateMachine.c_str()); + if (ImGui::InputText("Locomotion SM", smBuf, sizeof(smBuf))) { + pc.locomotionStateMachine = smBuf; + modified = true; + } + + char idleBuf[256]; + snprintf(idleBuf, sizeof(idleBuf), "%s", + pc.idleState.c_str()); + if (ImGui::InputText("Idle State", idleBuf, sizeof(idleBuf))) { + pc.idleState = idleBuf; + modified = true; + } + + char walkBuf[256]; + snprintf(walkBuf, sizeof(walkBuf), "%s", + pc.walkState.c_str()); + if (ImGui::InputText("Walk State", walkBuf, sizeof(walkBuf))) { + pc.walkState = walkBuf; + modified = true; + } + + char runBuf[256]; + snprintf(runBuf, sizeof(runBuf), "%s", + pc.runState.c_str()); + if (ImGui::InputText("Run State", runBuf, sizeof(runBuf))) { + pc.runState = runBuf; + modified = true; + } + + ImGui::Unindent(); + } + + return modified; +} diff --git a/src/features/editScene/ui/PlayerControllerEditor.hpp b/src/features/editScene/ui/PlayerControllerEditor.hpp new file mode 100644 index 0000000..2547ab6 --- /dev/null +++ b/src/features/editScene/ui/PlayerControllerEditor.hpp @@ -0,0 +1,18 @@ +#ifndef EDITSCENE_PLAYERCONTROLLEREDITOR_HPP +#define EDITSCENE_PLAYERCONTROLLEREDITOR_HPP +#pragma once + +#include "ComponentEditor.hpp" +#include "../components/PlayerController.hpp" + +/** + * Editor for PlayerControllerComponent + */ +class PlayerControllerEditor : public ComponentEditor { +public: + bool renderComponent(flecs::entity entity, + PlayerControllerComponent &pc) override; + const char *getName() const override { return "Player Controller"; } +}; + +#endif // EDITSCENE_PLAYERCONTROLLEREDITOR_HPP diff --git a/src/features/editScene/ui/StartupMenuEditor.cpp b/src/features/editScene/ui/StartupMenuEditor.cpp new file mode 100644 index 0000000..9227675 --- /dev/null +++ b/src/features/editScene/ui/StartupMenuEditor.cpp @@ -0,0 +1,48 @@ +#include "StartupMenuEditor.hpp" +#include + +bool StartupMenuEditor::renderComponent(flecs::entity entity, + StartupMenuComponent &sm) +{ + (void)entity; + bool modified = false; + + if (ImGui::CollapsingHeader("Startup Menu", + ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Indent(); + + char fontNameBuf[256]; + snprintf(fontNameBuf, sizeof(fontNameBuf), "%s", + sm.fontName.c_str()); + if (ImGui::InputText("Font Name", fontNameBuf, + sizeof(fontNameBuf))) { + sm.fontName = fontNameBuf; + modified = true; + } + + if (ImGui::DragFloat("Font Size", &sm.fontSize, 0.5f, 8.0f, + 128.0f)) { + modified = true; + } + + char sceneBuf[256]; + snprintf(sceneBuf, sizeof(sceneBuf), "%s", + sm.newGameScene.c_str()); + if (ImGui::InputText("New Game Scene", sceneBuf, + sizeof(sceneBuf))) { + sm.newGameScene = sceneBuf; + modified = true; + } + + if (ImGui::Checkbox("Show Load Game", &sm.showLoadGame)) + modified = true; + if (ImGui::Checkbox("Show Options", &sm.showOptions)) + modified = true; + if (ImGui::Checkbox("Show Quit", &sm.showQuit)) + modified = true; + + ImGui::Unindent(); + } + + return modified; +} diff --git a/src/features/editScene/ui/StartupMenuEditor.hpp b/src/features/editScene/ui/StartupMenuEditor.hpp new file mode 100644 index 0000000..cf8e8ff --- /dev/null +++ b/src/features/editScene/ui/StartupMenuEditor.hpp @@ -0,0 +1,18 @@ +#ifndef EDITSCENE_STARTUPMENUEDITOR_HPP +#define EDITSCENE_STARTUPMENUEDITOR_HPP +#pragma once + +#include "ComponentEditor.hpp" +#include "../components/StartupMenu.hpp" + +/** + * Editor for StartupMenuComponent + */ +class StartupMenuEditor : public ComponentEditor { +public: + bool renderComponent(flecs::entity entity, + StartupMenuComponent &sm) override; + const char *getName() const override { return "Startup Menu"; } +}; + +#endif // EDITSCENE_STARTUPMENUEDITOR_HPP