From c80d9c96e6094102be18543b3a315e865710ce5a Mon Sep 17 00:00:00 2001 From: Sergey Lapin Date: Mon, 27 Apr 2026 05:24:45 +0300 Subject: [PATCH] AI motion refactoring --- src/features/editScene/CMakeLists.txt | 18 + src/features/editScene/EditorApp.cpp | 48 +++ src/features/editScene/EditorApp.hpp | 6 + .../editScene/components/ActionDebug.hpp | 60 +-- .../editScene/components/GoapAction.hpp | 33 +- .../editScene/components/GoapBlackboard.cpp | 30 +- .../editScene/components/GoapBlackboard.hpp | 5 + .../editScene/components/GoapPlanner.hpp | 99 +++++ .../components/GoapPlannerModule.cpp | 21 + .../editScene/components/GoapRunner.hpp | 50 +++ .../editScene/components/GoapRunnerModule.cpp | 21 + .../editScene/components/PathFollowing.hpp | 77 ++++ .../components/PathFollowingModule.cpp | 21 + src/features/editScene/gizmo/Cursor3D.cpp | 393 ++++++++++++------ src/features/editScene/gizmo/Cursor3D.hpp | 88 ++-- .../editScene/systems/EditorUISystem.cpp | 151 ++++++- .../editScene/systems/EditorUISystem.hpp | 29 +- .../editScene/systems/GoapPlannerSystem.cpp | 266 ++++++++++++ .../editScene/systems/GoapPlannerSystem.hpp | 68 +++ .../editScene/systems/GoapRunnerSystem.cpp | 325 +++++++++++++++ .../editScene/systems/GoapRunnerSystem.hpp | 68 +++ .../editScene/systems/PathFollowingSystem.cpp | 205 +++++++++ .../editScene/systems/PathFollowingSystem.hpp | 45 ++ .../editScene/systems/PrefabSystem.cpp | 35 +- .../editScene/systems/PrefabSystem.hpp | 7 + .../editScene/systems/SceneSerializer.cpp | 136 ++++-- .../editScene/systems/SceneSerializer.hpp | 6 + .../editScene/systems/SmartObjectSystem.cpp | 50 ++- .../editScene/systems/SmartObjectSystem.hpp | 7 + .../editScene/ui/ActionDatabaseEditor.cpp | 33 ++ .../editScene/ui/ActionDebugEditor.cpp | 234 +++++------ .../editScene/ui/ActionDebugEditor.hpp | 3 +- .../editScene/ui/GoapPlannerEditor.cpp | 296 +++++++++++++ .../editScene/ui/GoapPlannerEditor.hpp | 37 ++ .../editScene/ui/GoapRunnerEditor.cpp | 99 +++++ .../editScene/ui/GoapRunnerEditor.hpp | 26 ++ .../editScene/ui/PathFollowingEditor.cpp | 103 +++++ .../editScene/ui/PathFollowingEditor.hpp | 23 + 38 files changed, 2789 insertions(+), 433 deletions(-) create mode 100644 src/features/editScene/components/GoapPlanner.hpp create mode 100644 src/features/editScene/components/GoapPlannerModule.cpp create mode 100644 src/features/editScene/components/GoapRunner.hpp create mode 100644 src/features/editScene/components/GoapRunnerModule.cpp create mode 100644 src/features/editScene/components/PathFollowing.hpp create mode 100644 src/features/editScene/components/PathFollowingModule.cpp create mode 100644 src/features/editScene/systems/GoapPlannerSystem.cpp create mode 100644 src/features/editScene/systems/GoapPlannerSystem.hpp create mode 100644 src/features/editScene/systems/GoapRunnerSystem.cpp create mode 100644 src/features/editScene/systems/GoapRunnerSystem.hpp create mode 100644 src/features/editScene/systems/PathFollowingSystem.cpp create mode 100644 src/features/editScene/systems/PathFollowingSystem.hpp create mode 100644 src/features/editScene/ui/GoapPlannerEditor.cpp create mode 100644 src/features/editScene/ui/GoapPlannerEditor.hpp create mode 100644 src/features/editScene/ui/GoapRunnerEditor.cpp create mode 100644 src/features/editScene/ui/GoapRunnerEditor.hpp create mode 100644 src/features/editScene/ui/PathFollowingEditor.cpp create mode 100644 src/features/editScene/ui/PathFollowingEditor.hpp diff --git a/src/features/editScene/CMakeLists.txt b/src/features/editScene/CMakeLists.txt index 5be6948..c395f43 100644 --- a/src/features/editScene/CMakeLists.txt +++ b/src/features/editScene/CMakeLists.txt @@ -46,6 +46,15 @@ set(EDITSCENE_SOURCES systems/SmartObjectSystem.cpp components/SmartObjectModule.cpp ui/SmartObjectEditor.cpp + components/GoapPlannerModule.cpp + systems/GoapRunnerSystem.cpp + systems/PathFollowingSystem.cpp + systems/GoapPlannerSystem.cpp + components/GoapRunnerModule.cpp + components/PathFollowingModule.cpp + ui/GoapRunnerEditor.cpp + ui/PathFollowingEditor.cpp + ui/GoapPlannerEditor.cpp systems/PrefabSystem.cpp ui/PrefabInstanceEditor.cpp @@ -173,7 +182,16 @@ set(EDITSCENE_HEADERS systems/SmartObjectSystem.hpp components/SmartObject.hpp ui/SmartObjectEditor.hpp + components/GoapPlanner.hpp + components/PathFollowing.hpp + components/GoapRunner.hpp + ui/GoapPlannerEditor.hpp + ui/GoapRunnerEditor.hpp + ui/PathFollowingEditor.hpp systems/PrefabSystem.hpp + systems/GoapRunnerSystem.hpp + systems/PathFollowingSystem.hpp + systems/GoapPlannerSystem.hpp components/PrefabInstance.hpp ui/PrefabInstanceEditor.hpp diff --git a/src/features/editScene/EditorApp.cpp b/src/features/editScene/EditorApp.cpp index b01c989..64debf5 100644 --- a/src/features/editScene/EditorApp.cpp +++ b/src/features/editScene/EditorApp.cpp @@ -19,6 +19,9 @@ #include "systems/NavMeshSystem.hpp" #include "systems/CharacterSystem.hpp" #include "systems/SmartObjectSystem.hpp" +#include "systems/GoapRunnerSystem.hpp" +#include "systems/PathFollowingSystem.hpp" +#include "systems/GoapPlannerSystem.hpp" #include "systems/CellGridSystem.hpp" #include "systems/NormalDebugSystem.hpp" @@ -66,6 +69,9 @@ #include "systems/PrefabSystem.hpp" #include "components/NavMesh.hpp" #include "components/SmartObject.hpp" +#include "components/GoapPlanner.hpp" +#include "components/GoapRunner.hpp" +#include "components/PathFollowing.hpp" #include #include @@ -237,6 +243,7 @@ void EditorApp::setup() if (m_uiSystem) m_uiSystem->setEditorUIEnabled(m_gameMode == GameMode::Editor); + m_uiSystem->setEditorCamera(m_camera.get()); // Setup physics system m_physicsSystem = std::make_unique( @@ -335,6 +342,25 @@ void EditorApp::setup() // Wire up EditorApp for game mode detection m_smartObjectSystem->setEditorApp(this); + // Setup GOAP Runner system + m_goapRunnerSystem = std::make_unique( + m_world, m_sceneMgr, m_smartObjectSystem.get(), + m_behaviorTreeSystem.get(), m_navMeshSystem.get()); + m_goapRunnerSystem->setEditorApp(this); + m_goapRunnerSystem->setAnimationTreeSystem( + m_animationTreeSystem.get()); + + // Setup GOAP Planner system + m_goapPlannerSystem = std::make_unique( + m_world); + m_goapPlannerSystem->setEditorApp(this); + + // Setup Path Following system + m_pathFollowingSystem = std::make_unique( + m_world, m_sceneMgr, m_navMeshSystem.get()); + m_pathFollowingSystem->setAnimationTreeSystem( + m_animationTreeSystem.get()); + // Setup CellGrid system m_cellGridSystem = std::make_unique(m_world, m_sceneMgr); @@ -575,6 +601,15 @@ void EditorApp::setupECS() // Register Smart Object component m_world.component(); + // Register GOAP Planner component + m_world.component(); + + // Register GOAP Runner component + m_world.component(); + + // Register Path Following component + m_world.component(); + // Register Navigation components m_world.component(); m_world.component(); @@ -733,6 +768,9 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt) if (m_behaviorTreeSystem) m_behaviorTreeSystem->update(evt.timeSinceLastFrame); } + if (m_pathFollowingSystem) { + m_pathFollowingSystem->update(evt.timeSinceLastFrame); + } if (m_proceduralMeshSystem) { m_proceduralMeshSystem->update(); } @@ -760,6 +798,16 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt) m_smartObjectSystem->update(evt.timeSinceLastFrame); } + /* --- GOAP Planner system (plan generation) --- */ + if (m_goapPlannerSystem) { + m_goapPlannerSystem->update(evt.timeSinceLastFrame); + } + + /* --- GOAP Runner system (plan execution) --- */ + if (m_goapRunnerSystem) { + m_goapRunnerSystem->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 d2c5c24..89834f7 100644 --- a/src/features/editScene/EditorApp.hpp +++ b/src/features/editScene/EditorApp.hpp @@ -36,6 +36,9 @@ class EditorSkyboxSystem; class EditorWaterPlaneSystem; class NormalDebugSystem; class SmartObjectSystem; +class GoapRunnerSystem; +class PathFollowingSystem; +class GoapPlannerSystem; class EditorApp; /** @@ -222,6 +225,9 @@ private: std::unique_ptr m_normalDebugSystem; std::unique_ptr m_roomLayoutSystem; std::unique_ptr m_smartObjectSystem; + std::unique_ptr m_goapRunnerSystem; + std::unique_ptr m_pathFollowingSystem; + std::unique_ptr m_goapPlannerSystem; // Game systems diff --git a/src/features/editScene/components/ActionDebug.hpp b/src/features/editScene/components/ActionDebug.hpp index c0b0a91..e964265 100644 --- a/src/features/editScene/components/ActionDebug.hpp +++ b/src/features/editScene/components/ActionDebug.hpp @@ -7,37 +7,13 @@ #include #include -/** - * Configuration for one path following animation state. - * - * Each state (e.g. "idle", "walk", "run", "swim-idle", "swim", "swim-fast") - * consists of unlimited name-value pairs where: - * - name = state machine name (e.g. "main", "locomotion") - * - value = state name within that machine (e.g. "Idle", "Walking") - * - * When a path following state is activated, ALL its name-value pairs - * are applied via AnimationTreeSystem::setState(). - */ -struct PathFollowingState { - /** Logical name (e.g. "idle", "walk", "run", "swim-idle", "swim", "swim-fast") */ - Ogre::String name; - - /** State machine name -> state name pairs */ - std::vector > stateMachineStates; - - PathFollowingState() = default; - - PathFollowingState(const Ogre::String &name_) - : name(name_) - { - } -}; - /** * Per-character action debug component. * * Allows test-running individual actions and inspecting the character's * local blackboard state. Used for debugging AI behavior in the editor. + * + * Path following animation states have been moved to PathFollowingComponent. */ struct ActionDebug { // Character's local GOAP blackboard @@ -57,39 +33,7 @@ struct ActionDebug { // Debug output Ogre::String lastResult; - // --- Path following animation states --- - // Each state has unlimited state machine name -> state name pairs. - // Default entries: idle, walk, run - std::vector pathFollowingStates = { - { "idle" }, - { "walk" }, - { "run" }, - }; - - // Walk speed (m/s) used for root motion scaling - float walkSpeed = 2.5f; - - // Run speed (m/s) used for root motion scaling - float runSpeed = 5.0f; - - // Whether to use root motion (true) or manual velocity (false) - bool useRootMotion = true; - ActionDebug() = default; - - /** - * Get the state machine/state pairs for a given path following state name. - * Returns nullptr if not found. - */ - const PathFollowingState * - findPathState(const Ogre::String &stateName) const - { - for (const auto &state : pathFollowingStates) { - if (state.name == stateName) - return &state; - } - return nullptr; - } }; #endif // EDITSCENE_ACTION_DEBUG_HPP diff --git a/src/features/editScene/components/GoapAction.hpp b/src/features/editScene/components/GoapAction.hpp index be8d87c..1d05cd3 100644 --- a/src/features/editScene/components/GoapAction.hpp +++ b/src/features/editScene/components/GoapAction.hpp @@ -21,6 +21,10 @@ struct GoapAction { GoapBlackboard preconditions; GoapBlackboard effects; + // Bitmask for precondition checking. Only bits set here are compared. + // Defaults to all 1s (check all bits). + uint64_t preconditionMask = ~0ULL; + // Behavior tree to execute when this action is selected BehaviorTreeNode behaviorTree; @@ -35,10 +39,35 @@ struct GoapAction { { } - // Check if the given blackboard satisfies this action's preconditions + // Check if the given blackboard satisfies this action's preconditions. + // Only bits in preconditionMask are compared. bool canRun(const GoapBlackboard &blackboard) const { - return blackboard.satisfies(preconditions); + // Fast-path: if mask covers all set bits, use standard check + if (preconditionMask == ~0ULL) + return blackboard.satisfies(preconditions); + + // Masked check: only compare bits in preconditionMask + uint64_t relevantBits = preconditionMask; + uint64_t bbBits = blackboard.bits & relevantBits; + uint64_t preBits = preconditions.bits & relevantBits; + uint64_t bbMask = blackboard.mask & relevantBits; + uint64_t preMask = preconditions.mask & relevantBits; + + // All precondition bits must be present in blackboard + if ((bbMask & preMask) != preMask) + return false; + if ((bbBits & preMask) != preBits) + return false; + + // Check integer values + for (const auto &kv : preconditions.values) { + if (!blackboard.hasValue(kv.first)) + return false; + if (blackboard.getValue(kv.first) != kv.second) + return false; + } + return true; } }; diff --git a/src/features/editScene/components/GoapBlackboard.cpp b/src/features/editScene/components/GoapBlackboard.cpp index 3b4d553..4a5253b 100644 --- a/src/features/editScene/components/GoapBlackboard.cpp +++ b/src/features/editScene/components/GoapBlackboard.cpp @@ -42,12 +42,17 @@ int GoapBlackboard::findBitByName(const std::string &name) bool GoapBlackboard::satisfies(const GoapBlackboard &other) const { - // Check bits: for every bit set in other's mask, our bit must match - uint64_t commonMask = mask & other.mask; + // Only compare bits that both sides consider relevant + uint64_t relevantMask = bitmask & other.bitmask; + + // Check bits: for every bit set in other's mask (within relevant mask), + // our bit must match + uint64_t commonMask = mask & other.mask & relevantMask; if ((bits & commonMask) != (other.bits & commonMask)) return false; - // Also check bits that other has set but we don't - uint64_t missingMask = other.mask & ~mask; + + // Also check bits that other has set but we don't (within relevant mask) + uint64_t missingMask = other.mask & ~mask & relevantMask; if (missingMask) { // Other requires bits we don't have set -> fail // But only if other has those bits set to 1 @@ -97,20 +102,23 @@ bool GoapBlackboard::getScalarValue(const std::string &key, } int GoapBlackboard::distanceTo(const GoapBlackboard &target, - bool ignoreValues) const + bool ignoreValues) const { int distance = 0; - // Bit differences - uint64_t commonMask = mask & target.mask; + // Only compare bits that both sides consider relevant + uint64_t relevantMask = bitmask & target.bitmask; + + // Bit differences (within relevant mask) + uint64_t commonMask = mask & target.mask & relevantMask; distance += __builtin_popcountll((bits ^ target.bits) & commonMask); - // Bits target cares about but we don't have - uint64_t missingInUs = target.mask & ~mask; + // Bits target cares about but we don't have (within relevant mask) + uint64_t missingInUs = target.mask & ~mask & relevantMask; distance += __builtin_popcountll(target.bits & missingInUs); - // Bits we care about but target doesn't (we may need to unset them) - uint64_t missingInTarget = mask & ~target.mask; + // Bits we care about but target doesn't (within relevant mask) + uint64_t missingInTarget = mask & ~target.mask & relevantMask; distance += __builtin_popcountll(bits & missingInTarget); if (ignoreValues) diff --git a/src/features/editScene/components/GoapBlackboard.hpp b/src/features/editScene/components/GoapBlackboard.hpp index 378beed..fdd32e6 100644 --- a/src/features/editScene/components/GoapBlackboard.hpp +++ b/src/features/editScene/components/GoapBlackboard.hpp @@ -22,6 +22,10 @@ struct GoapBlackboard { uint64_t bits = 0; uint64_t mask = 0; // which bits are actually set + // Bitmask for comparison: only bits set here are compared. + // Defaults to all 1s (compare all bits). + uint64_t bitmask = ~0ULL; + // Named integer values (health, hunger, etc.) — used by preconditions/effects std::unordered_map values; @@ -184,6 +188,7 @@ struct GoapBlackboard { bool operator==(const GoapBlackboard &other) const { return bits == other.bits && mask == other.mask && + bitmask == other.bitmask && values == other.values && floatValues == other.floatValues && vec3Values == other.vec3Values; diff --git a/src/features/editScene/components/GoapPlanner.hpp b/src/features/editScene/components/GoapPlanner.hpp new file mode 100644 index 0000000..26fabcc --- /dev/null +++ b/src/features/editScene/components/GoapPlanner.hpp @@ -0,0 +1,99 @@ +#ifndef EDITSCENE_GOAP_PLANNER_HPP +#define EDITSCENE_GOAP_PLANNER_HPP +#pragma once + +#include +#include +#include +#include + +/** + * GOAP Planner component. + * + * Holds a curated list of action and goal names from an ActionDatabase, + * plus configuration for smart-object action discovery. + * The planner resolves names against an ActionDatabase at runtime. + * + * The actionNames and goalNames lists act as external references: + * prefabs can store them even when the ActionDatabase is not + * part of the prefab itself. + */ +struct GoapPlannerComponent { + // Selected action names from ActionDatabase + std::vector actionNames; + + // Selected goal names from ActionDatabase + std::vector goalNames; + + // Maximum distance to search for smart objects with matching actions + float smartObjectDistance = 50.0f; + + // Whether to include smart object actions in planning + bool includeSmartObjects = true; + + // Optional reference to an external ActionDatabase entity by name. + Ogre::String actionDatabaseRef; + + // ----------------------------------------------------------------- + // Runtime plan queue (not serialized) + // ----------------------------------------------------------------- + struct Plan { + std::vector actions; + int totalCost = 0; + Ogre::String goalName; + }; + std::vector planQueue; + + // Planner status + enum class Status { + Idle, // No planning requested + Planning, // Currently planning + PlansAvailable, // One or more plans in queue + NoPlanFound // Planning finished but no valid plan found + }; + Status status = Status::Idle; + + // Goal name that was used for the current plan batch + Ogre::String currentGoalName; + + // Planning control + bool planDirty = true; + int maxPlans = 3; // stop after generating this many plans + + // Planning progress (for status display) + int plansGenerated = 0; + int nodesExplored = 0; + + GoapPlannerComponent() = default; + + void clearPlans() + { + planQueue.clear(); + plansGenerated = 0; + status = Status::Idle; + } + + // Pop the cheapest plan from the queue + Plan popCheapestPlan() + { + if (planQueue.empty()) + return Plan(); + size_t bestIdx = 0; + for (size_t i = 1; i < planQueue.size(); i++) { + if (planQueue[i].totalCost < planQueue[bestIdx].totalCost) + bestIdx = i; + } + Plan result = std::move(planQueue[bestIdx]); + planQueue.erase(planQueue.begin() + bestIdx); + if (planQueue.empty() && status == Status::PlansAvailable) + status = Status::Idle; + return result; + } + + bool hasPlans() const + { + return !planQueue.empty(); + } +}; + +#endif // EDITSCENE_GOAP_PLANNER_HPP diff --git a/src/features/editScene/components/GoapPlannerModule.cpp b/src/features/editScene/components/GoapPlannerModule.cpp new file mode 100644 index 0000000..fe820a8 --- /dev/null +++ b/src/features/editScene/components/GoapPlannerModule.cpp @@ -0,0 +1,21 @@ +#include "GoapPlanner.hpp" +#include "../ui/ComponentRegistration.hpp" +#include "../ui/GoapPlannerEditor.hpp" + +REGISTER_COMPONENT_GROUP("GOAP Planner", "AI", GoapPlannerComponent, + GoapPlannerEditor) +{ + registry.registerComponent( + "GOAP Planner", "AI", + 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/GoapRunner.hpp b/src/features/editScene/components/GoapRunner.hpp new file mode 100644 index 0000000..fc05933 --- /dev/null +++ b/src/features/editScene/components/GoapRunner.hpp @@ -0,0 +1,50 @@ +#ifndef EDITSCENE_GOAP_RUNNER_HPP +#define EDITSCENE_GOAP_RUNNER_HPP +#pragma once + +#include +#include +#include + +/** + * GOAP Runner component. + * + * Executes plans from GoapPlannerComponent. + * For normal actions: runs the action's behavior tree. + * For smart object actions: pathfinds to the smart object and executes. + * After plan completion, marks the planner dirty for replanning. + */ +struct GoapRunnerComponent { + // Current plan execution state + enum class State { + Idle, // No plan running + RunningAction, // Executing a normal action + MovingToSmartObject, // Pathfinding to a smart object + ExecutingSmartObject, // Executing smart object action + PlanComplete // Plan finished, waiting for replan + }; + + State state = State::Idle; + + // Index of current action in the plan + int currentActionIndex = 0; + + // Name of the currently executing action + Ogre::String currentActionName; + + // Timer for action execution + float actionTimer = 0.0f; + + // Entity ID of target smart object (if applicable) + uint64_t targetSmartObjectId = 0; + + // Active plan actions (copied from planner when plan starts) + std::vector planActions; + + // Whether to auto-replan after completion + bool autoReplan = true; + + GoapRunnerComponent() = default; +}; + +#endif // EDITSCENE_GOAP_RUNNER_HPP diff --git a/src/features/editScene/components/GoapRunnerModule.cpp b/src/features/editScene/components/GoapRunnerModule.cpp new file mode 100644 index 0000000..52fafff --- /dev/null +++ b/src/features/editScene/components/GoapRunnerModule.cpp @@ -0,0 +1,21 @@ +#include "GoapRunner.hpp" +#include "../ui/ComponentRegistration.hpp" +#include "../ui/GoapRunnerEditor.hpp" + +REGISTER_COMPONENT_GROUP("GOAP Runner", "AI", GoapRunnerComponent, + GoapRunnerEditor) +{ + registry.registerComponent( + "GOAP Runner", "AI", + 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/PathFollowing.hpp b/src/features/editScene/components/PathFollowing.hpp new file mode 100644 index 0000000..dd3f2e5 --- /dev/null +++ b/src/features/editScene/components/PathFollowing.hpp @@ -0,0 +1,77 @@ +#ifndef EDITSCENE_PATH_FOLLOWING_HPP +#define EDITSCENE_PATH_FOLLOWING_HPP +#pragma once + +#include +#include +#include +#include + +/** + * Animation state configuration for path following. + * + * Each state (e.g. "idle", "walk", "run") consists of unlimited + * state-machine-name -> state-name pairs applied via AnimationTreeSystem. + */ +struct PathFollowingState { + Ogre::String name; + std::vector > stateMachineStates; + + PathFollowingState() = default; + explicit PathFollowingState(const Ogre::String &name_) + : name(name_) + { + } +}; + +/** + * Path Following component. + * + * Configures animation states for locomotion (idle/walk/run) + * and stores the current locomotion state. Used by GoapRunner + * to animate characters during plan execution. + */ +struct PathFollowingComponent { + // Animation state configurations + std::vector pathFollowingStates = { + PathFollowingState("idle"), + PathFollowingState("walk"), + PathFollowingState("run"), + }; + + // Current locomotion state name + Ogre::String currentLocomotionState = "idle"; + + // Walk speed (m/s) for root motion scaling + float walkSpeed = 2.5f; + + // Run speed (m/s) for root motion scaling + float runSpeed = 5.0f; + + // Whether to use root motion + bool useRootMotion = true; + + // Target position for path following (set by GoapRunner) + Ogre::Vector3 targetPosition = Ogre::Vector3::ZERO; + + // Whether we have an active target + bool hasTarget = false; + + // Path waypoints (set by GoapRunner, followed by PathFollowingSystem) + std::vector path; + int pathIndex = 0; + float pathRecalcTimer = 0.0f; + + PathFollowingComponent() = default; + + const PathFollowingState *findState(const Ogre::String &name) const + { + for (const auto &state : pathFollowingStates) { + if (state.name == name) + return &state; + } + return nullptr; + } +}; + +#endif // EDITSCENE_PATH_FOLLOWING_HPP diff --git a/src/features/editScene/components/PathFollowingModule.cpp b/src/features/editScene/components/PathFollowingModule.cpp new file mode 100644 index 0000000..52a476f --- /dev/null +++ b/src/features/editScene/components/PathFollowingModule.cpp @@ -0,0 +1,21 @@ +#include "PathFollowing.hpp" +#include "../ui/ComponentRegistration.hpp" +#include "../ui/PathFollowingEditor.hpp" + +REGISTER_COMPONENT_GROUP("Path Following", "AI", PathFollowingComponent, + PathFollowingEditor) +{ + registry.registerComponent( + "Path Following", "AI", + 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/gizmo/Cursor3D.cpp b/src/features/editScene/gizmo/Cursor3D.cpp index 0c01c5c..562b712 100644 --- a/src/features/editScene/gizmo/Cursor3D.cpp +++ b/src/features/editScene/gizmo/Cursor3D.cpp @@ -2,38 +2,68 @@ #include "../components/Transform.hpp" #include -// Colors for cursor - bright cyan/white for visibility -static const float COLOR_CYAN[3] = { 0.0f, 1.0f, 1.0f }; -static const float COLOR_WHITE[3] = { 1.0f, 1.0f, 1.0f }; static const float COLOR_RED[3] = { 1.0f, 0.2f, 0.2f }; static const float COLOR_GREEN[3] = { 0.2f, 1.0f, 0.2f }; static const float COLOR_BLUE[3] = { 0.2f, 0.4f, 1.0f }; +static const float COLOR_CYAN[3] = { 0.0f, 1.0f, 1.0f }; +static const float COLOR_YELLOW[3] = { 1.0f, 1.0f, 0.0f }; + +static bool projectRayOntoAxis(const Ogre::Ray &ray, + const Ogre::Vector3 &axisOrigin, + const Ogre::Vector3 &axisDir, float &outT) +{ + Ogre::Vector3 rayOrigin = ray.getOrigin(); + Ogre::Vector3 rayDir = ray.getDirection(); + Ogre::Vector3 w0 = rayOrigin - axisOrigin; + float a = rayDir.dotProduct(rayDir); + float b = rayDir.dotProduct(axisDir); + float c = axisDir.dotProduct(axisDir); + float d = rayDir.dotProduct(w0); + float e = axisDir.dotProduct(w0); + float denom = a * c - b * b; + if (std::abs(denom) < 0.0001f) + return false; + outT = (a * e - b * d) / denom; + return true; +} Cursor3D::Cursor3D(Ogre::SceneManager *sceneMgr) : m_sceneMgr(sceneMgr) , m_cursorNode(nullptr) - , m_axesObj(nullptr) - , m_markerObj(nullptr) + , m_axisX(nullptr) + , m_axisY(nullptr) + , m_axisZ(nullptr) + , m_centerMarker(nullptr) , m_position(Ogre::Vector3::ZERO) , m_orientation(Ogre::Quaternion::IDENTITY) , m_size(1.0f) , m_visible(false) + , m_mode(Mode::Translate) + , m_selectedAxis(Axis::None) + , m_hoveredAxis(Axis::None) + , m_isDragging(false) + , m_dragStartT(0.0f) + , m_dragStartAngle(0.0f) { m_cursorNode = m_sceneMgr->getRootSceneNode()->createChildSceneNode( "Cursor3DNode"); - m_axesObj = m_sceneMgr->createManualObject("Cursor3DAxes"); - m_markerObj = m_sceneMgr->createManualObject("Cursor3DMarker"); + m_axisX = m_sceneMgr->createManualObject("CursorAxisX"); + m_axisY = m_sceneMgr->createManualObject("CursorAxisY"); + m_axisZ = m_sceneMgr->createManualObject("CursorAxisZ"); + m_centerMarker = m_sceneMgr->createManualObject("CursorCenter"); - m_cursorNode->attachObject(m_axesObj); - m_cursorNode->attachObject(m_markerObj); + m_cursorNode->attachObject(m_axisX); + m_cursorNode->attachObject(m_axisY); + m_cursorNode->attachObject(m_axisZ); + m_cursorNode->attachObject(m_centerMarker); - // Draw on top of everything - m_axesObj->setRenderQueueGroup(Ogre::RENDER_QUEUE_OVERLAY); - m_markerObj->setRenderQueueGroup(Ogre::RENDER_QUEUE_OVERLAY); + m_axisX->setRenderQueueGroup(Ogre::RENDER_QUEUE_OVERLAY); + m_axisY->setRenderQueueGroup(Ogre::RENDER_QUEUE_OVERLAY); + m_axisZ->setRenderQueueGroup(Ogre::RENDER_QUEUE_OVERLAY); + m_centerMarker->setRenderQueueGroup(Ogre::RENDER_QUEUE_OVERLAY); m_cursorNode->setVisible(false); - createGeometry(); } @@ -41,35 +71,34 @@ void Cursor3D::shutdown() { if (!m_sceneMgr) return; - - if (m_cursorNode && m_axesObj) { - try { - m_cursorNode->detachObject(m_axesObj); - } catch (...) { + auto detach = [&](Ogre::ManualObject *obj) { + if (m_cursorNode && obj) { + try { + m_cursorNode->detachObject(obj); + } catch (...) { + } } - } - if (m_cursorNode && m_markerObj) { - try { - m_cursorNode->detachObject(m_markerObj); - } catch (...) { + }; + auto destroy = [&](Ogre::ManualObject *obj) { + if (obj) { + try { + m_sceneMgr->destroyManualObject(obj); + } catch (...) { + } } - } - - if (m_axesObj) { - try { - m_sceneMgr->destroyManualObject(m_axesObj); - } catch (...) { - } - m_axesObj = nullptr; - } - if (m_markerObj) { - try { - m_sceneMgr->destroyManualObject(m_markerObj); - } catch (...) { - } - m_markerObj = nullptr; - } - + }; + detach(m_axisX); + detach(m_axisY); + detach(m_axisZ); + detach(m_centerMarker); + destroy(m_axisX); + m_axisX = nullptr; + destroy(m_axisY); + m_axisY = nullptr; + destroy(m_axisZ); + m_axisZ = nullptr; + destroy(m_centerMarker); + m_centerMarker = nullptr; if (m_cursorNode) { try { m_sceneMgr->destroySceneNode(m_cursorNode); @@ -77,7 +106,6 @@ void Cursor3D::shutdown() } m_cursorNode = nullptr; } - m_sceneMgr = nullptr; } @@ -133,8 +161,7 @@ void Cursor3D::applyToTransform(TransformComponent &transform) const { if (!transform.node) return; - - Ogre::SceneNode *parent = static_cast( + Ogre::SceneNode *parent = static_cast( transform.node->getParent()); if (parent) { transform.position = parent->convertWorldToLocalPosition( @@ -149,22 +176,6 @@ void Cursor3D::applyToTransform(TransformComponent &transform) const transform.markChanged(); } -bool Cursor3D::hitTest(const Ogre::Ray &mouseRay) const -{ - if (!m_cursorNode || !m_visible) - return false; - - // Check if ray passes near cursor center - Ogre::Vector3 toCursor = m_position - mouseRay.getOrigin(); - float tca = toCursor.dotProduct(mouseRay.getDirection()); - if (tca < 0.01f) - return false; - - float d2 = toCursor.dotProduct(toCursor) - tca * tca; - float threshold = 0.3f * m_size; - return d2 <= threshold * threshold; -} - void Cursor3D::updateNodeTransform() { if (m_cursorNode) { @@ -178,36 +189,49 @@ void Cursor3D::createGeometry() float len = 0.5f * m_size; float half = 0.05f * m_size; - // Axes - short lines in RGB - m_axesObj->clear(); - m_axesObj->begin("Ogre/AxisGizmo", - Ogre::RenderOperation::OT_LINE_LIST); + // X Axis + bool xSel = (m_selectedAxis == Axis::X || m_hoveredAxis == Axis::X); + m_axisX->clear(); + m_axisX->begin("Ogre/AxisGizmo", + Ogre::RenderOperation::OT_LINE_LIST); + m_axisX->colour(xSel ? COLOR_YELLOW[0] : COLOR_RED[0], + xSel ? COLOR_YELLOW[1] : COLOR_RED[1], + xSel ? COLOR_YELLOW[2] : COLOR_RED[2]); + m_axisX->position(0, 0, 0); + m_axisX->position(len, 0, 0); + m_axisX->end(); - // X axis - red - m_axesObj->colour(COLOR_RED[0], COLOR_RED[1], COLOR_RED[2]); - m_axesObj->position(0, 0, 0); - m_axesObj->position(len, 0, 0); + // Y Axis + bool ySel = (m_selectedAxis == Axis::Y || m_hoveredAxis == Axis::Y); + m_axisY->clear(); + m_axisY->begin("Ogre/AxisGizmo", + Ogre::RenderOperation::OT_LINE_LIST); + m_axisY->colour(ySel ? COLOR_YELLOW[0] : COLOR_GREEN[0], + ySel ? COLOR_YELLOW[1] : COLOR_GREEN[1], + ySel ? COLOR_YELLOW[2] : COLOR_GREEN[2]); + m_axisY->position(0, 0, 0); + m_axisY->position(0, len, 0); + m_axisY->end(); - // Y axis - green - m_axesObj->colour(COLOR_GREEN[0], COLOR_GREEN[1], COLOR_GREEN[2]); - m_axesObj->position(0, 0, 0); - m_axesObj->position(0, len, 0); + // Z Axis + bool zSel = (m_selectedAxis == Axis::Z || m_hoveredAxis == Axis::Z); + m_axisZ->clear(); + m_axisZ->begin("Ogre/AxisGizmo", + Ogre::RenderOperation::OT_LINE_LIST); + m_axisZ->colour(zSel ? COLOR_YELLOW[0] : COLOR_BLUE[0], + zSel ? COLOR_YELLOW[1] : COLOR_BLUE[1], + zSel ? COLOR_YELLOW[2] : COLOR_BLUE[2]); + m_axisZ->position(0, 0, 0); + m_axisZ->position(0, 0, len); + m_axisZ->end(); - // Z axis - blue - m_axesObj->colour(COLOR_BLUE[0], COLOR_BLUE[1], COLOR_BLUE[2]); - m_axesObj->position(0, 0, 0); - m_axesObj->position(0, 0, len); - - m_axesObj->end(); - - // Center marker - small wireframe cube in cyan - m_markerObj->clear(); - m_markerObj->begin("Ogre/AxisGizmo", - Ogre::RenderOperation::OT_LINE_LIST); - m_markerObj->colour(COLOR_CYAN[0], COLOR_CYAN[1], COLOR_CYAN[2]); - - // Cube corners - Ogre::Vector3 corners[8] = { + // Center marker + m_centerMarker->clear(); + m_centerMarker->begin("Ogre/AxisGizmo", + Ogre::RenderOperation::OT_LINE_LIST); + m_centerMarker->colour(COLOR_CYAN[0], COLOR_CYAN[1], + COLOR_CYAN[2]); + Ogre::Vector3 c[8] = { Ogre::Vector3(-half, -half, -half), Ogre::Vector3(half, -half, -half), Ogre::Vector3(half, half, -half), @@ -217,36 +241,169 @@ void Cursor3D::createGeometry() Ogre::Vector3(half, half, half), Ogre::Vector3(-half, half, half), }; - - // Bottom face - m_markerObj->position(corners[0]); - m_markerObj->position(corners[1]); - m_markerObj->position(corners[1]); - m_markerObj->position(corners[2]); - m_markerObj->position(corners[2]); - m_markerObj->position(corners[3]); - m_markerObj->position(corners[3]); - m_markerObj->position(corners[0]); - - // Top face - m_markerObj->position(corners[4]); - m_markerObj->position(corners[5]); - m_markerObj->position(corners[5]); - m_markerObj->position(corners[6]); - m_markerObj->position(corners[6]); - m_markerObj->position(corners[7]); - m_markerObj->position(corners[7]); - m_markerObj->position(corners[4]); - - // Vertical edges - m_markerObj->position(corners[0]); - m_markerObj->position(corners[4]); - m_markerObj->position(corners[1]); - m_markerObj->position(corners[5]); - m_markerObj->position(corners[2]); - m_markerObj->position(corners[6]); - m_markerObj->position(corners[3]); - m_markerObj->position(corners[7]); - - m_markerObj->end(); + auto line = [&](int a, int b) { + m_centerMarker->position(c[a]); + m_centerMarker->position(c[b]); + }; + line(0, 1); + line(1, 2); + line(2, 3); + line(3, 0); + line(4, 5); + line(5, 6); + line(6, 7); + line(7, 4); + line(0, 4); + line(1, 5); + line(2, 6); + line(3, 7); + m_centerMarker->end(); +} + +Cursor3D::Axis Cursor3D::hitTest(const Ogre::Ray &mouseRay) +{ + if (!m_cursorNode || !m_axisX->isVisible()) + return Axis::None; + + Ogre::Vector3 cursorPos = m_cursorNode->getPosition(); + float len = 0.5f * m_size; + float threshold = 0.15f * m_size; + + float bestDist = 1000000.0f; + Axis bestAxis = Axis::None; + + for (int i = 0; i < 3; ++i) { + Ogre::Vector3 axisDir; + if (i == 0) + axisDir = m_cursorNode->getOrientation() * + Ogre::Vector3::UNIT_X; + else if (i == 1) + axisDir = m_cursorNode->getOrientation() * + Ogre::Vector3::UNIT_Y; + else + axisDir = m_cursorNode->getOrientation() * + Ogre::Vector3::UNIT_Z; + + for (int j = 0; j <= 8; ++j) { + float t = len * j / 8.0f; + Ogre::Vector3 pointOnAxis = cursorPos + axisDir * t; + Ogre::Vector3 L = pointOnAxis - mouseRay.getOrigin(); + float tca = L.dotProduct(mouseRay.getDirection()); + if (tca < 0.01f) + continue; + float d2 = L.dotProduct(L) - tca * tca; + if (d2 <= threshold * threshold) { + if (tca < bestDist) { + bestDist = tca; + bestAxis = static_cast(i + 1); + } + } + } + } + return bestAxis; +} + +bool Cursor3D::onMousePressed(const Ogre::Ray &mouseRay, + const Ogre::Camera *camera) +{ + (void)camera; + if (!m_axisX->isVisible()) + return false; + + m_selectedAxis = hitTest(mouseRay); + + if (m_selectedAxis != Axis::None) { + m_isDragging = true; + m_dragStartPosition = m_position; + m_dragStartRotation = m_orientation; + + Ogre::Vector3 axisDir; + switch (m_selectedAxis) { + case Axis::X: + axisDir = m_orientation * Ogre::Vector3::UNIT_X; + break; + case Axis::Y: + axisDir = m_orientation * Ogre::Vector3::UNIT_Y; + break; + case Axis::Z: + axisDir = m_orientation * Ogre::Vector3::UNIT_Z; + break; + default: + axisDir = Ogre::Vector3::UNIT_X; + break; + } + m_dragAxisDir = axisDir; + + if (m_mode == Mode::Translate) { + projectRayOntoAxis(mouseRay, m_dragStartPosition, + m_dragAxisDir, m_dragStartT); + } else if (m_mode == Mode::Rotate) { + (void)camera; + m_dragStartAngle = 0.0f; + } + + createGeometry(); + return true; + } + + return false; +} + +bool Cursor3D::onMouseMoved(const Ogre::Ray &mouseRay, + const Ogre::Vector2 &mouseDelta) +{ + if (m_isDragging && m_selectedAxis != Axis::None) { + if (m_mode == Mode::Translate) { + float currentT; + if (!projectRayOntoAxis(mouseRay, m_dragStartPosition, + m_dragAxisDir, currentT)) { + return true; + } + float deltaT = currentT - m_dragStartT; + Ogre::Vector3 newPos = + m_dragStartPosition + m_dragAxisDir * deltaT; + setPosition(newPos); + return true; + } else if (m_mode == Mode::Rotate) { + // Simple axis-based rotation from mouse delta + float deltaAngle = 0.0f; + if (m_selectedAxis == Axis::X) { + // Up/down rotates around X + deltaAngle = mouseDelta.y * 0.5f; + } else if (m_selectedAxis == Axis::Y) { + // Left/right rotates around Y + deltaAngle = -mouseDelta.x * 0.5f; + } else if (m_selectedAxis == Axis::Z) { + // Left/right rotates around Z + deltaAngle = -mouseDelta.x * 0.5f; + } + m_dragStartAngle += deltaAngle; + + Ogre::Quaternion rot( + Ogre::Degree(m_dragStartAngle), + m_dragAxisDir); + Ogre::Quaternion newRot = rot * m_dragStartRotation; + setOrientation(newRot); + return true; + } + } else if (m_axisX->isVisible()) { + Axis prevHover = m_hoveredAxis; + m_hoveredAxis = hitTest(mouseRay); + if (prevHover != m_hoveredAxis) + createGeometry(); + } + + return false; +} + +bool Cursor3D::onMouseReleased() +{ + if (m_isDragging) { + m_isDragging = false; + m_selectedAxis = Axis::None; + m_hoveredAxis = Axis::None; + createGeometry(); + return true; + } + return false; } diff --git a/src/features/editScene/gizmo/Cursor3D.hpp b/src/features/editScene/gizmo/Cursor3D.hpp index 677c4e8..e01709e 100644 --- a/src/features/editScene/gizmo/Cursor3D.hpp +++ b/src/features/editScene/gizmo/Cursor3D.hpp @@ -9,80 +9,88 @@ struct TransformComponent; /** - * 3D Cursor - a visual marker for prefab placement and transform reference - * Shows a small crosshair with axis indicators in world space + * 3D Cursor - a visual marker for prefab placement and transform reference. + * Supports axis-based translation and rotation interaction like the gizmo. */ class Cursor3D { public: + enum class Mode { + Translate, + Rotate + }; + + enum class Axis { + None, + X, + Y, + Z + }; + Cursor3D(Ogre::SceneManager *sceneMgr); ~Cursor3D(); - /** - * Shutdown and cleanup - must be called before SceneManager is destroyed - */ void shutdown(); - /** - * Set world position - */ void setPosition(const Ogre::Vector3 &pos); - Ogre::Vector3 getPosition() const - { - return m_position; - } + Ogre::Vector3 getPosition() const { return m_position; } - /** - * Set world orientation - */ void setOrientation(const Ogre::Quaternion &rot); - Ogre::Quaternion getOrientation() const - { - return m_orientation; - } + Ogre::Quaternion getOrientation() const { return m_orientation; } - /** - * Show/hide cursor - */ void setVisible(bool visible); bool isVisible() const; - /** - * Set cursor size/scale - */ void setSize(float size); - float getSize() const - { - return m_size; - } + float getSize() const { return m_size; } + + void setMode(Mode mode) { m_mode = mode; } + Mode getMode() const { return m_mode; } - /** - * Copy position and orientation from a TransformComponent (world space) - */ void snapToTransform(const TransformComponent &transform); - - /** - * Apply cursor position and orientation to a TransformComponent - */ void applyToTransform(TransformComponent &transform) const; /** - * Simple hit test - returns true if mouse ray passes near cursor center + * Handle mouse input for cursor interaction. + * Returns true if cursor handled the input. */ - bool hitTest(const Ogre::Ray &mouseRay) const; + bool onMousePressed(const Ogre::Ray &mouseRay, + const Ogre::Camera *camera); + bool onMouseMoved(const Ogre::Ray &mouseRay, + const Ogre::Vector2 &mouseDelta); + bool onMouseReleased(); + + bool isDragging() const { return m_isDragging; } + Axis getSelectedAxis() const { return m_selectedAxis; } private: void createGeometry(); void updateNodeTransform(); + Axis hitTest(const Ogre::Ray &mouseRay); Ogre::SceneManager *m_sceneMgr; Ogre::SceneNode *m_cursorNode; - Ogre::ManualObject *m_axesObj; - Ogre::ManualObject *m_markerObj; + Ogre::ManualObject *m_axisX; + Ogre::ManualObject *m_axisY; + Ogre::ManualObject *m_axisZ; + Ogre::ManualObject *m_centerMarker; Ogre::Vector3 m_position; Ogre::Quaternion m_orientation; float m_size; bool m_visible; + + Mode m_mode; + Axis m_selectedAxis; + Axis m_hoveredAxis; + bool m_isDragging; + + // Drag state + Ogre::Vector3 m_dragStartPosition; + Ogre::Quaternion m_dragStartRotation; + Ogre::Vector3 m_dragAxisDir; + float m_dragStartT; + float m_dragStartAngle; + Ogre::Vector2 m_dragScreenAxis; }; #endif // EDITSCENE_CURSOR3D_HPP diff --git a/src/features/editScene/systems/EditorUISystem.cpp b/src/features/editScene/systems/EditorUISystem.cpp index ba8e11b..792428a 100644 --- a/src/features/editScene/systems/EditorUISystem.cpp +++ b/src/features/editScene/systems/EditorUISystem.cpp @@ -1,6 +1,7 @@ #include "../components/GeneratedPhysicsTag.hpp" #include "EditorUISystem.hpp" #include "PrefabSystem.hpp" +#include "../camera/EditorCamera.hpp" #include "../components/EntityName.hpp" #include "../components/Transform.hpp" #include "../components/Renderable.hpp" @@ -35,6 +36,7 @@ #include "../components/GoapBlackboard.hpp" #include "../components/NavMesh.hpp" #include "../components/SmartObject.hpp" +#include "../components/GoapPlanner.hpp" #include "../components/PrefabInstance.hpp" #include "../ui/TransformEditor.hpp" @@ -87,11 +89,16 @@ bool EditorUISystem::onMousePressed(const Ogre::Ray &mouseRay) return true; } - // If cursor placement mode is active, raycast and place cursor - // (skip if ImGui wants the mouse for UI interaction) - if (m_cursor3D && m_cursorPlaceMode && m_physicsSystem && - m_physicsSystem->isInitialized() && - !ImGui::GetIO().WantCaptureMouse) { + // Skip if ImGui wants the mouse + if (ImGui::GetIO().WantCaptureMouse) + return false; + + if (!m_cursor3D || !m_cursor3D->isVisible()) + return false; + + // Cursor place mode: raycast and place cursor on surface + if (m_cursorMode == CursorInteractionMode::Place && + m_physicsSystem && m_physicsSystem->isInitialized()) { JoltPhysicsWrapper *physics = m_physicsSystem->getPhysicsWrapper(); if (physics) { @@ -103,30 +110,64 @@ bool EditorUISystem::onMousePressed(const Ogre::Ray &mouseRay) if (physics->raycastQuery(start, end, hitPos, hitBody)) { m_cursor3D->setPosition(hitPos); - m_cursorPlaceMode = - false; // disable after placement + m_cursorMode = CursorInteractionMode::None; return true; } } } + // Cursor translate/rotate: delegate to Cursor3D axis-based interaction + if (m_cursorMode == CursorInteractionMode::Translate) { + m_cursor3D->setMode(Cursor3D::Mode::Translate); + Ogre::Camera *cam = m_editorCamera ? + m_editorCamera->getCamera() : + nullptr; + if (m_cursor3D->onMousePressed(mouseRay, cam)) + return true; + } else if (m_cursorMode == CursorInteractionMode::Rotate) { + m_cursor3D->setMode(Cursor3D::Mode::Rotate); + Ogre::Camera *cam = m_editorCamera ? + m_editorCamera->getCamera() : + nullptr; + if (m_cursor3D->onMousePressed(mouseRay, cam)) + return true; + } + return false; } bool EditorUISystem::onMouseMoved(const Ogre::Ray &mouseRay, const Ogre::Vector2 &mouseDelta) { - if (m_gizmo) { - return m_gizmo->onMouseMoved(mouseRay, mouseDelta); + // Gizmo gets first shot + if (m_gizmo && m_gizmo->onMouseMoved(mouseRay, mouseDelta)) { + return true; } + + // Cursor translate/rotate drag + if (m_cursor3D && (m_cursorMode == CursorInteractionMode::Translate || + m_cursorMode == CursorInteractionMode::Rotate)) { + if (m_cursor3D->onMouseMoved(mouseRay, mouseDelta)) + return true; + } + return false; } bool EditorUISystem::onMouseReleased() { - if (m_gizmo) { - return m_gizmo->onMouseReleased(); + // Gizmo gets first shot + if (m_gizmo && m_gizmo->onMouseReleased()) { + return true; } + + // Cursor translate/rotate release + if (m_cursor3D && (m_cursorMode == CursorInteractionMode::Translate || + m_cursorMode == CursorInteractionMode::Rotate)) { + if (m_cursor3D->onMouseReleased()) + return true; + } + return false; } @@ -575,6 +616,8 @@ void EditorUISystem::renderEntityNode(flecs::entity entity, int depth) indicators += " [NavSrc]"; if (entity.has()) indicators += " [SO]"; + if (entity.has()) + indicators += " [Planner]"; snprintf(label, sizeof(label), "%s%s##%llu", name.c_str(), indicators.c_str(), (unsigned long long)entity.id()); @@ -998,6 +1041,13 @@ void EditorUISystem::renderComponentList(flecs::entity entity) componentCount++; } + // Render GoapPlanner if present + if (entity.has()) { + auto &planner = entity.get_mut(); + m_componentRegistry.render(entity, planner); + componentCount++; + } + // Show message if no components if (componentCount == 0) { @@ -1741,6 +1791,44 @@ void EditorUISystem::renderPrefabBrowser() setSelectedEntity(instance); } } + ImGui::SameLine(); + + // Delete prefab button + std::string delId = "Del##" + file; + if (ImGui::Button(delId.c_str())) { + m_prefabToDelete = path; + m_showDeletePrefabConfirm = true; + } + } + + // Delete prefab confirmation dialog + if (m_showDeletePrefabConfirm) { + ImGui::OpenPopup("Delete Prefab"); + } + if (ImGui::BeginPopupModal("Delete Prefab", + &m_showDeletePrefabConfirm, + ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::Text( + "Are you sure you want to delete this prefab?"); + ImGui::Text(" %s", m_prefabToDelete.c_str()); + ImGui::Separator(); + ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), + "This action cannot be undone!"); + + if (ImGui::Button("Delete", ImVec2(120, 0))) { + PrefabSystem prefabSys(m_world, m_sceneMgr); + if (prefabSys.deletePrefab(m_prefabToDelete)) { + m_refreshPrefabList = true; + } + m_showDeletePrefabConfirm = false; + m_prefabToDelete.clear(); + } + ImGui::SameLine(); + if (ImGui::Button("Cancel", ImVec2(120, 0))) { + m_showDeletePrefabConfirm = false; + m_prefabToDelete.clear(); + } + ImGui::EndPopup(); } } ImGui::End(); @@ -1766,15 +1854,48 @@ void EditorUISystem::renderCursorPanel() m_cursor3D->setVisible(visible); } - // Placement mode + ImGui::Separator(); + + // Interaction mode radio buttons + ImGui::Text("Interaction Mode:"); + int mode = static_cast(m_cursorMode); + if (ImGui::RadioButton("None", &mode, 0)) { + m_cursorMode = CursorInteractionMode::None; + } ImGui::SameLine(); - if (ImGui::Checkbox("Place on Click", &m_cursorPlaceMode)) { - if (m_cursorPlaceMode && !m_cursor3D->isVisible()) + if (ImGui::RadioButton("Place", &mode, 1)) { + m_cursorMode = CursorInteractionMode::Place; + if (!m_cursor3D->isVisible()) m_cursor3D->setVisible(true); } if (ImGui::IsItemHovered()) { ImGui::SetTooltip( - "Click in the 3D viewport to place cursor on surface"); + "Click in viewport to raycast-place cursor on surface"); + } + ImGui::SameLine(); + if (ImGui::RadioButton("Move", &mode, 2)) { + m_cursorMode = CursorInteractionMode::Translate; + if (!m_cursor3D->isVisible()) + m_cursor3D->setVisible(true); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip( + "Click and drag an axis to translate along it"); + } + ImGui::SameLine(); + if (ImGui::RadioButton("Rotate", &mode, 3)) { + m_cursorMode = CursorInteractionMode::Rotate; + if (!m_cursor3D->isVisible()) + m_cursor3D->setVisible(true); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip( + "Click and drag an axis to rotate around it"); + } + + if (m_cursor3D->isDragging()) { + ImGui::SameLine(); + ImGui::TextColored(ImVec4(0, 1, 0, 1), "[Dragging]"); } ImGui::Separator(); diff --git a/src/features/editScene/systems/EditorUISystem.hpp b/src/features/editScene/systems/EditorUISystem.hpp index d811d8b..eb94a1a 100644 --- a/src/features/editScene/systems/EditorUISystem.hpp +++ b/src/features/editScene/systems/EditorUISystem.hpp @@ -16,6 +16,7 @@ class EditorPhysicsSystem; class BuoyancySystem; class NormalDebugSystem; +class EditorCamera; namespace Ogre { @@ -142,6 +143,14 @@ public: m_normalDebugSystem = normalDebug; } + /** + * Set editor camera for cursor drag operations + */ + void setEditorCamera(EditorCamera *camera) + { + m_editorCamera = camera; + } + /** * Enable/disable editor UI rendering */ @@ -267,12 +276,26 @@ private: std::vector m_prefabFiles; bool m_refreshPrefabList = true; bool m_prefabInstAtRoot = false; - bool m_prefabUseCursor = true; // Use 3D cursor for prefab placement - float m_prefabRaycastMargin = 0.0f; // Vertical margin for raycast placement + bool m_prefabUseCursor = true; // Use 3D cursor for prefab placement + float m_prefabRaycastMargin = + 0.0f; // Vertical margin for raycast placement + + // Prefab delete confirmation + bool m_showDeletePrefabConfirm = false; + std::string m_prefabToDelete; // 3D Cursor state bool m_showCursorPanel = false; - bool m_cursorPlaceMode = false; // Click in viewport to place cursor + enum class CursorInteractionMode { + None, + Place, // Click to raycast-place on surface + Translate, // Drag axis to translate cursor + Rotate // Drag axis to rotate cursor + }; + CursorInteractionMode m_cursorMode = CursorInteractionMode::None; + + // Camera reference for cursor placement/rotation + EditorCamera *m_editorCamera = nullptr; // Queries flecs::query m_nameQuery; diff --git a/src/features/editScene/systems/GoapPlannerSystem.cpp b/src/features/editScene/systems/GoapPlannerSystem.cpp new file mode 100644 index 0000000..42c6e41 --- /dev/null +++ b/src/features/editScene/systems/GoapPlannerSystem.cpp @@ -0,0 +1,266 @@ +#include "GoapPlannerSystem.hpp" +#include "../components/GoapPlanner.hpp" +#include "../components/GoapBlackboard.hpp" +#include "../components/ActionDatabase.hpp" +#include "../components/ActionDebug.hpp" +#include "../components/GoapAction.hpp" +#include "../components/GoapGoal.hpp" +#include "../EditorApp.hpp" +#include +#include +#include +#include + +namespace { + +struct PlannerNode { + GoapBlackboard state; + std::vector actions; + int cost = 0; + + int heuristic(const GoapBlackboard &goal) const + { + (void)goal; + return 0; + } + + int f(const GoapBlackboard &goal) const + { + return cost + heuristic(goal); + } +}; + +struct NodeCompare { + const GoapBlackboard *goal; + bool operator()(const PlannerNode &a, const PlannerNode &b) const + { + return a.f(*goal) > b.f(*goal); + } +}; + +} // namespace + +GoapPlannerSystem::GoapPlannerSystem(flecs::world &world) + : m_world(world) +{ +} + +GoapPlannerSystem::~GoapPlannerSystem() = default; + + +size_t GoapPlannerSystem::hashState(const GoapBlackboard &bb) +{ + std::hash hasher; + size_t h = hasher(bb.bits); + h ^= hasher(bb.mask) + 0x9e3779b9 + (h << 6) + (h >> 2); + h ^= hasher(bb.bitmask) + 0x9e3779b9 + (h << 6) + (h >> 2); + for (const auto &pair : bb.values) { + h ^= std::hash{}(pair.first) + 0x9e3779b9 + + (h << 6) + (h >> 2); + h ^= std::hash{}(pair.second) + 0x9e3779b9 + (h << 6) + + (h >> 2); + } + return h; +} + +bool GoapPlannerSystem::statesEqual(const GoapBlackboard &a, + const GoapBlackboard &b) +{ + return a.bits == b.bits && a.mask == b.mask && + a.bitmask == b.bitmask && a.values == b.values; +} + +std::vector GoapPlannerSystem::resolveActions( + const GoapPlannerComponent &planner, const ActionDatabase *db) +{ + std::vector result; + if (!db) + return result; + + for (const auto &name : planner.actionNames) { + const GoapAction *action = db->findAction(name); + if (action) + result.push_back(action); + } + return result; +} + +const GoapGoal *GoapPlannerSystem::selectGoal( + const GoapPlannerComponent &planner, const ActionDatabase *db, + const GoapBlackboard &blackboard) +{ + if (!db) + return nullptr; + + const GoapGoal *best = nullptr; + int bestPriority = -1; + + for (const auto &name : planner.goalNames) { + const GoapGoal *goal = db->findGoal(name); + if (!goal) + continue; + if (!goal->isValid(blackboard)) + continue; + if (goal->priority > bestPriority) { + bestPriority = goal->priority; + best = goal; + } + } + return best; +} + +void GoapPlannerSystem::planForEntity(flecs::entity e, + GoapPlannerComponent &planner, + const GoapBlackboard &blackboard) +{ + (void)e; + + // Find ActionDatabase + const ActionDatabase *db = nullptr; + m_world.query().each( + [&](flecs::entity, ActionDatabase &database) { + if (!db) + db = &database; + }); + + // Select best valid goal + const GoapGoal *goal = selectGoal(planner, db, blackboard); + if (!goal) { + planner.status = GoapPlannerComponent::Status::NoPlanFound; + planner.planDirty = false; + planner.currentGoalName.clear(); + Ogre::LogManager::getSingleton().logMessage( + "[GoapPlanner] No valid goal found."); + return; + } + + planner.currentGoalName = goal->name; + + // Resolve actions + std::vector actions = + resolveActions(planner, db); + + if (actions.empty()) { + planner.status = GoapPlannerComponent::Status::NoPlanFound; + planner.planDirty = false; + Ogre::LogManager::getSingleton().logMessage( + "[GoapPlanner] No actions available for planning."); + return; + } + + // A* search + NodeCompare cmp{&goal->target}; + std::priority_queue, + NodeCompare> + openSet(cmp); + + // Closed set: map from state hash -> best cost seen for that state + std::unordered_map closedSet; + + // Track found plans to avoid duplicates + std::unordered_set foundPlanSignatures; + + PlannerNode start; + start.state = blackboard; + start.cost = 0; + openSet.push(start); + + planner.nodesExplored = 0; + planner.plansGenerated = 0; + int maxNodes = 10000; // safety limit + + while (!openSet.empty() && planner.plansGenerated < planner.maxPlans && + planner.nodesExplored < maxNodes) { + PlannerNode current = openSet.top(); + openSet.pop(); + planner.nodesExplored++; + + // Check if goal is satisfied + if (current.state.satisfies(goal->target)) { + // Build plan signature for deduplication + std::string sig; + for (const auto &name : current.actions) + sig += name + ","; + if (foundPlanSignatures.insert(sig).second) { + GoapPlannerComponent::Plan plan; + plan.actions = current.actions; + plan.totalCost = current.cost; + plan.goalName = goal->name; + planner.planQueue.push_back(std::move(plan)); + planner.plansGenerated++; + } + continue; + } + + // Check closed set + size_t stateHash = hashState(current.state); + auto it = closedSet.find(stateHash); + if (it != closedSet.end() && it->second <= current.cost) + continue; + closedSet[stateHash] = current.cost; + + // Expand: try each action + for (const GoapAction *action : actions) { + if (!action) + continue; + if (!action->canRun(current.state)) + continue; + + PlannerNode next = current; + next.state.apply(action->effects); + next.actions.push_back(action->name); + next.cost += action->cost; + + openSet.push(std::move(next)); + } + } + + if (planner.plansGenerated > 0) { + planner.status = + GoapPlannerComponent::Status::PlansAvailable; + Ogre::LogManager::getSingleton().logMessage( + "[GoapPlanner] Generated " + + Ogre::StringConverter::toString( + planner.plansGenerated) + + " plan(s) for goal: " + goal->name); + } else { + planner.status = + GoapPlannerComponent::Status::NoPlanFound; + Ogre::LogManager::getSingleton().logMessage( + "[GoapPlanner] No plan found for goal: " + + goal->name); + } + + planner.planDirty = false; +} + +void GoapPlannerSystem::update(float deltaTime) +{ + (void)deltaTime; + + bool shouldRun = true; + if (m_editorApp) { + if (m_editorApp->getGameMode() == EditorApp::GameMode::Game && + m_editorApp->getGamePlayState() != + EditorApp::GamePlayState::Playing) { + shouldRun = false; + } + } + if (!shouldRun) + return; + + m_world + .query() + .each([&](flecs::entity e, GoapPlannerComponent &planner, + GoapBlackboard &blackboard) { + (void)e; + if (!planner.planDirty) + return; + + // Clear old plans before replanning + planner.clearPlans(); + planner.status = + GoapPlannerComponent::Status::Planning; + planForEntity(e, planner, blackboard); + }); +} diff --git a/src/features/editScene/systems/GoapPlannerSystem.hpp b/src/features/editScene/systems/GoapPlannerSystem.hpp new file mode 100644 index 0000000..18e15c8 --- /dev/null +++ b/src/features/editScene/systems/GoapPlannerSystem.hpp @@ -0,0 +1,68 @@ +#ifndef EDITSCENE_GOAP_PLANNER_SYSTEM_HPP +#define EDITSCENE_GOAP_PLANNER_SYSTEM_HPP +#pragma once + +#include +#include +#include +#include +#include +#include + +// Forward declarations +class EditorApp; +class ActionDatabase; +struct GoapPlannerComponent; +struct GoapAction; +struct GoapGoal; +struct GoapBlackboard; + +/** + * GOAP Planner system. + * + * Runs A* planning for entities with GoapPlannerComponent + GoapBlackboard. + * When planDirty is set: + * 1. Selects the highest-priority valid goal from the planner's goal list + * 2. Resolves action names against ActionDatabase + * 3. Runs forward A* search to find action sequences reaching the goal + * 4. Stores up to maxPlans plans in the planner's planQueue + * 5. Sets status to PlansAvailable or NoPlanFound + */ +class GoapPlannerSystem { +public: + GoapPlannerSystem(flecs::world &world); + ~GoapPlannerSystem(); + + void update(float deltaTime); + + /** + * Set the EditorApp for game mode detection. + * Planning only runs when playing (game mode) or always in editor. + */ + void setEditorApp(EditorApp *app) + { + m_editorApp = app; + } + +private: + void planForEntity(flecs::entity e, GoapPlannerComponent &planner, + const GoapBlackboard &blackboard); + + std::vector + resolveActions(const GoapPlannerComponent &planner, + const ActionDatabase *db); + + const GoapGoal *selectGoal(const GoapPlannerComponent &planner, + const ActionDatabase *db, + const GoapBlackboard &blackboard); + + // Hash a blackboard state for closed-set deduplication + static size_t hashState(const GoapBlackboard &bb); + static bool statesEqual(const GoapBlackboard &a, + const GoapBlackboard &b); + + flecs::world &m_world; + EditorApp *m_editorApp = nullptr; +}; + +#endif // EDITSCENE_GOAP_PLANNER_SYSTEM_HPP diff --git a/src/features/editScene/systems/GoapRunnerSystem.cpp b/src/features/editScene/systems/GoapRunnerSystem.cpp new file mode 100644 index 0000000..62eba7a --- /dev/null +++ b/src/features/editScene/systems/GoapRunnerSystem.cpp @@ -0,0 +1,325 @@ +#include "GoapRunnerSystem.hpp" +#include "SmartObjectSystem.hpp" +#include "BehaviorTreeSystem.hpp" +#include "AnimationTreeSystem.hpp" +#include "NavMeshSystem.hpp" +#include "../components/GoapPlanner.hpp" +#include "../components/GoapRunner.hpp" +#include "../components/ActionDatabase.hpp" +#include "../components/ActionDebug.hpp" +#include "../components/SmartObject.hpp" +#include "../components/PathFollowing.hpp" +#include "../components/Transform.hpp" +#include "../components/Character.hpp" +#include "../components/NavMesh.hpp" +#include "../EditorApp.hpp" +#include +#include + +GoapRunnerSystem::GoapRunnerSystem(flecs::world &world, + Ogre::SceneManager *sceneMgr, + SmartObjectSystem *soSystem, + BehaviorTreeSystem *btSystem, + NavMeshSystem *navSystem) + : m_world(world) + , m_sceneMgr(sceneMgr) + , m_soSystem(soSystem) + , m_btSystem(btSystem) + , m_navSystem(navSystem) +{ +} + +GoapRunnerSystem::~GoapRunnerSystem() = default; + +flecs::entity GoapRunnerSystem::findNearestSmartObject( + flecs::entity character, const Ogre::String &actionName, + float maxDistance) +{ + if (!character.is_alive() || !character.has()) + return flecs::entity::null(); + + Ogre::Vector3 charPos = + character.get().node + ->_getDerivedPosition(); + + flecs::entity nearest = flecs::entity::null(); + float bestDist = maxDistance; + + m_world.query() + .each([&](flecs::entity so, SmartObjectComponent &soComp, + TransformComponent &soTrans) { + bool hasAction = false; + for (const auto &name : soComp.actionNames) { + if (name == actionName) { + hasAction = true; + break; + } + } + if (!hasAction) + return; + + Ogre::Vector3 soPos = + soTrans.node->_getDerivedPosition(); + float dist = charPos.distance(soPos); + if (dist < bestDist) { + bestDist = dist; + nearest = so; + } + }); + + return nearest; +} + +bool GoapRunnerSystem::startNextAction(flecs::entity e) +{ + if (!e.has() || !e.has()) + return false; + + auto &planner = e.get_mut(); + auto &runner = e.get_mut(); + + if (runner.currentActionIndex >= (int)runner.planActions.size()) { + // Plan complete + Ogre::LogManager::getSingleton().logMessage( + "[GoapRunner] Plan complete."); + runner.planActions.clear(); + runner.currentActionIndex = 0; + runner.currentActionName.clear(); + runner.state = GoapRunnerComponent::State::Idle; + + // Clear PathFollowing target + if (e.has()) { + auto &pf = e.get_mut(); + pf.hasTarget = false; + pf.currentLocomotionState = "idle"; + pf.path.clear(); + pf.pathIndex = 0; + } + return false; + } + + const Ogre::String &actionName = + runner.planActions[runner.currentActionIndex]; + runner.currentActionName = actionName; + + // Find action database + ActionDatabase *db = nullptr; + m_world.query().each( + [&](flecs::entity, ActionDatabase &database) { + if (!db) + db = &database; + }); + + const GoapAction *action = db ? db->findAction(actionName) : nullptr; + + if (action) { + // Normal action: run behavior tree via ActionDebug + runner.state = GoapRunnerComponent::State::RunningAction; + + if (!e.has()) { + e.set({}); + m_managedActionDebugs.insert(e.id()); + } + auto &debug = e.get_mut(); + debug.isRunning = true; + debug.runTimer = 0.0f; + debug.currentActionName = actionName; + debug.selectedActionName = actionName; + + Ogre::LogManager::getSingleton().logMessage( + "[GoapRunner] Starting normal action: " + actionName); + } else { + // Smart object action: pathfind and set up PathFollowingComponent + flecs::entity so = findNearestSmartObject( + e, actionName, planner.smartObjectDistance); + if (so.is_alive()) { + runner.state = + GoapRunnerComponent::State::MovingToSmartObject; + runner.targetSmartObjectId = so.id(); + + if (e.has()) { + auto &pf = e.get_mut(); + auto &soTrans = so.get(); + pf.targetPosition = + soTrans.node->_getDerivedPosition(); + pf.hasTarget = true; + pf.path.clear(); + pf.pathIndex = 0; + pf.pathRecalcTimer = 0.0f; + + // Find path via navmesh + if (m_navSystem) { + flecs::entity navmeshEntity = + flecs::entity::null(); + m_world + .query() + .each([&](flecs::entity ne, + NavMeshComponent &) { + if (!navmeshEntity.is_alive()) + navmeshEntity = ne; + }); + if (navmeshEntity.is_alive()) { + auto &charTrans = + e.get(); + bool found = m_navSystem->findPath( + navmeshEntity, + charTrans.node + ->_getDerivedPosition(), + pf.targetPosition, + pf.path); + if (!found || pf.path.empty()) { + // Fallback: direct path + pf.path.clear(); + pf.path.push_back( + pf.targetPosition); + } + } else { + pf.path.clear(); + pf.path.push_back(pf.targetPosition); + } + } else { + pf.path.clear(); + pf.path.push_back(pf.targetPosition); + } + + // Choose walk or run based on distance + auto &charTrans = e.get(); + float dist = charTrans.node + ->_getDerivedPosition() + .distance(pf.targetPosition); + if (dist > pf.walkSpeed * 3.0f) + pf.currentLocomotionState = "run"; + else + pf.currentLocomotionState = "walk"; + } + + Ogre::LogManager::getSingleton().logMessage( + "[GoapRunner] Starting smart object action: " + + actionName); + } else { + Ogre::LogManager::getSingleton().logMessage( + "[GoapRunner] No smart object found for action: " + + actionName); + // Skip this action + runner.currentActionIndex++; + return startNextAction(e); + } + } + + return true; +} + +void GoapRunnerSystem::update(float deltaTime) +{ + bool shouldRun = true; + if (m_editorApp) { + if (m_editorApp->getGameMode() == EditorApp::GameMode::Game && + m_editorApp->getGamePlayState() != + EditorApp::GamePlayState::Playing) { + shouldRun = false; + } + } + if (!shouldRun) + return; + + m_world.query() + .each([&](flecs::entity e, GoapPlannerComponent &planner, + GoapRunnerComponent &runner) { + // If idle and plans available, pop one and start + if (runner.state == + GoapRunnerComponent::State::Idle && + planner.hasPlans()) { + auto plan = planner.popCheapestPlan(); + runner.planActions = std::move(plan.actions); + runner.currentActionIndex = 0; + runner.currentActionName.clear(); + startNextAction(e); + return; + } + + // If idle and no plans, mark planner dirty for replanning + if (runner.state == + GoapRunnerComponent::State::Idle && + !planner.hasPlans()) { + if (!runner.planActions.empty()) { + // Just finished a plan batch, need more + runner.planActions.clear(); + runner.currentActionIndex = 0; + } + planner.planDirty = true; + return; + } + + // If still idle, nothing to do + if (runner.state == + GoapRunnerComponent::State::Idle) + return; + + // Update action timer + runner.actionTimer += deltaTime; + + // Handle smart object navigation completion + if (runner.state == GoapRunnerComponent::State:: + MovingToSmartObject) { + if (e.has()) { + auto &pf = e.get(); + if (!pf.hasTarget) { + // PathFollowingSystem has reached the + // destination - execute the action + Ogre::LogManager::getSingleton() + .logMessage( + "[GoapRunner] Reached smart object, executing: " + + runner.currentActionName); + + // Apply action effects + if (e.has()) { + ActionDatabase *db = nullptr; + m_world + .query() + .each([&](flecs::entity, + ActionDatabase + &database) { + if (!db) + db = &database; + }); + if (db) { + const GoapAction *action = + db->findAction( + runner.currentActionName); + if (action) { + auto &bb = e.get_mut(); + bb.apply(action->effects); + } + } + } + + // Advance to next action + runner.currentActionIndex++; + runner.actionTimer = 0.0f; + runner.targetSmartObjectId = 0; + startNextAction(e); + } + } + return; + } + + // Check if current normal action is complete + if (runner.state == GoapRunnerComponent::State:: + RunningAction && + m_btSystem && e.has()) { + auto &debug = e.get_mut(); + debug.runTimer += deltaTime; + + auto &btState = + m_btSystem->getActionDebugState(e.id()); + if (debug.runTimer > 0.5f && + btState.treeResult != + BehaviorTreeSystem::Status::running) { + debug.isRunning = false; + runner.currentActionIndex++; + runner.actionTimer = 0.0f; + startNextAction(e); + } + } + }); +} diff --git a/src/features/editScene/systems/GoapRunnerSystem.hpp b/src/features/editScene/systems/GoapRunnerSystem.hpp new file mode 100644 index 0000000..e1952c1 --- /dev/null +++ b/src/features/editScene/systems/GoapRunnerSystem.hpp @@ -0,0 +1,68 @@ +#ifndef EDITSCENE_GOAP_RUNNER_SYSTEM_HPP +#define EDITSCENE_GOAP_RUNNER_SYSTEM_HPP +#pragma once + +#include +#include +#include + +// Forward declarations +class SmartObjectSystem; +class BehaviorTreeSystem; +class EditorApp; +class AnimationTreeSystem; +class NavMeshSystem; + +/** + * System that executes GOAP plans from GoapPlannerComponent's plan queue. + * + * For each entity with GoapPlannerComponent + GoapRunnerComponent: + * 1. If no active plan, pop the cheapest plan from planner.planQueue + * 2. Normal actions: run via BehaviorTreeSystem (using ActionDebug) + * 3. Smart object actions: pathfind to smart object using NavMeshSystem, + * store path in PathFollowingComponent, then execute on arrival + * 4. Advance to next action when current completes + * 5. When plan finishes, pop next plan. If queue empty, mark planner dirty. + */ +class GoapRunnerSystem { +public: + GoapRunnerSystem(flecs::world &world, Ogre::SceneManager *sceneMgr, + SmartObjectSystem *soSystem, + BehaviorTreeSystem *btSystem, + NavMeshSystem *navSystem); + ~GoapRunnerSystem(); + + void update(float deltaTime); + + void setAnimationTreeSystem(AnimationTreeSystem *system) + { + m_animTreeSystem = system; + } + + /** + * Set the EditorApp for game mode detection. + */ + void setEditorApp(EditorApp *app) + { + m_editorApp = app; + } + +private: + bool startNextAction(flecs::entity e); + flecs::entity findNearestSmartObject(flecs::entity character, + const Ogre::String &actionName, + float maxDistance); + + flecs::world &m_world; + Ogre::SceneManager *m_sceneMgr; + SmartObjectSystem *m_soSystem; + BehaviorTreeSystem *m_btSystem; + NavMeshSystem *m_navSystem; + AnimationTreeSystem *m_animTreeSystem = nullptr; + EditorApp *m_editorApp = nullptr; + + // Track which entities we set up ActionDebug for + std::unordered_set m_managedActionDebugs; +}; + +#endif // EDITSCENE_GOAP_RUNNER_SYSTEM_HPP diff --git a/src/features/editScene/systems/PathFollowingSystem.cpp b/src/features/editScene/systems/PathFollowingSystem.cpp new file mode 100644 index 0000000..b8cad44 --- /dev/null +++ b/src/features/editScene/systems/PathFollowingSystem.cpp @@ -0,0 +1,205 @@ +#include "PathFollowingSystem.hpp" +#include "AnimationTreeSystem.hpp" +#include "NavMeshSystem.hpp" +#include "../components/PathFollowing.hpp" +#include "../components/Character.hpp" +#include "../components/CharacterSlots.hpp" +#include "../components/Transform.hpp" +#include "../components/NavMesh.hpp" +#include +#include + +PathFollowingSystem::PathFollowingSystem(flecs::world &world, + Ogre::SceneManager *sceneMgr, + NavMeshSystem *navSystem) + : m_world(world) + , m_sceneMgr(sceneMgr) + , m_navSystem(navSystem) +{ +} + +PathFollowingSystem::~PathFollowingSystem() = default; + +Ogre::Vector3 PathFollowingSystem::getEntityPosition(flecs::entity e) +{ + if (e.has()) { + auto &trans = e.get(); + if (trans.node) + return trans.node->_getDerivedPosition(); + return trans.position; + } + return Ogre::Vector3::ZERO; +} + +void PathFollowingSystem::rotateTowards(flecs::entity e, + const Ogre::Vector3 &direction, + float deltaTime) +{ + if (!e.has()) + return; + auto &trans = e.get_mut(); + if (!trans.node) + return; + + Ogre::Vector3 flatDir = direction; + flatDir.y = 0; + if (flatDir.squaredLength() < 0.0001f) + return; + flatDir.normalise(); + + // Get the character's front-facing axis + Ogre::Vector3 frontAxis = Ogre::Vector3::NEGATIVE_UNIT_Z; + if (e.has()) { + auto &slots = e.get(); + frontAxis = slots.frontAxis; + } + + // Use yaw rotation only (Y plane) + Ogre::Quaternion currentRot = trans.node->getOrientation(); + Ogre::Quaternion targetRot = frontAxis.getRotationTo(flatDir); + + // Slerp for smooth rotation + Ogre::Quaternion newRot = Ogre::Quaternion::Slerp( + deltaTime * 10.0f, currentRot, targetRot, true); + + // Extract only the Y-axis rotation (yaw) to keep character upright + Ogre::Radian yaw = newRot.getYaw(); + trans.node->setOrientation( + Ogre::Quaternion(yaw, Ogre::Vector3::UNIT_Y)); + trans.rotation = Ogre::Quaternion(yaw, Ogre::Vector3::UNIT_Y); +} + +void PathFollowingSystem::applyLocomotionState(flecs::entity e) +{ + if (!e.has() || !m_animTreeSystem) + return; + + auto &path = e.get(); + const PathFollowingState *state = + path.findState(path.currentLocomotionState); + if (!state) + return; + + for (const auto &pair : state->stateMachineStates) { + m_animTreeSystem->setState(e, pair.first, pair.second, false); + } +} + +void PathFollowingSystem::update(float deltaTime) +{ + // Find the navmesh entity + flecs::entity navmeshEntity = flecs::entity::null(); + m_world.query().each( + [&](flecs::entity e, NavMeshComponent &) { + if (!navmeshEntity.is_alive()) + navmeshEntity = e; + }); + + m_world + .query() + .each([&](flecs::entity e, PathFollowingComponent &pf, + CharacterComponent &cc, TransformComponent &trans) { + (void)trans; + + if (!pf.hasTarget) + return; + + Ogre::Vector3 charPos = getEntityPosition(e); + Ogre::Vector3 targetPos = pf.targetPosition; + + // Check if we've reached the destination + Ogre::Vector3 toFinal = targetPos - charPos; + toFinal.y = 0; + float distToFinal = toFinal.length(); + if (distToFinal < 0.5f) { + cc.linearVelocity = Ogre::Vector3::ZERO; + pf.hasTarget = false; + pf.currentLocomotionState = "idle"; + pf.path.clear(); + pf.pathIndex = 0; + applyLocomotionState(e); + return; + } + + // Recalculate path periodically + pf.pathRecalcTimer += deltaTime; + if (pf.pathRecalcTimer > 2.0f) { + pf.pathRecalcTimer = 0.0f; + if (navmeshEntity.is_alive() && m_navSystem) { + std::vector newPath; + bool found = m_navSystem->findPath( + navmeshEntity, charPos, targetPos, + newPath); + if (found && !newPath.empty()) { + pf.path = std::move(newPath); + pf.pathIndex = 0; + } + } + } + + // Fallback: if no path, go direct + if (pf.path.empty()) { + pf.path.clear(); + pf.path.push_back(targetPos); + pf.pathIndex = 0; + } + + // Advance waypoints + if (pf.pathIndex >= (int)pf.path.size()) { + pf.path.clear(); + pf.path.push_back(targetPos); + pf.pathIndex = 0; + } + + Ogre::Vector3 waypointPos = pf.path[pf.pathIndex]; + Ogre::Vector3 toTarget = waypointPos - charPos; + + const float WAYPOINT_THRESHOLD = 0.5f; + if (toTarget.length() < WAYPOINT_THRESHOLD) { + pf.pathIndex++; + if (pf.pathIndex >= (int)pf.path.size()) { + // Last waypoint - check if close to final target + if (distToFinal < 0.5f) { + cc.linearVelocity = Ogre::Vector3::ZERO; + pf.hasTarget = false; + pf.currentLocomotionState = "idle"; + pf.path.clear(); + pf.pathIndex = 0; + applyLocomotionState(e); + return; + } + // Extend with direct target + pf.path.clear(); + pf.path.push_back(targetPos); + pf.pathIndex = 0; + } + waypointPos = pf.path[pf.pathIndex]; + toTarget = waypointPos - charPos; + } + + // Move toward waypoint + toTarget.y = 0; + if (toTarget.squaredLength() > 0.0001f) { + toTarget.normalise(); + + // Determine speed based on distance to final target + float speed = pf.walkSpeed; + if (distToFinal > pf.walkSpeed * 3.0f) { + speed = pf.runSpeed; + pf.currentLocomotionState = "run"; + } else { + speed = pf.walkSpeed; + pf.currentLocomotionState = "walk"; + } + + cc.linearVelocity = toTarget * speed; + + // Rotate character to face movement direction + rotateTowards(e, toTarget, deltaTime); + } else { + cc.linearVelocity = Ogre::Vector3::ZERO; + } + + applyLocomotionState(e); + }); +} diff --git a/src/features/editScene/systems/PathFollowingSystem.hpp b/src/features/editScene/systems/PathFollowingSystem.hpp new file mode 100644 index 0000000..f207a37 --- /dev/null +++ b/src/features/editScene/systems/PathFollowingSystem.hpp @@ -0,0 +1,45 @@ +#ifndef EDITSCENE_PATH_FOLLOWING_SYSTEM_HPP +#define EDITSCENE_PATH_FOLLOWING_SYSTEM_HPP +#pragma once + +#include +#include + +// Forward declarations +class AnimationTreeSystem; +class NavMeshSystem; + +/** + * System that follows navmesh paths and drives character movement. + * + * Reads path waypoints from PathFollowingComponent, advances waypoints, + * rotates the character toward the movement direction (Y-axis only), + * sets linear velocity for root motion, and applies animation states + * via AnimationTreeSystem. + */ +class PathFollowingSystem { +public: + PathFollowingSystem(flecs::world &world, Ogre::SceneManager *sceneMgr, + NavMeshSystem *navSystem); + ~PathFollowingSystem(); + + void setAnimationTreeSystem(AnimationTreeSystem *system) + { + m_animTreeSystem = system; + } + + void update(float deltaTime); + +private: + void applyLocomotionState(flecs::entity e); + Ogre::Vector3 getEntityPosition(flecs::entity e); + void rotateTowards(flecs::entity e, const Ogre::Vector3 &direction, + float deltaTime); + + flecs::world &m_world; + Ogre::SceneManager *m_sceneMgr; + NavMeshSystem *m_navSystem; + AnimationTreeSystem *m_animTreeSystem = nullptr; +}; + +#endif // EDITSCENE_PATH_FOLLOWING_SYSTEM_HPP diff --git a/src/features/editScene/systems/PrefabSystem.cpp b/src/features/editScene/systems/PrefabSystem.cpp index df2980c..02ca2a6 100644 --- a/src/features/editScene/systems/PrefabSystem.cpp +++ b/src/features/editScene/systems/PrefabSystem.cpp @@ -13,8 +13,7 @@ std::string PrefabSystem::getPrefabsDirectory() return "prefabs"; } -PrefabSystem::PrefabSystem(flecs::world &world, - Ogre::SceneManager *sceneMgr) +PrefabSystem::PrefabSystem(flecs::world &world, Ogre::SceneManager *sceneMgr) : m_world(world) , m_sceneMgr(sceneMgr) { @@ -35,7 +34,8 @@ void PrefabSystem::resolveInstances() m_lastError = serializer.getLastError(); Ogre::LogManager::getSingleton().logMessage( "PrefabSystem: Failed to instantiate '" + - prefab.prefabPath + "': " + m_lastError); + prefab.prefabPath + + "': " + m_lastError); } }); } @@ -116,12 +116,12 @@ flecs::entity PrefabSystem::createInstance(const std::string &prefabPath, transform.node->_getDerivedPosition()) + " parent=" + (parent.is_valid() && parent != 0 ? - std::to_string(parent.id()) : - "") + + std::to_string(parent.id()) : + "") + " nodeParent=" + (parentNode != m_sceneMgr->getRootSceneNode() ? - parentNode->getName() : - "")); + parentNode->getName() : + "")); if (uiSystem) uiSystem->addEntity(instance); @@ -151,3 +151,24 @@ bool PrefabSystem::savePrefab(flecs::entity rootEntity, } return true; } + +bool PrefabSystem::deletePrefab(const std::string &prefabPath) +{ + try { + if (!std::filesystem::exists(prefabPath)) { + m_lastError = "Prefab file not found: " + prefabPath; + return false; + } + if (!std::filesystem::remove(prefabPath)) { + m_lastError = "Failed to delete prefab: " + prefabPath; + return false; + } + Ogre::LogManager::getSingleton().logMessage( + "PrefabSystem: Deleted prefab '" + prefabPath + "'"); + return true; + } catch (const std::exception &e) { + m_lastError = + std::string("Failed to delete prefab: ") + e.what(); + return false; + } +} diff --git a/src/features/editScene/systems/PrefabSystem.hpp b/src/features/editScene/systems/PrefabSystem.hpp index b710ce3..fa7343a 100644 --- a/src/features/editScene/systems/PrefabSystem.hpp +++ b/src/features/editScene/systems/PrefabSystem.hpp @@ -52,6 +52,13 @@ public: bool savePrefab(flecs::entity rootEntity, const std::string &prefabPath); + /** + * @brief Delete a prefab JSON file. + * @param prefabPath Path to the prefab file to delete. + * @return true on success. + */ + bool deletePrefab(const std::string &prefabPath); + /** * @brief Get the directory where prefabs are stored. */ diff --git a/src/features/editScene/systems/SceneSerializer.cpp b/src/features/editScene/systems/SceneSerializer.cpp index f217189..1f72b14 100644 --- a/src/features/editScene/systems/SceneSerializer.cpp +++ b/src/features/editScene/systems/SceneSerializer.cpp @@ -31,6 +31,8 @@ #include "../components/ActionDatabase.hpp" #include "../components/ActionDebug.hpp" #include "../components/SmartObject.hpp" +#include "../components/GoapPlanner.hpp" +#include "../components/PathFollowing.hpp" #include "../components/BehaviorTree.hpp" #include "../components/GoapBlackboard.hpp" #include "../components/NavMesh.hpp" @@ -295,9 +297,15 @@ nlohmann::json SceneSerializer::serializeEntity(flecs::entity entity) if (entity.has()) { json["actionDebug"] = serializeActionDebug(entity); } + if (entity.has()) { + json["pathFollowing"] = serializePathFollowing(entity); + } if (entity.has()) { json["smartObject"] = serializeSmartObject(entity); } + if (entity.has()) { + json["goapPlanner"] = serializeGoapPlanner(entity); + } if (entity.has()) { json["behaviorTree"] = serializeBehaviorTree(entity); } @@ -495,9 +503,15 @@ void SceneSerializer::deserializeEntity(const nlohmann::json &json, if (json.contains("actionDebug")) { deserializeActionDebug(entity, json["actionDebug"]); } + if (json.contains("pathFollowing")) { + deserializePathFollowing(entity, json["pathFollowing"]); + } if (json.contains("smartObject")) { deserializeSmartObject(entity, json["smartObject"]); } + if (json.contains("goapPlanner")) { + deserializeGoapPlanner(entity, json["goapPlanner"]); + } if (json.contains("behaviorTree")) { deserializeBehaviorTree(entity, json["behaviorTree"]); } @@ -771,9 +785,15 @@ void SceneSerializer::deserializeEntityComponents( if (json.contains("actionDebug")) { deserializeActionDebug(entity, json["actionDebug"]); } + if (json.contains("pathFollowing")) { + deserializePathFollowing(entity, json["pathFollowing"]); + } if (json.contains("smartObject")) { deserializeSmartObject(entity, json["smartObject"]); } + if (json.contains("goapPlanner")) { + deserializeGoapPlanner(entity, json["goapPlanner"]); + } if (json.contains("behaviorTree")) { deserializeBehaviorTree(entity, json["behaviorTree"]); } @@ -3172,6 +3192,8 @@ static nlohmann::json serializeGoapBlackboard(const GoapBlackboard &bb) nlohmann::json json; json["bits"] = (uint64_t)bb.bits; json["mask"] = (uint64_t)bb.mask; + if (bb.bitmask != ~0ULL) + json["bitmask"] = (uint64_t)bb.bitmask; if (!bb.values.empty()) { json["values"] = nlohmann::json::object(); for (const auto &pair : bb.values) @@ -3200,6 +3222,7 @@ static void deserializeGoapBlackboard(GoapBlackboard &bb, { bb.bits = json.value("bits", (uint64_t)0); bb.mask = json.value("mask", (uint64_t)0); + bb.bitmask = json.value("bitmask", ~0ULL); bb.values.clear(); if (json.contains("values") && json["values"].is_object()) { for (auto &[key, val] : json["values"].items()) @@ -3262,6 +3285,8 @@ static nlohmann::json serializeGoapAction(const GoapAction &action) json["cost"] = action.cost; json["preconditions"] = serializeGoapBlackboard(action.preconditions); json["effects"] = serializeGoapBlackboard(action.effects); + if (action.preconditionMask != ~0ULL) + json["preconditionMask"] = action.preconditionMask; json["behaviorTree"] = serializeBehaviorTreeNode(action.behaviorTree); if (!action.behaviorTreeName.empty()) json["behaviorTreeName"] = action.behaviorTreeName; @@ -3278,6 +3303,7 @@ static void deserializeGoapAction(GoapAction &action, json["preconditions"]); if (json.contains("effects")) deserializeGoapBlackboard(action.effects, json["effects"]); + action.preconditionMask = json.value("preconditionMask", ~0ULL); if (json.contains("behaviorTree")) deserializeBehaviorTreeNode(action.behaviorTree, json["behaviorTree"]); @@ -3377,10 +3403,15 @@ nlohmann::json SceneSerializer::serializeActionDebug(flecs::entity entity) json["selectedActionName"] = debug.selectedActionName; if (!debug.selectedGoalName.empty()) json["selectedGoalName"] = debug.selectedGoalName; + return json; +} - // Serialize path following animation states +nlohmann::json SceneSerializer::serializePathFollowing(flecs::entity entity) +{ + const PathFollowingComponent &pf = entity.get(); + nlohmann::json json; json["pathFollowingStates"] = nlohmann::json::array(); - for (const auto &pfState : debug.pathFollowingStates) { + for (const auto &pfState : pf.pathFollowingStates) { nlohmann::json entry; entry["name"] = pfState.name; entry["stateMachineStates"] = nlohmann::json::array(); @@ -3392,26 +3423,30 @@ nlohmann::json SceneSerializer::serializeActionDebug(flecs::entity entity) } json["pathFollowingStates"].push_back(entry); } - - json["walkSpeed"] = debug.walkSpeed; - json["runSpeed"] = debug.runSpeed; - json["useRootMotion"] = debug.useRootMotion; + json["walkSpeed"] = pf.walkSpeed; + json["runSpeed"] = pf.runSpeed; + json["useRootMotion"] = pf.useRootMotion; return json; } void SceneSerializer::deserializeActionDebug(flecs::entity entity, - const nlohmann::json &json) + const nlohmann::json &json) { ActionDebug debug; if (json.contains("blackboard")) deserializeGoapBlackboard(debug.blackboard, json["blackboard"]); debug.selectedActionName = json.value("selectedActionName", ""); debug.selectedGoalName = json.value("selectedGoalName", ""); + entity.set(debug); +} - // Deserialize path following animation states +void SceneSerializer::deserializePathFollowing(flecs::entity entity, + const nlohmann::json &json) +{ + PathFollowingComponent pf; if (json.contains("pathFollowingStates") && json["pathFollowingStates"].is_array()) { - debug.pathFollowingStates.clear(); + pf.pathFollowingStates.clear(); for (const auto &entry : json["pathFollowingStates"]) { PathFollowingState pfState; pfState.name = entry.value("name", ""); @@ -3427,12 +3462,12 @@ void SceneSerializer::deserializeActionDebug(flecs::entity entity, { sm, state }); } } - debug.pathFollowingStates.push_back(pfState); + pf.pathFollowingStates.push_back(pfState); } } else if (json.contains("animStates") && json["animStates"].is_array()) { // Backward compatibility: old animStates format - debug.pathFollowingStates.clear(); + pf.pathFollowingStates.clear(); for (const auto &entry : json["animStates"]) { PathFollowingState pfState; pfState.name = entry.value("name", ""); @@ -3445,40 +3480,20 @@ void SceneSerializer::deserializeActionDebug(flecs::entity entity, pfState.stateMachineStates.push_back({ mainSM, subSM }); pfState.stateMachineStates.push_back( { subSM, stateName }); - debug.pathFollowingStates.push_back(pfState); + pf.pathFollowingStates.push_back(pfState); } } else { - // Backward compatibility: old individual fields format - debug.pathFollowingStates.clear(); - Ogre::String sm = - json.value("locomotionStateMachine", "Locomotion"); - { - PathFollowingState idle("idle"); - idle.stateMachineStates.push_back({ "main", sm }); - idle.stateMachineStates.push_back( - { sm, json.value("idleStateName", "Idle") }); - debug.pathFollowingStates.push_back(idle); - } - { - PathFollowingState walk("walk"); - walk.stateMachineStates.push_back({ "main", sm }); - walk.stateMachineStates.push_back( - { sm, json.value("walkStateName", "Walk") }); - debug.pathFollowingStates.push_back(walk); - } - { - PathFollowingState run("run"); - run.stateMachineStates.push_back({ "main", sm }); - run.stateMachineStates.push_back( - { sm, json.value("runStateName", "Run") }); - debug.pathFollowingStates.push_back(run); - } + // Default states + pf.pathFollowingStates.clear(); + pf.pathFollowingStates.push_back(PathFollowingState("idle")); + pf.pathFollowingStates.push_back(PathFollowingState("walk")); + pf.pathFollowingStates.push_back(PathFollowingState("run")); } - debug.walkSpeed = json.value("walkSpeed", 2.5f); - debug.runSpeed = json.value("runSpeed", 5.0f); - debug.useRootMotion = json.value("useRootMotion", true); - entity.set(debug); + pf.walkSpeed = json.value("walkSpeed", 2.5f); + pf.runSpeed = json.value("runSpeed", 5.0f); + pf.useRootMotion = json.value("useRootMotion", true); + entity.set(pf); } nlohmann::json SceneSerializer::serializeSmartObject(flecs::entity entity) @@ -3507,6 +3522,45 @@ void SceneSerializer::deserializeSmartObject(flecs::entity entity, entity.set(so); } +nlohmann::json SceneSerializer::serializeGoapPlanner(flecs::entity entity) +{ + const GoapPlannerComponent &planner = entity.get(); + nlohmann::json json; + json["actionNames"] = planner.actionNames; + if (!planner.goalNames.empty()) + json["goalNames"] = planner.goalNames; + json["smartObjectDistance"] = planner.smartObjectDistance; + json["includeSmartObjects"] = planner.includeSmartObjects; + if (!planner.actionDatabaseRef.empty()) + json["actionDatabaseRef"] = planner.actionDatabaseRef; + return json; +} + +void SceneSerializer::deserializeGoapPlanner(flecs::entity entity, + const nlohmann::json &json) +{ + GoapPlannerComponent planner; + if (json.contains("actionNames") && json["actionNames"].is_array()) { + planner.actionNames.clear(); + for (const auto &name : json["actionNames"]) { + if (name.is_string()) + planner.actionNames.push_back(name); + } + } + if (json.contains("goalNames") && json["goalNames"].is_array()) { + planner.goalNames.clear(); + for (const auto &name : json["goalNames"]) { + if (name.is_string()) + planner.goalNames.push_back(name); + } + } + planner.smartObjectDistance = json.value("smartObjectDistance", 50.0f); + planner.includeSmartObjects = json.value("includeSmartObjects", true); + planner.actionDatabaseRef = json.value("actionDatabaseRef", ""); + planner.planDirty = true; + entity.set(planner); +} + nlohmann::json SceneSerializer::serializeBehaviorTree(flecs::entity entity) { const BehaviorTreeComponent &bt = entity.get(); diff --git a/src/features/editScene/systems/SceneSerializer.hpp b/src/features/editScene/systems/SceneSerializer.hpp index 8eb127d..e8ae2f4 100644 --- a/src/features/editScene/systems/SceneSerializer.hpp +++ b/src/features/editScene/systems/SceneSerializer.hpp @@ -208,14 +208,20 @@ private: // AI/GOAP serialization nlohmann::json serializeActionDatabase(flecs::entity entity); nlohmann::json serializeActionDebug(flecs::entity entity); + nlohmann::json serializePathFollowing(flecs::entity entity); nlohmann::json serializeSmartObject(flecs::entity entity); + nlohmann::json serializeGoapPlanner(flecs::entity entity); nlohmann::json serializeBehaviorTree(flecs::entity entity); void deserializeActionDatabase(flecs::entity entity, const nlohmann::json &json); void deserializeActionDebug(flecs::entity entity, const nlohmann::json &json); + void deserializePathFollowing(flecs::entity entity, + const nlohmann::json &json); void deserializeSmartObject(flecs::entity entity, const nlohmann::json &json); + void deserializeGoapPlanner(flecs::entity entity, + const nlohmann::json &json); void deserializeBehaviorTree(flecs::entity entity, const nlohmann::json &json); diff --git a/src/features/editScene/systems/SmartObjectSystem.cpp b/src/features/editScene/systems/SmartObjectSystem.cpp index 43e5582..292268a 100644 --- a/src/features/editScene/systems/SmartObjectSystem.cpp +++ b/src/features/editScene/systems/SmartObjectSystem.cpp @@ -6,6 +6,7 @@ #include "../components/GoapBlackboard.hpp" #include "../components/ActionDatabase.hpp" #include "../components/ActionDebug.hpp" +#include "../components/PathFollowing.hpp" #include "../components/NavMesh.hpp" #include "../components/AnimationTree.hpp" #include "../components/PlayerController.hpp" @@ -115,11 +116,11 @@ void SmartObjectSystem::setLocomotionState(flecs::entity e, if (!e.has()) return; - // Look up the path following state in ActionDebug - if (e.has()) { - auto &debug = e.get(); + // Look up the path following state in PathFollowingComponent + if (e.has()) { + auto &pf = e.get(); const PathFollowingState *pfState = - debug.findPathState(animName); + pf.findState(animName); if (pfState) { // Apply ALL state machine name -> state name pairs for (const auto &pair : pfState->stateMachineStates) { @@ -174,6 +175,16 @@ bool SmartObjectSystem::testSmartObjectAction(flecs::entity character, return true; } +bool SmartObjectSystem::isIdle(flecs::entity character) const +{ + if (!character.is_alive()) + return true; + auto it = m_states.find(character.id()); + if (it == m_states.end()) + return true; + return it->second.state == State::Idle; +} + void SmartObjectSystem::update(float deltaTime) { // Find the navmesh entity @@ -550,16 +561,16 @@ void SmartObjectSystem::update(float deltaTime) float speed = 2.5f; // walk speed // Use run speed if far away - if (e.has()) { - auto &debug = - e.get(); + if (e.has()) { + auto &pf = + e.get(); if (distToTarget > - debug.walkSpeed * 3.0f) { - speed = debug.runSpeed; + pf.walkSpeed * 3.0f) { + speed = pf.runSpeed; setLocomotionState( e, "run"); } else { - speed = debug.walkSpeed; + speed = pf.walkSpeed; setLocomotionState( e, "walk"); } @@ -919,14 +930,19 @@ void SmartObjectSystem::update(float deltaTime) float distToTarget = (charPos - objPos).length(); - float speed = debug.walkSpeed; - - if (distToTarget > - debug.walkSpeed * 3.0f) { - speed = debug.runSpeed; - setLocomotionState(e, "run"); + float speed = 2.5f; + if (e.has()) { + auto &pf = e.get(); + speed = pf.walkSpeed; + if (distToTarget > + pf.walkSpeed * 3.0f) { + speed = pf.runSpeed; + setLocomotionState(e, "run"); + } else { + speed = pf.walkSpeed; + setLocomotionState(e, "walk"); + } } else { - speed = debug.walkSpeed; setLocomotionState(e, "walk"); } diff --git a/src/features/editScene/systems/SmartObjectSystem.hpp b/src/features/editScene/systems/SmartObjectSystem.hpp index 3be9be4..e48fb3e 100644 --- a/src/features/editScene/systems/SmartObjectSystem.hpp +++ b/src/features/editScene/systems/SmartObjectSystem.hpp @@ -65,6 +65,13 @@ public: flecs::entity smartObject, const Ogre::String &actionName); + /** + * Check if a character's smart object interaction is idle. + * Returns true if the character is not currently pathfinding, + * moving, or executing a smart object action. + */ + bool isIdle(flecs::entity character) const; + private: struct SmartObjectTarget { flecs::entity smartObject; diff --git a/src/features/editScene/ui/ActionDatabaseEditor.cpp b/src/features/editScene/ui/ActionDatabaseEditor.cpp index 42ad7e1..f0692b6 100644 --- a/src/features/editScene/ui/ActionDatabaseEditor.cpp +++ b/src/features/editScene/ui/ActionDatabaseEditor.cpp @@ -173,6 +173,39 @@ void ActionDatabaseEditor::renderActionEditor(GoapAction &action) ImGui::TreePop(); } + // Precondition mask editor + ImGui::Separator(); + ImGui::Text("Precondition Mask (bits to check)"); + for (int i = 0; i < 64; i += 8) { + for (int j = 0; j < 8 && (i + j) < 64; j++) { + int bit = i + j; + bool enabled = (action.preconditionMask >> + bit) & 1ULL; + char label[8]; + snprintf(label, sizeof(label), "%d", bit); + if (j > 0) + ImGui::SameLine(); + if (ImGui::Checkbox(label, &enabled)) { + if (enabled) + action.preconditionMask |= + (1ULL << bit); + else + action.preconditionMask &= + ~(1ULL << bit); + } + } + } + ImGui::Text("Mask: 0x%016llX", + (unsigned long long)action.preconditionMask); + ImGui::SameLine(); + if (ImGui::SmallButton("Set All##mask")) { + action.preconditionMask = ~0ULL; + } + ImGui::SameLine(); + if (ImGui::SmallButton("Clear All##mask")) { + action.preconditionMask = 0ULL; + } + if (ImGui::TreeNode("Effects")) { GoapBlackboardEditor::render(action.effects, "effects"); ImGui::TreePop(); diff --git a/src/features/editScene/ui/ActionDebugEditor.cpp b/src/features/editScene/ui/ActionDebugEditor.cpp index 0b4ec12..22b4c9b 100644 --- a/src/features/editScene/ui/ActionDebugEditor.cpp +++ b/src/features/editScene/ui/ActionDebugEditor.cpp @@ -34,14 +34,14 @@ bool ActionDebugEditor::renderComponent(flecs::entity entity, if (ImGui::CollapsingHeader("Goal Tester")) renderGoalTester(entity, debug); - if (ImGui::CollapsingHeader("Animation Config", - ImGuiTreeNodeFlags_DefaultOpen)) - renderAnimationConfig(debug); - if (ImGui::CollapsingHeader("Smart Object Tester", ImGuiTreeNodeFlags_DefaultOpen)) renderSmartObjectTester(entity, debug); + if (ImGui::CollapsingHeader("Planner", + ImGuiTreeNodeFlags_DefaultOpen)) + renderPlanner(entity, debug); + if (!debug.lastResult.empty()) { ImGui::Separator(); ImGui::Text("Last result: %s", debug.lastResult.c_str()); @@ -151,122 +151,6 @@ void ActionDebugEditor::renderGoalTester(flecs::entity entity, ImGui::Unindent(); } -void ActionDebugEditor::renderAnimationConfig(ActionDebug &debug) -{ - ImGui::Text("Path Following Animation States:"); - ImGui::TextDisabled( - "Each state (idle/walk/run/swim-idle/swim/swim-fast) has unlimited\n" - "state machine name -> state name pairs applied when activated."); - ImGui::Separator(); - - // Render each path following state - int removeStateIdx = -1; - for (int i = 0; i < (int)debug.pathFollowingStates.size(); i++) { - auto &pfState = debug.pathFollowingStates[i]; - ImGui::PushID(i); - - char buf[256]; - - ImGui::Text("State %d:", i); - ImGui::Indent(); - - // Logical name (e.g. "idle", "walk", "run") - strncpy(buf, pfState.name.c_str(), sizeof(buf) - 1); - buf[sizeof(buf) - 1] = '\0'; - if (ImGui::InputText("Name", buf, sizeof(buf))) { - pfState.name = buf; - } - - ImGui::Text("State Machine -> State pairs:"); - ImGui::Indent(); - - // Render each name-value pair - int removePairIdx = -1; - for (int j = 0; j < (int)pfState.stateMachineStates.size(); - j++) { - auto &pair = pfState.stateMachineStates[j]; - ImGui::PushID(j); - - char smBuf[128]; - char stateBuf[128]; - - strncpy(smBuf, pair.first.c_str(), sizeof(smBuf) - 1); - smBuf[sizeof(smBuf) - 1] = '\0'; - ImGui::SetNextItemWidth(120.0f); - if (ImGui::InputText("##sm", smBuf, sizeof(smBuf))) { - pair.first = smBuf; - } - - ImGui::SameLine(); - ImGui::Text("->"); - - ImGui::SameLine(); - strncpy(stateBuf, pair.second.c_str(), - sizeof(stateBuf) - 1); - stateBuf[sizeof(stateBuf) - 1] = '\0'; - ImGui::SetNextItemWidth(120.0f); - if (ImGui::InputText("##state", stateBuf, - sizeof(stateBuf))) { - pair.second = stateBuf; - } - - ImGui::SameLine(); - if (ImGui::SmallButton("X")) { - removePairIdx = j; - } - - ImGui::PopID(); - } - - if (removePairIdx >= 0) { - pfState.stateMachineStates.erase( - pfState.stateMachineStates.begin() + - removePairIdx); - } - - if (ImGui::SmallButton("+ Add Pair")) { - pfState.stateMachineStates.push_back( - { "main", "Idle" }); - } - - ImGui::Unindent(); - - // Remove state button - if (debug.pathFollowingStates.size() > 1) { - if (ImGui::SmallButton("Remove State")) { - removeStateIdx = i; - } - } - - ImGui::Unindent(); - ImGui::Separator(); - ImGui::PopID(); - } - - if (removeStateIdx >= 0) { - debug.pathFollowingStates.erase( - debug.pathFollowingStates.begin() + removeStateIdx); - } - - // Add new state button - if (ImGui::Button("Add Path Following State")) { - debug.pathFollowingStates.push_back( - PathFollowingState("new_state")); - } - - ImGui::Separator(); - ImGui::Text("Movement Speeds:"); - ImGui::Indent(); - - ImGui::DragFloat("Walk Speed (m/s)", &debug.walkSpeed, 0.1f, 0.5f, - 20.0f); - ImGui::DragFloat("Run Speed (m/s)", &debug.runSpeed, 0.1f, 0.5f, 20.0f); - - ImGui::Unindent(); - - ImGui::Separator(); - ImGui::Checkbox("Use Root Motion", &debug.useRootMotion); -} void ActionDebugEditor::renderSmartObjectTester(flecs::entity entity, ActionDebug &debug) @@ -339,3 +223,113 @@ void ActionDebugEditor::renderSmartObjectTester(flecs::entity entity, ImGui::Unindent(); } + +void ActionDebugEditor::renderPlanner(flecs::entity entity, + ActionDebug &debug) +{ + (void)debug; + + if (!entity.has()) { + ImGui::TextDisabled( + "No GoapPlannerComponent found on this entity."); + return; + } + + auto &planner = entity.get_mut(); + + // Status + const char *statusStr = "Idle"; + ImVec4 statusColor(0.7f, 0.7f, 0.7f, 1.0f); + switch (planner.status) { + case GoapPlannerComponent::Status::Idle: + statusStr = "Idle"; + break; + case GoapPlannerComponent::Status::Planning: + statusStr = "Planning..."; + statusColor = ImVec4(1.0f, 0.8f, 0.2f, 1.0f); + break; + case GoapPlannerComponent::Status::PlansAvailable: + statusStr = "Plans Available"; + statusColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); + break; + case GoapPlannerComponent::Status::NoPlanFound: + statusStr = "No Plan Found"; + statusColor = ImVec4(1.0f, 0.3f, 0.3f, 1.0f); + break; + } + ImGui::Text("Status: "); + ImGui::SameLine(); + ImGui::TextColored(statusColor, "%s", statusStr); + + if (planner.status == GoapPlannerComponent::Status::Planning) { + ImGui::Text("Nodes explored: %d", + planner.nodesExplored); + } + + if (!planner.currentGoalName.empty()) { + ImGui::Text("Current Goal: %s", + planner.currentGoalName.c_str()); + } + + ImGui::Separator(); + + // Display plan queue + if (planner.planQueue.empty()) { + ImGui::TextDisabled("No plans in queue."); + } else { + ImGui::Text("Plan Queue (%zu plans):", + planner.planQueue.size()); + ImGui::Indent(); + + ActionDatabase *db = findDatabase(entity); + + for (int p = 0; p < (int)planner.planQueue.size(); p++) { + const auto &plan = planner.planQueue[p]; + ImGui::Text("Plan %d (cost: %d, goal: %s):", + p + 1, plan.totalCost, + plan.goalName.c_str()); + ImGui::Indent(); + for (int i = 0; + i < (int)plan.actions.size(); i++) { + const auto &actionName = plan.actions[i]; + bool isSmartObjectAction = false; + if (db) { + const GoapAction *action = + db->findAction(actionName); + if (!action) + isSmartObjectAction = true; + } + if (isSmartObjectAction) { + ImGui::TextColored( + ImVec4(0.3f, 0.8f, 1.0f, + 1.0f), + "%d. %s [SO]", i + 1, + actionName.c_str()); + } else { + ImGui::Text("%d. %s", i + 1, + actionName.c_str()); + } + } + ImGui::Unindent(); + } + + ImGui::Unindent(); + } + + ImGui::Separator(); + + // Trigger replan button + if (ImGui::Button("Force Replan")) { + planner.planDirty = true; + } + + // Show selected planner actions + ImGui::Separator(); + ImGui::Text("Planner Action Pool (%zu):", + planner.actionNames.size()); + ImGui::Indent(); + for (const auto &name : planner.actionNames) { + ImGui::Text("%s", name.c_str()); + } + ImGui::Unindent(); +} diff --git a/src/features/editScene/ui/ActionDebugEditor.hpp b/src/features/editScene/ui/ActionDebugEditor.hpp index 807bfc7..691d195 100644 --- a/src/features/editScene/ui/ActionDebugEditor.hpp +++ b/src/features/editScene/ui/ActionDebugEditor.hpp @@ -6,6 +6,7 @@ #include "../components/ActionDebug.hpp" #include "../components/ActionDatabase.hpp" #include "../components/SmartObject.hpp" +#include "../components/GoapPlanner.hpp" /** * Editor for ActionDebug component. @@ -28,8 +29,8 @@ private: bool renderBlackboard(ActionDebug &debug); void renderActionTester(flecs::entity entity, ActionDebug &debug); void renderGoalTester(flecs::entity entity, ActionDebug &debug); - void renderAnimationConfig(ActionDebug &debug); void renderSmartObjectTester(flecs::entity entity, ActionDebug &debug); + void renderPlanner(flecs::entity entity, ActionDebug &debug); ActionDatabase *findDatabase(flecs::entity entity); }; diff --git a/src/features/editScene/ui/GoapPlannerEditor.cpp b/src/features/editScene/ui/GoapPlannerEditor.cpp new file mode 100644 index 0000000..a2bba17 --- /dev/null +++ b/src/features/editScene/ui/GoapPlannerEditor.cpp @@ -0,0 +1,296 @@ +#include "GoapPlannerEditor.hpp" +#include "../components/EntityName.hpp" +#include + +ActionDatabase *GoapPlannerEditor::findDatabase( + flecs::entity entity, GoapPlannerComponent &planner) +{ + auto world = entity.world(); + + // 1. Try to find by referenced entity name + if (!planner.actionDatabaseRef.empty()) { + ActionDatabase *found = nullptr; + world.query() + .each([&](flecs::entity dbEntity, ActionDatabase &db) { + if (found) + return; + if (dbEntity.has()) { + auto &name = + dbEntity.get(); + if (name.name == + planner.actionDatabaseRef) + found = &db; + } + }); + if (found) + return found; + } + + // 2. Fall back to any ActionDatabase in the world + ActionDatabase *db = nullptr; + world.query().each( + [&](flecs::entity, ActionDatabase &database) { + if (!db) + db = &database; + }); + return db; +} + +bool GoapPlannerEditor::renderComponent(flecs::entity entity, + GoapPlannerComponent &planner) +{ + bool modified = false; + ImGui::PushID("GoapPlanner"); + + // External ActionDatabase reference + char refBuf[256]; + snprintf(refBuf, sizeof(refBuf), "%s", + planner.actionDatabaseRef.c_str()); + if (ImGui::InputText("Action Database Ref", refBuf, + sizeof(refBuf))) { + planner.actionDatabaseRef = refBuf; + modified = true; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip( + "Name of the entity that owns the ActionDatabase. " + "Used to resolve action and goal names when editing prefabs."); + } + + ImGui::Separator(); + + // Smart object settings + if (ImGui::DragFloat("Smart Object Distance", + &planner.smartObjectDistance, 0.5f, + 1.0f, 500.0f)) { + modified = true; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip( + "Maximum distance to search for smart objects " + "with matching actions"); + } + + if (ImGui::Checkbox("Include Smart Objects", + &planner.includeSmartObjects)) { + modified = true; + } + + ImGui::Separator(); + + // Action and Goal selectors + ActionDatabase *db = findDatabase(entity, planner); + renderActionSelector(planner, db); + + ImGui::Separator(); + + renderGoalSelector(planner, db); + + ImGui::PopID(); + return modified; +} + +void GoapPlannerEditor::renderActionSelector( + GoapPlannerComponent &planner, ActionDatabase *db) +{ + ImGui::Text("Selected Actions (%zu):", + planner.actionNames.size()); + + // Show currently selected actions with remove buttons + int removeIdx = -1; + for (int i = 0; i < (int)planner.actionNames.size(); i++) { + ImGui::PushID(i); + ImGui::Text("%s", planner.actionNames[i].c_str()); + ImGui::SameLine(); + if (ImGui::SmallButton("X")) { + removeIdx = i; + } + ImGui::PopID(); + } + if (removeIdx >= 0) { + planner.actionNames.erase( + planner.actionNames.begin() + removeIdx); + planner.planDirty = true; + } + + if (planner.actionNames.empty()) { + ImGui::TextDisabled("No actions selected"); + } + + ImGui::Separator(); + + // Add actions from database + if (!db) { + ImGui::TextDisabled( + "No ActionDatabase found. Add actions manually:"); + static char manualAction[256] = { 0 }; + if (ImGui::InputText("Action Name", manualAction, + sizeof(manualAction), + ImGuiInputTextFlags_EnterReturnsTrue)) { + if (manualAction[0] != '\0') { + planner.actionNames.push_back(manualAction); + planner.planDirty = true; + manualAction[0] = '\0'; + } + } + return; + } + + ImGui::Text("Available Actions from Database:"); + ImGui::Indent(); + + for (const auto &action : db->actions) { + bool alreadySelected = false; + for (const auto &name : planner.actionNames) { + if (name == action.name) { + alreadySelected = true; + break; + } + } + + ImGui::PushID(action.name.c_str()); + + if (alreadySelected) { + ImGui::BeginDisabled(); + } + + if (ImGui::Button("+")) { + planner.actionNames.push_back(action.name); + planner.planDirty = true; + } + + if (alreadySelected) { + ImGui::EndDisabled(); + } + + ImGui::SameLine(); + ImGui::Text("%s (cost: %d)", action.name.c_str(), + action.cost); + + ImGui::PopID(); + } + + ImGui::Unindent(); +} + +void GoapPlannerEditor::renderGoalSelector( + GoapPlannerComponent &planner, ActionDatabase *db) +{ + ImGui::Text("Selected Goals (%zu):", + planner.goalNames.size()); + + // Show currently selected goals with remove buttons + int removeIdx = -1; + for (int i = 0; i < (int)planner.goalNames.size(); i++) { + ImGui::PushID(i + 10000); + ImGui::Text("%s", planner.goalNames[i].c_str()); + ImGui::SameLine(); + if (ImGui::SmallButton("X")) { + removeIdx = i; + } + ImGui::PopID(); + } + if (removeIdx >= 0) { + planner.goalNames.erase( + planner.goalNames.begin() + removeIdx); + planner.planDirty = true; + } + + if (planner.goalNames.empty()) { + ImGui::TextDisabled("No goals selected"); + } + + ImGui::Separator(); + + // Add goals from database + if (!db) { + ImGui::TextDisabled( + "No ActionDatabase found. Add goals manually:"); + static char manualGoal[256] = { 0 }; + if (ImGui::InputText("Goal Name", manualGoal, + sizeof(manualGoal), + ImGuiInputTextFlags_EnterReturnsTrue)) { + if (manualGoal[0] != '\0') { + planner.goalNames.push_back(manualGoal); + planner.planDirty = true; + manualGoal[0] = '\0'; + } + } + return; + } + + ImGui::Text("Available Goals from Database:"); + ImGui::Indent(); + + for (const auto &goal : db->goals) { + bool alreadySelected = false; + for (const auto &name : planner.goalNames) { + if (name == goal.name) { + alreadySelected = true; + break; + } + } + + ImGui::PushID(goal.name.c_str()); + + if (alreadySelected) { + ImGui::BeginDisabled(); + } + + if (ImGui::Button("+")) { + planner.goalNames.push_back(goal.name); + planner.planDirty = true; + } + + if (alreadySelected) { + ImGui::EndDisabled(); + } + + ImGui::SameLine(); + ImGui::Text("%s (prio: %d)", goal.name.c_str(), + goal.priority); + + ImGui::PopID(); + } + + ImGui::Unindent(); +} + +void GoapPlannerEditor::renderPreconditionMaskEditor( + GoapAction &action) +{ + ImGui::Separator(); + ImGui::Text("Precondition Mask (bit filter)"); + ImGui::TextDisabled( + "Only bits set here are checked during planning."); + + for (int i = 0; i < 64; i += 8) { + for (int j = 0; j < 8 && (i + j) < 64; j++) { + int bit = i + j; + bool enabled = (action.preconditionMask >> + bit) & 1ULL; + char label[8]; + snprintf(label, sizeof(label), "%d", bit); + if (j > 0) + ImGui::SameLine(); + if (ImGui::Checkbox(label, &enabled)) { + if (enabled) + action.preconditionMask |= (1ULL << bit); + else + action.preconditionMask &= + ~(1ULL << bit); + } + } + } + + ImGui::Text("Mask: 0x%016llX", + (unsigned long long)action.preconditionMask); + ImGui::SameLine(); + if (ImGui::SmallButton("Set All")) { + action.preconditionMask = ~0ULL; + } + ImGui::SameLine(); + if (ImGui::SmallButton("Clear All")) { + action.preconditionMask = 0ULL; + } +} diff --git a/src/features/editScene/ui/GoapPlannerEditor.hpp b/src/features/editScene/ui/GoapPlannerEditor.hpp new file mode 100644 index 0000000..f87e7c6 --- /dev/null +++ b/src/features/editScene/ui/GoapPlannerEditor.hpp @@ -0,0 +1,37 @@ +#ifndef EDITSCENE_GOAP_PLANNER_EDITOR_HPP +#define EDITSCENE_GOAP_PLANNER_EDITOR_HPP +#pragma once + +#include "ComponentEditor.hpp" +#include "../components/GoapPlanner.hpp" +#include "../components/ActionDatabase.hpp" + +/** + * Editor for GoapPlannerComponent. + * + * Allows selecting actions from an ActionDatabase, + * configuring smart-object distance, and referencing + * an external ActionDatabase entity. + */ +class GoapPlannerEditor : public ComponentEditor { +public: + const char *getName() const override + { + return "GOAP Planner"; + } + +protected: + bool renderComponent(flecs::entity entity, + GoapPlannerComponent &planner) override; + +private: + ActionDatabase *findDatabase(flecs::entity entity, + GoapPlannerComponent &planner); + void renderActionSelector(GoapPlannerComponent &planner, + ActionDatabase *db); + void renderGoalSelector(GoapPlannerComponent &planner, + ActionDatabase *db); + void renderPreconditionMaskEditor(GoapAction &action); +}; + +#endif // EDITSCENE_GOAP_PLANNER_EDITOR_HPP diff --git a/src/features/editScene/ui/GoapRunnerEditor.cpp b/src/features/editScene/ui/GoapRunnerEditor.cpp new file mode 100644 index 0000000..4fe7873 --- /dev/null +++ b/src/features/editScene/ui/GoapRunnerEditor.cpp @@ -0,0 +1,99 @@ +#include "GoapRunnerEditor.hpp" +#include "../components/GoapPlanner.hpp" +#include + +const char *GoapRunnerEditor::stateName(GoapRunnerComponent::State state) +{ + switch (state) { + case GoapRunnerComponent::State::Idle: + return "Idle"; + case GoapRunnerComponent::State::RunningAction: + return "Running Action"; + case GoapRunnerComponent::State::MovingToSmartObject: + return "Moving to Smart Object"; + case GoapRunnerComponent::State::ExecutingSmartObject: + return "Executing Smart Object"; + case GoapRunnerComponent::State::PlanComplete: + return "Plan Complete"; + } + return "Unknown"; +} + +bool GoapRunnerEditor::renderComponent(flecs::entity entity, + GoapRunnerComponent &runner) +{ + bool modified = false; + ImGui::PushID("GoapRunner"); + + ImGui::Text("State: %s", stateName(runner.state)); + ImGui::Text("Action Index: %d", runner.currentActionIndex); + if (!runner.currentActionName.empty()) { + ImGui::Text("Current Action: %s", + runner.currentActionName.c_str()); + } + if (runner.targetSmartObjectId != 0) { + ImGui::Text("Target Smart Object: %llu", + (unsigned long long)runner.targetSmartObjectId); + } + + if (ImGui::Checkbox("Auto Replan", &runner.autoReplan)) + modified = true; + + ImGui::Separator(); + + // Display active plan + if (!runner.planActions.empty()) { + ImGui::Text("Active Plan (%zu actions):", + runner.planActions.size()); + ImGui::Indent(); + for (int i = 0; i < (int)runner.planActions.size(); i++) { + bool isCurrent = (i == runner.currentActionIndex); + if (isCurrent) { + ImGui::TextColored(ImVec4(0, 1, 0, 1), + "-> %d. %s", i + 1, + runner.planActions[i].c_str()); + } else { + ImGui::Text(" %d. %s", i + 1, + runner.planActions[i].c_str()); + } + } + ImGui::Unindent(); + } else { + ImGui::TextDisabled("No active plan."); + } + + // Display plan queue + if (entity.has()) { + auto &planner = entity.get(); + if (!planner.planQueue.empty()) { + ImGui::Separator(); + ImGui::Text("Plan Queue (%zu plans):", + planner.planQueue.size()); + ImGui::Indent(); + for (int p = 0; + p < (int)planner.planQueue.size(); p++) { + const auto &plan = planner.planQueue[p]; + ImGui::Text("Plan %d: cost=%d, %zu actions", + p + 1, plan.totalCost, + plan.actions.size()); + } + ImGui::Unindent(); + } + } + + // Manual controls + if (runner.state != GoapRunnerComponent::State::Idle) { + ImGui::Separator(); + if (ImGui::Button("Stop")) { + runner.state = GoapRunnerComponent::State::Idle; + runner.currentActionIndex = 0; + runner.currentActionName.clear(); + runner.targetSmartObjectId = 0; + runner.planActions.clear(); + modified = true; + } + } + + ImGui::PopID(); + return modified; +} diff --git a/src/features/editScene/ui/GoapRunnerEditor.hpp b/src/features/editScene/ui/GoapRunnerEditor.hpp new file mode 100644 index 0000000..2532e18 --- /dev/null +++ b/src/features/editScene/ui/GoapRunnerEditor.hpp @@ -0,0 +1,26 @@ +#ifndef EDITSCENE_GOAP_RUNNER_EDITOR_HPP +#define EDITSCENE_GOAP_RUNNER_EDITOR_HPP +#pragma once + +#include "ComponentEditor.hpp" +#include "../components/GoapRunner.hpp" + +/** + * Editor for GoapRunnerComponent. + */ +class GoapRunnerEditor : public ComponentEditor { +public: + const char *getName() const override + { + return "GOAP Runner"; + } + +protected: + bool renderComponent(flecs::entity entity, + GoapRunnerComponent &runner) override; + +private: + const char *stateName(GoapRunnerComponent::State state); +}; + +#endif // EDITSCENE_GOAP_RUNNER_EDITOR_HPP diff --git a/src/features/editScene/ui/PathFollowingEditor.cpp b/src/features/editScene/ui/PathFollowingEditor.cpp new file mode 100644 index 0000000..552e9ce --- /dev/null +++ b/src/features/editScene/ui/PathFollowingEditor.cpp @@ -0,0 +1,103 @@ +#include "PathFollowingEditor.hpp" +#include + +bool PathFollowingEditor::renderComponent(flecs::entity entity, + PathFollowingComponent &pf) +{ + (void)entity; + bool modified = false; + ImGui::PushID("PathFollowing"); + + // Current state display + ImGui::Text("Current State: %s", pf.currentLocomotionState.c_str()); + ImGui::Separator(); + + // Path following states + ImGui::Text("Animation States:"); + int removeStateIdx = -1; + for (int i = 0; i < (int)pf.pathFollowingStates.size(); i++) { + auto &pfState = pf.pathFollowingStates[i]; + ImGui::PushID(i); + + char buf[256]; + snprintf(buf, sizeof(buf), "%s", pfState.name.c_str()); + if (ImGui::InputText("Name", buf, sizeof(buf))) { + pfState.name = buf; + modified = true; + } + + ImGui::Indent(); + int removePairIdx = -1; + for (int j = 0; j < (int)pfState.stateMachineStates.size(); j++) { + auto &pair = pfState.stateMachineStates[j]; + ImGui::PushID(j); + + char smBuf[128]; + char stateBuf[128]; + snprintf(smBuf, sizeof(smBuf), "%s", pair.first.c_str()); + snprintf(stateBuf, sizeof(stateBuf), "%s", + pair.second.c_str()); + + ImGui::SetNextItemWidth(120.0f); + if (ImGui::InputText("##sm", smBuf, sizeof(smBuf))) { + pair.first = smBuf; + modified = true; + } + ImGui::SameLine(); + ImGui::Text("->"); + ImGui::SameLine(); + ImGui::SetNextItemWidth(120.0f); + if (ImGui::InputText("##state", stateBuf, + sizeof(stateBuf))) { + pair.second = stateBuf; + modified = true; + } + ImGui::SameLine(); + if (ImGui::SmallButton("X")) { + removePairIdx = j; + } + ImGui::PopID(); + } + + if (removePairIdx >= 0) { + pfState.stateMachineStates.erase( + pfState.stateMachineStates.begin() + + removePairIdx); + modified = true; + } + + if (ImGui::SmallButton("+ Add Pair")) { + pfState.stateMachineStates.push_back({ "main", "Idle" }); + modified = true; + } + + if (pf.pathFollowingStates.size() > 1) { + if (ImGui::SmallButton("Remove State")) { + removeStateIdx = i; + modified = true; + } + } + ImGui::Unindent(); + ImGui::Separator(); + ImGui::PopID(); + } + + if (removeStateIdx >= 0) { + pf.pathFollowingStates.erase( + pf.pathFollowingStates.begin() + removeStateIdx); + } + + if (ImGui::Button("Add State")) { + pf.pathFollowingStates.push_back(PathFollowingState("new_state")); + modified = true; + } + + ImGui::Separator(); + ImGui::DragFloat("Walk Speed", &pf.walkSpeed, 0.1f, 0.5f, 20.0f); + ImGui::DragFloat("Run Speed", &pf.runSpeed, 0.1f, 0.5f, 20.0f); + if (ImGui::Checkbox("Use Root Motion", &pf.useRootMotion)) + modified = true; + + ImGui::PopID(); + return modified; +} diff --git a/src/features/editScene/ui/PathFollowingEditor.hpp b/src/features/editScene/ui/PathFollowingEditor.hpp new file mode 100644 index 0000000..baff56f --- /dev/null +++ b/src/features/editScene/ui/PathFollowingEditor.hpp @@ -0,0 +1,23 @@ +#ifndef EDITSCENE_PATH_FOLLOWING_EDITOR_HPP +#define EDITSCENE_PATH_FOLLOWING_EDITOR_HPP +#pragma once + +#include "ComponentEditor.hpp" +#include "../components/PathFollowing.hpp" + +/** + * Editor for PathFollowingComponent. + */ +class PathFollowingEditor : public ComponentEditor { +public: + const char *getName() const override + { + return "Path Following"; + } + +protected: + bool renderComponent(flecs::entity entity, + PathFollowingComponent &pf) override; +}; + +#endif // EDITSCENE_PATH_FOLLOWING_EDITOR_HPP