From b9cce0248a85414551683df76c098e1d031b5898 Mon Sep 17 00:00:00 2001 From: Sergey Lapin Date: Mon, 27 Apr 2026 09:03:47 +0300 Subject: [PATCH] Labels and actuators work perfectly! --- src/features/editScene/EditorApp.cpp | 8 + src/features/editScene/EditorApp.hpp | 4 + .../editScene/components/PlayerController.hpp | 6 + .../editScene/systems/ActuatorSystem.cpp | 264 ++++++++++-------- .../editScene/systems/ActuatorSystem.hpp | 24 +- .../editScene/systems/BehaviorTreeSystem.cpp | 22 +- .../editScene/systems/BehaviorTreeSystem.hpp | 9 + .../editScene/systems/EditorUISystem.cpp | 8 + .../systems/PlayerControllerSystem.cpp | 15 +- .../editScene/systems/SceneSerializer.cpp | 22 ++ src/features/editScene/ui/ActuatorEditor.cpp | 120 ++++++-- src/features/editScene/ui/ActuatorEditor.hpp | 5 + .../editScene/ui/PlayerControllerEditor.cpp | 12 + 13 files changed, 369 insertions(+), 150 deletions(-) diff --git a/src/features/editScene/EditorApp.cpp b/src/features/editScene/EditorApp.cpp index 06ae8b6..14ca33d 100644 --- a/src/features/editScene/EditorApp.cpp +++ b/src/features/editScene/EditorApp.cpp @@ -112,6 +112,14 @@ void ImGuiRenderListener::preViewportUpdate( m_uiSystem->update(m_deltaTime); } + // Render actuator markers in game mode (after NewFrame so draw + // commands survive) + if (m_editorApp) { + ActuatorSystem *actuatorSys = m_editorApp->getActuatorSystem(); + if (actuatorSys) + actuatorSys->render(); + } + // Render startup menu in game mode (inside ImGui frame scope) if (m_editorApp && m_editorApp->getGameMode() == EditorApp::GameMode::Game && diff --git a/src/features/editScene/EditorApp.hpp b/src/features/editScene/EditorApp.hpp index a30f079..c775399 100644 --- a/src/features/editScene/EditorApp.hpp +++ b/src/features/editScene/EditorApp.hpp @@ -186,6 +186,10 @@ public: { return m_startupMenuSystem.get(); } + ActuatorSystem *getActuatorSystem() const + { + return m_actuatorSystem.get(); + } Ogre::ImGuiOverlay *getImGuiOverlay() const { return m_imguiOverlay; diff --git a/src/features/editScene/components/PlayerController.hpp b/src/features/editScene/components/PlayerController.hpp index bbe19a3..44b59c3 100644 --- a/src/features/editScene/components/PlayerController.hpp +++ b/src/features/editScene/components/PlayerController.hpp @@ -31,6 +31,12 @@ struct PlayerControllerComponent { float actuatorDistance = 25.0f; float actuatorCooldown = 1.5f; Ogre::Vector3 actuatorColor = Ogre::Vector3(0.0f, 0.4f, 1.0f); + float distantCircleRadius = 8.0f; + float nearCircleRadius = 14.0f; + float actuatorLabelFontSize = 14.0f; + + /* Runtime: set by ActuatorSystem while executing an action */ + bool inputLocked = false; }; #endif // EDITSCENE_PLAYERCONTROLLER_HPP diff --git a/src/features/editScene/systems/ActuatorSystem.cpp b/src/features/editScene/systems/ActuatorSystem.cpp index 7e1b101..036fc38 100644 --- a/src/features/editScene/systems/ActuatorSystem.cpp +++ b/src/features/editScene/systems/ActuatorSystem.cpp @@ -6,7 +6,6 @@ #include "../components/Transform.hpp" #include "../components/Character.hpp" #include "../components/ActionDatabase.hpp" -#include "../components/ActionDebug.hpp" #include "../components/EntityName.hpp" #include "../camera/EditorCamera.hpp" #include @@ -16,9 +15,9 @@ #include ActuatorSystem::ActuatorSystem(flecs::world &world, - Ogre::SceneManager *sceneMgr, - EditorApp *editorApp, - BehaviorTreeSystem *btSystem) + Ogre::SceneManager *sceneMgr, + EditorApp *editorApp, + BehaviorTreeSystem *btSystem) : m_world(world) , m_sceneMgr(sceneMgr) , m_editorApp(editorApp) @@ -75,6 +74,14 @@ bool ActuatorSystem::isInRange(const Ogre::Vector3 &charPos, return true; } +void ActuatorSystem::setPlayerInputLocked(bool locked) +{ + m_world.query().each( + [&](flecs::entity, PlayerControllerComponent &pc) { + pc.inputLocked = locked; + }); +} + void ActuatorSystem::executeAction(flecs::entity character, flecs::entity actuatorEntity, const Ogre::String &actionName) @@ -90,87 +97,46 @@ void ActuatorSystem::executeAction(flecs::entity character, m_executingActuatorId = actuatorEntity.id(); m_executingCharacterId = character.id(); + m_executingActionName = actionName; + m_actionFirstFrame = true; - // Set up ActionDebug on the character to run the behavior tree - if (!character.has()) { - character.set({}); - } - auto &debug = character.get_mut(); - debug.isRunning = true; - debug.runTimer = 0.0f; - debug.currentActionName = actionName; - debug.selectedActionName = actionName; + // Lock player input while action executes + setPlayerInputLocked(true); Ogre::LogManager::getSingleton().logMessage( "[ActuatorSystem] Executing action: " + actionName); } -bool ActuatorSystem::isActionComplete(flecs::entity character) +bool ActuatorSystem::isActionComplete(flecs::entity character, + float deltaTime) { if (!m_btSystem || !character.is_alive()) return true; - if (!character.has()) + if (m_executingActionName.empty()) return true; - auto &debug = character.get(); - if (!debug.isRunning) - return true; - - auto &btState = m_btSystem->getActionDebugState(character.id()); - return btState.treeResult != BehaviorTreeSystem::Status::running; -} - -void ActuatorSystem::drawActuatorMarkers( - const std::vector &markers, - const ScreenActuator *target, const Ogre::String &labelText) -{ - ImDrawList *drawList = ImGui::GetBackgroundDrawList(); - if (!drawList) - return; - - // Default circle color from player controller - ImVec4 defaultColorVec(0.0f, 0.4f, 1.0f, 1.0f); - m_world.query().each( - [&](flecs::entity, PlayerControllerComponent &pc) { - defaultColorVec = ImVec4(pc.actuatorColor.x, - pc.actuatorColor.y, - pc.actuatorColor.z, 1.0f); + // Look up the action in the database to get its behavior tree + ActionDatabase *db = nullptr; + m_world.query().each( + [&](flecs::entity, ActionDatabase &database) { + if (!db) + db = &database; }); - ImColor defaultColor(defaultColorVec); - // Target is brighter - ImColor targetColor( - ImVec4(std::min(defaultColorVec.x + 0.2f, 1.0f), - std::min(defaultColorVec.y + 0.3f, 1.0f), - std::min(defaultColorVec.z + 0.0f, 1.0f), 1.0f)); + if (!db) + return true; - for (const auto &marker : markers) { - bool isTarget = target && target->entity.id() == marker.entity.id(); - float circleRadius = isTarget ? 12.0f : 8.0f; - ImColor circleCol = isTarget ? targetColor : defaultColor; + const GoapAction *action = db->findAction(m_executingActionName); + if (!action) + return true; - ImVec2 center(marker.screenPos.x, marker.screenPos.y); - drawList->AddCircleFilled(center, circleRadius, circleCol); - drawList->AddCircle(center, circleRadius, - IM_COL32(255, 255, 255, 180), 0, 2.0f); - } + // Evaluate the behavior tree directly (no ActionDebug) + auto status = m_btSystem->evaluatePlayerAction( + character.id(), action->behaviorTree, deltaTime, + m_actionFirstFrame); + m_actionFirstFrame = false; - // Draw label for target - if (target && !labelText.empty()) { - ImVec2 center(target->screenPos.x, target->screenPos.y); - float circleRadius = 12.0f; - ImVec2 textSize = ImGui::CalcTextSize(labelText.c_str()); - ImVec2 textPos(center.x - (textSize.x * 0.5f), - center.y + circleRadius + 6.0f); - - // Shadow - drawList->AddText( - ImVec2(textPos.x + 1, textPos.y + 1), - IM_COL32(0, 0, 0, 200), labelText.c_str()); - // Text - drawList->AddText(textPos, IM_COL32(255, 255, 255, 255), - labelText.c_str()); - } + return status != BehaviorTreeSystem::Status::running; } void ActuatorSystem::drawActionMenu(flecs::entity actuatorEntity) @@ -209,6 +175,7 @@ void ActuatorSystem::drawActionMenu(flecs::entity actuatorEntity) m_menuOpen = false; m_menuActuatorId = 0; m_eHoldTime = 0.0f; + m_eWasHeld = false; if (m_editorApp) m_editorApp->setWindowGrab(true); } @@ -226,9 +193,6 @@ void ActuatorSystem::update(float deltaTime) if (actuator.cooldownTimer < 0.0f) actuator.cooldownTimer = 0.0f; } - if (actuator.cooldownTimer <= 0.0f && !actuator.isExecuting) { - // Fully reset - } }); // Only run in game mode while playing @@ -241,13 +205,12 @@ void ActuatorSystem::update(float deltaTime) // Check if an action is currently executing if (m_executingActuatorId != 0) { flecs::entity character = m_world.entity(m_executingCharacterId); - if (isActionComplete(character)) { + if (isActionComplete(character, deltaTime)) { // Action finished - start cooldown flecs::entity actuator = m_world.entity(m_executingActuatorId); if (actuator.is_alive() && actuator.has()) { auto &ac = actuator.get_mut(); ac.isExecuting = false; - // Get cooldown from player controller config float cooldown = 1.5f; m_world .query() @@ -259,29 +222,31 @@ void ActuatorSystem::update(float deltaTime) } m_executingActuatorId = 0; m_executingCharacterId = 0; + m_executingActionName.clear(); - // Clear ActionDebug on character - if (character.is_alive() && character.has()) { - auto &debug = character.get_mut(); - debug.isRunning = false; - debug.currentActionName.clear(); - } - } else { - // Action still running - don't draw any markers - return; + // Unlock player input + setPlayerInputLocked(false); } + // Don't collect or draw anything while executing + m_visibleActuators.clear(); + m_targetIndex = -1; + m_labelText.clear(); + return; } // Find player character flecs::entity playerCharacter = flecs::entity::null(); float actuatorDistance = 25.0f; float actuatorCooldown = 1.5f; + m_circleColor = ImVec4(0.0f, 0.4f, 1.0f, 1.0f); + m_distantRadius = 8.0f; + m_nearRadius = 14.0f; + m_labelFontSize = 14.0f; m_world.query().each( [&](flecs::entity e, PlayerControllerComponent &pc) { (void)e; if (!playerCharacter.is_alive()) { - // Find character by name m_world.query() .each([&](flecs::entity ec, EntityNameComponent &en) { @@ -291,6 +256,13 @@ void ActuatorSystem::update(float deltaTime) }); actuatorDistance = pc.actuatorDistance; actuatorCooldown = pc.actuatorCooldown; + m_circleColor = ImVec4(pc.actuatorColor.x, + pc.actuatorColor.y, + pc.actuatorColor.z, + 1.0f); + m_distantRadius = pc.distantCircleRadius; + m_nearRadius = pc.nearCircleRadius; + m_labelFontSize = pc.actuatorLabelFontSize; } }); @@ -303,7 +275,7 @@ void ActuatorSystem::update(float deltaTime) ->_getDerivedPosition(); // Collect actuators within distance - std::vector visibleActuators; + m_visibleActuators.clear(); std::vector inRangeActuators; m_world.query() @@ -329,12 +301,11 @@ void ActuatorSystem::update(float deltaTime) sa.entity = e; sa.screenPos = screenPos; sa.distance = dist; - // Distance to screen center (X only for horizontal preference) ImVec2 vpSize = ImGui::GetMainViewport()->Size; sa.distToScreenCenter = std::abs(screenPos.x - vpSize.x * 0.5f); - visibleActuators.push_back(sa); + m_visibleActuators.push_back(sa); if (isInRange(charPos, objPos, actuator.radius, actuator.height)) { @@ -343,9 +314,8 @@ void ActuatorSystem::update(float deltaTime) }); // Determine target actuator for interaction - const ScreenActuator *target = nullptr; + m_targetIndex = -1; if (!inRangeActuators.empty()) { - // Pick closest to screen X-center, then bottommost (largest Y) size_t bestIdx = 0; for (size_t i = 1; i < inRangeActuators.size(); i++) { if (inRangeActuators[i].distToScreenCenter < @@ -353,30 +323,35 @@ void ActuatorSystem::update(float deltaTime) bestIdx = i; } else if (inRangeActuators[i].distToScreenCenter == inRangeActuators[bestIdx].distToScreenCenter) { - // Bottommost = larger Y if (inRangeActuators[i].screenPos.y > inRangeActuators[bestIdx].screenPos.y) bestIdx = i; } } - target = &inRangeActuators[bestIdx]; - } - - // Build label text - Ogre::String labelText; - if (target && target->entity.is_alive() && - target->entity.has()) { - auto &actuator = target->entity.get(); - if (actuator.actionNames.size() == 1 && - !actuator.actionNames[0].empty()) { - labelText = "E - " + actuator.actionNames[0]; - } else if (actuator.actionNames.size() > 1) { - labelText = "E"; + // Find the index in m_visibleActuators + for (size_t i = 0; i < m_visibleActuators.size(); i++) { + if (m_visibleActuators[i].entity.id() == + inRangeActuators[bestIdx].entity.id()) { + m_targetIndex = static_cast(i); + break; + } } } - // Draw markers - drawActuatorMarkers(visibleActuators, target, labelText); + // Build label text + m_labelText.clear(); + if (m_targetIndex >= 0) { + flecs::entity targetEntity = m_visibleActuators[m_targetIndex].entity; + if (targetEntity.is_alive() && targetEntity.has()) { + auto &actuator = targetEntity.get(); + if (actuator.actionNames.size() == 1 && + !actuator.actionNames[0].empty()) { + m_labelText = "E " + actuator.actionNames[0]; + } else if (actuator.actionNames.size() > 1) { + m_labelText = "E"; + } + } + } // Handle input GameInputState &input = m_editorApp->getGameInputState(); @@ -384,10 +359,9 @@ void ActuatorSystem::update(float deltaTime) // Handle menu state first if (m_menuOpen) { flecs::entity menuActuator = m_world.entity(m_menuActuatorId); - drawActionMenu(menuActuator); + // Menu rendering happens in render() if (!m_pendingActionName.empty()) { - // Action selected from menu executeAction(playerCharacter, menuActuator, m_pendingActionName); m_pendingActionName.clear(); @@ -399,7 +373,6 @@ void ActuatorSystem::update(float deltaTime) m_editorApp->setWindowGrab(true); } - // Also close menu if E is released without selection if (!input.e && m_eWasHeld) { m_menuOpen = false; m_menuActuatorId = 0; @@ -412,23 +385,23 @@ void ActuatorSystem::update(float deltaTime) } // Handle E key - if (target && input.e) { - auto &actuator = target->entity.get(); + if (m_targetIndex >= 0 && input.e) { + flecs::entity targetEntity = + m_visibleActuators[m_targetIndex].entity; + auto &actuator = targetEntity.get(); m_eHoldTime += deltaTime; if (actuator.actionNames.size() == 1 && !actuator.actionNames[0].empty()) { - // Single action: execute on press if (input.ePressed) { - executeAction(playerCharacter, target->entity, + executeAction(playerCharacter, targetEntity, actuator.actionNames[0]); m_eHoldTime = 0.0f; } } else if (actuator.actionNames.size() > 1) { - // Multiple actions: hold to open menu if (m_eHoldTime > 0.3f && !m_menuOpen) { m_menuOpen = true; - m_menuActuatorId = target->entity.id(); + m_menuActuatorId = targetEntity.id(); m_eWasHeld = true; if (m_editorApp) m_editorApp->setWindowGrab(false); @@ -439,3 +412,66 @@ void ActuatorSystem::update(float deltaTime) m_eWasHeld = false; } } + +void ActuatorSystem::render() +{ + // Only run in game mode while playing + if (!m_editorApp || + m_editorApp->getGameMode() != EditorApp::GameMode::Game || + m_editorApp->getGamePlayState() != + EditorApp::GamePlayState::Playing) + return; + + ImDrawList *drawList = ImGui::GetBackgroundDrawList(); + if (!drawList) + return; + + ImColor defaultColor(m_circleColor); + ImColor targetColor( + ImVec4(std::min(m_circleColor.x + 0.2f, 1.0f), + std::min(m_circleColor.y + 0.3f, 1.0f), + std::min(m_circleColor.z + 0.0f, 1.0f), + 1.0f)); + + for (size_t i = 0; i < m_visibleActuators.size(); i++) { + bool isTarget = (static_cast(i) == m_targetIndex); + float circleRadius = isTarget ? m_nearRadius : m_distantRadius; + ImColor circleCol = isTarget ? targetColor : defaultColor; + + ImVec2 center(m_visibleActuators[i].screenPos.x, + m_visibleActuators[i].screenPos.y); + drawList->AddCircleFilled(center, circleRadius, circleCol); + drawList->AddCircle(center, circleRadius, + IM_COL32(255, 255, 255, 180), 0, 2.0f); + } + + // Draw label for target + if (m_targetIndex >= 0 && !m_labelText.empty()) { + float circleRadius = m_nearRadius; + ImVec2 center(m_visibleActuators[m_targetIndex].screenPos.x, + m_visibleActuators[m_targetIndex].screenPos.y); + + ImFont *font = ImGui::GetFont(); + ImVec2 textSize = font->CalcTextSizeA( + m_labelFontSize, FLT_MAX, -1.0f, + m_labelText.c_str()); + + ImVec2 textPos(center.x - (textSize.x * 0.5f), + center.y + circleRadius + 6.0f); + + // Shadow + drawList->AddText(font, m_labelFontSize, + ImVec2(textPos.x + 1, textPos.y + 1), + IM_COL32(0, 0, 0, 200), m_labelText.c_str()); + // Text + drawList->AddText(font, m_labelFontSize, textPos, + IM_COL32(255, 255, 255, 255), + m_labelText.c_str()); + } + + // Draw action menu if open + if (m_menuOpen) { + flecs::entity menuActuator = m_world.entity(m_menuActuatorId); + drawActionMenu(menuActuator); + } +} diff --git a/src/features/editScene/systems/ActuatorSystem.hpp b/src/features/editScene/systems/ActuatorSystem.hpp index d815d98..6d6d279 100644 --- a/src/features/editScene/systems/ActuatorSystem.hpp +++ b/src/features/editScene/systems/ActuatorSystem.hpp @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -14,10 +15,13 @@ class BehaviorTreeSystem; * System that handles player interaction with Actuator entities. * * In game mode: - * - Draws on-screen circle markers for actuators within range + * - update() finds nearby actuators and handles input + * - render() draws on-screen circle markers (called in preViewportUpdate after NewFrame) * - Shows "E - ActionName" (or just "E" for multi-action) when in reach * - Handles E press / hold for action activation * - Manages per-actuator cooldowns + * - Executes actions via BehaviorTreeSystem directly (no ActionDebug) + * - Disables player controls during action execution */ class ActuatorSystem { public: @@ -26,6 +30,7 @@ public: ~ActuatorSystem(); void update(float deltaTime); + void render(); private: struct ScreenActuator { @@ -40,17 +45,24 @@ private: const Ogre::Vector3 &objPos, float radius, float height); void executeAction(flecs::entity character, flecs::entity actuatorEntity, const Ogre::String &actionName); - bool isActionComplete(flecs::entity character); - void drawActuatorMarkers(const std::vector &markers, - const ScreenActuator *target, - const Ogre::String &labelText); + bool isActionComplete(flecs::entity character, float deltaTime); void drawActionMenu(flecs::entity actuatorEntity); + void setPlayerInputLocked(bool locked); flecs::world &m_world; Ogre::SceneManager *m_sceneMgr; EditorApp *m_editorApp; BehaviorTreeSystem *m_btSystem; + // Cached data between update() and render() + std::vector m_visibleActuators; + int m_targetIndex = -1; + Ogre::String m_labelText; + ImVec4 m_circleColor; + float m_distantRadius = 8.0f; + float m_nearRadius = 14.0f; + float m_labelFontSize = 14.0f; + // Multi-action menu state bool m_menuOpen = false; flecs::entity_t m_menuActuatorId = 0; @@ -61,6 +73,8 @@ private: // Currently executing action state flecs::entity_t m_executingActuatorId = 0; flecs::entity_t m_executingCharacterId = 0; + Ogre::String m_executingActionName; + bool m_actionFirstFrame = false; }; #endif // EDITSCENE_ACTUATOR_SYSTEM_HPP diff --git a/src/features/editScene/systems/BehaviorTreeSystem.cpp b/src/features/editScene/systems/BehaviorTreeSystem.cpp index b830529..e1c6145 100644 --- a/src/features/editScene/systems/BehaviorTreeSystem.cpp +++ b/src/features/editScene/systems/BehaviorTreeSystem.cpp @@ -8,7 +8,6 @@ #include "../components/SmartObject.hpp" #include "../components/Transform.hpp" #include "../components/EntityName.hpp" -#include "../components/Relationship.hpp" #include "../components/Character.hpp" #include #include @@ -675,3 +674,24 @@ void BehaviorTreeSystem::update(float deltaTime) debug.runTimer += deltaTime; }); } + +BehaviorTreeSystem::Status +BehaviorTreeSystem::evaluatePlayerAction(flecs::entity_t id, + const BehaviorTreeNode &root, + float deltaTime, bool reset) +{ + auto &state = m_playerActionStates[id]; + if (reset) { + state.lastActiveLeaves.clear(); + state.firstRun = true; + state.nodeTimers.clear(); + state.treeResult = Status::running; + } + state.currentActiveLeaves.clear(); + Status result = evaluateNode(root, m_world.entity(id), state, + deltaTime); + state.lastActiveLeaves = state.currentActiveLeaves; + state.firstRun = false; + state.treeResult = result; + return result; +} diff --git a/src/features/editScene/systems/BehaviorTreeSystem.hpp b/src/features/editScene/systems/BehaviorTreeSystem.hpp index 8100793..58f2b6a 100644 --- a/src/features/editScene/systems/BehaviorTreeSystem.hpp +++ b/src/features/editScene/systems/BehaviorTreeSystem.hpp @@ -67,6 +67,14 @@ public: return defaultState; } + /** Evaluate a behavior tree directly for an entity. + * Used by ActuatorSystem for player action execution. + * Pass reset=true on the first call for a new action. + * Returns running/success/failure. */ + Status evaluatePlayerAction(flecs::entity_t id, + const BehaviorTreeNode &root, float deltaTime, + bool reset); + private: Status evaluateNode(const BehaviorTreeNode &node, flecs::entity e, RunnerState &state, float deltaTime); @@ -84,6 +92,7 @@ private: std::unordered_map m_runnerStates; std::unordered_map m_actionDebugStates; + std::unordered_map m_playerActionStates; }; #endif // EDITSCENE_BEHAVIOR_TREE_SYSTEM_HPP diff --git a/src/features/editScene/systems/EditorUISystem.cpp b/src/features/editScene/systems/EditorUISystem.cpp index 9528f6f..550ca84 100644 --- a/src/features/editScene/systems/EditorUISystem.cpp +++ b/src/features/editScene/systems/EditorUISystem.cpp @@ -39,6 +39,7 @@ #include "../components/GoapPlanner.hpp" #include "../components/GoapRunner.hpp" #include "../components/PathFollowing.hpp" +#include "../components/Actuator.hpp" #include "../components/PrefabInstance.hpp" #include "../ui/TransformEditor.hpp" @@ -1063,6 +1064,13 @@ void EditorUISystem::renderComponentList(flecs::entity entity) componentCount++; } + // Render Actuator if present + if (entity.has()) { + auto &actuator = entity.get_mut(); + m_componentRegistry.render(entity, actuator); + componentCount++; + } + // Show message if no components if (componentCount == 0) { diff --git a/src/features/editScene/systems/PlayerControllerSystem.cpp b/src/features/editScene/systems/PlayerControllerSystem.cpp index aef7dfa..d007757 100644 --- a/src/features/editScene/systems/PlayerControllerSystem.cpp +++ b/src/features/editScene/systems/PlayerControllerSystem.cpp @@ -164,9 +164,9 @@ void PlayerControllerSystem::updateTPSCamera(PlayerControllerComponent &pc, state.faceHidden = false; } - // Read mouse input + // Read mouse input (skip if input locked by action) GameInputState &input = m_editorApp->getGameInputState(); - if (input.mouseMoved) { + if (!pc.inputLocked && input.mouseMoved) { state.yaw -= input.mouseDeltaX * pc.mouseSensitivity; state.pitch -= input.mouseDeltaY * pc.mouseSensitivity; // Clamp pitch @@ -277,9 +277,9 @@ void PlayerControllerSystem::updateFPSCamera(PlayerControllerComponent &pc, camNode->setPosition(boneWorldPos + offset); - // Apply mouse look + // Apply mouse look (skip if input locked by action) GameInputState &input = m_editorApp->getGameInputState(); - if (input.mouseMoved) { + if (!pc.inputLocked && input.mouseMoved) { state.yaw -= input.mouseDeltaX * pc.mouseSensitivity; state.pitch -= input.mouseDeltaY * pc.mouseSensitivity; if (state.pitch > 89.0f) @@ -302,6 +302,13 @@ void PlayerControllerSystem::updateLocomotion(PlayerControllerComponent &pc, if (!state.targetEntity.has()) return; + // Skip locomotion if input is locked by an executing action + if (pc.inputLocked) { + auto &cc = state.targetEntity.get_mut(); + cc.linearVelocity = Ogre::Vector3::ZERO; + return; + } + GameInputState &input = m_editorApp->getGameInputState(); auto &cc = state.targetEntity.get_mut(); diff --git a/src/features/editScene/systems/SceneSerializer.cpp b/src/features/editScene/systems/SceneSerializer.cpp index b999492..4cb6a8b 100644 --- a/src/features/editScene/systems/SceneSerializer.cpp +++ b/src/features/editScene/systems/SceneSerializer.cpp @@ -2867,6 +2867,13 @@ nlohmann::json SceneSerializer::serializePlayerController(flecs::entity entity) json["swimIdleState"] = pc.swimIdleState; json["swimState"] = pc.swimState; json["swimFastState"] = pc.swimFastState; + json["actuatorDistance"] = pc.actuatorDistance; + json["actuatorCooldown"] = pc.actuatorCooldown; + json["actuatorColor"] = { pc.actuatorColor.x, pc.actuatorColor.y, + pc.actuatorColor.z }; + json["distantCircleRadius"] = pc.distantCircleRadius; + json["nearCircleRadius"] = pc.nearCircleRadius; + json["actuatorLabelFontSize"] = pc.actuatorLabelFontSize; return json; } @@ -2903,6 +2910,21 @@ void SceneSerializer::deserializePlayerController(flecs::entity entity, pc.swimIdleState = json.value("swimIdleState", pc.swimIdleState); pc.swimState = json.value("swimState", pc.swimState); pc.swimFastState = json.value("swimFastState", pc.swimFastState); + pc.actuatorDistance = json.value("actuatorDistance", pc.actuatorDistance); + pc.actuatorCooldown = json.value("actuatorCooldown", pc.actuatorCooldown); + if (json.contains("actuatorColor") && json["actuatorColor"].is_array() && + json["actuatorColor"].size() >= 3) { + pc.actuatorColor = Ogre::Vector3(json["actuatorColor"][0], + json["actuatorColor"][1], + json["actuatorColor"][2]); + } + pc.distantCircleRadius = + json.value("distantCircleRadius", pc.distantCircleRadius); + pc.nearCircleRadius = json.value("nearCircleRadius", pc.nearCircleRadius); + pc.actuatorLabelFontSize = + json.value("actuatorLabelFontSize", pc.actuatorLabelFontSize); + // inputLocked is runtime-only, always reset to false on load + pc.inputLocked = false; entity.set(pc); } diff --git a/src/features/editScene/ui/ActuatorEditor.cpp b/src/features/editScene/ui/ActuatorEditor.cpp index a8dd5f2..df7bc01 100644 --- a/src/features/editScene/ui/ActuatorEditor.cpp +++ b/src/features/editScene/ui/ActuatorEditor.cpp @@ -1,45 +1,113 @@ #include "ActuatorEditor.hpp" +#include "../components/ActionDatabase.hpp" #include +ActionDatabase *ActuatorEditor::findDatabase(flecs::entity entity) +{ + auto world = entity.world(); + ActionDatabase *db = nullptr; + world.query().each( + [&](flecs::entity, ActionDatabase &database) { + if (!db) + db = &database; + }); + return db; +} + bool ActuatorEditor::renderComponent(flecs::entity entity, ActuatorComponent &actuator) { - (void)entity; bool modified = false; + ImGui::PushID("Actuator"); - modified |= ImGui::DragFloat("Radius", &actuator.radius, 0.1f, - 0.1f, 100.0f); - modified |= ImGui::DragFloat("Height", &actuator.height, 0.1f, - 0.1f, 100.0f); + ImGui::Text("Actuator Settings"); + ImGui::Separator(); + + if (ImGui::DragFloat("Radius", &actuator.radius, 0.1f, 0.1f, 100.0f, + "%.1f")) { + if (actuator.radius < 0.1f) + actuator.radius = 0.1f; + modified = true; + } + ImGui::SameLine(); + ImGui::TextDisabled("(XZ interaction distance)"); + + if (ImGui::DragFloat("Height", &actuator.height, 0.1f, 0.1f, 100.0f, + "%.1f")) { + if (actuator.height < 0.1f) + actuator.height = 0.1f; + modified = true; + } + ImGui::SameLine(); + ImGui::TextDisabled("(Y interaction threshold)"); ImGui::Separator(); ImGui::Text("Actions:"); + ImGui::Indent(); - int removeIdx = -1; - for (int i = 0; i < (int)actuator.actionNames.size(); i++) { - ImGui::PushID(i); - char buf[256]; - snprintf(buf, sizeof(buf), "%s", - actuator.actionNames[i].c_str()); - if (ImGui::InputText("##name", buf, sizeof(buf))) { - actuator.actionNames[i] = buf; - modified = true; - } + // List currently selected actions + for (size_t i = 0; i < actuator.actionNames.size(); i++) { + ImGui::PushID(static_cast(i)); + ImGui::Text("%s", actuator.actionNames[i].c_str()); ImGui::SameLine(); - if (ImGui::SmallButton("X")) - removeIdx = i; + if (ImGui::SmallButton("X")) { + actuator.actionNames.erase( + actuator.actionNames.begin() + i); + modified = true; + ImGui::PopID(); + break; + } ImGui::PopID(); } - if (removeIdx >= 0) { - actuator.actionNames.erase( - actuator.actionNames.begin() + removeIdx); - modified = true; - } - - if (ImGui::Button("Add Action")) { - actuator.actionNames.push_back(""); - modified = true; + + // Add action from database + ActionDatabase *db = findDatabase(entity); + if (db && !db->actions.empty()) { + ImGui::Separator(); + ImGui::Text("Add action:"); + static int selectedAction = -1; + + std::vector availableNames; + std::vector availableNamesStorage; + for (const auto &action : db->actions) { + bool alreadySelected = false; + for (const auto &selected : actuator.actionNames) { + if (selected == action.name) { + alreadySelected = true; + break; + } + } + if (!alreadySelected) { + availableNamesStorage.push_back(action.name); + availableNames.push_back( + availableNamesStorage.back().c_str()); + } + } + + if (!availableNames.empty()) { + if (selectedAction >= (int)availableNames.size()) + selectedAction = 0; + if (ImGui::Combo("##actionSelect", &selectedAction, + availableNames.data(), + (int)availableNames.size())) { + } + ImGui::SameLine(); + if (ImGui::Button("Add")) { + if (selectedAction >= 0 && + selectedAction < (int)availableNames.size()) { + actuator.actionNames.push_back( + availableNamesStorage[selectedAction]); + modified = true; + } + } + } else { + ImGui::TextDisabled("All actions already selected"); + } + } else { + ImGui::TextDisabled("No actions in database"); } + ImGui::Unindent(); + ImGui::PopID(); return modified; } diff --git a/src/features/editScene/ui/ActuatorEditor.hpp b/src/features/editScene/ui/ActuatorEditor.hpp index 93948dd..02f8674 100644 --- a/src/features/editScene/ui/ActuatorEditor.hpp +++ b/src/features/editScene/ui/ActuatorEditor.hpp @@ -5,6 +5,8 @@ #include "ComponentEditor.hpp" #include "../components/Actuator.hpp" +class ActionDatabase; + class ActuatorEditor : public ComponentEditor { public: const char *getName() const override @@ -15,6 +17,9 @@ public: protected: bool renderComponent(flecs::entity entity, ActuatorComponent &actuator) override; + +private: + ActionDatabase *findDatabase(flecs::entity entity); }; #endif // EDITSCENE_ACTUATOR_EDITOR_HPP diff --git a/src/features/editScene/ui/PlayerControllerEditor.cpp b/src/features/editScene/ui/PlayerControllerEditor.cpp index ebac8ff..5af3925 100644 --- a/src/features/editScene/ui/PlayerControllerEditor.cpp +++ b/src/features/editScene/ui/PlayerControllerEditor.cpp @@ -116,6 +116,18 @@ bool PlayerControllerEditor::renderComponent(flecs::entity entity, if (ImGui::DragFloat("Actuator Cooldown", &pc.actuatorCooldown, 0.1f, 0.0f, 10.0f)) modified = true; + if (ImGui::DragFloat("Distant Circle Radius", + &pc.distantCircleRadius, 0.5f, 1.0f, + 64.0f)) + modified = true; + if (ImGui::DragFloat("Near Circle Radius", + &pc.nearCircleRadius, 0.5f, 1.0f, + 64.0f)) + modified = true; + if (ImGui::DragFloat("Label Font Size", + &pc.actuatorLabelFontSize, 0.5f, 8.0f, + 32.0f)) + modified = true; float color[3] = { pc.actuatorColor.x, pc.actuatorColor.y, pc.actuatorColor.z }; if (ImGui::ColorEdit3("Actuator Color", color)) {