diff --git a/src/features/editScene/CMakeLists.txt b/src/features/editScene/CMakeLists.txt index c395f43..3cd6426 100644 --- a/src/features/editScene/CMakeLists.txt +++ b/src/features/editScene/CMakeLists.txt @@ -55,6 +55,9 @@ set(EDITSCENE_SOURCES ui/GoapRunnerEditor.cpp ui/PathFollowingEditor.cpp ui/GoapPlannerEditor.cpp + systems/ActuatorSystem.cpp + ui/ActuatorEditor.cpp + components/ActuatorModule.cpp systems/PrefabSystem.cpp ui/PrefabInstanceEditor.cpp @@ -192,6 +195,8 @@ set(EDITSCENE_HEADERS systems/GoapRunnerSystem.hpp systems/PathFollowingSystem.hpp systems/GoapPlannerSystem.hpp + components/Actuator.hpp + ui/ActuatorEditor.hpp components/PrefabInstance.hpp ui/PrefabInstanceEditor.hpp diff --git a/src/features/editScene/EditorApp.cpp b/src/features/editScene/EditorApp.cpp index 64debf5..06ae8b6 100644 --- a/src/features/editScene/EditorApp.cpp +++ b/src/features/editScene/EditorApp.cpp @@ -69,9 +69,11 @@ #include "systems/PrefabSystem.hpp" #include "components/NavMesh.hpp" #include "components/SmartObject.hpp" +#include "components/Actuator.hpp" #include "components/GoapPlanner.hpp" #include "components/GoapRunner.hpp" #include "components/PathFollowing.hpp" +#include "systems/ActuatorSystem.hpp" #include #include @@ -342,6 +344,10 @@ void EditorApp::setup() // Wire up EditorApp for game mode detection m_smartObjectSystem->setEditorApp(this); + // Setup Actuator system + m_actuatorSystem = std::make_unique( + m_world, m_sceneMgr, this, m_behaviorTreeSystem.get()); + // Setup GOAP Runner system m_goapRunnerSystem = std::make_unique( m_world, m_sceneMgr, m_smartObjectSystem.get(), @@ -601,6 +607,9 @@ void EditorApp::setupECS() // Register Smart Object component m_world.component(); + // Register Actuator component + m_world.component(); + // Register GOAP Planner component m_world.component(); @@ -808,6 +817,11 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt) m_goapRunnerSystem->update(evt.timeSinceLastFrame); } + /* --- Actuator system (player interaction prompts) --- */ + if (m_actuatorSystem) { + m_actuatorSystem->update(evt.timeSinceLastFrame); + } + /* --- Dynamic physics (characters after static world) --- */ if (m_characterSystem) { diff --git a/src/features/editScene/EditorApp.hpp b/src/features/editScene/EditorApp.hpp index 89834f7..a30f079 100644 --- a/src/features/editScene/EditorApp.hpp +++ b/src/features/editScene/EditorApp.hpp @@ -39,6 +39,7 @@ class SmartObjectSystem; class GoapRunnerSystem; class PathFollowingSystem; class GoapPlannerSystem; +class ActuatorSystem; class EditorApp; /** @@ -228,6 +229,7 @@ private: std::unique_ptr m_goapRunnerSystem; std::unique_ptr m_pathFollowingSystem; std::unique_ptr m_goapPlannerSystem; + std::unique_ptr m_actuatorSystem; // Game systems diff --git a/src/features/editScene/components/Actuator.hpp b/src/features/editScene/components/Actuator.hpp new file mode 100644 index 0000000..0e7be27 --- /dev/null +++ b/src/features/editScene/components/Actuator.hpp @@ -0,0 +1,44 @@ +#ifndef EDITSCENE_ACTUATOR_HPP +#define EDITSCENE_ACTUATOR_HPP +#pragma once + +#include +#include +#include + +/** + * Actuator component. + * + * An interactive object visible only to the player character. + * When the player is within radius + height range, an on-screen + * prompt appears and the action can be triggered with the action key. + * + * Unlike SmartObject, Actuators do not use pathfinding or path + * following — they are instantaneous interactions. + */ +struct ActuatorComponent { + // Interaction radius in XZ plane + float radius = 1.5f; + + // Maximum height difference for interaction + float height = 1.8f; + + // Names of GOAP actions (from ActionDatabase) that this actuator provides + std::vector actionNames; + + // Runtime: cooldown timer (seconds remaining) + float cooldownTimer = 0.0f; + + // Runtime: currently executing an action + bool isExecuting = false; + + ActuatorComponent() = default; + + explicit ActuatorComponent(float radius_, float height_) + : radius(radius_) + , height(height_) + { + } +}; + +#endif // EDITSCENE_ACTUATOR_HPP diff --git a/src/features/editScene/components/ActuatorModule.cpp b/src/features/editScene/components/ActuatorModule.cpp new file mode 100644 index 0000000..0de902f --- /dev/null +++ b/src/features/editScene/components/ActuatorModule.cpp @@ -0,0 +1,19 @@ +#include "Actuator.hpp" +#include "../ui/ComponentRegistration.hpp" +#include "../ui/ActuatorEditor.hpp" + +REGISTER_COMPONENT_GROUP("Actuator", "Game", ActuatorComponent, ActuatorEditor) +{ + registry.registerComponent( + "Actuator", "Game", 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/PlayerController.hpp b/src/features/editScene/components/PlayerController.hpp index 1a595c0..bbe19a3 100644 --- a/src/features/editScene/components/PlayerController.hpp +++ b/src/features/editScene/components/PlayerController.hpp @@ -26,6 +26,11 @@ struct PlayerControllerComponent { Ogre::String swimIdleState = "swim-idle"; Ogre::String swimState = "swim"; Ogre::String swimFastState = "swim-fast"; + + /* Actuator interaction settings */ + float actuatorDistance = 25.0f; + float actuatorCooldown = 1.5f; + Ogre::Vector3 actuatorColor = Ogre::Vector3(0.0f, 0.4f, 1.0f); }; #endif // EDITSCENE_PLAYERCONTROLLER_HPP diff --git a/src/features/editScene/systems/ActuatorSystem.cpp b/src/features/editScene/systems/ActuatorSystem.cpp new file mode 100644 index 0000000..7e1b101 --- /dev/null +++ b/src/features/editScene/systems/ActuatorSystem.cpp @@ -0,0 +1,441 @@ +#include "ActuatorSystem.hpp" +#include "../EditorApp.hpp" +#include "BehaviorTreeSystem.hpp" +#include "../components/Actuator.hpp" +#include "../components/PlayerController.hpp" +#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 +#include +#include +#include +#include + +ActuatorSystem::ActuatorSystem(flecs::world &world, + Ogre::SceneManager *sceneMgr, + EditorApp *editorApp, + BehaviorTreeSystem *btSystem) + : m_world(world) + , m_sceneMgr(sceneMgr) + , m_editorApp(editorApp) + , m_btSystem(btSystem) +{ +} + +ActuatorSystem::~ActuatorSystem() = default; + +Ogre::Vector2 ActuatorSystem::projectToScreen(const Ogre::Vector3 &worldPoint) +{ + if (!m_editorApp) + return Ogre::Vector2(-1, -1); + + EditorCamera *editorCam = m_editorApp->getEditorCamera(); + if (!editorCam) + return Ogre::Vector2(-1, -1); + Ogre::Camera *camera = editorCam->getCamera(); + if (!camera) + return Ogre::Vector2(-1, -1); + + ImVec2 vpSize = ImGui::GetMainViewport()->Size; + float width = vpSize.x; + float height = vpSize.y; + + // Convert to camera space + Ogre::Vector3 eyeSpacePoint = camera->getViewMatrix() * worldPoint; + + // Project to clip space + Ogre::Vector3 clipSpacePoint = + camera->getProjectionMatrix() * eyeSpacePoint; + if (clipSpacePoint.z < 0.0f) + return Ogre::Vector2(-1, -1); + + // Convert from clip space (-1 to 1) to screen space (0 to 1) + float screenX = (clipSpacePoint.x / 2.0f) + 0.5f; + float screenY = 1.0f - ((clipSpacePoint.y / 2.0f) + 0.5f); + + // Map to actual pixel dimensions + return Ogre::Vector2(screenX * width, screenY * height); +} + +bool ActuatorSystem::isInRange(const Ogre::Vector3 &charPos, + const Ogre::Vector3 &objPos, float radius, + float height) +{ + Ogre::Vector3 diff = charPos - objPos; + float xzDist = std::sqrt(diff.x * diff.x + diff.z * diff.z); + if (xzDist > radius) + return false; + float yDiff = std::abs(diff.y); + if (yDiff > height) + return false; + return true; +} + +void ActuatorSystem::executeAction(flecs::entity character, + flecs::entity actuatorEntity, + const Ogre::String &actionName) +{ + if (!character.is_alive() || !actuatorEntity.is_alive()) + return; + + if (!actuatorEntity.has()) + return; + + auto &actuator = actuatorEntity.get_mut(); + actuator.isExecuting = true; + + m_executingActuatorId = actuatorEntity.id(); + m_executingCharacterId = character.id(); + + // 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; + + Ogre::LogManager::getSingleton().logMessage( + "[ActuatorSystem] Executing action: " + actionName); +} + +bool ActuatorSystem::isActionComplete(flecs::entity character) +{ + if (!m_btSystem || !character.is_alive()) + return true; + + if (!character.has()) + 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); + }); + 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)); + + 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; + + 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); + } + + // 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()); + } +} + +void ActuatorSystem::drawActionMenu(flecs::entity actuatorEntity) +{ + if (!actuatorEntity.is_alive() || + !actuatorEntity.has()) + return; + + auto &actuator = actuatorEntity.get(); + + ImVec2 center = ImGui::GetMainViewport()->GetCenter(); + ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, + ImVec2(0.5f, 0.5f)); + ImGui::SetNextWindowSize(ImVec2(300, 0), ImGuiCond_Appearing); + + ImGuiWindowFlags flags = + ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse | + ImGuiWindowFlags_AlwaysAutoResize; + + if (ImGui::Begin("Select Action", nullptr, flags)) { + ImGui::Text("Select an action:"); + ImGui::Separator(); + + for (const auto &name : actuator.actionNames) { + if (name.empty()) + continue; + if (ImGui::Button(name.c_str(), + ImVec2(ImGui::GetContentRegionAvail().x, + 0))) { + m_pendingActionName = name; + } + } + + ImGui::Separator(); + if (ImGui::Button("Cancel", ImVec2(120, 0))) { + m_menuOpen = false; + m_menuActuatorId = 0; + m_eHoldTime = 0.0f; + if (m_editorApp) + m_editorApp->setWindowGrab(true); + } + } + ImGui::End(); +} + +void ActuatorSystem::update(float deltaTime) +{ + // Update cooldown timers for all actuators + m_world.query().each( + [&](flecs::entity, ActuatorComponent &actuator) { + if (actuator.cooldownTimer > 0.0f) { + actuator.cooldownTimer -= 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 + if (!m_editorApp || + m_editorApp->getGameMode() != EditorApp::GameMode::Game || + m_editorApp->getGamePlayState() != + EditorApp::GamePlayState::Playing) + return; + + // Check if an action is currently executing + if (m_executingActuatorId != 0) { + flecs::entity character = m_world.entity(m_executingCharacterId); + if (isActionComplete(character)) { + // 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() + .each([&](flecs::entity, + PlayerControllerComponent &pc) { + cooldown = pc.actuatorCooldown; + }); + ac.cooldownTimer = cooldown; + } + m_executingActuatorId = 0; + m_executingCharacterId = 0; + + // 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; + } + } + + // Find player character + flecs::entity playerCharacter = flecs::entity::null(); + float actuatorDistance = 25.0f; + float actuatorCooldown = 1.5f; + + 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) { + if (!playerCharacter.is_alive() && + en.name == pc.targetCharacterName) + playerCharacter = ec; + }); + actuatorDistance = pc.actuatorDistance; + actuatorCooldown = pc.actuatorCooldown; + } + }); + + if (!playerCharacter.is_alive() || + !playerCharacter.has()) + return; + + Ogre::Vector3 charPos = + playerCharacter.get().node + ->_getDerivedPosition(); + + // Collect actuators within distance + std::vector visibleActuators; + std::vector inRangeActuators; + + m_world.query() + .each([&](flecs::entity e, ActuatorComponent &actuator, + TransformComponent &trans) { + // Skip if on cooldown or executing + if (actuator.cooldownTimer > 0.0f || actuator.isExecuting) + return; + + if (!trans.node) + return; + + Ogre::Vector3 objPos = trans.node->_getDerivedPosition(); + float dist = charPos.distance(objPos); + if (dist > actuatorDistance) + return; + + Ogre::Vector2 screenPos = projectToScreen(objPos); + if (screenPos.x < 0) + return; + + ScreenActuator sa; + 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); + + if (isInRange(charPos, objPos, actuator.radius, + actuator.height)) { + inRangeActuators.push_back(sa); + } + }); + + // Determine target actuator for interaction + const ScreenActuator *target = nullptr; + 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 < + inRangeActuators[bestIdx].distToScreenCenter) { + 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"; + } + } + + // Draw markers + drawActuatorMarkers(visibleActuators, target, labelText); + + // Handle input + GameInputState &input = m_editorApp->getGameInputState(); + + // Handle menu state first + if (m_menuOpen) { + flecs::entity menuActuator = m_world.entity(m_menuActuatorId); + drawActionMenu(menuActuator); + + if (!m_pendingActionName.empty()) { + // Action selected from menu + executeAction(playerCharacter, menuActuator, + m_pendingActionName); + m_pendingActionName.clear(); + m_menuOpen = false; + m_menuActuatorId = 0; + m_eHoldTime = 0.0f; + m_eWasHeld = false; + if (m_editorApp) + m_editorApp->setWindowGrab(true); + } + + // Also close menu if E is released without selection + if (!input.e && m_eWasHeld) { + m_menuOpen = false; + m_menuActuatorId = 0; + m_eHoldTime = 0.0f; + m_eWasHeld = false; + if (m_editorApp) + m_editorApp->setWindowGrab(true); + } + return; + } + + // Handle E key + if (target && input.e) { + auto &actuator = target->entity.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, + 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_eWasHeld = true; + if (m_editorApp) + m_editorApp->setWindowGrab(false); + } + } + } else { + m_eHoldTime = 0.0f; + m_eWasHeld = false; + } +} diff --git a/src/features/editScene/systems/ActuatorSystem.hpp b/src/features/editScene/systems/ActuatorSystem.hpp new file mode 100644 index 0000000..d815d98 --- /dev/null +++ b/src/features/editScene/systems/ActuatorSystem.hpp @@ -0,0 +1,66 @@ +#ifndef EDITSCENE_ACTUATOR_SYSTEM_HPP +#define EDITSCENE_ACTUATOR_SYSTEM_HPP +#pragma once + +#include +#include +#include +#include + +class EditorApp; +class BehaviorTreeSystem; + +/** + * System that handles player interaction with Actuator entities. + * + * In game mode: + * - Draws on-screen circle markers for actuators within range + * - Shows "E - ActionName" (or just "E" for multi-action) when in reach + * - Handles E press / hold for action activation + * - Manages per-actuator cooldowns + */ +class ActuatorSystem { +public: + ActuatorSystem(flecs::world &world, Ogre::SceneManager *sceneMgr, + EditorApp *editorApp, BehaviorTreeSystem *btSystem); + ~ActuatorSystem(); + + void update(float deltaTime); + +private: + struct ScreenActuator { + flecs::entity entity; + Ogre::Vector2 screenPos; + float distance; + float distToScreenCenter; + }; + + Ogre::Vector2 projectToScreen(const Ogre::Vector3 &worldPoint); + bool isInRange(const Ogre::Vector3 &charPos, + 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); + void drawActionMenu(flecs::entity actuatorEntity); + + flecs::world &m_world; + Ogre::SceneManager *m_sceneMgr; + EditorApp *m_editorApp; + BehaviorTreeSystem *m_btSystem; + + // Multi-action menu state + bool m_menuOpen = false; + flecs::entity_t m_menuActuatorId = 0; + float m_eHoldTime = 0.0f; + bool m_eWasHeld = false; + Ogre::String m_pendingActionName; + + // Currently executing action state + flecs::entity_t m_executingActuatorId = 0; + flecs::entity_t m_executingCharacterId = 0; +}; + +#endif // EDITSCENE_ACTUATOR_SYSTEM_HPP diff --git a/src/features/editScene/systems/SceneSerializer.cpp b/src/features/editScene/systems/SceneSerializer.cpp index 1f72b14..b999492 100644 --- a/src/features/editScene/systems/SceneSerializer.cpp +++ b/src/features/editScene/systems/SceneSerializer.cpp @@ -31,6 +31,7 @@ #include "../components/ActionDatabase.hpp" #include "../components/ActionDebug.hpp" #include "../components/SmartObject.hpp" +#include "../components/Actuator.hpp" #include "../components/GoapPlanner.hpp" #include "../components/PathFollowing.hpp" #include "../components/BehaviorTree.hpp" @@ -303,6 +304,9 @@ nlohmann::json SceneSerializer::serializeEntity(flecs::entity entity) if (entity.has()) { json["smartObject"] = serializeSmartObject(entity); } + if (entity.has()) { + json["actuator"] = serializeActuator(entity); + } if (entity.has()) { json["goapPlanner"] = serializeGoapPlanner(entity); } @@ -509,6 +513,9 @@ void SceneSerializer::deserializeEntity(const nlohmann::json &json, if (json.contains("smartObject")) { deserializeSmartObject(entity, json["smartObject"]); } + if (json.contains("actuator")) { + deserializeActuator(entity, json["actuator"]); + } if (json.contains("goapPlanner")) { deserializeGoapPlanner(entity, json["goapPlanner"]); } @@ -791,6 +798,9 @@ void SceneSerializer::deserializeEntityComponents( if (json.contains("smartObject")) { deserializeSmartObject(entity, json["smartObject"]); } + if (json.contains("actuator")) { + deserializeActuator(entity, json["actuator"]); + } if (json.contains("goapPlanner")) { deserializeGoapPlanner(entity, json["goapPlanner"]); } @@ -3641,3 +3651,30 @@ void SceneSerializer::deserializeNavMeshGeometrySource( src.include = json.value("include", true); entity.set(src); } + + +nlohmann::json SceneSerializer::serializeActuator(flecs::entity entity) +{ + const ActuatorComponent &actuator = entity.get(); + nlohmann::json json; + json["radius"] = actuator.radius; + json["height"] = actuator.height; + json["actionNames"] = actuator.actionNames; + return json; +} + +void SceneSerializer::deserializeActuator(flecs::entity entity, + const nlohmann::json &json) +{ + ActuatorComponent actuator; + actuator.radius = json.value("radius", 1.5f); + actuator.height = json.value("height", 1.8f); + if (json.contains("actionNames") && json["actionNames"].is_array()) { + actuator.actionNames.clear(); + for (const auto &name : json["actionNames"]) { + if (name.is_string()) + actuator.actionNames.push_back(name); + } + } + entity.set(actuator); +} diff --git a/src/features/editScene/systems/SceneSerializer.hpp b/src/features/editScene/systems/SceneSerializer.hpp index e8ae2f4..bb7974c 100644 --- a/src/features/editScene/systems/SceneSerializer.hpp +++ b/src/features/editScene/systems/SceneSerializer.hpp @@ -210,6 +210,7 @@ private: nlohmann::json serializeActionDebug(flecs::entity entity); nlohmann::json serializePathFollowing(flecs::entity entity); nlohmann::json serializeSmartObject(flecs::entity entity); + nlohmann::json serializeActuator(flecs::entity entity); nlohmann::json serializeGoapPlanner(flecs::entity entity); nlohmann::json serializeBehaviorTree(flecs::entity entity); void deserializeActionDatabase(flecs::entity entity, @@ -220,6 +221,8 @@ private: const nlohmann::json &json); void deserializeSmartObject(flecs::entity entity, const nlohmann::json &json); + void deserializeActuator(flecs::entity entity, + const nlohmann::json &json); void deserializeGoapPlanner(flecs::entity entity, const nlohmann::json &json); void deserializeBehaviorTree(flecs::entity entity, diff --git a/src/features/editScene/ui/ActuatorEditor.cpp b/src/features/editScene/ui/ActuatorEditor.cpp new file mode 100644 index 0000000..a8dd5f2 --- /dev/null +++ b/src/features/editScene/ui/ActuatorEditor.cpp @@ -0,0 +1,45 @@ +#include "ActuatorEditor.hpp" +#include + +bool ActuatorEditor::renderComponent(flecs::entity entity, + ActuatorComponent &actuator) +{ + (void)entity; + bool modified = false; + + 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::Separator(); + ImGui::Text("Actions:"); + + 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; + } + ImGui::SameLine(); + if (ImGui::SmallButton("X")) + removeIdx = i; + 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; + } + + return modified; +} diff --git a/src/features/editScene/ui/ActuatorEditor.hpp b/src/features/editScene/ui/ActuatorEditor.hpp new file mode 100644 index 0000000..93948dd --- /dev/null +++ b/src/features/editScene/ui/ActuatorEditor.hpp @@ -0,0 +1,20 @@ +#ifndef EDITSCENE_ACTUATOR_EDITOR_HPP +#define EDITSCENE_ACTUATOR_EDITOR_HPP +#pragma once + +#include "ComponentEditor.hpp" +#include "../components/Actuator.hpp" + +class ActuatorEditor : public ComponentEditor { +public: + const char *getName() const override + { + return "Actuator"; + } + +protected: + bool renderComponent(flecs::entity entity, + ActuatorComponent &actuator) override; +}; + +#endif // EDITSCENE_ACTUATOR_EDITOR_HPP diff --git a/src/features/editScene/ui/PlayerControllerEditor.cpp b/src/features/editScene/ui/PlayerControllerEditor.cpp index c0f6136..ebac8ff 100644 --- a/src/features/editScene/ui/PlayerControllerEditor.cpp +++ b/src/features/editScene/ui/PlayerControllerEditor.cpp @@ -107,6 +107,23 @@ bool PlayerControllerEditor::renderComponent(flecs::entity entity, modified = true; } + ImGui::Separator(); + ImGui::Text("Actuator Interaction"); + + if (ImGui::DragFloat("Actuator Distance", &pc.actuatorDistance, + 0.5f, 1.0f, 200.0f)) + modified = true; + if (ImGui::DragFloat("Actuator Cooldown", &pc.actuatorCooldown, + 0.1f, 0.0f, 10.0f)) + modified = true; + float color[3] = { pc.actuatorColor.x, pc.actuatorColor.y, + pc.actuatorColor.z }; + if (ImGui::ColorEdit3("Actuator Color", color)) { + pc.actuatorColor = Ogre::Vector3(color[0], color[1], + color[2]); + modified = true; + } + ImGui::Unindent(); }