Compare commits
18 Commits
a75db85027
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 39a053d4ee | |||
| c5da977857 | |||
| 3e7b0169d5 | |||
| f918c5cefb | |||
| 976ced3731 | |||
| 0fd8deaf53 | |||
| 4d843c18c7 | |||
| 0ed83966da | |||
| 998984f75a | |||
| 02fa78764a | |||
| abe6eef6b3 | |||
| cca732b41b | |||
| 8507a3a501 | |||
| b9cce0248a | |||
| fa49bb5005 | |||
| 37441aa8fd | |||
| a1b74aa2d5 | |||
| c80d9c96e6 |
@@ -69,3 +69,27 @@ material Debug/Red2
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug material for normal visualization overlay.
|
||||
* Renders on top of everything (depth_check off, depth_write off)
|
||||
* Uses vertex colours so each normal line can have its own colour.
|
||||
* Rendered in overlay queue to appear on top of all geometry.
|
||||
*/
|
||||
material Debug/NormalOverlay
|
||||
{
|
||||
technique
|
||||
{
|
||||
pass
|
||||
{
|
||||
lighting off
|
||||
depth_check off
|
||||
depth_write off
|
||||
ambient 1.0 1.0 1.0 1.0
|
||||
diffuse vertexcolour
|
||||
specular 0.0 0.0 0.0 1.0
|
||||
cull_software none
|
||||
cull_hardware none
|
||||
scene_blend alpha_blend
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,9 +46,31 @@ 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/ActuatorSystem.cpp
|
||||
ui/ActuatorEditor.cpp
|
||||
components/ActuatorModule.cpp
|
||||
systems/EventBus.cpp
|
||||
systems/EventHandlerSystem.cpp
|
||||
ui/EventHandlerEditor.cpp
|
||||
components/EventHandlerModule.cpp
|
||||
systems/PrefabSystem.cpp
|
||||
ui/PrefabInstanceEditor.cpp
|
||||
|
||||
systems/ItemSystem.cpp
|
||||
components/ItemModule.cpp
|
||||
components/InventoryModule.cpp
|
||||
ui/ItemEditor.cpp
|
||||
ui/InventoryEditor.cpp
|
||||
|
||||
ui/TransformEditor.cpp
|
||||
ui/RenderableEditor.cpp
|
||||
ui/PhysicsColliderEditor.cpp
|
||||
@@ -85,6 +107,7 @@ set(EDITSCENE_SOURCES
|
||||
ui/InlineBehaviorTreeEditor.cpp
|
||||
ui/NavMeshEditor.cpp
|
||||
ui/ActionDatabaseEditor.cpp
|
||||
ui/ActionDatabaseSingletonEditor.cpp
|
||||
ui/ActionDebugEditor.cpp
|
||||
ui/ComponentRegistration.cpp
|
||||
components/GoapBlackboard.cpp
|
||||
@@ -113,6 +136,9 @@ set(EDITSCENE_SOURCES
|
||||
components/CellGrid.cpp
|
||||
components/StartupMenuModule.cpp
|
||||
components/PlayerControllerModule.cpp
|
||||
components/DialogueComponentModule.cpp
|
||||
systems/DialogueSystem.cpp
|
||||
ui/DialogueEditor.cpp
|
||||
components/BuoyancyInfoModule.cpp
|
||||
components/WaterPhysicsModule.cpp
|
||||
components/WaterPlaneModule.cpp
|
||||
@@ -122,6 +148,12 @@ set(EDITSCENE_SOURCES
|
||||
gizmo/Gizmo.cpp
|
||||
gizmo/Cursor3D.cpp
|
||||
physics/physics.cpp
|
||||
lua/LuaState.cpp
|
||||
lua/LuaEntityApi.cpp
|
||||
lua/LuaComponentApi.cpp
|
||||
lua/LuaEventApi.cpp
|
||||
lua/LuaActionApi.cpp
|
||||
lua/LuaBehaviorTreeApi.cpp
|
||||
)
|
||||
|
||||
set(EDITSCENE_HEADERS
|
||||
@@ -153,6 +185,9 @@ set(EDITSCENE_HEADERS
|
||||
components/CellGrid.hpp
|
||||
components/StartupMenu.hpp
|
||||
components/PlayerController.hpp
|
||||
components/DialogueComponent.hpp
|
||||
systems/DialogueSystem.hpp
|
||||
ui/DialogueEditor.hpp
|
||||
systems/StartupMenuSystem.hpp
|
||||
systems/PlayerControllerSystem.hpp
|
||||
systems/EditorUISystem.hpp
|
||||
@@ -173,10 +208,31 @@ 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/Actuator.hpp
|
||||
ui/ActuatorEditor.hpp
|
||||
systems/EventBus.hpp
|
||||
components/EventHandler.hpp
|
||||
systems/EventHandlerSystem.hpp
|
||||
ui/EventHandlerEditor.hpp
|
||||
components/PrefabInstance.hpp
|
||||
ui/PrefabInstanceEditor.hpp
|
||||
|
||||
systems/ItemSystem.hpp
|
||||
components/Item.hpp
|
||||
components/Inventory.hpp
|
||||
ui/ItemEditor.hpp
|
||||
ui/InventoryEditor.hpp
|
||||
|
||||
systems/ProceduralTextureSystem.hpp
|
||||
systems/StaticGeometrySystem.hpp
|
||||
systems/SceneSerializer.hpp
|
||||
@@ -229,6 +285,7 @@ set(EDITSCENE_HEADERS
|
||||
ui/NavMeshEditor.hpp
|
||||
ui/NavMeshGeometrySourceEditor.hpp
|
||||
ui/ActionDatabaseEditor.hpp
|
||||
ui/ActionDatabaseSingletonEditor.hpp
|
||||
ui/ActionDebugEditor.hpp
|
||||
components/GoapBlackboard.hpp
|
||||
components/GoapExpression.hpp
|
||||
@@ -242,6 +299,12 @@ set(EDITSCENE_HEADERS
|
||||
gizmo/Gizmo.hpp
|
||||
gizmo/Cursor3D.hpp
|
||||
physics/physics.h
|
||||
lua/LuaState.hpp
|
||||
lua/LuaEntityApi.hpp
|
||||
lua/LuaComponentApi.hpp
|
||||
lua/LuaEventApi.hpp
|
||||
lua/LuaActionApi.hpp
|
||||
lua/LuaBehaviorTreeApi.hpp
|
||||
)
|
||||
|
||||
add_executable(editSceneEditor ${EDITSCENE_SOURCES} ${EDITSCENE_HEADERS})
|
||||
@@ -263,6 +326,7 @@ target_link_libraries(editSceneEditor
|
||||
RecastNavigation::DetourTileCache
|
||||
RecastNavigation::DetourCrowd
|
||||
RecastNavigation::DebugUtils
|
||||
lua
|
||||
)
|
||||
|
||||
target_include_directories(editSceneEditor PRIVATE
|
||||
@@ -272,8 +336,108 @@ target_include_directories(editSceneEditor PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/recastnavigation/DetourTileCache/Include
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/recastnavigation/DetourCrowd/Include
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/recastnavigation/DebugUtils/Include
|
||||
${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src
|
||||
${CMAKE_SOURCE_DIR}/src/lua/lpeg-1.1.0
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: Lua API standalone tests
|
||||
# ---------------------------------------------------------------------------
|
||||
# These standalone tests verify the Lua API functions work correctly.
|
||||
# They do not require OGRE or Flecs - only Lua and the core component types.
|
||||
|
||||
# Test: Entity Lua API
|
||||
add_executable(entity_lua_test
|
||||
tests/entity_lua_test.cpp
|
||||
tests/lua_test_stubs.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(entity_lua_test
|
||||
lua
|
||||
)
|
||||
|
||||
target_include_directories(entity_lua_test PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tests
|
||||
${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src
|
||||
)
|
||||
|
||||
# Test: Component Lua API
|
||||
add_executable(component_lua_test
|
||||
tests/component_lua_test.cpp
|
||||
tests/lua_test_stubs.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(component_lua_test
|
||||
lua
|
||||
)
|
||||
|
||||
target_include_directories(component_lua_test PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tests
|
||||
${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src
|
||||
)
|
||||
|
||||
# Test: Event Lua API
|
||||
add_executable(event_lua_test
|
||||
tests/event_lua_test.cpp
|
||||
tests/lua_test_stubs.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(event_lua_test
|
||||
lua
|
||||
)
|
||||
|
||||
target_include_directories(event_lua_test PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tests
|
||||
${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src
|
||||
)
|
||||
|
||||
# Test: ActionDatabase Lua API
|
||||
add_executable(action_db_lua_test
|
||||
tests/action_db_lua_test.cpp
|
||||
components/ActionDatabase.cpp
|
||||
components/GoapBlackboard.cpp
|
||||
components/GoapGoal.cpp
|
||||
components/GoapExpression.cpp
|
||||
lua/LuaActionApi.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(action_db_lua_test
|
||||
lua
|
||||
flecs::flecs_static
|
||||
nlohmann_json::nlohmann_json
|
||||
)
|
||||
|
||||
target_include_directories(action_db_lua_test PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tests
|
||||
${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src
|
||||
)
|
||||
|
||||
# Test: Behavior Tree Lua API
|
||||
add_executable(behavior_tree_lua_test
|
||||
tests/behavior_tree_lua_test.cpp
|
||||
lua/LuaBehaviorTreeApi.cpp
|
||||
lua/LuaEntityApi.cpp
|
||||
components/GoapBlackboard.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(behavior_tree_lua_test
|
||||
lua
|
||||
flecs::flecs_static
|
||||
OgreMain
|
||||
)
|
||||
|
||||
target_include_directories(behavior_tree_lua_test PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tests
|
||||
${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src
|
||||
)
|
||||
|
||||
target_compile_definitions(behavior_tree_lua_test PRIVATE flecs_STATIC)
|
||||
|
||||
# Copy local resources (materials, etc.)
|
||||
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/resources")
|
||||
file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/resources"
|
||||
|
||||
@@ -19,11 +19,15 @@
|
||||
#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"
|
||||
#include "systems/RoomLayoutSystem.hpp"
|
||||
#include "systems/StartupMenuSystem.hpp"
|
||||
#include "systems/DialogueSystem.hpp"
|
||||
#include "systems/PlayerControllerSystem.hpp"
|
||||
#include "systems/SceneSerializer.hpp"
|
||||
#include "camera/EditorCamera.hpp"
|
||||
@@ -55,6 +59,7 @@
|
||||
#include "components/AnimationTreeTemplate.hpp"
|
||||
#include "components/Character.hpp"
|
||||
#include "components/StartupMenu.hpp"
|
||||
#include "components/DialogueComponent.hpp"
|
||||
#include "components/PlayerController.hpp"
|
||||
#include "components/CellGrid.hpp"
|
||||
#include "components/CellGridModule.hpp"
|
||||
@@ -66,9 +71,24 @@
|
||||
#include "systems/PrefabSystem.hpp"
|
||||
#include "components/NavMesh.hpp"
|
||||
#include "components/SmartObject.hpp"
|
||||
#include "components/Actuator.hpp"
|
||||
#include "components/GoapPlanner.hpp"
|
||||
#include "components/GoapRunner.hpp"
|
||||
#include "components/PathFollowing.hpp"
|
||||
#include "systems/ActuatorSystem.hpp"
|
||||
#include "systems/EventHandlerSystem.hpp"
|
||||
#include "systems/ItemSystem.hpp"
|
||||
#include "components/EventHandler.hpp"
|
||||
#include "components/Item.hpp"
|
||||
#include "components/Inventory.hpp"
|
||||
|
||||
#include <OgreRTShaderSystem.h>
|
||||
#include <imgui.h>
|
||||
#include "lua/LuaEntityApi.hpp"
|
||||
#include "lua/LuaComponentApi.hpp"
|
||||
#include "lua/LuaEventApi.hpp"
|
||||
#include "lua/LuaActionApi.hpp"
|
||||
#include "lua/LuaBehaviorTreeApi.hpp"
|
||||
|
||||
//=============================================================================
|
||||
// ImGuiRenderListener Implementation
|
||||
@@ -104,6 +124,14 @@ void ImGuiRenderListener::preViewportUpdate(
|
||||
m_uiSystem->update(m_deltaTime);
|
||||
}
|
||||
|
||||
// Render actuator markers in game mode (after NewFrame so draw
|
||||
// commands survive)
|
||||
if (m_editorApp) {
|
||||
ActuatorSystem *actuatorSys = m_editorApp->getActuatorSystem();
|
||||
if (actuatorSys)
|
||||
actuatorSys->render();
|
||||
}
|
||||
|
||||
// Render startup menu in game mode (inside ImGui frame scope)
|
||||
if (m_editorApp &&
|
||||
m_editorApp->getGameMode() == EditorApp::GameMode::Game &&
|
||||
@@ -112,6 +140,16 @@ void ImGuiRenderListener::preViewportUpdate(
|
||||
if (sms)
|
||||
sms->update(m_deltaTime);
|
||||
}
|
||||
|
||||
// Render dialogue box in game mode (inside ImGui frame scope)
|
||||
if (m_editorApp &&
|
||||
m_editorApp->getGameMode() == EditorApp::GameMode::Game &&
|
||||
m_editorApp->getGamePlayState() ==
|
||||
EditorApp::GamePlayState::Playing) {
|
||||
DialogueSystem *ds = m_editorApp->getDialogueSystem();
|
||||
if (ds)
|
||||
ds->update(m_deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
void ImGuiRenderListener::postViewportUpdate(
|
||||
@@ -157,22 +195,27 @@ EditorApp::~EditorApp()
|
||||
// This ensures all components with Ogre resources are cleaned up while SceneManager exists
|
||||
// Collect entities first, then delete after iteration (can't modify during iteration)
|
||||
std::vector<flecs::entity> entitiesToDelete;
|
||||
m_world.query<EditorMarkerComponent>().each(
|
||||
[&](flecs::entity e, EditorMarkerComponent) {
|
||||
entitiesToDelete.push_back(e);
|
||||
});
|
||||
for (auto &e : entitiesToDelete) {
|
||||
if (e.is_alive()) {
|
||||
e.destruct();
|
||||
}
|
||||
}
|
||||
|
||||
// Release all systems
|
||||
m_playerControllerSystem.reset();
|
||||
// Destroy dialogue system before other systems
|
||||
m_dialogueSystem.reset();
|
||||
|
||||
m_startupMenuSystem.reset();
|
||||
m_characterSlotSystem.reset();
|
||||
m_animationTreeSystem.reset();
|
||||
m_playerControllerSystem.reset();
|
||||
m_itemSystem.reset();
|
||||
m_eventHandlerSystem.reset();
|
||||
m_actuatorSystem.reset();
|
||||
m_goapPlannerSystem.reset();
|
||||
m_pathFollowingSystem.reset();
|
||||
m_goapRunnerSystem.reset();
|
||||
m_smartObjectSystem.reset();
|
||||
m_roomLayoutSystem.reset();
|
||||
m_normalDebugSystem.reset();
|
||||
m_cellGridSystem.reset();
|
||||
m_characterSystem.reset();
|
||||
m_navMeshSystem.reset();
|
||||
m_behaviorTreeSystem.reset();
|
||||
m_animationTreeSystem.reset();
|
||||
m_characterSlotSystem.reset();
|
||||
m_proceduralMeshSystem.reset();
|
||||
m_proceduralMaterialSystem.reset();
|
||||
m_proceduralTextureSystem.reset();
|
||||
@@ -237,6 +280,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<EditorPhysicsSystem>(
|
||||
@@ -335,6 +379,40 @@ void EditorApp::setup()
|
||||
// Wire up EditorApp for game mode detection
|
||||
m_smartObjectSystem->setEditorApp(this);
|
||||
|
||||
// Setup Actuator system
|
||||
m_actuatorSystem = std::make_unique<ActuatorSystem>(
|
||||
m_world, m_sceneMgr, this, m_behaviorTreeSystem.get());
|
||||
|
||||
// Setup Event Handler system
|
||||
m_eventHandlerSystem = std::make_unique<EventHandlerSystem>(
|
||||
m_world, m_behaviorTreeSystem.get());
|
||||
|
||||
// Setup Item system
|
||||
m_itemSystem = std::make_unique<ItemSystem>(
|
||||
m_world, m_sceneMgr, this, m_behaviorTreeSystem.get());
|
||||
|
||||
// Wire ItemSystem into SmartObjectSystem for BT access
|
||||
m_smartObjectSystem->setItemSystem(m_itemSystem.get());
|
||||
|
||||
// Setup GOAP Runner system
|
||||
m_goapRunnerSystem = std::make_unique<GoapRunnerSystem>(
|
||||
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<GoapPlannerSystem>(m_world);
|
||||
m_goapPlannerSystem->setEditorApp(this);
|
||||
|
||||
// Setup Path Following system
|
||||
m_pathFollowingSystem = std::make_unique<PathFollowingSystem>(
|
||||
m_world, m_sceneMgr, m_navMeshSystem.get());
|
||||
m_pathFollowingSystem->setAnimationTreeSystem(
|
||||
m_animationTreeSystem.get());
|
||||
|
||||
// Setup CellGrid system
|
||||
m_cellGridSystem =
|
||||
std::make_unique<CellGridSystem>(m_world, m_sceneMgr);
|
||||
@@ -362,6 +440,8 @@ void EditorApp::setup()
|
||||
// Setup game systems
|
||||
m_startupMenuSystem = std::make_unique<StartupMenuSystem>(
|
||||
m_world, m_sceneMgr, this);
|
||||
m_dialogueSystem = std::make_unique<DialogueSystem>(
|
||||
m_world, m_sceneMgr, this);
|
||||
m_playerControllerSystem =
|
||||
std::make_unique<PlayerControllerSystem>(
|
||||
m_world, m_sceneMgr, this);
|
||||
@@ -384,10 +464,11 @@ void EditorApp::setup()
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-load menu font before showing overlay
|
||||
// (OGRE builds the atlas in createFontTexture() during show())
|
||||
// Pre-load fonts before showing overlay
|
||||
if (m_startupMenuSystem)
|
||||
m_startupMenuSystem->prepareFont();
|
||||
if (m_dialogueSystem)
|
||||
m_dialogueSystem->prepareFont();
|
||||
|
||||
// Now show the overlay — font atlas will be built with our font
|
||||
if (m_imguiOverlay)
|
||||
@@ -408,6 +489,41 @@ void EditorApp::setup()
|
||||
addInputListener(this);
|
||||
addInputListener(getImGuiInputListener());
|
||||
|
||||
// Initialize Lua scripting
|
||||
{
|
||||
lua_State *L = m_lua.getState();
|
||||
|
||||
// Store the Flecs world pointer in the Lua registry
|
||||
// so Lua API functions can access it.
|
||||
flecs::world *worldPtr = &m_world;
|
||||
lua_pushlightuserdata(L, worldPtr);
|
||||
lua_setfield(L, LUA_REGISTRYINDEX,
|
||||
"EditSceneFlecsWorld");
|
||||
|
||||
// Register all Lua API modules.
|
||||
// Order matters: Entity API creates the "ecs" table,
|
||||
// Component and Event APIs add to it.
|
||||
editScene::registerLuaEntityApi(L);
|
||||
editScene::registerLuaComponentApi(L);
|
||||
editScene::registerLuaEventApi(L);
|
||||
editScene::registerLuaActionApi(L);
|
||||
editScene::registerLuaBehaviorTreeApi(L);
|
||||
|
||||
// Run late setup: load data.lua and initial scripts.
|
||||
m_lua.lateSetup();
|
||||
|
||||
// Auto-load Action Database from "actions.json" in the General
|
||||
// resource group. If the file is missing or contains errors
|
||||
// the error is logged and the database stays empty (scene
|
||||
// components will still be processed below).
|
||||
ActionDatabase::loadFromJson("actions.json");
|
||||
|
||||
// Re-process scene components so that any
|
||||
// ActionDatabaseComponent entities in the scene override or
|
||||
// append actions from the file.
|
||||
ActionDatabase::reloadFromSceneComponents(m_world);
|
||||
}
|
||||
|
||||
// Game mode can be set externally before setup() is called
|
||||
m_setupComplete = true;
|
||||
|
||||
@@ -559,6 +675,7 @@ void EditorApp::setupECS()
|
||||
|
||||
// Register game components
|
||||
m_world.component<StartupMenuComponent>();
|
||||
m_world.component<DialogueComponent>();
|
||||
m_world.component<PlayerControllerComponent>();
|
||||
m_world.component<InWater>();
|
||||
|
||||
@@ -567,7 +684,7 @@ void EditorApp::setupECS()
|
||||
m_world.component<SkyboxComponent>();
|
||||
|
||||
// Register AI/GOAP components
|
||||
m_world.component<ActionDatabase>();
|
||||
// ActionDatabase is now a singleton, registered in ActionDatabaseModule
|
||||
m_world.component<ActionDebug>();
|
||||
m_world.component<BehaviorTreeComponent>();
|
||||
m_world.component<GoapBlackboard>();
|
||||
@@ -575,6 +692,21 @@ void EditorApp::setupECS()
|
||||
// Register Smart Object component
|
||||
m_world.component<SmartObjectComponent>();
|
||||
|
||||
// Register Actuator component
|
||||
m_world.component<ActuatorComponent>();
|
||||
|
||||
// Register Event Handler component
|
||||
m_world.component<EventHandlerComponent>();
|
||||
|
||||
// Register GOAP Planner component
|
||||
m_world.component<GoapPlannerComponent>();
|
||||
|
||||
// Register GOAP Runner component
|
||||
m_world.component<GoapRunnerComponent>();
|
||||
|
||||
// Register Path Following component
|
||||
m_world.component<PathFollowingComponent>();
|
||||
|
||||
// Register Navigation components
|
||||
m_world.component<NavMeshComponent>();
|
||||
m_world.component<NavMeshGeometrySource>();
|
||||
@@ -584,6 +716,10 @@ void EditorApp::setupECS()
|
||||
|
||||
// Register PrefabInstance component
|
||||
m_world.component<PrefabInstanceComponent>();
|
||||
|
||||
// Register Item and Inventory components
|
||||
m_world.component<ItemComponent>();
|
||||
m_world.component<InventoryComponent>();
|
||||
}
|
||||
|
||||
void EditorApp::createDefaultEntities()
|
||||
@@ -733,6 +869,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 +899,26 @@ 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);
|
||||
}
|
||||
|
||||
/* --- Actuator system (player interaction prompts) --- */
|
||||
if (m_actuatorSystem) {
|
||||
m_actuatorSystem->update(evt.timeSinceLastFrame);
|
||||
}
|
||||
|
||||
/* --- Event Handler system (event-driven BTs) --- */
|
||||
if (m_eventHandlerSystem) {
|
||||
m_eventHandlerSystem->update(evt.timeSinceLastFrame);
|
||||
}
|
||||
|
||||
/* --- Dynamic physics (characters after static world) --- */
|
||||
|
||||
if (m_characterSystem) {
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <OgreRenderTargetListener.h>
|
||||
#include <flecs.h>
|
||||
#include <memory>
|
||||
#include "lua/LuaState.hpp"
|
||||
|
||||
// Forward declarations
|
||||
class EditorUISystem;
|
||||
@@ -29,6 +30,7 @@ class CharacterSystem;
|
||||
class CellGridSystem;
|
||||
class RoomLayoutSystem;
|
||||
class StartupMenuSystem;
|
||||
class DialogueSystem;
|
||||
class PlayerControllerSystem;
|
||||
class BuoyancySystem;
|
||||
class EditorSunSystem;
|
||||
@@ -36,6 +38,12 @@ class EditorSkyboxSystem;
|
||||
class EditorWaterPlaneSystem;
|
||||
class NormalDebugSystem;
|
||||
class SmartObjectSystem;
|
||||
class GoapRunnerSystem;
|
||||
class PathFollowingSystem;
|
||||
class GoapPlannerSystem;
|
||||
class ActuatorSystem;
|
||||
class EventHandlerSystem;
|
||||
class ItemSystem;
|
||||
class EditorApp;
|
||||
|
||||
/**
|
||||
@@ -182,6 +190,18 @@ public:
|
||||
{
|
||||
return m_startupMenuSystem.get();
|
||||
}
|
||||
DialogueSystem *getDialogueSystem() const
|
||||
{
|
||||
return m_dialogueSystem.get();
|
||||
}
|
||||
ActuatorSystem *getActuatorSystem() const
|
||||
{
|
||||
return m_actuatorSystem.get();
|
||||
}
|
||||
EventHandlerSystem *getEventHandlerSystem() const
|
||||
{
|
||||
return m_eventHandlerSystem.get();
|
||||
}
|
||||
Ogre::ImGuiOverlay *getImGuiOverlay() const
|
||||
{
|
||||
return m_imguiOverlay;
|
||||
@@ -222,10 +242,16 @@ private:
|
||||
std::unique_ptr<NormalDebugSystem> m_normalDebugSystem;
|
||||
std::unique_ptr<RoomLayoutSystem> m_roomLayoutSystem;
|
||||
std::unique_ptr<SmartObjectSystem> m_smartObjectSystem;
|
||||
std::unique_ptr<GoapRunnerSystem> m_goapRunnerSystem;
|
||||
std::unique_ptr<PathFollowingSystem> m_pathFollowingSystem;
|
||||
std::unique_ptr<GoapPlannerSystem> m_goapPlannerSystem;
|
||||
std::unique_ptr<ActuatorSystem> m_actuatorSystem;
|
||||
std::unique_ptr<EventHandlerSystem> m_eventHandlerSystem;
|
||||
std::unique_ptr<ItemSystem> m_itemSystem;
|
||||
|
||||
// Game systems
|
||||
|
||||
std::unique_ptr<StartupMenuSystem> m_startupMenuSystem;
|
||||
std::unique_ptr<DialogueSystem> m_dialogueSystem;
|
||||
std::unique_ptr<PlayerControllerSystem> m_playerControllerSystem;
|
||||
|
||||
// State
|
||||
@@ -236,6 +262,9 @@ private:
|
||||
bool m_setupComplete = false;
|
||||
bool m_debugBuoyancy = false;
|
||||
|
||||
// Lua scripting
|
||||
editScene::LuaState m_lua;
|
||||
|
||||
// Editor visualization nodes
|
||||
Ogre::SceneNode *m_gridNode = nullptr;
|
||||
Ogre::SceneNode *m_axisNode = nullptr;
|
||||
|
||||
@@ -1,4 +1,32 @@
|
||||
#include "ActionDatabase.hpp"
|
||||
#ifndef OGRE_STUB_H
|
||||
#include <OgreLogManager.h>
|
||||
#include <OgreResourceGroupManager.h>
|
||||
#endif
|
||||
|
||||
#include <flecs.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <fstream>
|
||||
#include <filesystem>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Singleton
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
ActionDatabase &ActionDatabase::getSingleton()
|
||||
{
|
||||
static ActionDatabase instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
ActionDatabase *ActionDatabase::getSingletonPtr()
|
||||
{
|
||||
return &getSingleton();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Find methods
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const GoapAction *ActionDatabase::findAction(const Ogre::String &name) const
|
||||
{
|
||||
@@ -36,6 +64,36 @@ GoapGoal *ActionDatabase::findGoal(const Ogre::String &name)
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Add or replace
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void ActionDatabase::addOrReplaceAction(const GoapAction &action)
|
||||
{
|
||||
for (auto &a : actions) {
|
||||
if (a.name == action.name) {
|
||||
a = action;
|
||||
return;
|
||||
}
|
||||
}
|
||||
actions.push_back(action);
|
||||
}
|
||||
|
||||
void ActionDatabase::addOrReplaceGoal(const GoapGoal &goal)
|
||||
{
|
||||
for (auto &g : goals) {
|
||||
if (g.name == goal.name) {
|
||||
g = goal;
|
||||
return;
|
||||
}
|
||||
}
|
||||
goals.push_back(goal);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Remove methods
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
bool ActionDatabase::removeAction(const Ogre::String &name)
|
||||
{
|
||||
for (auto it = actions.begin(); it != actions.end(); ++it) {
|
||||
@@ -58,8 +116,12 @@ bool ActionDatabase::removeGoal(const Ogre::String &name)
|
||||
return false;
|
||||
}
|
||||
|
||||
const GoapGoal *ActionDatabase::selectBestGoal(
|
||||
const GoapBlackboard &blackboard) const
|
||||
// ---------------------------------------------------------------------------
|
||||
// Selection / validation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const GoapGoal *
|
||||
ActionDatabase::selectBestGoal(const GoapBlackboard &blackboard) const
|
||||
{
|
||||
const GoapGoal *best = nullptr;
|
||||
int bestPriority = -1;
|
||||
@@ -75,8 +137,8 @@ const GoapGoal *ActionDatabase::selectBestGoal(
|
||||
return best;
|
||||
}
|
||||
|
||||
std::vector<const GoapAction *> ActionDatabase::getValidActions(
|
||||
const GoapBlackboard &blackboard) const
|
||||
std::vector<const GoapAction *>
|
||||
ActionDatabase::getValidActions(const GoapBlackboard &blackboard) const
|
||||
{
|
||||
std::vector<const GoapAction *> result;
|
||||
for (const auto &action : actions) {
|
||||
@@ -85,3 +147,369 @@ std::vector<const GoapAction *> ActionDatabase::getValidActions(
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Clear
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void ActionDatabase::clear()
|
||||
{
|
||||
actions.clear();
|
||||
goals.clear();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ActionDatabaseComponent
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void ActionDatabaseComponent::syncToSingleton() const
|
||||
{
|
||||
auto &db = ActionDatabase::getSingleton();
|
||||
db.clear();
|
||||
for (const auto &action : actions)
|
||||
db.addOrReplaceAction(action);
|
||||
for (const auto &goal : goals)
|
||||
db.addOrReplaceGoal(goal);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// JSON serialization helpers (local to this translation unit)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
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)
|
||||
json["values"][pair.first] = pair.second;
|
||||
}
|
||||
if (!bb.floatValues.empty()) {
|
||||
json["floatValues"] = nlohmann::json::object();
|
||||
for (const auto &pair : bb.floatValues)
|
||||
json["floatValues"][pair.first] = pair.second;
|
||||
}
|
||||
if (!bb.vec3Values.empty()) {
|
||||
json["vec3Values"] = nlohmann::json::object();
|
||||
for (const auto &pair : bb.vec3Values) {
|
||||
nlohmann::json v;
|
||||
v.push_back(pair.second.x);
|
||||
v.push_back(pair.second.y);
|
||||
v.push_back(pair.second.z);
|
||||
json["vec3Values"][pair.first] = v;
|
||||
}
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
static void deserializeGoapBlackboard(GoapBlackboard &bb,
|
||||
const nlohmann::json &json)
|
||||
{
|
||||
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())
|
||||
bb.values[key] = val.get<int>();
|
||||
}
|
||||
bb.floatValues.clear();
|
||||
if (json.contains("floatValues") && json["floatValues"].is_object()) {
|
||||
for (auto &[key, val] : json["floatValues"].items())
|
||||
bb.floatValues[key] = val.get<float>();
|
||||
}
|
||||
bb.vec3Values.clear();
|
||||
if (json.contains("vec3Values") && json["vec3Values"].is_object()) {
|
||||
for (auto &[key, val] : json["vec3Values"].items()) {
|
||||
if (val.is_array() && val.size() >= 3)
|
||||
bb.vec3Values[key] =
|
||||
Ogre::Vector3(val[0].get<float>(),
|
||||
val[1].get<float>(),
|
||||
val[2].get<float>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static nlohmann::json serializeBehaviorTreeNode(const BehaviorTreeNode &node)
|
||||
{
|
||||
nlohmann::json json;
|
||||
json["type"] = node.type;
|
||||
if (!node.name.empty())
|
||||
json["name"] = node.name;
|
||||
if (!node.params.empty())
|
||||
json["params"] = node.params;
|
||||
if (!node.children.empty()) {
|
||||
json["children"] = nlohmann::json::array();
|
||||
for (const auto &child : node.children)
|
||||
json["children"].push_back(
|
||||
serializeBehaviorTreeNode(child));
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
static void deserializeBehaviorTreeNode(BehaviorTreeNode &node,
|
||||
const nlohmann::json &json)
|
||||
{
|
||||
node.type = json.value("type", "task");
|
||||
node.name = json.value("name", "");
|
||||
node.params = json.value("params", "");
|
||||
node.children.clear();
|
||||
if (json.contains("children") && json["children"].is_array()) {
|
||||
for (const auto &childJson : json["children"]) {
|
||||
BehaviorTreeNode child;
|
||||
deserializeBehaviorTreeNode(child, childJson);
|
||||
node.children.push_back(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static nlohmann::json serializeGoapAction(const GoapAction &action)
|
||||
{
|
||||
nlohmann::json json;
|
||||
json["name"] = action.name;
|
||||
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;
|
||||
return json;
|
||||
}
|
||||
|
||||
static void deserializeGoapAction(GoapAction &action,
|
||||
const nlohmann::json &json)
|
||||
{
|
||||
action.name = json.value("name", "Unnamed");
|
||||
action.cost = json.value("cost", 1);
|
||||
if (json.contains("preconditions"))
|
||||
deserializeGoapBlackboard(action.preconditions,
|
||||
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"]);
|
||||
action.behaviorTreeName = json.value("behaviorTreeName", "");
|
||||
}
|
||||
|
||||
static nlohmann::json serializeGoapGoal(const GoapGoal &goal)
|
||||
{
|
||||
nlohmann::json json;
|
||||
json["name"] = goal.name;
|
||||
json["priority"] = goal.priority;
|
||||
json["target"] = serializeGoapBlackboard(goal.target);
|
||||
if (!goal.condition.empty())
|
||||
json["condition"] = goal.condition;
|
||||
return json;
|
||||
}
|
||||
|
||||
static void deserializeGoapGoal(GoapGoal &goal, const nlohmann::json &json)
|
||||
{
|
||||
goal.name = json.value("name", "Unnamed");
|
||||
goal.priority = json.value("priority", 1);
|
||||
if (json.contains("target"))
|
||||
deserializeGoapBlackboard(goal.target, json["target"]);
|
||||
goal.condition = json.value("condition", "");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// saveToJson / loadFromJson
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
bool ActionDatabase::saveToJson(const std::string &filename)
|
||||
{
|
||||
try {
|
||||
// Resolve the filesystem path from the "General" resource group
|
||||
Ogre::ResourceGroupManager &rgm =
|
||||
Ogre::ResourceGroupManager::getSingleton();
|
||||
const Ogre::ResourceGroupManager::LocationList &locations =
|
||||
rgm.getResourceLocationList("General");
|
||||
if (locations.empty()) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"ActionDatabase::saveToJson: "
|
||||
"no resource locations for group 'General'");
|
||||
return false;
|
||||
}
|
||||
// Use the first location's path
|
||||
std::string dir = locations.begin()->archive->getName();
|
||||
|
||||
std::string filepath = dir + "/" + filename;
|
||||
|
||||
// Backup existing file
|
||||
if (std::filesystem::exists(filepath)) {
|
||||
std::string backup = filepath + ".bak";
|
||||
try {
|
||||
std::filesystem::copy_file(
|
||||
filepath, backup,
|
||||
std::filesystem::copy_options::
|
||||
overwrite_existing);
|
||||
} catch (const std::exception &e) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"ActionDatabase::saveToJson: "
|
||||
"backup failed: " +
|
||||
Ogre::String(e.what()));
|
||||
}
|
||||
}
|
||||
|
||||
const ActionDatabase &db = getSingleton();
|
||||
nlohmann::json root;
|
||||
|
||||
root["actions"] = nlohmann::json::array();
|
||||
for (const auto &action : db.actions)
|
||||
root["actions"].push_back(serializeGoapAction(action));
|
||||
|
||||
root["goals"] = nlohmann::json::array();
|
||||
for (const auto &goal : db.goals)
|
||||
root["goals"].push_back(serializeGoapGoal(goal));
|
||||
|
||||
// Save bit names
|
||||
nlohmann::json bitNames = nlohmann::json::array();
|
||||
for (int i = 0; i < 64; i++) {
|
||||
const char *name = GoapBlackboard::getBitName(i);
|
||||
if (name) {
|
||||
nlohmann::json entry;
|
||||
entry["index"] = i;
|
||||
entry["name"] = name;
|
||||
bitNames.push_back(entry);
|
||||
}
|
||||
}
|
||||
if (!bitNames.empty())
|
||||
root["bitNames"] = bitNames;
|
||||
|
||||
std::ofstream file(filepath);
|
||||
if (!file.is_open()) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"ActionDatabase::saveToJson: "
|
||||
"failed to open " +
|
||||
filepath);
|
||||
return false;
|
||||
}
|
||||
file << root.dump(4);
|
||||
file.close();
|
||||
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"ActionDatabase saved to " + filepath);
|
||||
return true;
|
||||
|
||||
} catch (const std::exception &e) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"ActionDatabase::saveToJson error: " +
|
||||
Ogre::String(e.what()));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool ActionDatabase::loadFromJson(const std::string &filename)
|
||||
{
|
||||
try {
|
||||
// Resolve the filesystem path from the "General" resource group
|
||||
Ogre::ResourceGroupManager &rgm =
|
||||
Ogre::ResourceGroupManager::getSingleton();
|
||||
const Ogre::ResourceGroupManager::LocationList &locations =
|
||||
rgm.getResourceLocationList("General");
|
||||
if (locations.empty()) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"ActionDatabase::loadFromJson: "
|
||||
"no resource locations for group 'General'");
|
||||
return false;
|
||||
}
|
||||
std::string dir = locations.begin()->archive->getName();
|
||||
|
||||
std::string filepath = dir + "/" + filename;
|
||||
|
||||
if (!std::filesystem::exists(filepath)) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"ActionDatabase::loadFromJson: "
|
||||
"file not found: " +
|
||||
filepath);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::ifstream file(filepath);
|
||||
if (!file.is_open()) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"ActionDatabase::loadFromJson: "
|
||||
"failed to open " +
|
||||
filepath);
|
||||
return false;
|
||||
}
|
||||
|
||||
nlohmann::json root;
|
||||
try {
|
||||
file >> root;
|
||||
} catch (const nlohmann::json::parse_error &e) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"ActionDatabase::loadFromJson: "
|
||||
"JSON parse error in " +
|
||||
filepath + ": " + Ogre::String(e.what()));
|
||||
return false;
|
||||
}
|
||||
file.close();
|
||||
|
||||
ActionDatabase &db = getSingleton();
|
||||
|
||||
// Load actions (add/replace)
|
||||
if (root.contains("actions") && root["actions"].is_array()) {
|
||||
for (const auto &actionJson : root["actions"]) {
|
||||
GoapAction action;
|
||||
deserializeGoapAction(action, actionJson);
|
||||
db.addOrReplaceAction(action);
|
||||
}
|
||||
}
|
||||
|
||||
// Load goals (add/replace)
|
||||
if (root.contains("goals") && root["goals"].is_array()) {
|
||||
for (const auto &goalJson : root["goals"]) {
|
||||
GoapGoal goal;
|
||||
deserializeGoapGoal(goal, goalJson);
|
||||
db.addOrReplaceGoal(goal);
|
||||
}
|
||||
}
|
||||
|
||||
// Load bit names
|
||||
if (root.contains("bitNames") && root["bitNames"].is_array()) {
|
||||
for (const auto &entry : root["bitNames"]) {
|
||||
if (entry.contains("index") &&
|
||||
entry.contains("name"))
|
||||
GoapBlackboard::setBitName(
|
||||
entry["index"].get<int>(),
|
||||
entry["name"]
|
||||
.get<std::string>());
|
||||
}
|
||||
}
|
||||
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"ActionDatabase loaded from " + filepath);
|
||||
return true;
|
||||
|
||||
} catch (const std::exception &e) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"ActionDatabase::loadFromJson error: " +
|
||||
Ogre::String(e.what()));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// reloadFromSceneComponents
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void ActionDatabase::reloadFromSceneComponents(flecs::world &world)
|
||||
{
|
||||
// First, load from file (if available) — this is done by the caller
|
||||
// before calling this function. Here we just re-sync from scene
|
||||
// entities so that scene-defined actions are applied on top.
|
||||
|
||||
// Iterate all entities with ActionDatabaseComponent
|
||||
world.each([](flecs::entity e, ActionDatabaseComponent &dbComp) {
|
||||
(void)e;
|
||||
dbComp.syncToSingleton();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,14 +5,29 @@
|
||||
#include "GoapAction.hpp"
|
||||
#include "GoapGoal.hpp"
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
#include <string>
|
||||
|
||||
// Forward declaration for reloadFromSceneComponents
|
||||
namespace flecs
|
||||
{
|
||||
class world;
|
||||
}
|
||||
|
||||
/**
|
||||
* Global action database component.
|
||||
* Global action database singleton.
|
||||
*
|
||||
* Holds the master list of GOAP actions and goals that characters can use.
|
||||
* Typically attached to a single "game manager" entity.
|
||||
* This is a singleton accessible from anywhere in the codebase.
|
||||
* The ActionDatabaseComponent on a scene entity stores the actions/goals
|
||||
* and syncs them to the singleton on scene load.
|
||||
*/
|
||||
struct ActionDatabase {
|
||||
class ActionDatabase {
|
||||
public:
|
||||
/** Get the singleton instance */
|
||||
static ActionDatabase &getSingleton();
|
||||
static ActionDatabase *getSingletonPtr();
|
||||
|
||||
std::vector<GoapAction> actions;
|
||||
std::vector<GoapGoal> goals;
|
||||
|
||||
@@ -24,6 +39,12 @@ struct ActionDatabase {
|
||||
const GoapGoal *findGoal(const Ogre::String &name) const;
|
||||
GoapGoal *findGoal(const Ogre::String &name);
|
||||
|
||||
// Add or replace an action by name
|
||||
void addOrReplaceAction(const GoapAction &action);
|
||||
|
||||
// Add or replace a goal by name
|
||||
void addOrReplaceGoal(const GoapGoal &goal);
|
||||
|
||||
// Remove an action by name
|
||||
bool removeAction(const Ogre::String &name);
|
||||
|
||||
@@ -35,8 +56,59 @@ struct ActionDatabase {
|
||||
const GoapGoal *selectBestGoal(const GoapBlackboard &blackboard) const;
|
||||
|
||||
// Build a list of actions that can run from a given blackboard state
|
||||
std::vector<const GoapAction *> getValidActions(
|
||||
const GoapBlackboard &blackboard) const;
|
||||
std::vector<const GoapAction *>
|
||||
getValidActions(const GoapBlackboard &blackboard) const;
|
||||
|
||||
// Clear all actions and goals
|
||||
void clear();
|
||||
|
||||
/**
|
||||
* Save the action database to a JSON file.
|
||||
* Creates a backup of the existing file (if any) by appending ".bak".
|
||||
* The file is written to the filesystem path resolved from the
|
||||
* "General" resource group.
|
||||
*
|
||||
* @param filename The filename (e.g. "actions.json").
|
||||
* @return true on success.
|
||||
*/
|
||||
static bool saveToJson(const std::string &filename);
|
||||
|
||||
/**
|
||||
* Load the action database from a JSON file.
|
||||
* The file is located via the "General" resource group.
|
||||
* On failure (file not found, parse error) the error is logged
|
||||
* and the database is left unchanged.
|
||||
*
|
||||
* @param filename The filename (e.g. "actions.json").
|
||||
* @return true on success.
|
||||
*/
|
||||
static bool loadFromJson(const std::string &filename);
|
||||
|
||||
/**
|
||||
* Re-process all ActionDatabaseComponent entities in the given
|
||||
* Flecs world: clear the singleton and re-sync from every entity
|
||||
* that carries the component. This is used after a reload so
|
||||
* scene-defined actions are re-applied on top of the file.
|
||||
*/
|
||||
static void reloadFromSceneComponents(flecs::world &world);
|
||||
|
||||
private:
|
||||
ActionDatabase() = default;
|
||||
~ActionDatabase() = default;
|
||||
ActionDatabase(const ActionDatabase &) = delete;
|
||||
ActionDatabase &operator=(const ActionDatabase &) = delete;
|
||||
};
|
||||
|
||||
/**
|
||||
* Flecs component that stores action database data on a scene entity.
|
||||
* When set on an entity, it syncs its contents to the ActionDatabase singleton.
|
||||
*/
|
||||
struct ActionDatabaseComponent {
|
||||
std::vector<GoapAction> actions;
|
||||
std::vector<GoapGoal> goals;
|
||||
|
||||
/** Sync this component's data to the ActionDatabase singleton */
|
||||
void syncToSingleton() const;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_ACTION_DATABASE_HPP
|
||||
|
||||
@@ -8,21 +8,21 @@
|
||||
#include "../ui/ActionDatabaseEditor.hpp"
|
||||
#include "../ui/BehaviorTreeEditor.hpp"
|
||||
|
||||
REGISTER_COMPONENT_GROUP("Action Database", "AI", ActionDatabase,
|
||||
REGISTER_COMPONENT_GROUP("Action Database", "AI", ActionDatabaseComponent,
|
||||
ActionDatabaseEditor)
|
||||
{
|
||||
registry.registerComponent<ActionDatabase>(
|
||||
registry.registerComponent<ActionDatabaseComponent>(
|
||||
"Action Database", "AI",
|
||||
std::make_unique<ActionDatabaseEditor>(),
|
||||
// Adder
|
||||
[](flecs::entity e) {
|
||||
if (!e.has<ActionDatabase>())
|
||||
e.set<ActionDatabase>({});
|
||||
if (!e.has<ActionDatabaseComponent>())
|
||||
e.set<ActionDatabaseComponent>({});
|
||||
},
|
||||
// Remover
|
||||
[](flecs::entity e) {
|
||||
if (e.has<ActionDatabase>())
|
||||
e.remove<ActionDatabase>();
|
||||
if (e.has<ActionDatabaseComponent>())
|
||||
e.remove<ActionDatabaseComponent>();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -30,8 +30,7 @@ REGISTER_COMPONENT_GROUP("Behavior Tree", "AI", BehaviorTreeComponent,
|
||||
BehaviorTreeEditor)
|
||||
{
|
||||
registry.registerComponent<BehaviorTreeComponent>(
|
||||
"Behavior Tree", "AI",
|
||||
std::make_unique<BehaviorTreeEditor>(),
|
||||
"Behavior Tree", "AI", std::make_unique<BehaviorTreeEditor>(),
|
||||
// Adder
|
||||
[](flecs::entity e) {
|
||||
if (!e.has<BehaviorTreeComponent>()) {
|
||||
|
||||
@@ -7,37 +7,13 @@
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
|
||||
/**
|
||||
* 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<std::pair<Ogre::String, Ogre::String> > 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<PathFollowingState> 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
|
||||
|
||||
44
src/features/editScene/components/Actuator.hpp
Normal file
44
src/features/editScene/components/Actuator.hpp
Normal file
@@ -0,0 +1,44 @@
|
||||
#ifndef EDITSCENE_ACTUATOR_HPP
|
||||
#define EDITSCENE_ACTUATOR_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
/**
|
||||
* Actuator component.
|
||||
*
|
||||
* An interactive object visible only to the player character.
|
||||
* When the player is within radius + height range, an on-screen
|
||||
* prompt appears and the action can be triggered with the action key.
|
||||
*
|
||||
* Unlike SmartObject, Actuators do not use pathfinding or path
|
||||
* following — they are instantaneous interactions.
|
||||
*/
|
||||
struct ActuatorComponent {
|
||||
// Interaction radius in XZ plane
|
||||
float radius = 1.5f;
|
||||
|
||||
// Maximum height difference for interaction
|
||||
float height = 1.8f;
|
||||
|
||||
// Names of GOAP actions (from ActionDatabase) that this actuator provides
|
||||
std::vector<Ogre::String> actionNames;
|
||||
|
||||
// Runtime: cooldown timer (seconds remaining)
|
||||
float cooldownTimer = 0.0f;
|
||||
|
||||
// Runtime: currently executing an action
|
||||
bool isExecuting = false;
|
||||
|
||||
ActuatorComponent() = default;
|
||||
|
||||
explicit ActuatorComponent(float radius_, float height_)
|
||||
: radius(radius_)
|
||||
, height(height_)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_ACTUATOR_HPP
|
||||
19
src/features/editScene/components/ActuatorModule.cpp
Normal file
19
src/features/editScene/components/ActuatorModule.cpp
Normal file
@@ -0,0 +1,19 @@
|
||||
#include "Actuator.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/ActuatorEditor.hpp"
|
||||
|
||||
REGISTER_COMPONENT_GROUP("Actuator", "Game", ActuatorComponent, ActuatorEditor)
|
||||
{
|
||||
registry.registerComponent<ActuatorComponent>(
|
||||
"Actuator", "Game", std::make_unique<ActuatorEditor>(),
|
||||
// Adder
|
||||
[](flecs::entity e) {
|
||||
if (!e.has<ActuatorComponent>())
|
||||
e.set<ActuatorComponent>({});
|
||||
},
|
||||
// Remover
|
||||
[](flecs::entity e) {
|
||||
if (e.has<ActuatorComponent>())
|
||||
e.remove<ActuatorComponent>();
|
||||
});
|
||||
}
|
||||
@@ -32,6 +32,33 @@
|
||||
* system so physics no longer interferes with animation.
|
||||
* "enablePhysics" - Leaf: re-adds character's JPH::BodyID to physics
|
||||
* system to restore physics simulation.
|
||||
*
|
||||
* --- Item / Inventory nodes ---
|
||||
* "hasItem" - Leaf check: true if character's inventory has an item
|
||||
* matching the given itemId (name=itemId).
|
||||
* "hasItemByName" - Leaf check: true if character's inventory has an item
|
||||
* matching the given itemName (name=itemName).
|
||||
* "countItem" - Leaf check: true if character's inventory has at least
|
||||
* N of itemId (name=itemId, params=count as int).
|
||||
* "pickupItem" - Leaf: picks up the nearest ItemComponent entity within
|
||||
* range into the character's inventory.
|
||||
* name=itemId filter (optional, empty = any).
|
||||
* "dropItem" - Leaf: drops an item from inventory into the world.
|
||||
* name=itemId, params=count (optional, default 1).
|
||||
* "useItem" - Leaf: uses an item from inventory (executes its
|
||||
* useAction behavior tree). name=itemId.
|
||||
* "addItemToInventory"- Leaf: adds an item directly to character's inventory
|
||||
* (for quest rewards, etc.).
|
||||
* params="itemId,itemName,itemType,count,weight,value"
|
||||
*
|
||||
* --- Lua node ---
|
||||
* "luaTask" - Leaf: calls a registered Lua function.
|
||||
* name = registered node handler name.
|
||||
* params = "key=val,key2=val2" passed to the Lua function.
|
||||
* The Lua function receives (entity_id, params_table)
|
||||
* and must return "success", "failure", or "running".
|
||||
* Register handlers via:
|
||||
* ecs.behavior_tree.register_node("name", function)
|
||||
*/
|
||||
struct BehaviorTreeNode {
|
||||
Ogre::String type = "task";
|
||||
@@ -73,7 +100,12 @@ struct BehaviorTreeNode {
|
||||
type == "checkBit" || type == "setValue" ||
|
||||
type == "checkValue" || type == "blackboardDump" ||
|
||||
type == "delay" || type == "teleportToChild" ||
|
||||
type == "disablePhysics" || type == "enablePhysics";
|
||||
type == "disablePhysics" || type == "enablePhysics" ||
|
||||
type == "sendEvent" || type == "hasItem" ||
|
||||
type == "hasItemByName" || type == "countItem" ||
|
||||
type == "pickupItem" || type == "dropItem" ||
|
||||
type == "useItem" || type == "addItemToInventory" ||
|
||||
type == "luaTask";
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -37,6 +37,12 @@ struct CharacterComponent {
|
||||
/* Dirty flag — triggers rebuild of the Jolt character */
|
||||
bool dirty = true;
|
||||
|
||||
/* When true, the scene node position is driven by root motion
|
||||
* (AnimationTreeSystem), not by physics. The physics character
|
||||
* position is synced to match the scene node each frame, and
|
||||
* physics does NOT write its position back to the scene node. */
|
||||
bool useRootMotion = false;
|
||||
|
||||
/* Floor detection: raycast downward to find ground before enabling gravity */
|
||||
bool hasFloor = false;
|
||||
float floorCheckDistance = 2.0f;
|
||||
|
||||
131
src/features/editScene/components/DialogueComponent.hpp
Normal file
131
src/features/editScene/components/DialogueComponent.hpp
Normal file
@@ -0,0 +1,131 @@
|
||||
#ifndef EDITSCENE_DIALOGUE_COMPONENT_HPP
|
||||
#define EDITSCENE_DIALOGUE_COMPONENT_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
/**
|
||||
* Visual-novel style dialogue box component.
|
||||
*
|
||||
* Displays a narration text box at the bottom of the screen with optional
|
||||
* player choices. The dialogue can be driven via the EventBus system
|
||||
* (using "dialogue_show" event) or directly via the component API.
|
||||
*
|
||||
* Only active in game mode (GamePlayState::Playing).
|
||||
*
|
||||
* Event payload (GoapBlackboard) parameters:
|
||||
* "text" (string) - Narration text to display
|
||||
* "choices" (string) - Comma-separated list of choice labels
|
||||
* "speaker" (string) - Optional speaker name
|
||||
* "auto_progress" (int) - If 1, clicking anywhere progresses (no choices)
|
||||
*
|
||||
* Component state transitions:
|
||||
* Idle -> Showing (on show() or event)
|
||||
* Showing -> AwaitingChoice (if choices provided)
|
||||
* Showing -> Idle (if no choices, on click progress)
|
||||
* AwaitingChoice -> Idle (on choice selected)
|
||||
*/
|
||||
struct DialogueComponent {
|
||||
/** Current state of the dialogue box */
|
||||
enum class State {
|
||||
Idle, ///< No dialogue active
|
||||
Showing, ///< Text is being displayed
|
||||
AwaitingChoice ///< Waiting for player to pick a choice
|
||||
};
|
||||
|
||||
State state = State::Idle;
|
||||
|
||||
/** The narration text to display */
|
||||
Ogre::String text;
|
||||
|
||||
/** Optional speaker name (displayed above the text) */
|
||||
Ogre::String speaker;
|
||||
|
||||
/** Player choice labels (empty = no choices, click to progress) */
|
||||
std::vector<Ogre::String> choices;
|
||||
|
||||
/** Font configuration */
|
||||
Ogre::String fontName = "Jupiteroid-Regular.ttf";
|
||||
float fontSize = 24.0f;
|
||||
|
||||
/** Speaker name font size (slightly smaller) */
|
||||
float speakerFontSize = 20.0f;
|
||||
|
||||
/** Background opacity (0.0 - 1.0) */
|
||||
float backgroundOpacity = 0.85f;
|
||||
|
||||
/** Height of the dialogue box as fraction of screen height (0.0 - 1.0) */
|
||||
float boxHeightFraction = 0.25f;
|
||||
|
||||
/** Vertical position as fraction from top (0.0 = top, 0.75 = bottom quarter) */
|
||||
float boxPositionFraction = 0.75f;
|
||||
|
||||
/** Whether the dialogue box is enabled (can be toggled) */
|
||||
bool enabled = true;
|
||||
|
||||
/** Callback invoked when a choice is selected (choice index, 1-based) */
|
||||
std::function<void(int)> onChoiceSelected;
|
||||
|
||||
/** Callback invoked when dialogue is dismissed (no choices mode) */
|
||||
std::function<void()> onDismissed;
|
||||
|
||||
/** Callback invoked when dialogue starts showing */
|
||||
std::function<void()> onShow;
|
||||
|
||||
/* --- API --- */
|
||||
|
||||
/** Show dialogue with given text and optional choices */
|
||||
void show(const Ogre::String &narrationText,
|
||||
const std::vector<Ogre::String> &choiceLabels = {},
|
||||
const Ogre::String &speakerName = "")
|
||||
{
|
||||
text = narrationText;
|
||||
choices = choiceLabels;
|
||||
speaker = speakerName;
|
||||
state = choices.empty() ? State::Showing :
|
||||
State::AwaitingChoice;
|
||||
if (onShow)
|
||||
onShow();
|
||||
}
|
||||
|
||||
/** Progress the dialogue (click-through when no choices) */
|
||||
void progress()
|
||||
{
|
||||
if (state == State::Showing && choices.empty()) {
|
||||
state = State::Idle;
|
||||
if (onDismissed)
|
||||
onDismissed();
|
||||
}
|
||||
}
|
||||
|
||||
/** Select a choice by 1-based index */
|
||||
void selectChoice(int index)
|
||||
{
|
||||
if (state == State::AwaitingChoice && index >= 1 &&
|
||||
index <= (int)choices.size()) {
|
||||
state = State::Idle;
|
||||
if (onChoiceSelected)
|
||||
onChoiceSelected(index);
|
||||
}
|
||||
}
|
||||
|
||||
/** Check if dialogue is currently active */
|
||||
bool isActive() const
|
||||
{
|
||||
return state != State::Idle;
|
||||
}
|
||||
|
||||
/** Reset dialogue to idle state */
|
||||
void reset()
|
||||
{
|
||||
state = State::Idle;
|
||||
text.clear();
|
||||
choices.clear();
|
||||
speaker.clear();
|
||||
}
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_DIALOGUE_COMPONENT_HPP
|
||||
@@ -0,0 +1,23 @@
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/DialogueEditor.hpp"
|
||||
#include "DialogueComponent.hpp"
|
||||
|
||||
REGISTER_COMPONENT_GROUP("Dialogue Box", "Game", DialogueComponent,
|
||||
DialogueEditor)
|
||||
{
|
||||
registry.registerComponent<DialogueComponent>(
|
||||
DialogueComponent_name, DialogueComponent_group,
|
||||
std::make_unique<DialogueEditor>(),
|
||||
// Adder
|
||||
[](flecs::entity e) {
|
||||
if (!e.has<DialogueComponent>()) {
|
||||
e.set<DialogueComponent>({});
|
||||
}
|
||||
},
|
||||
// Remover
|
||||
[](flecs::entity e) {
|
||||
if (e.has<DialogueComponent>()) {
|
||||
e.remove<DialogueComponent>();
|
||||
}
|
||||
});
|
||||
}
|
||||
21
src/features/editScene/components/EventHandler.hpp
Normal file
21
src/features/editScene/components/EventHandler.hpp
Normal file
@@ -0,0 +1,21 @@
|
||||
#ifndef EDITSCENE_EVENT_HANDLER_HPP
|
||||
#define EDITSCENE_EVENT_HANDLER_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
|
||||
/**
|
||||
* Event-driven behavior tree handler component.
|
||||
*
|
||||
* When the specified event is received, the referenced GoapAction's
|
||||
* behavior tree is executed for this entity. Event parameters are
|
||||
* merged into the entity's GoapBlackboard before the tree runs and
|
||||
* cleaned up when the tree completes.
|
||||
*/
|
||||
struct EventHandlerComponent {
|
||||
Ogre::String eventName;
|
||||
Ogre::String actionName;
|
||||
bool enabled = true;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_EVENT_HANDLER_HPP
|
||||
21
src/features/editScene/components/EventHandlerModule.cpp
Normal file
21
src/features/editScene/components/EventHandlerModule.cpp
Normal file
@@ -0,0 +1,21 @@
|
||||
#include "EventHandler.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/EventHandlerEditor.hpp"
|
||||
|
||||
REGISTER_COMPONENT_GROUP("Event Handler", "Game", EventHandlerComponent,
|
||||
EventHandlerEditor)
|
||||
{
|
||||
registry.registerComponent<EventHandlerComponent>(
|
||||
"Event Handler", "Game",
|
||||
std::make_unique<EventHandlerEditor>(),
|
||||
// Adder
|
||||
[](flecs::entity e) {
|
||||
if (!e.has<EventHandlerComponent>())
|
||||
e.set<EventHandlerComponent>({});
|
||||
},
|
||||
// Remover
|
||||
[](flecs::entity e) {
|
||||
if (e.has<EventHandlerComponent>())
|
||||
e.remove<EventHandlerComponent>();
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -180,9 +188,32 @@ Ogre::String GoapBlackboard::dump() const
|
||||
")\n";
|
||||
}
|
||||
|
||||
if (!stringValues.empty()) {
|
||||
result += " String values:\n";
|
||||
for (const auto &pair : stringValues)
|
||||
result += " " + pair.first + " = " + pair.second + "\n";
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void GoapBlackboard::merge(const GoapBlackboard &other)
|
||||
{
|
||||
// Merge bits
|
||||
bits = (bits & ~other.mask) | (other.bits & other.mask);
|
||||
mask |= other.mask;
|
||||
|
||||
// Merge values
|
||||
for (const auto &pair : other.values)
|
||||
values[pair.first] = pair.second;
|
||||
for (const auto &pair : other.floatValues)
|
||||
floatValues[pair.first] = pair.second;
|
||||
for (const auto &pair : other.vec3Values)
|
||||
vec3Values[pair.first] = pair.second;
|
||||
for (const auto &pair : other.stringValues)
|
||||
stringValues[pair.first] = pair.second;
|
||||
}
|
||||
|
||||
std::vector<int> GoapBlackboard::getSetBits() const
|
||||
{
|
||||
std::vector<int> result;
|
||||
|
||||
@@ -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<std::string, int> values;
|
||||
|
||||
@@ -31,6 +35,9 @@ struct GoapBlackboard {
|
||||
// Named Vector3 values — runtime character state
|
||||
std::unordered_map<std::string, Ogre::Vector3> vec3Values;
|
||||
|
||||
// Named string values — event params, tags, etc.
|
||||
std::unordered_map<std::string, Ogre::String> stringValues;
|
||||
|
||||
GoapBlackboard() = default;
|
||||
|
||||
/* --- Bit accessors --- */
|
||||
@@ -144,6 +151,34 @@ struct GoapBlackboard {
|
||||
vec3Values.erase(key);
|
||||
}
|
||||
|
||||
/* --- String value accessors --- */
|
||||
void setStringValue(const std::string &key, const Ogre::String &value)
|
||||
{
|
||||
stringValues[key] = value;
|
||||
}
|
||||
|
||||
Ogre::String getStringValue(const std::string &key,
|
||||
const Ogre::String &defaultValue = "") const
|
||||
{
|
||||
auto it = stringValues.find(key);
|
||||
if (it != stringValues.end())
|
||||
return it->second;
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
bool hasStringValue(const std::string &key) const
|
||||
{
|
||||
return stringValues.find(key) != stringValues.end();
|
||||
}
|
||||
|
||||
void removeStringValue(const std::string &key)
|
||||
{
|
||||
stringValues.erase(key);
|
||||
}
|
||||
|
||||
/* --- Merge another blackboard into this one --- */
|
||||
void merge(const GoapBlackboard &other);
|
||||
|
||||
/* --- Generic scalar lookup (tries int then float) --- */
|
||||
bool getScalarValue(const std::string &key, float &out) const;
|
||||
|
||||
@@ -167,6 +202,7 @@ struct GoapBlackboard {
|
||||
values.clear();
|
||||
floatValues.clear();
|
||||
vec3Values.clear();
|
||||
stringValues.clear();
|
||||
}
|
||||
|
||||
Ogre::String dump() const;
|
||||
@@ -184,9 +220,11 @@ 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;
|
||||
vec3Values == other.vec3Values &&
|
||||
stringValues == other.stringValues;
|
||||
}
|
||||
|
||||
bool operator!=(const GoapBlackboard &other) const
|
||||
|
||||
99
src/features/editScene/components/GoapPlanner.hpp
Normal file
99
src/features/editScene/components/GoapPlanner.hpp
Normal file
@@ -0,0 +1,99 @@
|
||||
#ifndef EDITSCENE_GOAP_PLANNER_HPP
|
||||
#define EDITSCENE_GOAP_PLANNER_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <queue>
|
||||
|
||||
/**
|
||||
* 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<Ogre::String> actionNames;
|
||||
|
||||
// Selected goal names from ActionDatabase
|
||||
std::vector<Ogre::String> 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<Ogre::String> actions;
|
||||
int totalCost = 0;
|
||||
Ogre::String goalName;
|
||||
};
|
||||
std::vector<Plan> 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
|
||||
21
src/features/editScene/components/GoapPlannerModule.cpp
Normal file
21
src/features/editScene/components/GoapPlannerModule.cpp
Normal file
@@ -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<GoapPlannerComponent>(
|
||||
"GOAP Planner", "AI",
|
||||
std::make_unique<GoapPlannerEditor>(),
|
||||
// Adder
|
||||
[](flecs::entity e) {
|
||||
if (!e.has<GoapPlannerComponent>())
|
||||
e.set<GoapPlannerComponent>({});
|
||||
},
|
||||
// Remover
|
||||
[](flecs::entity e) {
|
||||
if (e.has<GoapPlannerComponent>())
|
||||
e.remove<GoapPlannerComponent>();
|
||||
});
|
||||
}
|
||||
50
src/features/editScene/components/GoapRunner.hpp
Normal file
50
src/features/editScene/components/GoapRunner.hpp
Normal file
@@ -0,0 +1,50 @@
|
||||
#ifndef EDITSCENE_GOAP_RUNNER_HPP
|
||||
#define EDITSCENE_GOAP_RUNNER_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
/**
|
||||
* 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<Ogre::String> planActions;
|
||||
|
||||
// Whether to auto-replan after completion
|
||||
bool autoReplan = true;
|
||||
|
||||
GoapRunnerComponent() = default;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_GOAP_RUNNER_HPP
|
||||
21
src/features/editScene/components/GoapRunnerModule.cpp
Normal file
21
src/features/editScene/components/GoapRunnerModule.cpp
Normal file
@@ -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<GoapRunnerComponent>(
|
||||
"GOAP Runner", "AI",
|
||||
std::make_unique<GoapRunnerEditor>(),
|
||||
// Adder
|
||||
[](flecs::entity e) {
|
||||
if (!e.has<GoapRunnerComponent>())
|
||||
e.set<GoapRunnerComponent>({});
|
||||
},
|
||||
// Remover
|
||||
[](flecs::entity e) {
|
||||
if (e.has<GoapRunnerComponent>())
|
||||
e.remove<GoapRunnerComponent>();
|
||||
});
|
||||
}
|
||||
165
src/features/editScene/components/Inventory.hpp
Normal file
165
src/features/editScene/components/Inventory.hpp
Normal file
@@ -0,0 +1,165 @@
|
||||
#ifndef EDITSCENE_INVENTORY_HPP
|
||||
#define EDITSCENE_INVENTORY_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
#include <flecs.h>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <cstdint>
|
||||
|
||||
/**
|
||||
* A single slot in an inventory.
|
||||
* Stores a reference to an item entity (if the item is a world entity)
|
||||
* or stores item data directly for items that exist only in inventory.
|
||||
*/
|
||||
struct InventorySlot {
|
||||
// Flecs entity ID of the item (0 if slot is empty)
|
||||
flecs::entity_t itemEntity = 0;
|
||||
|
||||
// Item data for items that exist only in inventory (no world entity)
|
||||
Ogre::String itemId;
|
||||
Ogre::String itemName;
|
||||
Ogre::String itemType;
|
||||
int stackSize = 0;
|
||||
int maxStackSize = 99;
|
||||
float weight = 0.1f;
|
||||
int value = 1;
|
||||
Ogre::String useActionName;
|
||||
|
||||
bool isEmpty() const
|
||||
{
|
||||
return itemEntity == 0 && stackSize <= 0;
|
||||
}
|
||||
|
||||
void clear()
|
||||
{
|
||||
itemEntity = 0;
|
||||
itemId.clear();
|
||||
itemName.clear();
|
||||
itemType.clear();
|
||||
stackSize = 0;
|
||||
maxStackSize = 99;
|
||||
weight = 0.1f;
|
||||
value = 1;
|
||||
useActionName.clear();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Inventory component.
|
||||
*
|
||||
* Attached to a character entity to hold items.
|
||||
* Can also be attached to container entities (chests, barrels, etc.)
|
||||
* to define their contents.
|
||||
*
|
||||
* The inventory stores items as InventorySlot entries, each of which
|
||||
* may reference a world ItemComponent entity or hold item data directly.
|
||||
*/
|
||||
struct InventoryComponent {
|
||||
// Maximum number of slots
|
||||
int maxSlots = 20;
|
||||
|
||||
// Current slots
|
||||
std::vector<InventorySlot> slots;
|
||||
|
||||
// Total weight of all items (computed)
|
||||
float totalWeight = 0.0f;
|
||||
|
||||
// Maximum weight capacity (0 = unlimited)
|
||||
float maxWeight = 50.0f;
|
||||
|
||||
// Whether this inventory is a container (chest, barrel, etc.)
|
||||
// Containers can be opened by characters to transfer items.
|
||||
bool isContainer = false;
|
||||
|
||||
// Whether this inventory is currently open (for containers being browsed)
|
||||
bool isOpen = false;
|
||||
|
||||
InventoryComponent() = default;
|
||||
|
||||
explicit InventoryComponent(int maxSlots_)
|
||||
: maxSlots(maxSlots_)
|
||||
{
|
||||
slots.reserve(maxSlots_);
|
||||
}
|
||||
|
||||
/** Find the first empty slot index, or -1 if full. */
|
||||
int findEmptySlot() const
|
||||
{
|
||||
for (int i = 0; i < maxSlots; i++) {
|
||||
if (i >= (int)slots.size())
|
||||
return i;
|
||||
if (slots[i].isEmpty())
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/** Find a slot containing an item with the given itemId. */
|
||||
int findItem(const Ogre::String &itemId) const
|
||||
{
|
||||
for (int i = 0; i < (int)slots.size(); i++) {
|
||||
if (!slots[i].isEmpty() && slots[i].itemId == itemId)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/** Find a slot containing an item with the given itemName. */
|
||||
int findItemByName(const Ogre::String &itemName) const
|
||||
{
|
||||
for (int i = 0; i < (int)slots.size(); i++) {
|
||||
if (!slots[i].isEmpty() &&
|
||||
slots[i].itemName == itemName)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/** Count total number of items (sum of stack sizes). */
|
||||
int countItems() const
|
||||
{
|
||||
int count = 0;
|
||||
for (const auto &slot : slots) {
|
||||
if (!slot.isEmpty())
|
||||
count += slot.stackSize;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/** Count how many of a specific itemId are in the inventory. */
|
||||
int countItem(const Ogre::String &itemId) const
|
||||
{
|
||||
int count = 0;
|
||||
for (const auto &slot : slots) {
|
||||
if (!slot.isEmpty() && slot.itemId == itemId)
|
||||
count += slot.stackSize;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/** Check if inventory has at least one of a specific itemId. */
|
||||
bool hasItem(const Ogre::String &itemId) const
|
||||
{
|
||||
return findItem(itemId) >= 0;
|
||||
}
|
||||
|
||||
/** Check if inventory has at least one of a specific itemName. */
|
||||
bool hasItemByName(const Ogre::String &itemName) const
|
||||
{
|
||||
return findItemByName(itemName) >= 0;
|
||||
}
|
||||
|
||||
/** Recalculate total weight. */
|
||||
void recalculateWeight()
|
||||
{
|
||||
totalWeight = 0.0f;
|
||||
for (const auto &slot : slots) {
|
||||
if (!slot.isEmpty())
|
||||
totalWeight += slot.weight * slot.stackSize;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_INVENTORY_HPP
|
||||
20
src/features/editScene/components/InventoryModule.cpp
Normal file
20
src/features/editScene/components/InventoryModule.cpp
Normal file
@@ -0,0 +1,20 @@
|
||||
#include "Inventory.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/InventoryEditor.hpp"
|
||||
|
||||
REGISTER_COMPONENT_GROUP("Inventory", "Game", InventoryComponent,
|
||||
InventoryEditor)
|
||||
{
|
||||
registry.registerComponent<InventoryComponent>(
|
||||
"Inventory", "Game", std::make_unique<InventoryEditor>(),
|
||||
// Adder
|
||||
[](flecs::entity e) {
|
||||
if (!e.has<InventoryComponent>())
|
||||
e.set<InventoryComponent>({});
|
||||
},
|
||||
// Remover
|
||||
[](flecs::entity e) {
|
||||
if (e.has<InventoryComponent>())
|
||||
e.remove<InventoryComponent>();
|
||||
});
|
||||
}
|
||||
59
src/features/editScene/components/Item.hpp
Normal file
59
src/features/editScene/components/Item.hpp
Normal file
@@ -0,0 +1,59 @@
|
||||
#ifndef EDITSCENE_ITEM_HPP
|
||||
#define EDITSCENE_ITEM_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
/**
|
||||
* Item definition component.
|
||||
*
|
||||
* Attached to a world entity that represents a pickable item.
|
||||
* The ActuatorSystem detects items (entities with ItemComponent)
|
||||
* and shows "E - Pick up [ItemName]" prompts to the player.
|
||||
*
|
||||
* Items can also be placed in containers (chests, etc.) which
|
||||
* have an InventoryComponent.
|
||||
*
|
||||
* For AI characters, behavior tree nodes (hasItem, pickupItem,
|
||||
* dropItem, useItem, addItemToInventory) provide inventory access.
|
||||
*/
|
||||
struct ItemComponent {
|
||||
// Display name of the item (e.g. "Apple", "Sword", "Key")
|
||||
Ogre::String itemName = "Item";
|
||||
|
||||
// Item type for categorization (e.g. "food", "weapon", "key", "quest")
|
||||
Ogre::String itemType = "misc";
|
||||
|
||||
// Unique identifier for this item definition
|
||||
// Multiple entities can share the same itemId (e.g. multiple coins)
|
||||
Ogre::String itemId;
|
||||
|
||||
// Stack size: how many of this item are in this stack
|
||||
int stackSize = 1;
|
||||
|
||||
// Maximum stack size (0 = no stacking)
|
||||
int maxStackSize = 99;
|
||||
|
||||
// Weight per unit (for encumbrance calculations)
|
||||
float weight = 0.1f;
|
||||
|
||||
// Value (for trading)
|
||||
int value = 1;
|
||||
|
||||
// Name of the GOAP action to execute when "using" this item
|
||||
// (e.g. "eat", "equip", "read"). Empty = no use action.
|
||||
Ogre::String useActionName;
|
||||
|
||||
ItemComponent() = default;
|
||||
|
||||
explicit ItemComponent(const Ogre::String &name,
|
||||
const Ogre::String &type = "misc")
|
||||
: itemName(name)
|
||||
, itemType(type)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_ITEM_HPP
|
||||
19
src/features/editScene/components/ItemModule.cpp
Normal file
19
src/features/editScene/components/ItemModule.cpp
Normal file
@@ -0,0 +1,19 @@
|
||||
#include "Item.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/ItemEditor.hpp"
|
||||
|
||||
REGISTER_COMPONENT_GROUP("Item", "Game", ItemComponent, ItemEditor)
|
||||
{
|
||||
registry.registerComponent<ItemComponent>(
|
||||
"Item", "Game", std::make_unique<ItemEditor>(),
|
||||
// Adder
|
||||
[](flecs::entity e) {
|
||||
if (!e.has<ItemComponent>())
|
||||
e.set<ItemComponent>({});
|
||||
},
|
||||
// Remover
|
||||
[](flecs::entity e) {
|
||||
if (e.has<ItemComponent>())
|
||||
e.remove<ItemComponent>();
|
||||
});
|
||||
}
|
||||
77
src/features/editScene/components/PathFollowing.hpp
Normal file
77
src/features/editScene/components/PathFollowing.hpp
Normal file
@@ -0,0 +1,77 @@
|
||||
#ifndef EDITSCENE_PATH_FOLLOWING_HPP
|
||||
#define EDITSCENE_PATH_FOLLOWING_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
/**
|
||||
* 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<std::pair<Ogre::String, Ogre::String> > 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<PathFollowingState> 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<Ogre::Vector3> 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
|
||||
21
src/features/editScene/components/PathFollowingModule.cpp
Normal file
21
src/features/editScene/components/PathFollowingModule.cpp
Normal file
@@ -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<PathFollowingComponent>(
|
||||
"Path Following", "AI",
|
||||
std::make_unique<PathFollowingEditor>(),
|
||||
// Adder
|
||||
[](flecs::entity e) {
|
||||
if (!e.has<PathFollowingComponent>())
|
||||
e.set<PathFollowingComponent>({});
|
||||
},
|
||||
// Remover
|
||||
[](flecs::entity e) {
|
||||
if (e.has<PathFollowingComponent>())
|
||||
e.remove<PathFollowingComponent>();
|
||||
});
|
||||
}
|
||||
@@ -26,6 +26,17 @@ struct PlayerControllerComponent {
|
||||
Ogre::String swimIdleState = "swim-idle";
|
||||
Ogre::String swimState = "swim";
|
||||
Ogre::String swimFastState = "swim-fast";
|
||||
|
||||
/* Actuator interaction settings */
|
||||
float actuatorDistance = 25.0f;
|
||||
float actuatorCooldown = 1.5f;
|
||||
Ogre::Vector3 actuatorColor = Ogre::Vector3(0.0f, 0.4f, 1.0f);
|
||||
float distantCircleRadius = 8.0f;
|
||||
float nearCircleRadius = 14.0f;
|
||||
float actuatorLabelFontSize = 14.0f;
|
||||
|
||||
/* Runtime: set by ActuatorSystem while executing an action */
|
||||
bool inputLocked = false;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_PLAYERCONTROLLER_HPP
|
||||
|
||||
@@ -2,38 +2,68 @@
|
||||
#include "../components/Transform.hpp"
|
||||
#include <cmath>
|
||||
|
||||
// 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*>(
|
||||
Ogre::SceneNode *parent = static_cast<Ogre::SceneNode *>(
|
||||
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<Axis>(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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
418
src/features/editScene/lua-examples/action_db_example.lua
Normal file
418
src/features/editScene/lua-examples/action_db_example.lua
Normal file
@@ -0,0 +1,418 @@
|
||||
-- =============================================================================
|
||||
-- Action Database Lua API Examples
|
||||
-- =============================================================================
|
||||
-- This file demonstrates how to create, query, and manage GOAP actions and
|
||||
-- goals from Lua using the ecs.action_db API.
|
||||
--
|
||||
-- The ActionDatabase is a global singleton. Actions and goals defined here
|
||||
-- are immediately available to all characters in the scene.
|
||||
-- =============================================================================
|
||||
|
||||
-- =============================================================================
|
||||
-- Defining Bit Names
|
||||
-- =============================================================================
|
||||
-- Before using bits in preconditions/effects, you should define meaningful
|
||||
-- names for the 64 available bit slots. This makes your actions readable.
|
||||
--
|
||||
-- Bit names are global across the entire game session. They map
|
||||
-- human-readable names (like "has_axe", "is_hungry") to bit indices
|
||||
-- (0-63) used in GoapBlackboard preconditions and effects.
|
||||
--
|
||||
-- You can define bits explicitly at startup:
|
||||
-- =============================================================================
|
||||
|
||||
-- Explicitly assign bit names to specific indices:
|
||||
ecs.action_db.set_bit_name(0, "has_axe")
|
||||
ecs.action_db.set_bit_name(1, "has_wood")
|
||||
ecs.action_db.set_bit_name(2, "is_hungry")
|
||||
ecs.action_db.set_bit_name(3, "near_tree")
|
||||
ecs.action_db.set_bit_name(4, "near_well")
|
||||
ecs.action_db.set_bit_name(5, "has_bucket")
|
||||
ecs.action_db.set_bit_name(6, "has_food")
|
||||
ecs.action_db.set_bit_name(7, "near_fire")
|
||||
ecs.action_db.set_bit_name(8, "has_cooked_food")
|
||||
ecs.action_db.set_bit_name(9, "is_awake")
|
||||
ecs.action_db.set_bit_name(10, "at_market")
|
||||
ecs.action_db.set_bit_name(11, "at_home")
|
||||
ecs.action_db.set_bit_name(12, "near_chair")
|
||||
ecs.action_db.set_bit_name(13, "is_sitting")
|
||||
ecs.action_db.set_bit_name(14, "near_forest")
|
||||
ecs.action_db.set_bit_name(15, "is_strong")
|
||||
ecs.action_db.set_bit_name(16, "has_strength")
|
||||
|
||||
-- Or use auto_assign_bit() to let the system pick the index:
|
||||
local idx = ecs.action_db.auto_assign_bit("has_water")
|
||||
print("'has_water' assigned to bit " .. idx)
|
||||
|
||||
-- Look up a bit by name:
|
||||
local bit_idx = ecs.action_db.find_bit_by_name("has_axe")
|
||||
print("'has_axe' is at bit " .. bit_idx)
|
||||
|
||||
-- Get the name for a bit index:
|
||||
local name = ecs.action_db.get_bit_name(0)
|
||||
print("Bit 0 is named '" .. name .. "'")
|
||||
|
||||
-- List all currently assigned bit names:
|
||||
local bits = ecs.action_db.list_bit_names()
|
||||
print("Assigned bit names:")
|
||||
for _, b in ipairs(bits) do
|
||||
print(" Bit " .. b.index .. ": " .. b.name)
|
||||
end
|
||||
|
||||
-- NOTE: If you use a bit name in an action's preconditions/effects
|
||||
-- that hasn't been explicitly assigned, it will be auto-assigned
|
||||
-- to the first free slot automatically. So you don't HAVE to
|
||||
-- pre-define them, but it's good practice for clarity.
|
||||
|
||||
-- =============================================================================
|
||||
-- Creating Actions
|
||||
-- =============================================================================
|
||||
|
||||
-- Simple action with just a name and cost:
|
||||
ecs.action_db.add_action("idle", 1)
|
||||
|
||||
-- Action with preconditions (what must be true before the action can run):
|
||||
ecs.action_db.add_action("chop_wood", 2,
|
||||
{
|
||||
bits = { has_axe = true },
|
||||
values = { stamina = 10 }
|
||||
},
|
||||
{ -- effects (what becomes true after the action runs)
|
||||
bits = { has_wood = true },
|
||||
values = { stamina = -5 }
|
||||
}
|
||||
)
|
||||
|
||||
-- Action with only preconditions, no effects:
|
||||
ecs.action_db.add_action("fetch_water", 3,
|
||||
{
|
||||
bits = { near_well = true, has_bucket = true },
|
||||
values = { thirst = 50 }
|
||||
}
|
||||
)
|
||||
|
||||
-- Action with only effects, no preconditions:
|
||||
ecs.action_db.add_action("rest", 1,
|
||||
{},
|
||||
{
|
||||
values = { stamina = 100, energy = 100 }
|
||||
}
|
||||
)
|
||||
|
||||
-- Action with float and string values in blackboard:
|
||||
ecs.action_db.add_action("cook_food", 4,
|
||||
{
|
||||
bits = { has_food = true, near_fire = true },
|
||||
values = { cooking_skill = 3 },
|
||||
floatValues = { hunger = 50.0 }
|
||||
},
|
||||
{
|
||||
bits = { has_cooked_food = true },
|
||||
values = { hunger = -30 },
|
||||
stringValues = { last_action = "cooking" }
|
||||
}
|
||||
)
|
||||
|
||||
-- =============================================================================
|
||||
-- Creating Actions WITH Behavior Trees
|
||||
-- =============================================================================
|
||||
|
||||
-- Action with a simple leaf behavior tree (plays an animation):
|
||||
ecs.action_db.add_action("wave", 1,
|
||||
{}, -- no preconditions
|
||||
{}, -- no effects
|
||||
{ -- behavior tree (arg 5)
|
||||
type = "sequence",
|
||||
children = {
|
||||
{ type = "setAnimationState", name = "SM/Wave" },
|
||||
{ type = "delay", params = "2.0" },
|
||||
{ type = "setAnimationState", name = "SM/Idle" }
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
-- Action with a selector behavior tree (try to chop, fall back to idle):
|
||||
ecs.action_db.add_action("chop_tree", 3,
|
||||
{
|
||||
bits = { near_tree = true },
|
||||
values = { stamina = 15 }
|
||||
},
|
||||
{
|
||||
bits = { has_wood = true },
|
||||
values = { stamina = -8, wood_count = 1 }
|
||||
},
|
||||
{
|
||||
type = "selector",
|
||||
children = {
|
||||
{
|
||||
type = "sequence",
|
||||
children = {
|
||||
{ type = "checkBit", name = "has_axe", params = "1" },
|
||||
{ type = "setAnimationState", name = "SM/Chop" },
|
||||
{ type = "delay", params = "3.0" },
|
||||
{ type = "setAnimationState", name = "SM/Idle" },
|
||||
{ type = "setBit", name = "has_wood", params = "1" }
|
||||
}
|
||||
},
|
||||
{
|
||||
type = "sequence",
|
||||
children = {
|
||||
{ type = "debugPrint", name = "No axe! Picking up stick..." },
|
||||
{ type = "setAnimationState", name = "SM/Pickup" },
|
||||
{ type = "delay", params = "1.0" },
|
||||
{ type = "setBit", name = "has_wood", params = "1" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
-- Action with blackboard checks and value manipulation:
|
||||
ecs.action_db.add_action("travel_to_market", 5,
|
||||
{
|
||||
bits = { is_awake = true },
|
||||
values = { energy = 20 }
|
||||
},
|
||||
{
|
||||
bits = { at_market = true },
|
||||
values = { energy = -15 }
|
||||
},
|
||||
{
|
||||
type = "sequence",
|
||||
children = {
|
||||
{ type = "checkValue", name = "energy", params = ">= 20" },
|
||||
{ type = "setAnimationState", name = "SM/Walk" },
|
||||
{ type = "delay", params = "5.0" },
|
||||
{ type = "setAnimationState", name = "SM/Idle" },
|
||||
{ type = "setBit", name = "at_market", params = "1" },
|
||||
{ type = "setBit", name = "at_home", params = "0" }
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
-- Action with inventory operations:
|
||||
ecs.action_db.add_action("gather_wood", 2,
|
||||
{
|
||||
bits = { near_forest = true }
|
||||
},
|
||||
{
|
||||
values = { wood_count = 3 }
|
||||
},
|
||||
{
|
||||
type = "sequence",
|
||||
children = {
|
||||
{ type = "setAnimationState", name = "SM/Gather" },
|
||||
{ type = "delay", params = "2.0" },
|
||||
{ type = "addItemToInventory", params = "wood,Firewood,material,3,1.0,0" },
|
||||
{ type = "setAnimationState", name = "SM/Idle" }
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
-- Action that teleports character to a smart object child:
|
||||
ecs.action_db.add_action("sit_on_chair", 1,
|
||||
{
|
||||
bits = { near_chair = true }
|
||||
},
|
||||
{
|
||||
bits = { is_sitting = true }
|
||||
},
|
||||
{
|
||||
type = "sequence",
|
||||
children = {
|
||||
{ type = "teleportToChild", name = "SitTarget" },
|
||||
{ type = "disablePhysics" },
|
||||
{ type = "setAnimationState", name = "SM/Sit" },
|
||||
{ type = "delay", params = "5.0" },
|
||||
{ type = "setAnimationState", name = "SM/Stand" },
|
||||
{ type = "enablePhysics" }
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
-- =============================================================================
|
||||
-- Creating Goals
|
||||
-- =============================================================================
|
||||
|
||||
-- Simple goal with just a name and priority:
|
||||
ecs.action_db.add_goal("survive", 100)
|
||||
|
||||
-- Goal with a target blackboard state:
|
||||
ecs.action_db.add_goal("gather_resources", 50,
|
||||
{
|
||||
bits = { has_wood = true, has_water = true },
|
||||
values = { wood_count = 5, water_count = 3 }
|
||||
}
|
||||
)
|
||||
|
||||
-- Goal with a condition expression (evaluated against character's blackboard):
|
||||
ecs.action_db.add_goal("stay_healthy", 80,
|
||||
{
|
||||
values = { health = 100, stamina = 80 }
|
||||
},
|
||||
"health < 50 || stamina < 30" -- only valid when character needs healing
|
||||
)
|
||||
|
||||
-- Goal with full specification:
|
||||
ecs.action_db.add_goal("become_strong", 30,
|
||||
{
|
||||
bits = { is_strong = true },
|
||||
values = { strength = 100 }
|
||||
},
|
||||
"strength < 100" -- only valid if not already strong
|
||||
)
|
||||
|
||||
-- =============================================================================
|
||||
-- Querying Actions and Goals
|
||||
-- =============================================================================
|
||||
|
||||
-- Find an action by name:
|
||||
local action = ecs.action_db.find_action("chop_wood")
|
||||
if action then
|
||||
print("Found action: " .. action.name .. " (cost: " .. action.cost .. ")")
|
||||
-- action.preconditions and action.effects are tables with:
|
||||
-- .bits - table of boolean flags
|
||||
-- .values - table of integer values
|
||||
-- .floatValues - table of float values
|
||||
-- .stringValues - table of string values
|
||||
-- action.behaviorTree is a table with:
|
||||
-- .type - node type string
|
||||
-- .name - optional name
|
||||
-- .params - optional params
|
||||
-- .children - optional array of child nodes
|
||||
end
|
||||
|
||||
-- Find a goal by name:
|
||||
local goal = ecs.action_db.find_goal("gather_resources")
|
||||
if goal then
|
||||
print("Found goal: " .. goal.name .. " (priority: " .. goal.priority .. ")")
|
||||
-- goal.target is a blackboard table
|
||||
-- goal.condition is the condition string
|
||||
end
|
||||
|
||||
-- =============================================================================
|
||||
-- Listing All Actions and Goals
|
||||
-- =============================================================================
|
||||
|
||||
-- List all action names:
|
||||
local actions = ecs.action_db.list_actions()
|
||||
print("Available actions:")
|
||||
for i, name in ipairs(actions) do
|
||||
print(" " .. i .. ". " .. name)
|
||||
end
|
||||
|
||||
-- List all goal names:
|
||||
local goals = ecs.action_db.list_goals()
|
||||
print("Available goals:")
|
||||
for i, name in ipairs(goals) do
|
||||
print(" " .. i .. ". " .. name)
|
||||
end
|
||||
|
||||
-- =============================================================================
|
||||
-- Removing Actions and Goals
|
||||
-- =============================================================================
|
||||
|
||||
-- Remove an action by name:
|
||||
local removed = ecs.action_db.remove_action("idle")
|
||||
if removed then
|
||||
print("Removed action: idle")
|
||||
end
|
||||
|
||||
-- Remove a goal by name:
|
||||
ecs.action_db.remove_goal("become_strong")
|
||||
|
||||
-- =============================================================================
|
||||
-- Replacing Actions (same name = replace)
|
||||
-- =============================================================================
|
||||
|
||||
-- If you add an action with the same name as an existing one, it replaces it:
|
||||
ecs.action_db.add_action("chop_wood", 5,
|
||||
{
|
||||
bits = { has_axe = true, has_strength = true },
|
||||
values = { stamina = 20 }
|
||||
},
|
||||
{
|
||||
bits = { has_wood = true },
|
||||
values = { stamina = -10, wood_count = 2 }
|
||||
}
|
||||
)
|
||||
-- The old "chop_wood" action is replaced with this new definition.
|
||||
|
||||
-- =============================================================================
|
||||
-- Clearing Everything
|
||||
-- =============================================================================
|
||||
|
||||
-- Remove all actions and goals:
|
||||
-- ecs.action_db.clear()
|
||||
|
||||
-- =============================================================================
|
||||
-- Blackboard Table Format Reference
|
||||
-- =============================================================================
|
||||
--
|
||||
-- The blackboard table passed to add_action/add_goal has this structure:
|
||||
--
|
||||
-- {
|
||||
-- bits = {
|
||||
-- has_axe = true, -- boolean flags (use named bits)
|
||||
-- has_wood = false,
|
||||
-- is_hungry = true
|
||||
-- },
|
||||
-- values = { -- integer values
|
||||
-- health = 100,
|
||||
-- stamina = 50,
|
||||
-- wood_count = 0
|
||||
-- },
|
||||
-- floatValues = { -- float values
|
||||
-- hunger = 75.5,
|
||||
-- speed = 1.2
|
||||
-- },
|
||||
-- stringValues = { -- string values
|
||||
-- last_action = "idle",
|
||||
-- current_state = "exploring"
|
||||
-- }
|
||||
-- }
|
||||
--
|
||||
-- All sub-tables are optional. An empty table or nil means no constraints.
|
||||
-- =============================================================================
|
||||
|
||||
-- =============================================================================
|
||||
-- Behavior Tree Table Format Reference
|
||||
-- =============================================================================
|
||||
--
|
||||
-- The behaviorTree table (arg 5 of add_action) has this structure:
|
||||
--
|
||||
-- {
|
||||
-- type = "sequence", -- node type (required)
|
||||
-- name = "optional_name", -- depends on type (task name, anim state, etc.)
|
||||
-- params = "optional_params", -- extra parameters (delay seconds, bit index, etc.)
|
||||
-- children = { -- array of child nodes (for sequence/selector/invert)
|
||||
-- { type = "task", name = "myAction" },
|
||||
-- { type = "setAnimationState", name = "SM/Walk" },
|
||||
-- { type = "delay", params = "2.0" },
|
||||
-- { type = "checkBit", name = "has_axe", params = "1" }
|
||||
-- }
|
||||
-- }
|
||||
--
|
||||
-- Common node types:
|
||||
-- "sequence" - Execute children in order until one fails
|
||||
-- "selector" - Execute children in order until one succeeds
|
||||
-- "invert" - Invert the result of a single child
|
||||
-- "task" - Leaf: references a named task
|
||||
-- "check" - Leaf: references a named condition
|
||||
-- "debugPrint" - Leaf: prints 'name' to console
|
||||
-- "setAnimationState"- Leaf: sets animation state (name="SM/State")
|
||||
-- "isAnimationEnded" - Leaf: true if animation ended
|
||||
-- "setBit" - Leaf: sets blackboard bit (name=bit, params=0/1)
|
||||
-- "checkBit" - Leaf: true if blackboard bit is set
|
||||
-- "setValue" - Leaf: sets blackboard value (name=key, params=val)
|
||||
-- "checkValue" - Leaf: blackboard comparison (name=key, params="op val")
|
||||
-- "delay" - Leaf: waits N seconds (params=seconds as float)
|
||||
-- "teleportToChild" - Leaf: teleports to named child of Smart Object
|
||||
-- "disablePhysics" - Leaf: removes character from physics
|
||||
-- "enablePhysics" - Leaf: re-adds character to physics
|
||||
-- "hasItem" - Leaf check: true if inventory has itemId
|
||||
-- "pickupItem" - Leaf: picks up nearest item
|
||||
-- "dropItem" - Leaf: drops item from inventory
|
||||
-- "useItem" - Leaf: uses item from inventory
|
||||
-- "addItemToInventory"- Leaf: adds item directly to inventory
|
||||
-- =============================================================================
|
||||
514
src/features/editScene/lua-examples/behavior_tree_example.lua
Normal file
514
src/features/editScene/lua-examples/behavior_tree_example.lua
Normal file
@@ -0,0 +1,514 @@
|
||||
-- =============================================================================
|
||||
-- Behavior Tree Lua API Examples
|
||||
-- =============================================================================
|
||||
-- This file demonstrates how to create custom behavior tree nodes using
|
||||
-- Lua functions via the ecs.behavior_tree API, and how to use the
|
||||
-- built-in C++ node types via ecs.behavior_tree.create_node().
|
||||
--
|
||||
-- The API allows you to:
|
||||
-- 1. Register Lua functions as behavior tree node handlers
|
||||
-- 2. Create behavior tree nodes (both Lua and built-in C++ types)
|
||||
-- 3. Return "success", "failure", or "running" to control tree flow
|
||||
-- 4. Pass parameters from the behavior tree editor to your Lua function
|
||||
-- =============================================================================
|
||||
|
||||
-- =============================================================================
|
||||
-- Registering Lua Node Handlers
|
||||
-- =============================================================================
|
||||
-- Use ecs.behavior_tree.register_node(name, function) to register a Lua
|
||||
-- function as a behavior tree node handler.
|
||||
--
|
||||
-- The function receives two arguments:
|
||||
-- entity_id - The entity ID executing this behavior tree node
|
||||
-- params - A table of parameters parsed from the node's params string
|
||||
--
|
||||
-- The function must return one of:
|
||||
-- "success" - Node completed successfully (tree continues)
|
||||
-- "failure" - Node failed (tree stops with failure)
|
||||
-- "running" - Node is still running (will be called again next frame)
|
||||
-- =============================================================================
|
||||
|
||||
-- =============================================================================
|
||||
-- Example 1: Simple greeting node
|
||||
-- =============================================================================
|
||||
-- Prints a message and succeeds immediately.
|
||||
|
||||
ecs.behavior_tree.register_node("say_hello", function(entity_id, params)
|
||||
local message = params.message or "Hello!"
|
||||
print("Entity " .. entity_id .. " says: " .. message)
|
||||
return "success"
|
||||
end)
|
||||
|
||||
-- =============================================================================
|
||||
-- Example 2: Node that checks a blackboard value
|
||||
-- =============================================================================
|
||||
-- Checks if a blackboard integer value meets a minimum threshold.
|
||||
-- Succeeds if value >= min, fails otherwise.
|
||||
|
||||
ecs.behavior_tree.register_node("check_blackboard_value", function(entity_id, params)
|
||||
local key = params.key
|
||||
local min_val = tonumber(params.min) or 0
|
||||
|
||||
if not key then
|
||||
return "failure"
|
||||
end
|
||||
|
||||
local bb = ecs.get_component(entity_id, "GoapBlackboard")
|
||||
if not bb then
|
||||
return "failure"
|
||||
end
|
||||
|
||||
local value = bb.values[key]
|
||||
if value == nil then
|
||||
return "failure"
|
||||
end
|
||||
|
||||
if value >= min_val then
|
||||
return "success"
|
||||
else
|
||||
return "failure"
|
||||
end
|
||||
end)
|
||||
|
||||
-- =============================================================================
|
||||
-- Example 3: Node that runs over multiple frames (running state)
|
||||
-- =============================================================================
|
||||
-- Waits for a specified duration, storing progress in the blackboard's
|
||||
-- floatValues. Returns "running" each frame until the duration elapses.
|
||||
|
||||
ecs.behavior_tree.register_node("wait_for_duration", function(entity_id, params)
|
||||
local duration = tonumber(params.duration) or 1.0
|
||||
local timer_key = params.timer_key or "wait_timer"
|
||||
|
||||
local bb = ecs.get_component(entity_id, "GoapBlackboard")
|
||||
if not bb then
|
||||
return "failure"
|
||||
end
|
||||
|
||||
local elapsed = bb.floatValues[timer_key] or 0.0
|
||||
local dt = ecs.get_delta_time() or 0.016
|
||||
|
||||
elapsed = elapsed + dt
|
||||
bb.floatValues[timer_key] = elapsed
|
||||
ecs.set_component(entity_id, "GoapBlackboard", bb)
|
||||
|
||||
if elapsed >= duration then
|
||||
bb.floatValues[timer_key] = nil
|
||||
ecs.set_component(entity_id, "GoapBlackboard", bb)
|
||||
return "success"
|
||||
end
|
||||
|
||||
return "running"
|
||||
end)
|
||||
|
||||
-- =============================================================================
|
||||
-- Example 4: Node that modifies blackboard values
|
||||
-- =============================================================================
|
||||
-- Adds a configurable amount to a blackboard integer value.
|
||||
|
||||
ecs.behavior_tree.register_node("add_blackboard_value", function(entity_id, params)
|
||||
local key = params.key
|
||||
local amount = tonumber(params.amount) or 1
|
||||
|
||||
if not key then
|
||||
return "failure"
|
||||
end
|
||||
|
||||
local bb = ecs.get_component(entity_id, "GoapBlackboard")
|
||||
if not bb then
|
||||
return "failure"
|
||||
end
|
||||
|
||||
bb.values[key] = (bb.values[key] or 0) + amount
|
||||
ecs.set_component(entity_id, "GoapBlackboard", bb)
|
||||
return "success"
|
||||
end)
|
||||
|
||||
-- =============================================================================
|
||||
-- Example 5: Node that sets a blackboard bit
|
||||
-- =============================================================================
|
||||
-- Sets or clears a named bit in the blackboard.
|
||||
|
||||
ecs.behavior_tree.register_node("set_blackboard_bit", function(entity_id, params)
|
||||
local bit_name = params.bit
|
||||
local value = params.value == "1" or params.value == "true"
|
||||
|
||||
if not bit_name then
|
||||
return "failure"
|
||||
end
|
||||
|
||||
local bb = ecs.get_component(entity_id, "GoapBlackboard")
|
||||
if not bb then
|
||||
return "failure"
|
||||
end
|
||||
|
||||
bb.bits[bit_name] = value
|
||||
ecs.set_component(entity_id, "GoapBlackboard", bb)
|
||||
return "success"
|
||||
end)
|
||||
|
||||
-- =============================================================================
|
||||
-- Example 6: Node that checks a blackboard bit
|
||||
-- =============================================================================
|
||||
-- Checks if a named bit is set to a specific value.
|
||||
|
||||
ecs.behavior_tree.register_node("check_blackboard_bit", function(entity_id, params)
|
||||
local bit_name = params.bit
|
||||
local expected = params.value ~= "0" and params.value ~= "false"
|
||||
|
||||
if not bit_name then
|
||||
return "failure"
|
||||
end
|
||||
|
||||
local bb = ecs.get_component(entity_id, "GoapBlackboard")
|
||||
if not bb then
|
||||
return "failure"
|
||||
end
|
||||
|
||||
local actual = bb.bits[bit_name] == true
|
||||
if actual == expected then
|
||||
return "success"
|
||||
else
|
||||
return "failure"
|
||||
end
|
||||
end)
|
||||
|
||||
-- =============================================================================
|
||||
-- Example 7: Random chance node
|
||||
-- =============================================================================
|
||||
-- Succeeds with a configurable probability (0.0 to 1.0).
|
||||
|
||||
ecs.behavior_tree.register_node("random_chance", function(entity_id, params)
|
||||
local probability = tonumber(params.probability) or 0.5
|
||||
local roll = math.random()
|
||||
if roll < probability then
|
||||
return "success"
|
||||
else
|
||||
return "failure"
|
||||
end
|
||||
end)
|
||||
|
||||
-- =============================================================================
|
||||
-- Using Built-in Node Types via create_node()
|
||||
-- =============================================================================
|
||||
-- ecs.behavior_tree.create_node(type, name, params) creates a node table
|
||||
-- for any built-in C++ node type. If the first argument matches a registered
|
||||
-- Lua handler name, it creates a luaTask node instead.
|
||||
--
|
||||
-- Built-in node types:
|
||||
-- Control: sequence, selector, invert
|
||||
-- Animation: setAnimationState, isAnimationEnded
|
||||
-- Blackboard: setBit, checkBit, setValue, checkValue, blackboardDump
|
||||
-- Timing: delay
|
||||
-- Movement: teleportToChild
|
||||
-- Physics: disablePhysics, enablePhysics
|
||||
-- Events: sendEvent
|
||||
-- Inventory: hasItem, hasItemByName, countItem, pickupItem, dropItem,
|
||||
-- useItem, addItemToInventory
|
||||
-- Debug: debugPrint
|
||||
-- Lua: luaTask (auto-detected when name matches a registered handler)
|
||||
-- =============================================================================
|
||||
|
||||
-- =============================================================================
|
||||
-- Example 8: Action using built-in animation nodes
|
||||
-- =============================================================================
|
||||
-- Uses setAnimationState and isAnimationEnded to play an animation and
|
||||
-- wait for it to finish.
|
||||
|
||||
ecs.action_db.add_action("play_walk_animation", 1,
|
||||
{}, -- preconditions
|
||||
{}, -- effects
|
||||
{ -- behavior tree
|
||||
type = "sequence",
|
||||
children = {
|
||||
ecs.behavior_tree.create_node("setAnimationState", "locomotion/walk"),
|
||||
ecs.behavior_tree.create_node("isAnimationEnded", "locomotion"),
|
||||
ecs.behavior_tree.create_node("setAnimationState", "locomotion/idle")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
-- =============================================================================
|
||||
-- Example 9: Action using built-in delay node
|
||||
-- =============================================================================
|
||||
-- Waits for a specified duration using the built-in delay node.
|
||||
|
||||
ecs.action_db.add_action("wait_and_greet", 1,
|
||||
{},
|
||||
{},
|
||||
{
|
||||
type = "sequence",
|
||||
children = {
|
||||
ecs.behavior_tree.create_node("delay", "", "2.0"),
|
||||
ecs.behavior_tree.create_node("say_hello", "message=Waited 2 seconds!")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
-- =============================================================================
|
||||
-- Example 10: Action using built-in blackboard nodes
|
||||
-- =============================================================================
|
||||
-- Sets a blackboard bit, checks it, and sets a value.
|
||||
|
||||
ecs.action_db.add_action("blackboard_demo", 1,
|
||||
{},
|
||||
{
|
||||
bits = { has_sword = true },
|
||||
values = { gold = 100 }
|
||||
},
|
||||
{
|
||||
type = "sequence",
|
||||
children = {
|
||||
ecs.behavior_tree.create_node("setBit", "has_sword", "1"),
|
||||
ecs.behavior_tree.create_node("checkBit", "has_sword"),
|
||||
ecs.behavior_tree.create_node("setValue", "gold", "100"),
|
||||
ecs.behavior_tree.create_node("checkValue", "gold", ">= 50"),
|
||||
ecs.behavior_tree.create_node("blackboardDump", "After setup")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
-- =============================================================================
|
||||
-- Example 11: Action using built-in sendEvent node
|
||||
-- =============================================================================
|
||||
-- Sends an event with parameters.
|
||||
|
||||
ecs.action_db.add_action("trigger_quest_event", 1,
|
||||
{},
|
||||
{},
|
||||
{
|
||||
type = "sequence",
|
||||
children = {
|
||||
ecs.behavior_tree.create_node("sendEvent", "quest_started",
|
||||
"quest_id=the_ancient_sword,quest_giver=elder"),
|
||||
ecs.behavior_tree.create_node("debugPrint", "Quest event sent")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
-- =============================================================================
|
||||
-- Example 12: Action using built-in physics nodes
|
||||
-- =============================================================================
|
||||
-- Disables physics, waits, then re-enables.
|
||||
|
||||
ecs.action_db.add_action("physics_control", 1,
|
||||
{},
|
||||
{},
|
||||
{
|
||||
type = "sequence",
|
||||
children = {
|
||||
ecs.behavior_tree.create_node("disablePhysics"),
|
||||
ecs.behavior_tree.create_node("delay", "", "1.0"),
|
||||
ecs.behavior_tree.create_node("enablePhysics")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
-- =============================================================================
|
||||
-- Example 13: Action using built-in inventory nodes
|
||||
-- =============================================================================
|
||||
-- Adds an item to inventory, checks for it, uses it, then drops it.
|
||||
|
||||
ecs.action_db.add_action("inventory_demo", 1,
|
||||
{},
|
||||
{},
|
||||
{
|
||||
type = "sequence",
|
||||
children = {
|
||||
ecs.behavior_tree.create_node("addItemToInventory", "potion_01",
|
||||
"Health Potion,misc,1,0.5,10"),
|
||||
ecs.behavior_tree.create_node("hasItem", "potion_01"),
|
||||
ecs.behavior_tree.create_node("countItem", "potion_01", "1"),
|
||||
ecs.behavior_tree.create_node("useItem", "potion_01"),
|
||||
ecs.behavior_tree.create_node("dropItem", "potion_01", "1")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
-- =============================================================================
|
||||
-- Example 14: Action using built-in teleport node
|
||||
-- =============================================================================
|
||||
-- Teleports the entity to a named child transform.
|
||||
|
||||
ecs.action_db.add_action("teleport_to_entrance", 1,
|
||||
{},
|
||||
{},
|
||||
{
|
||||
type = "sequence",
|
||||
children = {
|
||||
ecs.behavior_tree.create_node("teleportToChild", "entrance"),
|
||||
ecs.behavior_tree.create_node("debugPrint", "Teleported to entrance")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
-- =============================================================================
|
||||
-- Example 15: Complex action mixing Lua and built-in nodes
|
||||
-- =============================================================================
|
||||
-- A selector that first tries to use a sword, and if not available,
|
||||
-- picks one up and equips it.
|
||||
|
||||
ecs.action_db.add_action("equip_sword", 2,
|
||||
{},
|
||||
{
|
||||
bits = { has_sword = true }
|
||||
},
|
||||
{
|
||||
type = "selector",
|
||||
children = {
|
||||
-- Try to use existing sword
|
||||
{
|
||||
type = "sequence",
|
||||
children = {
|
||||
ecs.behavior_tree.create_node("checkBit", "has_sword"),
|
||||
ecs.behavior_tree.create_node("debugPrint", "Already have a sword")
|
||||
}
|
||||
},
|
||||
-- Pick up and equip a sword
|
||||
{
|
||||
type = "sequence",
|
||||
children = {
|
||||
ecs.behavior_tree.create_node("setAnimationState", "locomotion/walk"),
|
||||
ecs.behavior_tree.create_node("delay", "", "3.0"),
|
||||
ecs.behavior_tree.create_node("addItemToInventory", "sword_01",
|
||||
"Iron Sword,weapon,1,2.5,50"),
|
||||
ecs.behavior_tree.create_node("setBit", "has_sword", "1"),
|
||||
ecs.behavior_tree.create_node("setAnimationState", "locomotion/idle"),
|
||||
ecs.behavior_tree.create_node("debugPrint", "Picked up the sword!")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
-- =============================================================================
|
||||
-- Example 16: Action with conditional logic using Lua nodes
|
||||
-- =============================================================================
|
||||
-- Uses Lua-registered nodes for blackboard checks and modifications.
|
||||
|
||||
ecs.action_db.add_action("gain_experience", 2,
|
||||
{
|
||||
values = { experience = 0 }
|
||||
},
|
||||
{
|
||||
values = { experience = 15 }
|
||||
},
|
||||
{
|
||||
type = "selector",
|
||||
children = {
|
||||
{
|
||||
type = "sequence",
|
||||
children = {
|
||||
ecs.behavior_tree.create_node("check_blackboard_value",
|
||||
"key=experience,min=50"),
|
||||
ecs.behavior_tree.create_node("say_hello",
|
||||
"message=You are experienced!"),
|
||||
ecs.behavior_tree.create_node("add_blackboard_value",
|
||||
"key=experience,amount=10")
|
||||
}
|
||||
},
|
||||
{
|
||||
type = "sequence",
|
||||
children = {
|
||||
ecs.behavior_tree.create_node("say_hello",
|
||||
"message=You are still learning..."),
|
||||
ecs.behavior_tree.create_node("add_blackboard_value",
|
||||
"key=experience,amount=5")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
-- =============================================================================
|
||||
-- Example 17: Action with random outcomes
|
||||
-- =============================================================================
|
||||
-- Uses the Lua random_chance node for probabilistic behavior.
|
||||
|
||||
ecs.action_db.add_action("try_gamble", 3,
|
||||
{},
|
||||
{
|
||||
values = { gold = 10 }
|
||||
},
|
||||
{
|
||||
type = "sequence",
|
||||
children = {
|
||||
ecs.behavior_tree.create_node("random_chance", "probability=0.3"),
|
||||
ecs.behavior_tree.create_node("say_hello", "message=You won the gamble!"),
|
||||
ecs.behavior_tree.create_node("add_blackboard_value", "key=gold,amount=10")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
-- =============================================================================
|
||||
-- Example 18: Action with running-state Lua node
|
||||
-- =============================================================================
|
||||
-- Uses the wait_for_duration Lua node which returns "running" each frame.
|
||||
|
||||
ecs.action_db.add_action("wait_and_continue", 1,
|
||||
{},
|
||||
{},
|
||||
{
|
||||
type = "sequence",
|
||||
children = {
|
||||
ecs.behavior_tree.create_node("wait_for_duration",
|
||||
"duration=2.5,timer_key=my_timer"),
|
||||
ecs.behavior_tree.create_node("say_hello", "message=Done waiting!")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
-- =============================================================================
|
||||
-- Example 19: Action with blackboard bit operations via Lua nodes
|
||||
-- =============================================================================
|
||||
|
||||
ecs.action_db.add_action("toggle_flag", 1,
|
||||
{},
|
||||
{
|
||||
bits = { quest_complete = true }
|
||||
},
|
||||
{
|
||||
type = "sequence",
|
||||
children = {
|
||||
ecs.behavior_tree.create_node("set_blackboard_bit",
|
||||
"bit=quest_complete,value=1"),
|
||||
ecs.behavior_tree.create_node("check_blackboard_bit",
|
||||
"bit=quest_complete,value=1"),
|
||||
ecs.behavior_tree.create_node("debugPrint", "Quest flag set and verified")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
-- =============================================================================
|
||||
-- Managing Registered Nodes
|
||||
-- =============================================================================
|
||||
|
||||
-- List all registered node handlers:
|
||||
local nodes = ecs.behavior_tree.list_nodes()
|
||||
print("Registered behavior tree nodes:")
|
||||
for i, name in ipairs(nodes) do
|
||||
print(" " .. i .. ". " .. name)
|
||||
end
|
||||
|
||||
-- Unregister a node handler:
|
||||
-- local removed = ecs.behavior_tree.unregister_node("say_hello")
|
||||
-- if removed then
|
||||
-- print("Unregistered node: say_hello")
|
||||
-- end
|
||||
|
||||
-- =============================================================================
|
||||
-- Parameter Format Reference
|
||||
-- =============================================================================
|
||||
--
|
||||
-- The params string passed to create_node() supports:
|
||||
--
|
||||
-- Integer: "count=42" -> params.count = 42 (number)
|
||||
-- Float: "speed=3.5" -> params.speed = 3.5 (number)
|
||||
-- String: "msg=hello" -> params.msg = "hello" (string)
|
||||
-- Quoted: 'msg="hello world"' -> params.msg = "hello world" (string)
|
||||
-- Multiple: "key=val,count=5" -> params.key = "val", params.count = 5
|
||||
-- Empty: "" -> params = {} (empty table)
|
||||
--
|
||||
-- The params table is passed as the second argument to your registered
|
||||
-- Lua function handler.
|
||||
-- =============================================================================
|
||||
711
src/features/editScene/lua-examples/component_example.lua
Normal file
711
src/features/editScene/lua-examples/component_example.lua
Normal file
@@ -0,0 +1,711 @@
|
||||
-- =============================================================================
|
||||
-- Component Lua API Examples
|
||||
-- =============================================================================
|
||||
-- This file demonstrates how to add, remove, query, and manipulate ECS
|
||||
-- components from Lua using the ecs.* Lua API.
|
||||
--
|
||||
-- Components are data attached to entities. They define what an entity IS
|
||||
-- and what it CAN DO. For example, a Transform component gives an entity
|
||||
-- a position in the world, while a Renderable component makes it visible.
|
||||
-- =============================================================================
|
||||
|
||||
-- =============================================================================
|
||||
-- Checking for Components
|
||||
-- =============================================================================
|
||||
|
||||
-- Create an entity and check what components it has:
|
||||
local entity = ecs.create_entity()
|
||||
ecs.set_entity_name(entity, "TestObject")
|
||||
|
||||
-- Check if an entity has a specific component:
|
||||
if ecs.has_component(entity, "Transform") then
|
||||
print("Entity has Transform component")
|
||||
end
|
||||
|
||||
-- New entities typically have EditorMarker by default:
|
||||
if ecs.has_component(entity, "EditorMarker") then
|
||||
print("Entity has EditorMarker (default for new entities)")
|
||||
end
|
||||
|
||||
-- =============================================================================
|
||||
-- Adding and Removing Components
|
||||
-- =============================================================================
|
||||
|
||||
-- Add a tag component (a component with no data):
|
||||
ecs.add_component(entity, "InWater")
|
||||
print("Added InWater tag")
|
||||
|
||||
-- Check it was added:
|
||||
if ecs.has_component(entity, "InWater") then
|
||||
print("Entity is now in water")
|
||||
end
|
||||
|
||||
-- Remove a tag component:
|
||||
ecs.remove_component(entity, "InWater")
|
||||
if not ecs.has_component(entity, "InWater") then
|
||||
print("Entity is no longer in water")
|
||||
end
|
||||
|
||||
-- =============================================================================
|
||||
-- Setting and Getting Components with Data
|
||||
-- =============================================================================
|
||||
|
||||
-- Set a component with data (creates or replaces it):
|
||||
ecs.set_component(entity, "Transform", {
|
||||
position = { 10, 20, 30 },
|
||||
rotation = { 1, 0, 0, 0 }, -- quaternion w, x, y, z
|
||||
scale = { 2, 2, 2 }
|
||||
})
|
||||
|
||||
-- Get the component back:
|
||||
local transform = ecs.get_component(entity, "Transform")
|
||||
if transform then
|
||||
print("Position: " .. transform.position[1] .. ", "
|
||||
.. transform.position[2] .. ", "
|
||||
.. transform.position[3])
|
||||
print("Scale: " .. transform.scale[1] .. ", "
|
||||
.. transform.scale[2] .. ", "
|
||||
.. transform.scale[3])
|
||||
end
|
||||
|
||||
-- =============================================================================
|
||||
-- Individual Field Access
|
||||
-- =============================================================================
|
||||
|
||||
-- Get a single field from a component:
|
||||
local pos_x = ecs.get_field(entity, "Transform", "position")
|
||||
print("Position X from get_field: " .. pos_x[1])
|
||||
|
||||
-- Set a single field:
|
||||
ecs.set_field(entity, "Transform", "position", { 0, 0, 0 })
|
||||
local new_pos = ecs.get_field(entity, "Transform", "position")
|
||||
print("New position: " .. new_pos[1] .. ", " .. new_pos[2] .. ", " .. new_pos[3])
|
||||
|
||||
-- =============================================================================
|
||||
-- Practical Examples: Common Components
|
||||
-- =============================================================================
|
||||
|
||||
-- Create a player character:
|
||||
function create_player(name, x, y, z)
|
||||
local player = ecs.create_entity()
|
||||
ecs.set_entity_name(player, name)
|
||||
|
||||
-- Transform (position in the world):
|
||||
ecs.set_component(player, "Transform", {
|
||||
position = { x, y, z },
|
||||
rotation = { 1, 0, 0, 0 },
|
||||
scale = { 1, 1, 1 }
|
||||
})
|
||||
|
||||
-- Renderable (visible mesh):
|
||||
ecs.set_component(player, "Renderable", {
|
||||
meshName = "character.mesh",
|
||||
visible = true
|
||||
})
|
||||
|
||||
-- Character (physics capsule for movement):
|
||||
ecs.set_component(player, "Character", {
|
||||
radius = 0.4,
|
||||
height = 1.8,
|
||||
offset = { 0, 0.9, 0 },
|
||||
enabled = true,
|
||||
useGravity = true
|
||||
})
|
||||
|
||||
-- PlayerController (camera and input):
|
||||
ecs.set_component(player, "PlayerController", {
|
||||
cameraMode = 1,
|
||||
tpsDistance = 5.0,
|
||||
tpsHeight = 2.0,
|
||||
mouseSensitivity = 0.5
|
||||
})
|
||||
|
||||
-- Inventory:
|
||||
ecs.set_component(player, "Inventory", {
|
||||
maxSlots = 20,
|
||||
maxWeight = 50.0,
|
||||
isContainer = true
|
||||
})
|
||||
|
||||
print("Created player: " .. name)
|
||||
return player
|
||||
end
|
||||
|
||||
local hero = create_player("Hero", 0, 0, 0)
|
||||
|
||||
-- Create a light source:
|
||||
function create_light(name, x, y, z, light_type)
|
||||
local light = ecs.create_entity()
|
||||
ecs.set_entity_name(light, name)
|
||||
|
||||
ecs.set_component(light, "Transform", {
|
||||
position = { x, y, z },
|
||||
rotation = { 1, 0, 0, 0 },
|
||||
scale = { 1, 1, 1 }
|
||||
})
|
||||
|
||||
ecs.set_component(light, "Light", {
|
||||
lightType = light_type or 0, -- 0=point, 1=directional, 2=spot
|
||||
diffuseColor = { 1, 1, 1, 1 },
|
||||
intensity = 1.5,
|
||||
range = 100,
|
||||
castShadows = true
|
||||
})
|
||||
|
||||
print("Created light: " .. name)
|
||||
return light
|
||||
end
|
||||
|
||||
local sun = create_light("Sun", 0, 100, 0, 1) -- directional light
|
||||
|
||||
-- Create a building with smart object interaction:
|
||||
function create_building(name, x, z)
|
||||
local building = ecs.create_entity()
|
||||
ecs.set_entity_name(building, name)
|
||||
|
||||
ecs.set_component(building, "Transform", {
|
||||
position = { x, 0, z },
|
||||
rotation = { 1, 0, 0, 0 },
|
||||
scale = { 1, 1, 1 }
|
||||
})
|
||||
|
||||
ecs.set_component(building, "Renderable", {
|
||||
meshName = "building.mesh",
|
||||
visible = true
|
||||
})
|
||||
|
||||
ecs.set_component(building, "SmartObject", {
|
||||
radius = 2.0,
|
||||
height = 3.0,
|
||||
actionNames = { "enter", "exit" }
|
||||
})
|
||||
|
||||
-- RigidBody for physics:
|
||||
ecs.set_component(building, "RigidBody", {
|
||||
bodyType = 0, -- 0=static
|
||||
mass = 0,
|
||||
friction = 0.5,
|
||||
restitution = 0.1,
|
||||
enabled = true
|
||||
})
|
||||
|
||||
print("Created building: " .. name)
|
||||
return building
|
||||
end
|
||||
|
||||
local house = create_building("House", 10, 10)
|
||||
|
||||
-- Create an item:
|
||||
function create_item(name, item_id, x, y, z)
|
||||
local item = ecs.create_entity()
|
||||
ecs.set_entity_name(item, name)
|
||||
|
||||
ecs.set_component(item, "Transform", {
|
||||
position = { x, y, z },
|
||||
rotation = { 1, 0, 0, 0 },
|
||||
scale = { 0.5, 0.5, 0.5 }
|
||||
})
|
||||
|
||||
ecs.set_component(item, "Renderable", {
|
||||
meshName = "potion.mesh",
|
||||
visible = true
|
||||
})
|
||||
|
||||
ecs.set_component(item, "Item", {
|
||||
itemName = name,
|
||||
itemType = "consumable",
|
||||
itemId = item_id,
|
||||
stackSize = 1,
|
||||
maxStackSize = 10,
|
||||
weight = 0.5,
|
||||
value = 50,
|
||||
useActionName = "drink_potion"
|
||||
})
|
||||
|
||||
print("Created item: " .. name)
|
||||
return item
|
||||
end
|
||||
|
||||
local potion = create_item("Health Potion", "potion_health", 5, 0.5, 5)
|
||||
|
||||
-- Create a NavMesh area:
|
||||
function create_navmesh_area(name)
|
||||
local nav = ecs.create_entity()
|
||||
ecs.set_entity_name(nav, name)
|
||||
|
||||
ecs.set_component(nav, "NavMesh", {
|
||||
cellSize = 0.3,
|
||||
cellHeight = 0.2,
|
||||
agentHeight = 2.0,
|
||||
agentRadius = 0.5,
|
||||
agentMaxClimb = 0.5,
|
||||
agentMaxSlope = 45.0,
|
||||
enabled = true
|
||||
})
|
||||
|
||||
print("Created NavMesh: " .. name)
|
||||
return nav
|
||||
end
|
||||
|
||||
local navmesh = create_navmesh_area("MainNavMesh")
|
||||
|
||||
-- =============================================================================
|
||||
-- Working with Tag Components
|
||||
-- =============================================================================
|
||||
|
||||
-- Tag components are boolean markers with no data fields.
|
||||
-- Common tags include:
|
||||
-- EditorMarker - marks entities visible in the editor
|
||||
-- InWater - entity is currently in water
|
||||
-- GeneratedPhysicsTag - physics was auto-generated
|
||||
-- ParentComponent - entity is a parent in a hierarchy
|
||||
-- NavMeshGeometrySource - entity contributes to navmesh generation
|
||||
|
||||
-- Example: Mark entities for different purposes:
|
||||
local marker1 = ecs.create_entity()
|
||||
ecs.add_component(marker1, "GeneratedPhysicsTag")
|
||||
|
||||
local marker2 = ecs.create_entity()
|
||||
ecs.add_component(marker2, "NavMeshGeometrySource")
|
||||
|
||||
-- =============================================================================
|
||||
-- Working with GOAP Components
|
||||
-- =============================================================================
|
||||
|
||||
-- Create an NPC with GOAP AI:
|
||||
function create_npc(name, x, z)
|
||||
local npc = ecs.create_entity()
|
||||
ecs.set_entity_name(npc, name)
|
||||
|
||||
ecs.set_component(npc, "Transform", {
|
||||
position = { x, 0, z },
|
||||
rotation = { 1, 0, 0, 0 },
|
||||
scale = { 1, 1, 1 }
|
||||
})
|
||||
|
||||
ecs.set_component(npc, "Character", {
|
||||
radius = 0.4,
|
||||
height = 1.8,
|
||||
offset = { 0, 0.9, 0 },
|
||||
enabled = true,
|
||||
useGravity = true
|
||||
})
|
||||
|
||||
-- GOAP blackboard (character's knowledge about the world):
|
||||
ecs.set_component(npc, "GoapBlackboard", {
|
||||
values = {
|
||||
health = 100,
|
||||
stamina = 100,
|
||||
hunger = 0,
|
||||
wood_count = 0
|
||||
},
|
||||
floatValues = {
|
||||
hunger = 0.0
|
||||
},
|
||||
stringValues = {
|
||||
state = "idle"
|
||||
}
|
||||
})
|
||||
|
||||
-- GOAP planner (plans actions to achieve goals):
|
||||
ecs.set_component(npc, "GoapPlanner", {
|
||||
enabled = true,
|
||||
maxIterations = 100
|
||||
})
|
||||
|
||||
-- GOAP runner (executes the plan):
|
||||
ecs.set_component(npc, "GoapRunner", {
|
||||
enabled = true,
|
||||
currentAction = "idle"
|
||||
})
|
||||
|
||||
-- Behavior tree for idle behavior:
|
||||
ecs.set_component(npc, "BehaviorTree", {
|
||||
treeName = "idle_behavior",
|
||||
enabled = true
|
||||
})
|
||||
|
||||
-- Path following for movement:
|
||||
ecs.set_component(npc, "PathFollowing", {
|
||||
enabled = true,
|
||||
speed = 1.5
|
||||
})
|
||||
|
||||
-- NavMesh agent for navigation:
|
||||
ecs.set_component(npc, "NavMeshAgent", {
|
||||
enabled = true,
|
||||
radius = 0.5,
|
||||
height = 2.0
|
||||
})
|
||||
|
||||
print("Created NPC: " .. name)
|
||||
return npc
|
||||
end
|
||||
|
||||
local npc = create_npc("Villager_01", -10, -10)
|
||||
|
||||
-- =============================================================================
|
||||
-- Working with Event Handler Components
|
||||
-- =============================================================================
|
||||
|
||||
-- Add event handlers to entities:
|
||||
ecs.set_component(npc, "EventHandler", {
|
||||
eventName = "collision",
|
||||
actionName = "handle_collision",
|
||||
enabled = true
|
||||
})
|
||||
|
||||
-- =============================================================================
|
||||
-- Working with Dialogue Components
|
||||
-- =============================================================================
|
||||
|
||||
-- Add dialogue to an NPC:
|
||||
ecs.set_component(npc, "Dialogue", {
|
||||
text = "Hello there, traveler!",
|
||||
speaker = "Villager_01",
|
||||
enabled = true
|
||||
})
|
||||
|
||||
-- =============================================================================
|
||||
-- Working with Water Components
|
||||
-- =============================================================================
|
||||
|
||||
-- Create a water plane:
|
||||
function create_water(name, y)
|
||||
local water = ecs.create_entity()
|
||||
ecs.set_entity_name(water, name)
|
||||
|
||||
ecs.set_component(water, "WaterPlane", {
|
||||
enabled = true,
|
||||
waterSurfaceY = y or 0.0,
|
||||
planeSize = 1000,
|
||||
reflectivity = 0.5,
|
||||
waveSpeed = 1.0
|
||||
})
|
||||
|
||||
ecs.set_component(water, "WaterPhysics", {
|
||||
enabled = true,
|
||||
waveHeight = 0.5
|
||||
})
|
||||
|
||||
print("Created water: " .. name)
|
||||
return water
|
||||
end
|
||||
|
||||
local ocean = create_water("Ocean", 0.0)
|
||||
|
||||
-- =============================================================================
|
||||
-- Working with Sky and Sun Components
|
||||
-- =============================================================================
|
||||
|
||||
-- Create a skybox:
|
||||
function create_sky(name)
|
||||
local sky = ecs.create_entity()
|
||||
ecs.set_entity_name(sky, name)
|
||||
|
||||
ecs.set_component(sky, "Skybox", {
|
||||
enabled = true,
|
||||
size = 500,
|
||||
starsEnabled = true
|
||||
})
|
||||
|
||||
print("Created sky: " .. name)
|
||||
return sky
|
||||
end
|
||||
|
||||
local skybox = create_sky("Skybox")
|
||||
|
||||
-- Update sun properties:
|
||||
ecs.set_component(sun, "Sun", {
|
||||
enabled = true,
|
||||
timeOfDay = 12.0,
|
||||
timeSpeed = 1.0,
|
||||
intensity = 1.0,
|
||||
castShadows = true
|
||||
})
|
||||
|
||||
-- =============================================================================
|
||||
-- Working with Camera Components
|
||||
-- =============================================================================
|
||||
|
||||
-- Create a camera:
|
||||
function create_camera(name)
|
||||
local cam = ecs.create_entity()
|
||||
ecs.set_entity_name(cam, name)
|
||||
|
||||
ecs.set_component(cam, "Camera", {
|
||||
fovY = 60,
|
||||
nearClip = 0.1,
|
||||
farClip = 1000,
|
||||
orthographic = false
|
||||
})
|
||||
|
||||
print("Created camera: " .. name)
|
||||
return cam
|
||||
end
|
||||
|
||||
local camera = create_camera("MainCamera")
|
||||
|
||||
-- =============================================================================
|
||||
-- Working with Prefab Components
|
||||
-- =============================================================================
|
||||
|
||||
-- Mark an entity as a prefab instance:
|
||||
ecs.set_component(house, "PrefabInstance", {
|
||||
prefabPath = "prefabs/house.json",
|
||||
instantiated = true
|
||||
})
|
||||
|
||||
-- =============================================================================
|
||||
-- Working with CellGrid Components
|
||||
-- =============================================================================
|
||||
|
||||
-- Create a cell grid for spatial partitioning:
|
||||
function create_cell_grid(name, width, height, depth)
|
||||
local grid = ecs.create_entity()
|
||||
ecs.set_entity_name(grid, name)
|
||||
|
||||
ecs.set_component(grid, "CellGrid", {
|
||||
width = width or 10,
|
||||
height = height or 5,
|
||||
depth = depth or 10,
|
||||
cellSize = 1.0,
|
||||
cellHeight = 0.5
|
||||
})
|
||||
|
||||
print("Created cell grid: " .. name)
|
||||
return grid
|
||||
end
|
||||
|
||||
local grid = create_cell_grid("WorldGrid", 20, 10, 20)
|
||||
|
||||
-- =============================================================================
|
||||
-- Working with Town/District/Lot/Room Components
|
||||
-- =============================================================================
|
||||
|
||||
-- Create a town with districts, lots, and rooms:
|
||||
function create_town(name)
|
||||
local town = ecs.create_entity()
|
||||
ecs.set_entity_name(town, name)
|
||||
|
||||
ecs.set_component(town, "Town", {
|
||||
townName = name,
|
||||
population = 500
|
||||
})
|
||||
|
||||
-- Create a district:
|
||||
local district = ecs.create_entity()
|
||||
ecs.set_entity_name(district, "Market District")
|
||||
ecs.set_component(district, "District", {
|
||||
districtName = "Market District",
|
||||
districtType = "commercial"
|
||||
})
|
||||
|
||||
-- Create a lot:
|
||||
local lot = ecs.create_entity()
|
||||
ecs.set_entity_name(lot, "Residential Lot 1")
|
||||
ecs.set_component(lot, "Lot", {
|
||||
lotName = "Residential Lot 1",
|
||||
lotType = "residential",
|
||||
width = 20,
|
||||
depth = 30
|
||||
})
|
||||
|
||||
-- Create a room:
|
||||
local room = ecs.create_entity()
|
||||
ecs.set_entity_name(room, "Kitchen")
|
||||
ecs.set_component(room, "Room", {
|
||||
roomName = "Kitchen",
|
||||
roomType = "kitchen",
|
||||
floor = 0
|
||||
})
|
||||
|
||||
print("Created town: " .. name)
|
||||
return town
|
||||
end
|
||||
|
||||
local rivendell = create_town("Rivendell")
|
||||
|
||||
-- =============================================================================
|
||||
-- Working with Furniture Components
|
||||
-- =============================================================================
|
||||
|
||||
-- Create a furniture template:
|
||||
function create_furniture(name, mesh, category)
|
||||
local furniture = ecs.create_entity()
|
||||
ecs.set_entity_name(furniture, name)
|
||||
|
||||
ecs.set_component(furniture, "FurnitureTemplate", {
|
||||
templateName = name,
|
||||
meshName = mesh or "chair.mesh",
|
||||
category = category or "seating"
|
||||
})
|
||||
|
||||
print("Created furniture: " .. name)
|
||||
return furniture
|
||||
end
|
||||
|
||||
local chair = create_furniture("Wooden Chair", "chair.mesh", "seating")
|
||||
|
||||
-- =============================================================================
|
||||
-- Working with Animation Components
|
||||
-- =============================================================================
|
||||
|
||||
-- Add animation tree to a character:
|
||||
ecs.set_component(npc, "AnimationTree", {
|
||||
treeName = "humanoid",
|
||||
enabled = true
|
||||
})
|
||||
|
||||
ecs.set_component(npc, "AnimationTreeTemplate", {
|
||||
templateName = "humanoid_base",
|
||||
blendTime = 0.2
|
||||
})
|
||||
|
||||
-- =============================================================================
|
||||
-- Working with Physics Components
|
||||
-- =============================================================================
|
||||
|
||||
-- Add a physics collider to an entity:
|
||||
ecs.set_component(house, "PhysicsCollider", {
|
||||
shapeType = "box",
|
||||
size = { 5, 3, 5 },
|
||||
enabled = true
|
||||
})
|
||||
|
||||
-- Add buoyancy info for water physics:
|
||||
ecs.set_component(house, "BuoyancyInfo", {
|
||||
enabled = true,
|
||||
buoyancy = 1.0,
|
||||
linearDrag = 0.5,
|
||||
angularDrag = 0.3
|
||||
})
|
||||
|
||||
-- =============================================================================
|
||||
-- Working with LOD Components
|
||||
-- =============================================================================
|
||||
|
||||
-- Add LOD settings:
|
||||
ecs.set_component(entity, "Lod", {
|
||||
lodLevel = 0,
|
||||
distance = 100.0
|
||||
})
|
||||
|
||||
ecs.set_component(entity, "LodSettings", {
|
||||
enabled = true,
|
||||
lodBias = 1.0
|
||||
})
|
||||
|
||||
-- =============================================================================
|
||||
-- Working with Static Geometry Components
|
||||
-- =============================================================================
|
||||
|
||||
-- Batch entities into static geometry:
|
||||
ecs.set_component(entity, "StaticGeometry", {
|
||||
batchName = "forest_batch",
|
||||
enabled = true
|
||||
})
|
||||
|
||||
ecs.set_component(entity, "StaticGeometryMember", {
|
||||
parentBatch = "forest_batch",
|
||||
enabled = true
|
||||
})
|
||||
|
||||
-- =============================================================================
|
||||
-- Working with Procedural Components
|
||||
-- =============================================================================
|
||||
|
||||
-- Create procedural textures and materials:
|
||||
ecs.set_component(entity, "ProceduralTexture", {
|
||||
textureName = "grass",
|
||||
width = 512,
|
||||
height = 512
|
||||
})
|
||||
|
||||
ecs.set_component(entity, "ProceduralMaterial", {
|
||||
materialName = "ground_mat",
|
||||
baseColor = { 0.5, 0.5, 0.5, 1.0 }
|
||||
})
|
||||
|
||||
-- =============================================================================
|
||||
-- Working with Primitive Components
|
||||
-- =============================================================================
|
||||
|
||||
-- Create a primitive shape:
|
||||
ecs.set_component(entity, "Primitive", {
|
||||
primitiveType = "box",
|
||||
size = { 1, 2, 1 }
|
||||
})
|
||||
|
||||
-- =============================================================================
|
||||
-- Working with Triangle Buffer Components
|
||||
-- =============================================================================
|
||||
|
||||
ecs.set_component(entity, "TriangleBuffer", {
|
||||
enabled = true,
|
||||
vertexCount = 100
|
||||
})
|
||||
|
||||
-- =============================================================================
|
||||
-- Working with Character Slots
|
||||
-- =============================================================================
|
||||
|
||||
ecs.set_component(entity, "CharacterSlots", {
|
||||
slotCount = 8
|
||||
})
|
||||
|
||||
-- =============================================================================
|
||||
-- Working with Roof Components
|
||||
-- =============================================================================
|
||||
|
||||
ecs.set_component(entity, "Roof", {
|
||||
roofType = "gable",
|
||||
height = 2.5,
|
||||
enabled = true
|
||||
})
|
||||
|
||||
-- =============================================================================
|
||||
-- Working with ClearArea Components
|
||||
-- =============================================================================
|
||||
|
||||
ecs.set_component(entity, "ClearArea", {
|
||||
radius = 5.0,
|
||||
enabled = true
|
||||
})
|
||||
|
||||
-- =============================================================================
|
||||
-- Working with Action Database Components
|
||||
-- =============================================================================
|
||||
|
||||
ecs.set_component(entity, "ActionDatabase", {
|
||||
enabled = true
|
||||
})
|
||||
|
||||
ecs.set_component(entity, "ActionDebug", {
|
||||
enabled = true,
|
||||
showDebugInfo = true
|
||||
})
|
||||
|
||||
-- =============================================================================
|
||||
-- Working with Startup Menu Components
|
||||
-- =============================================================================
|
||||
|
||||
ecs.set_component(entity, "StartupMenu", {
|
||||
enabled = true,
|
||||
showOnStart = true
|
||||
})
|
||||
|
||||
-- =============================================================================
|
||||
-- Component Lifecycle Summary
|
||||
-- =============================================================================
|
||||
|
||||
-- Components can be:
|
||||
-- 1. Added: ecs.add_component(entity, "ComponentName")
|
||||
-- 2. Removed: ecs.remove_component(entity, "ComponentName")
|
||||
-- 3. Checked: ecs.has_component(entity, "ComponentName")
|
||||
-- 4. Get: ecs.get_component(entity, "ComponentName")
|
||||
-- 5. Set: ecs.set_component(entity, "ComponentName", { fields... })
|
||||
-- 6. Field: ecs.get_field(entity, "ComponentName", "fieldName")
|
||||
-- 7. Field: ecs.set_field(entity, "ComponentName", "fieldName", value)
|
||||
|
||||
print("Component API examples completed successfully!")
|
||||
109
src/features/editScene/lua-examples/dialogue_basic_show.lua
Normal file
109
src/features/editScene/lua-examples/dialogue_basic_show.lua
Normal file
@@ -0,0 +1,109 @@
|
||||
-- =============================================================================
|
||||
-- Dialogue: Basic Show via Event System
|
||||
-- =============================================================================
|
||||
-- This example demonstrates how to show a simple dialogue box using the
|
||||
-- EventBus "dialogue_show" event.
|
||||
--
|
||||
-- The DialogueSystem listens for "dialogue_show" events and displays the
|
||||
-- text on any entity that has a DialogueComponent.
|
||||
--
|
||||
-- Event payload parameters:
|
||||
-- "text" (string) - Narration text to display
|
||||
-- "speaker" (string) - Optional speaker name (shown above text)
|
||||
-- "choices" (string) - Comma-separated choice labels (optional)
|
||||
-- "auto_progress" (int) - If 1, click anywhere progresses (no choices)
|
||||
-- =============================================================================
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 1. Create an entity with a DialogueComponent
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- First we need an entity that has the Dialogue component so the system
|
||||
-- knows where to render the dialogue box.
|
||||
|
||||
local dialogue_entity = ecs.create_entity()
|
||||
ecs.set_entity_name(dialogue_entity, "DialogueBox")
|
||||
|
||||
-- Add the Dialogue component with default settings:
|
||||
ecs.add_component(dialogue_entity, "Dialogue")
|
||||
|
||||
-- You can also configure the dialogue box appearance:
|
||||
ecs.set_component(dialogue_entity, "Dialogue", {
|
||||
fontName = "Jupiteroid-Regular.ttf",
|
||||
fontSize = 24.0,
|
||||
speakerFontSize = 20.0,
|
||||
backgroundOpacity = 0.85,
|
||||
boxHeightFraction = 0.25, -- 25% of screen height
|
||||
boxPositionFraction = 0.75, -- bottom quarter of screen
|
||||
enabled = true
|
||||
})
|
||||
|
||||
print("Dialogue entity created with ID: " .. dialogue_entity)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 2. Show a simple narration (no choices)
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Send a "dialogue_show" event with just text. The dialogue box will appear
|
||||
-- and the player can click anywhere to dismiss it.
|
||||
|
||||
ecs.send_event("dialogue_show", {
|
||||
stringValues = {
|
||||
text = "Welcome to the world of World2!",
|
||||
speaker = "Narrator"
|
||||
}
|
||||
})
|
||||
|
||||
print("Sent basic narration dialogue")
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 3. Show dialogue with player choices
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- When "choices" is provided (comma-separated), the dialogue box shows
|
||||
-- buttons instead of click-to-progress. The player must pick one.
|
||||
|
||||
ecs.send_event("dialogue_show", {
|
||||
stringValues = {
|
||||
text = "Where would you like to go?",
|
||||
speaker = "Guide",
|
||||
choices = "The Forest,The Village,The Mountains"
|
||||
}
|
||||
})
|
||||
|
||||
print("Sent dialogue with choices")
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 4. Show dialogue without a speaker name
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
ecs.send_event("dialogue_show", {
|
||||
stringValues = {
|
||||
text = "A mysterious voice echoes through the chamber..."
|
||||
}
|
||||
})
|
||||
|
||||
print("Sent anonymous narration")
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 5. Multi-line dialogue (use \n for line breaks)
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
ecs.send_event("dialogue_show", {
|
||||
stringValues = {
|
||||
text = "Greetings, traveler.\n\nI have been expecting you.\nThe prophecy spoke of your arrival.",
|
||||
speaker = "Elder Marcus"
|
||||
}
|
||||
})
|
||||
|
||||
print("Sent multi-line dialogue")
|
||||
|
||||
-- =============================================================================
|
||||
-- Summary
|
||||
-- =============================================================================
|
||||
-- To show dialogue from Lua:
|
||||
-- 1. Ensure an entity with DialogueComponent exists (create one if needed)
|
||||
-- 2. Call ecs.send_event("dialogue_show", { stringValues = { ... } })
|
||||
-- 3. Required: text = "The narration text"
|
||||
-- 4. Optional: speaker = "Speaker Name"
|
||||
-- 5. Optional: choices = "Choice1,Choice2,Choice3" (comma-separated)
|
||||
-- =============================================================================
|
||||
|
||||
print("Dialogue basic show examples completed!")
|
||||
224
src/features/editScene/lua-examples/dialogue_component_api.lua
Normal file
224
src/features/editScene/lua-examples/dialogue_component_api.lua
Normal file
@@ -0,0 +1,224 @@
|
||||
-- =============================================================================
|
||||
-- Dialogue: Direct Component API Control
|
||||
-- =============================================================================
|
||||
-- This example demonstrates how to control the DialogueComponent directly
|
||||
-- via the ECS component API, without using the EventBus.
|
||||
--
|
||||
-- The DialogueComponent has methods that can be called from C++:
|
||||
-- show(text, choices, speaker) - Display dialogue
|
||||
-- progress() - Dismiss (no-choices mode)
|
||||
-- selectChoice(index) - Select a choice (1-based)
|
||||
-- isActive() - Check if dialogue is active
|
||||
-- reset() - Reset to idle state
|
||||
--
|
||||
-- From Lua, you manipulate the component's fields directly using the
|
||||
-- ecs.set_component / ecs.get_component API.
|
||||
-- =============================================================================
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 1. Create an entity with DialogueComponent
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local dlg = ecs.create_entity()
|
||||
ecs.set_entity_name(dlg, "DialogueBox")
|
||||
ecs.add_component(dlg, "Dialogue")
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 2. Set dialogue text directly via component fields
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Instead of sending an event, you can set the component fields directly.
|
||||
-- The DialogueSystem will pick up the state change on the next frame.
|
||||
|
||||
ecs.set_component(dlg, "Dialogue", {
|
||||
text = "This dialogue was set directly via the component API!",
|
||||
speaker = "Lua Script",
|
||||
enabled = true
|
||||
})
|
||||
|
||||
-- Note: Setting the fields directly does NOT automatically change the state
|
||||
-- to Showing. You need to also set the state, or use the event system.
|
||||
-- The DialogueComponent's show() method handles state transitions.
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 3. Read dialogue state from the component
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local comp = ecs.get_component(dlg, "Dialogue")
|
||||
if comp then
|
||||
print("Dialogue text: " .. (comp.text or "(empty)"))
|
||||
print("Dialogue speaker: " .. (comp.speaker or "(none)"))
|
||||
print("Dialogue enabled: " .. tostring(comp.enabled))
|
||||
print("Font: " .. (comp.fontName or "default"))
|
||||
print("Font size: " .. (comp.fontSize or 24))
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 4. Modify individual dialogue fields
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- Change just the text:
|
||||
ecs.set_field(dlg, "Dialogue", "text", "Updated dialogue text!")
|
||||
|
||||
-- Change just the speaker:
|
||||
ecs.set_field(dlg, "Dialogue", "speaker", "Mysterious Stranger")
|
||||
|
||||
-- Change appearance settings:
|
||||
ecs.set_field(dlg, "Dialogue", "backgroundOpacity", 0.9)
|
||||
ecs.set_field(dlg, "Dialogue", "boxHeightFraction", 0.3)
|
||||
ecs.set_field(dlg, "Dialogue", "boxPositionFraction", 0.7)
|
||||
|
||||
-- Read back the changes:
|
||||
local updated_text = ecs.get_field(dlg, "Dialogue", "text")
|
||||
local updated_speaker = ecs.get_field(dlg, "Dialogue", "speaker")
|
||||
print("Updated text: " .. updated_text)
|
||||
print("Updated speaker: " .. updated_speaker)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 5. Toggle dialogue visibility
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- Disable the dialogue box:
|
||||
ecs.set_field(dlg, "Dialogue", "enabled", false)
|
||||
print("Dialogue disabled")
|
||||
|
||||
-- Re-enable it:
|
||||
ecs.set_field(dlg, "Dialogue", "enabled", true)
|
||||
print("Dialogue re-enabled")
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 6. Check if dialogue component exists
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
if ecs.has_component(dlg, "Dialogue") then
|
||||
print("Entity has a Dialogue component")
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 7. Remove the dialogue component entirely
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- ecs.remove_component(dlg, "Dialogue")
|
||||
-- print("Dialogue component removed")
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 8. Practical: Configure dialogue appearance per-NPC
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
function create_npc_with_dialogue(name, mesh, greeting_text)
|
||||
local npc = ecs.create_entity()
|
||||
ecs.set_entity_name(npc, name)
|
||||
|
||||
-- Basic NPC setup
|
||||
ecs.set_component(npc, "Transform", {
|
||||
position = { 0, 0, 0 },
|
||||
rotation = { 1, 0, 0, 0 },
|
||||
scale = { 1, 1, 1 }
|
||||
})
|
||||
|
||||
ecs.set_component(npc, "Renderable", {
|
||||
meshName = mesh or "character.mesh",
|
||||
visible = true
|
||||
})
|
||||
|
||||
-- Dialogue component with NPC-specific appearance
|
||||
ecs.set_component(npc, "Dialogue", {
|
||||
text = greeting_text or "Hello!",
|
||||
speaker = name,
|
||||
fontName = "Jupiteroid-Regular.ttf",
|
||||
fontSize = 24.0,
|
||||
speakerFontSize = 20.0,
|
||||
backgroundOpacity = 0.85,
|
||||
boxHeightFraction = 0.25,
|
||||
boxPositionFraction = 0.75,
|
||||
enabled = true
|
||||
})
|
||||
|
||||
print("Created NPC with dialogue: " .. name)
|
||||
return npc
|
||||
end
|
||||
|
||||
-- Create a few NPCs with different dialogue configurations
|
||||
local merchant = create_npc_with_dialogue(
|
||||
"Merchant",
|
||||
"merchant.mesh",
|
||||
"Welcome to my shop! Best wares in town."
|
||||
)
|
||||
|
||||
local guard = create_npc_with_dialogue(
|
||||
"Guard",
|
||||
"guard.mesh",
|
||||
"Halt! Who goes there?"
|
||||
)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 9. Practical: Update dialogue based on game events
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
function update_npc_dialogue(npc_entity, new_text, new_speaker)
|
||||
-- Update the dialogue text and speaker
|
||||
ecs.set_field(npc_entity, "Dialogue", "text", new_text)
|
||||
if new_speaker then
|
||||
ecs.set_field(npc_entity, "Dialogue", "speaker", new_speaker)
|
||||
end
|
||||
|
||||
-- Show the updated dialogue via event (this triggers the state change)
|
||||
ecs.send_event("dialogue_show", {
|
||||
stringValues = {
|
||||
text = new_text,
|
||||
speaker = new_speaker or ecs.get_field(npc_entity, "Dialogue", "speaker")
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
-- Update the merchant's dialogue after a transaction
|
||||
update_npc_dialogue(merchant, "Thank you for your business! Come again.")
|
||||
|
||||
-- Update the guard's dialogue when player has high reputation
|
||||
update_npc_dialogue(guard, "At ease, friend. The town is safe with you around.")
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 10. Practical: Dialogue with dynamic choices from component data
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
function show_dialogue_with_dynamic_choices(npc_entity, base_text, choice_list)
|
||||
-- choice_list is a table of strings
|
||||
local choices_str = table.concat(choice_list, ",")
|
||||
|
||||
-- Update the component
|
||||
ecs.set_field(npc_entity, "Dialogue", "text", base_text)
|
||||
|
||||
-- Show via event (which handles state transitions properly)
|
||||
ecs.send_event("dialogue_show", {
|
||||
stringValues = {
|
||||
text = base_text,
|
||||
speaker = ecs.get_field(npc_entity, "Dialogue", "speaker"),
|
||||
choices = choices_str
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
-- Example: Shop inventory as dialogue choices
|
||||
local shop_items = { "Buy Sword (50 gold)", "Buy Shield (30 gold)", "Buy Potion (10 gold)", "Leave" }
|
||||
show_dialogue_with_dynamic_choices(merchant, "What would you like to buy?", shop_items)
|
||||
|
||||
-- =============================================================================
|
||||
-- Summary
|
||||
-- =============================================================================
|
||||
-- Direct component API vs EventBus approach:
|
||||
--
|
||||
-- Component API (ecs.set_component / ecs.get_component):
|
||||
-- - Read/write any DialogueComponent field
|
||||
-- - Configure appearance (font, size, opacity, position)
|
||||
-- - Toggle enabled/disabled
|
||||
-- - Does NOT trigger state transitions (Showing/AwaitingChoice/Idle)
|
||||
--
|
||||
-- EventBus (ecs.send_event "dialogue_show"):
|
||||
-- - Triggers proper state transitions
|
||||
-- - Parses choices from comma-separated string
|
||||
-- - Best for showing dialogue to the player
|
||||
--
|
||||
-- Best practice: Use the EventBus to SHOW dialogue, and the component API
|
||||
-- to CONFIGURE the dialogue box appearance.
|
||||
-- =============================================================================
|
||||
|
||||
print("Dialogue component API examples completed!")
|
||||
275
src/features/editScene/lua-examples/dialogue_event_handler.lua
Normal file
275
src/features/editScene/lua-examples/dialogue_event_handler.lua
Normal file
@@ -0,0 +1,275 @@
|
||||
-- =============================================================================
|
||||
-- Dialogue: EventHandler Component Integration
|
||||
-- =============================================================================
|
||||
-- This example demonstrates how to use the EventHandlerComponent to trigger
|
||||
-- dialogue automatically when an event is received.
|
||||
--
|
||||
-- The EventHandlerComponent links an event name to a GoapAction. When the
|
||||
-- event fires, the action's behavior tree is executed. This allows you to
|
||||
-- wire up dialogue triggers declaratively without writing Lua code.
|
||||
--
|
||||
-- Combined with the EventBus, you can create complex event-driven dialogue
|
||||
-- sequences where one event triggers dialogue, and the player's choice
|
||||
-- triggers another event.
|
||||
-- =============================================================================
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 1. Create the dialogue entity
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local dlg = ecs.create_entity()
|
||||
ecs.set_entity_name(dlg, "DialogueBox")
|
||||
ecs.add_component(dlg, "Dialogue")
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 2. Create an NPC with EventHandler for dialogue triggers
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local npc = ecs.create_entity()
|
||||
ecs.set_entity_name(npc, "QuestGiver")
|
||||
|
||||
ecs.set_component(npc, "Transform", {
|
||||
position = { 5, 0, 5 },
|
||||
rotation = { 1, 0, 0, 0 },
|
||||
scale = { 1, 1, 1 }
|
||||
})
|
||||
|
||||
ecs.set_component(npc, "Renderable", {
|
||||
meshName = "character.mesh",
|
||||
visible = true
|
||||
})
|
||||
|
||||
-- Add an EventHandler that triggers dialogue when the player approaches
|
||||
ecs.set_component(npc, "EventHandler", {
|
||||
eventName = "player_approached",
|
||||
actionName = "npc_greeting",
|
||||
enabled = true
|
||||
})
|
||||
|
||||
print("Created NPC with EventHandler for player_approached event")
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 3. Create an EventHandler that triggers on quest completion
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local quest_npc = ecs.create_entity()
|
||||
ecs.set_entity_name(quest_npc, "QuestRewarder")
|
||||
|
||||
ecs.set_component(quest_npc, "EventHandler", {
|
||||
eventName = "quest_completed",
|
||||
actionName = "quest_reward_dialogue",
|
||||
enabled = true
|
||||
})
|
||||
|
||||
print("Created NPC with EventHandler for quest_completed event")
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 4. Trigger dialogue via events from other game systems
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- Simulate a proximity trigger: when the player gets close to an NPC,
|
||||
-- send an event that triggers the dialogue.
|
||||
|
||||
function on_player_near_npc(npc_name, distance)
|
||||
print("Player is " .. distance .. "m from " .. npc_name)
|
||||
|
||||
if distance < 5.0 then
|
||||
-- Send the event that the EventHandler is listening for
|
||||
ecs.send_event("player_approached", {
|
||||
stringValues = {
|
||||
npc_name = npc_name,
|
||||
location = "town_square"
|
||||
},
|
||||
floatValues = {
|
||||
distance = distance
|
||||
}
|
||||
})
|
||||
|
||||
-- Also show dialogue directly
|
||||
ecs.send_event("dialogue_show", {
|
||||
stringValues = {
|
||||
text = "Hello there! I have a quest for a brave adventurer.",
|
||||
speaker = npc_name,
|
||||
choices = "I'll help!,What's the reward?,Not interested"
|
||||
}
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
-- Simulate the player approaching
|
||||
on_player_near_npc("QuestGiver", 3.0)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 5. Chain events: choice -> event -> next dialogue
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- When the player makes a choice, we can send a new event that triggers
|
||||
-- another EventHandler, creating a chain reaction.
|
||||
|
||||
-- Subscribe to dialogue choices
|
||||
local choice_sub = ecs.subscribe_event("dialogue_choice", function(event, params)
|
||||
local choice_index = params.values and params.values.choice_index or 0
|
||||
local choice_text = params.stringValues and params.stringValues.choice_text or ""
|
||||
|
||||
if choice_text == "I'll help!" then
|
||||
-- Player accepted the quest - trigger quest acceptance event
|
||||
ecs.send_event("quest_accepted", {
|
||||
stringValues = {
|
||||
quest_name = "The Lost Artifact",
|
||||
giver = "QuestGiver"
|
||||
},
|
||||
values = {
|
||||
reward_gold = 100,
|
||||
reward_xp = 500
|
||||
}
|
||||
})
|
||||
|
||||
-- Show follow-up dialogue
|
||||
ecs.send_event("dialogue_show", {
|
||||
stringValues = {
|
||||
text = "Excellent! The ancient artifact was stolen from the temple.\nBring it back and you'll be richly rewarded!",
|
||||
speaker = "QuestGiver",
|
||||
choices = "Where is the temple?,I'm on it!,Tell me more"
|
||||
}
|
||||
})
|
||||
|
||||
elseif choice_text == "What's the reward?" then
|
||||
ecs.send_event("dialogue_show", {
|
||||
stringValues = {
|
||||
text = "100 gold pieces and a magical amulet! What do you say?",
|
||||
speaker = "QuestGiver",
|
||||
choices = "I'll help!,Sounds good,Maybe later"
|
||||
}
|
||||
})
|
||||
|
||||
elseif choice_text == "Not interested" then
|
||||
ecs.send_event("dialogue_show", {
|
||||
stringValues = {
|
||||
text = "Very well. The offer stands if you change your mind.",
|
||||
speaker = "QuestGiver"
|
||||
}
|
||||
})
|
||||
end
|
||||
end)
|
||||
|
||||
print("Subscribed to dialogue_choice for event chaining")
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 6. Subscribe to custom events for game logic
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- Listen for quest acceptance
|
||||
local quest_sub = ecs.subscribe_event("quest_accepted", function(event, params)
|
||||
local quest_name = params.stringValues and params.stringValues.quest_name or "unknown"
|
||||
local reward = params.values and params.values.reward_gold or 0
|
||||
local xp = params.values and params.values.reward_xp or 0
|
||||
|
||||
print("Quest accepted: " .. quest_name)
|
||||
print(" Reward: " .. reward .. " gold, " .. xp .. " XP")
|
||||
|
||||
-- This could trigger other EventHandlers on other entities
|
||||
ecs.send_event("quest_log_updated", {
|
||||
stringValues = { quest_name = quest_name },
|
||||
values = { active_quests = 1 }
|
||||
})
|
||||
end)
|
||||
|
||||
print("Subscribed to quest_accepted events")
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 7. Practical: Zone entry dialogue
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- When the player enters a new area, show contextual dialogue.
|
||||
|
||||
function on_zone_entered(zone_name)
|
||||
local zone_dialogues = {
|
||||
forest = {
|
||||
text = "You enter the Dark Forest. The trees loom overhead,\nblocking out the sunlight.",
|
||||
speaker = "Narrator"
|
||||
},
|
||||
village = {
|
||||
text = "Welcome to Greenhaven Village. Smoke rises from\nchimneys and children play in the streets.",
|
||||
speaker = "Narrator"
|
||||
},
|
||||
dungeon = {
|
||||
text = "The air grows cold and damp as you descend into\nthe ancient dungeon. Somewhere, water drips.",
|
||||
speaker = "Narrator"
|
||||
},
|
||||
beach = {
|
||||
text = "The sea stretches to the horizon. Waves crash\nagainst the shore. A ship is docked nearby.",
|
||||
speaker = "Narrator"
|
||||
}
|
||||
}
|
||||
|
||||
local dialogue = zone_dialogues[zone_name]
|
||||
if dialogue then
|
||||
ecs.send_event("dialogue_show", {
|
||||
stringValues = {
|
||||
text = dialogue.text,
|
||||
speaker = dialogue.speaker
|
||||
}
|
||||
})
|
||||
|
||||
-- Also send a zone-specific event for other systems
|
||||
ecs.send_event("zone_entered", {
|
||||
stringValues = { zone = zone_name }
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
-- Simulate zone transitions
|
||||
on_zone_entered("forest")
|
||||
on_zone_entered("village")
|
||||
on_zone_entered("dungeon")
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 8. Practical: Item pickup dialogue
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
function on_item_picked_up(item_name, item_count)
|
||||
local pickup_messages = {
|
||||
health_potion = "You pick up a Health Potion. It glows with a warm light.",
|
||||
ancient_key = "An ancient key, covered in rust. It must open something important.",
|
||||
gold_coins = "You find " .. item_count .. " gold coins. They clink satisfyingly.",
|
||||
mysterious_map = "A faded map with markings you can't decipher. Someone might know what it means.",
|
||||
sword = "A fine steel sword. It feels balanced in your hand."
|
||||
}
|
||||
|
||||
local message = pickup_messages[item_name]
|
||||
if message then
|
||||
ecs.send_event("dialogue_show", {
|
||||
stringValues = {
|
||||
text = message,
|
||||
speaker = "Narrator"
|
||||
}
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
-- Simulate item pickups
|
||||
on_item_picked_up("health_potion", 1)
|
||||
on_item_picked_up("ancient_key", 1)
|
||||
on_item_picked_up("gold_coins", 50)
|
||||
|
||||
-- =============================================================================
|
||||
-- Summary
|
||||
-- =============================================================================
|
||||
-- EventHandler + Dialogue integration patterns:
|
||||
--
|
||||
-- 1. Proximity triggers:
|
||||
-- Player near NPC -> send event -> EventHandler triggers action -> dialogue
|
||||
--
|
||||
-- 2. Choice chaining:
|
||||
-- Player picks choice -> send event -> EventHandler triggers -> next dialogue
|
||||
--
|
||||
-- 3. Zone entry:
|
||||
-- Player enters area -> send event -> dialogue shows description
|
||||
--
|
||||
-- 4. Item pickup:
|
||||
-- Player picks up item -> send event -> contextual dialogue
|
||||
--
|
||||
-- 5. Quest flow:
|
||||
-- Accept quest -> event -> update quest log -> next dialogue
|
||||
-- Complete quest -> event -> reward dialogue -> next dialogue
|
||||
-- =============================================================================
|
||||
|
||||
print("Dialogue EventHandler integration examples completed!")
|
||||
141
src/features/editScene/lua-examples/dialogue_event_subscribe.lua
Normal file
141
src/features/editScene/lua-examples/dialogue_event_subscribe.lua
Normal file
@@ -0,0 +1,141 @@
|
||||
-- =============================================================================
|
||||
-- Dialogue: Event Subscription & Choice Handling
|
||||
-- =============================================================================
|
||||
-- This example demonstrates how to subscribe to dialogue-related events
|
||||
-- and handle player choices from Lua.
|
||||
--
|
||||
-- The DialogueSystem fires events when the player interacts with the
|
||||
-- dialogue box. You can subscribe to these events to drive game logic.
|
||||
--
|
||||
-- Dialogue-related events you can subscribe to:
|
||||
-- "dialogue_show" - Fired when dialogue should be displayed
|
||||
-- "dialogue_choice" - Fired when player selects a choice
|
||||
-- "dialogue_dismiss" - Fired when dialogue is dismissed (no choices)
|
||||
-- =============================================================================
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 1. Create the dialogue entity
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local dialogue_entity = ecs.create_entity()
|
||||
ecs.set_entity_name(dialogue_entity, "DialogueBox")
|
||||
ecs.add_component(dialogue_entity, "Dialogue")
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 2. Subscribe to dialogue choice events
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- When the player selects a choice in the dialogue box, we can react to it.
|
||||
-- The DialogueComponent's onChoiceSelected callback fires with the 1-based
|
||||
-- choice index. We bridge this via the EventBus.
|
||||
|
||||
local choice_sub = ecs.subscribe_event("dialogue_choice", function(event, params)
|
||||
local choice_index = params.values and params.values.choice_index or 0
|
||||
local choice_text = params.stringValues and params.stringValues.choice_text or "unknown"
|
||||
|
||||
print("Player selected choice #" .. choice_index .. ": " .. choice_text)
|
||||
|
||||
-- React based on which choice was selected
|
||||
if choice_index == 1 then
|
||||
print(" -> Player chose the first option!")
|
||||
elseif choice_index == 2 then
|
||||
print(" -> Player chose the second option!")
|
||||
elseif choice_index == 3 then
|
||||
print(" -> Player chose the third option!")
|
||||
end
|
||||
end)
|
||||
|
||||
print("Subscribed to dialogue_choice events (ID: " .. choice_sub .. ")")
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 3. Subscribe to dialogue dismiss events
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- When dialogue is dismissed (clicked through with no choices), we can
|
||||
-- trigger follow-up actions.
|
||||
|
||||
local dismiss_sub = ecs.subscribe_event("dialogue_dismiss", function(event, params)
|
||||
print("Dialogue was dismissed by the player")
|
||||
|
||||
-- You could trigger follow-up dialogue or game logic here
|
||||
local next_text = params.stringValues and params.stringValues.next_text or ""
|
||||
if next_text ~= "" then
|
||||
print(" -> Next dialogue queued: " .. next_text)
|
||||
end
|
||||
end)
|
||||
|
||||
print("Subscribed to dialogue_dismiss events (ID: " .. dismiss_sub .. ")")
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 4. Subscribe to dialogue show events (for logging/tracking)
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local show_sub = ecs.subscribe_event("dialogue_show", function(event, params)
|
||||
local text = params.stringValues and params.stringValues.text or ""
|
||||
local speaker = params.stringValues and params.stringValues.speaker or "Unknown"
|
||||
local choices = params.stringValues and params.stringValues.choices or ""
|
||||
|
||||
print("[Dialogue Log] " .. speaker .. ": \"" .. text .. "\"")
|
||||
|
||||
if choices ~= "" then
|
||||
print("[Dialogue Log] Choices: " .. choices)
|
||||
end
|
||||
end)
|
||||
|
||||
print("Subscribed to dialogue_show events for logging (ID: " .. show_sub .. ")")
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 5. Example: Branching dialogue with choice handling
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- This shows a complete flow: show dialogue -> handle choice -> react
|
||||
|
||||
function show_branching_dialogue()
|
||||
-- Step 1: Show the dialogue with choices
|
||||
ecs.send_event("dialogue_show", {
|
||||
stringValues = {
|
||||
text = "You see a dark cave entrance. What do you do?",
|
||||
speaker = "Narrator",
|
||||
choices = "Enter the cave,Look around first,Leave"
|
||||
}
|
||||
})
|
||||
|
||||
-- Step 2: The choice will be handled by our subscriber above.
|
||||
-- In a real scenario, you'd use a state machine or coroutine to
|
||||
-- manage the flow. See dialogue_sequence.lua for a more advanced example.
|
||||
end
|
||||
|
||||
show_branching_dialogue()
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 6. Example: NPC greeting with follow-up
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
function npc_greeting(npc_name, greeting_text)
|
||||
-- Show initial greeting
|
||||
ecs.send_event("dialogue_show", {
|
||||
stringValues = {
|
||||
text = greeting_text,
|
||||
speaker = npc_name,
|
||||
choices = "Who are you?,Tell me about this place,Goodbye"
|
||||
}
|
||||
})
|
||||
|
||||
-- The choice subscriber will handle the response.
|
||||
-- You could extend this with a lookup table for NPC responses.
|
||||
end
|
||||
|
||||
npc_greeting("Elder Marcus", "Ah, a new face in our village! Welcome, traveler.")
|
||||
|
||||
-- =============================================================================
|
||||
-- Summary
|
||||
-- =============================================================================
|
||||
-- To handle dialogue choices from Lua:
|
||||
-- 1. Subscribe to "dialogue_choice" events
|
||||
-- 2. Check params.values.choice_index (1-based) to see which was picked
|
||||
-- 3. Check params.stringValues.choice_text for the label text
|
||||
-- 4. React accordingly in your game logic
|
||||
--
|
||||
-- To handle dialogue dismissal:
|
||||
-- 1. Subscribe to "dialogue_dismiss" events
|
||||
-- 2. Trigger follow-up actions as needed
|
||||
-- =============================================================================
|
||||
|
||||
print("Dialogue event subscription examples completed!")
|
||||
338
src/features/editScene/lua-examples/dialogue_sequence.lua
Normal file
338
src/features/editScene/lua-examples/dialogue_sequence.lua
Normal file
@@ -0,0 +1,338 @@
|
||||
-- =============================================================================
|
||||
-- Dialogue: Sequential Dialogue with Coroutines
|
||||
-- =============================================================================
|
||||
-- This example demonstrates how to create sequential, branching dialogue
|
||||
-- using Lua coroutines. This is the most practical approach for story-driven
|
||||
-- dialogue where you need to wait for player input between lines.
|
||||
--
|
||||
-- The pattern:
|
||||
-- 1. Show dialogue with choices
|
||||
-- 2. Wait for player to select a choice (via event subscription)
|
||||
-- 3. React and show next dialogue based on the choice
|
||||
-- 4. Repeat until the conversation ends
|
||||
-- =============================================================================
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 1. Create the dialogue entity
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local dialogue_entity = ecs.create_entity()
|
||||
ecs.set_entity_name(dialogue_entity, "DialogueBox")
|
||||
ecs.add_component(dialogue_entity, "Dialogue")
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 2. Dialogue Queue System
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- A simple queue that lets you chain dialogue lines and wait for player
|
||||
-- input between each one.
|
||||
|
||||
local DialogueQueue = {}
|
||||
local dialogue_queue_active = false
|
||||
local dialogue_queue_pending = false
|
||||
local dialogue_queue_choice = 0
|
||||
local dialogue_queue_choice_text = ""
|
||||
|
||||
-- Subscribe to choice events to unblock the queue
|
||||
local queue_choice_sub = ecs.subscribe_event("dialogue_choice", function(event, params)
|
||||
if dialogue_queue_pending then
|
||||
dialogue_queue_choice = params.values and params.values.choice_index or 0
|
||||
dialogue_queue_choice_text = params.stringValues and params.stringValues.choice_text or ""
|
||||
dialogue_queue_pending = false
|
||||
end
|
||||
end)
|
||||
|
||||
-- Subscribe to dismiss events to unblock the queue
|
||||
local queue_dismiss_sub = ecs.subscribe_event("dialogue_dismiss", function(event, params)
|
||||
if dialogue_queue_pending then
|
||||
dialogue_queue_choice = -1 -- signal dismissed
|
||||
dialogue_queue_pending = false
|
||||
end
|
||||
end)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 3. Helper: Show dialogue and wait for player response
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
--- Show a line of dialogue and wait for the player to respond.
|
||||
--- @param text string The narration text
|
||||
--- @param speaker string|nil Optional speaker name
|
||||
--- @param choices string|nil Comma-separated choices (nil = click to dismiss)
|
||||
--- @return number choice_index (0 if dismissed, 1+ for choices)
|
||||
function show_and_wait(text, speaker, choices)
|
||||
-- Send the dialogue event
|
||||
ecs.send_event("dialogue_show", {
|
||||
stringValues = {
|
||||
text = text,
|
||||
speaker = speaker or "",
|
||||
choices = choices or ""
|
||||
}
|
||||
})
|
||||
|
||||
-- Wait for player response
|
||||
dialogue_queue_pending = true
|
||||
dialogue_queue_choice = 0
|
||||
|
||||
-- Busy-wait loop (in a real coroutine-based system, you'd yield here)
|
||||
-- This is a simplified version; see the coroutine example below.
|
||||
local timeout = 1000
|
||||
while dialogue_queue_pending and timeout > 0 do
|
||||
-- In a real game loop, this would be a coroutine yield.
|
||||
-- For this example, we simulate with a counter.
|
||||
timeout = timeout - 1
|
||||
if timeout <= 0 then
|
||||
dialogue_queue_pending = false
|
||||
print("WARNING: Dialogue wait timed out!")
|
||||
end
|
||||
end
|
||||
|
||||
return dialogue_queue_choice
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 4. Example: Simple linear conversation
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
function simple_conversation()
|
||||
print("=== Simple Conversation ===")
|
||||
|
||||
-- Line 1: Narration with no choices (click to continue)
|
||||
ecs.send_event("dialogue_show", {
|
||||
stringValues = {
|
||||
text = "The old man sits by the fire, staring into the flames.",
|
||||
speaker = "Narrator"
|
||||
}
|
||||
})
|
||||
|
||||
-- In a real game, you'd wait for the dismiss event here.
|
||||
-- For this example, we just show the pattern.
|
||||
|
||||
-- Line 2: NPC speaks with choices
|
||||
ecs.send_event("dialogue_show", {
|
||||
stringValues = {
|
||||
text = "I've been expecting you. The darkness grows stronger each day.",
|
||||
speaker = "Old Man",
|
||||
choices = "Tell me more,How can I help?,I must go"
|
||||
}
|
||||
})
|
||||
|
||||
print(" (Player would now see choices and pick one)")
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 5. Example: Branching conversation tree
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- Define a conversation tree as a table of nodes
|
||||
local conversations = {
|
||||
village_elder = {
|
||||
greeting = {
|
||||
text = "Welcome to our village, stranger. What brings you here?",
|
||||
speaker = "Elder Marcus",
|
||||
choices = {
|
||||
{ text = "I seek adventure", next = "adventure" },
|
||||
{ text = "Just passing through", next = "passing" },
|
||||
{ text = "I need a place to stay", next = "lodging" }
|
||||
}
|
||||
},
|
||||
adventure = {
|
||||
text = "Adventure, you say? The old ruins to the east have been stirring.",
|
||||
speaker = "Elder Marcus",
|
||||
choices = {
|
||||
{ text = "Tell me about the ruins", next = "ruins" },
|
||||
{ text = "I'll check them out", next = "goodbye_adventure" },
|
||||
{ text = "Maybe another time", next = "goodbye" }
|
||||
}
|
||||
},
|
||||
passing = {
|
||||
text = "Safe travels! The road north is clear, but beware the forest at night.",
|
||||
speaker = "Elder Marcus",
|
||||
choices = {
|
||||
{ text = "Thank you for the warning", next = "goodbye" },
|
||||
{ text = "What's in the forest?", next = "forest" }
|
||||
}
|
||||
},
|
||||
lodging = {
|
||||
text = "The inn is just down the road. Tell them Marcus sent you.",
|
||||
speaker = "Elder Marcus",
|
||||
choices = {
|
||||
{ text = "Thank you, elder", next = "goodbye" },
|
||||
{ text = "Is there work in town?", next = "work" }
|
||||
}
|
||||
},
|
||||
ruins = {
|
||||
text = "Ancient ruins, full of traps and treasure. Several have entered, few returned.",
|
||||
speaker = "Elder Marcus",
|
||||
choices = {
|
||||
{ text = "I'm not afraid", next = "goodbye_adventure" },
|
||||
{ text = "Sounds too dangerous", next = "goodbye" }
|
||||
}
|
||||
},
|
||||
forest = {
|
||||
text = "Strange creatures have been seen. Wolves the size of horses!",
|
||||
speaker = "Elder Marcus",
|
||||
choices = {
|
||||
{ text = "I'll be careful", next = "goodbye" },
|
||||
{ text = "I can handle it", next = "goodbye_adventure" }
|
||||
}
|
||||
},
|
||||
work = {
|
||||
text = "The blacksmith needs an apprentice. Ask for Henrik.",
|
||||
speaker = "Elder Marcus",
|
||||
choices = {
|
||||
{ text = "I'll visit the blacksmith", next = "goodbye" },
|
||||
{ text = "Thanks, but I'll move on", next = "goodbye" }
|
||||
}
|
||||
},
|
||||
goodbye_adventure = {
|
||||
text = "Good luck, brave one. You'll need it.",
|
||||
speaker = "Elder Marcus",
|
||||
choices = {
|
||||
{ text = "Farewell!", next = nil }
|
||||
}
|
||||
},
|
||||
goodbye = {
|
||||
text = "May the winds guide you safely.",
|
||||
speaker = "Elder Marcus",
|
||||
choices = {
|
||||
{ text = "Farewell!", next = nil }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
--- Walk through a conversation tree.
|
||||
--- @param tree table The conversation tree definition
|
||||
--- @param start_node string The starting node name
|
||||
function run_conversation(tree, start_node)
|
||||
print("=== Starting Conversation ===")
|
||||
|
||||
local current_node_name = start_node
|
||||
local max_steps = 20
|
||||
local step = 0
|
||||
|
||||
while current_node_name and step < max_steps do
|
||||
step = step + 1
|
||||
local node = tree[current_node_name]
|
||||
if not node then
|
||||
print("ERROR: Unknown conversation node: " .. current_node_name)
|
||||
break
|
||||
end
|
||||
|
||||
-- Build choices string from the node's choices table
|
||||
local choices_str = ""
|
||||
local choice_map = {}
|
||||
if node.choices then
|
||||
local parts = {}
|
||||
for i, choice in ipairs(node.choices) do
|
||||
table.insert(parts, choice.text)
|
||||
choice_map[i] = choice
|
||||
end
|
||||
choices_str = table.concat(parts, ",")
|
||||
end
|
||||
|
||||
-- Show the dialogue
|
||||
ecs.send_event("dialogue_show", {
|
||||
stringValues = {
|
||||
text = node.text,
|
||||
speaker = node.speaker or "",
|
||||
choices = choices_str
|
||||
}
|
||||
})
|
||||
|
||||
-- In a real game, you'd wait for the player's choice here.
|
||||
-- For this example, we simulate by picking the first choice.
|
||||
print(" [Node: " .. current_node_name .. "] " .. node.speaker .. ": \"" .. node.text .. "\"")
|
||||
|
||||
-- Simulate picking a choice (in real game, wait for player input)
|
||||
if node.choices and #node.choices > 0 then
|
||||
local chosen = node.choices[1] -- Simulate picking first choice
|
||||
print(" [Player chose: " .. chosen.text .. "]")
|
||||
current_node_name = chosen.next
|
||||
else
|
||||
current_node_name = nil
|
||||
end
|
||||
end
|
||||
|
||||
print("=== Conversation Ended ===")
|
||||
end
|
||||
|
||||
-- Run the conversation tree
|
||||
run_conversation(conversations.village_elder, "greeting")
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 6. Example: NPC dialogue with state tracking
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- Track NPC dialogue state
|
||||
local npc_state = {
|
||||
marcus_met = false,
|
||||
marcus_friendship = 0,
|
||||
quest_active = false,
|
||||
quest_completed = false
|
||||
}
|
||||
|
||||
function talk_to_elder_marcus()
|
||||
if not npc_state.marcus_met then
|
||||
-- First meeting
|
||||
npc_state.marcus_met = true
|
||||
npc_state.marcus_friendship = 10
|
||||
|
||||
ecs.send_event("dialogue_show", {
|
||||
stringValues = {
|
||||
text = "Ah, a new face! I am Elder Marcus, keeper of this village.\nIt's been so long since we had visitors.",
|
||||
speaker = "Elder Marcus",
|
||||
choices = "Pleasure to meet you,I've heard stories about you,Hello"
|
||||
}
|
||||
})
|
||||
elseif npc_state.quest_active and npc_state.quest_completed then
|
||||
-- Quest completed
|
||||
npc_state.marcus_friendship = npc_state.marcus_friendship + 50
|
||||
|
||||
ecs.send_event("dialogue_show", {
|
||||
stringValues = {
|
||||
text = "You did it! The village is safe thanks to you.\nPlease, take this reward.",
|
||||
speaker = "Elder Marcus",
|
||||
choices = "Thank you, elder,I was happy to help"
|
||||
}
|
||||
})
|
||||
elseif npc_state.quest_active then
|
||||
-- Quest in progress
|
||||
ecs.send_event("dialogue_show", {
|
||||
stringValues = {
|
||||
text = "Have you dealt with those bandits yet?\nThe villagers are growing anxious.",
|
||||
speaker = "Elder Marcus",
|
||||
choices = "I'm working on it,I need more information,Not yet"
|
||||
}
|
||||
})
|
||||
else
|
||||
-- Regular greeting
|
||||
ecs.send_event("dialogue_show", {
|
||||
stringValues = {
|
||||
text = "Welcome back, friend. The village is peaceful today.",
|
||||
speaker = "Elder Marcus",
|
||||
choices = "Any news?,I need supplies,Goodbye"
|
||||
}
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
-- Simulate talking to Marcus a few times
|
||||
print("=== NPC State Tracking ===")
|
||||
talk_to_elder_marcus() -- First meeting
|
||||
npc_state.quest_active = true
|
||||
talk_to_elder_marcus() -- Quest active
|
||||
npc_state.quest_completed = true
|
||||
talk_to_elder_marcus() -- Quest completed
|
||||
|
||||
-- =============================================================================
|
||||
-- Summary
|
||||
-- =============================================================================
|
||||
-- For sequential dialogue:
|
||||
-- 1. Use a queue/coroutine pattern to chain dialogue lines
|
||||
-- 2. Subscribe to "dialogue_choice" and "dialogue_dismiss" events
|
||||
-- 3. Wait for player input between each line
|
||||
-- 4. Use conversation trees for branching narratives
|
||||
-- 5. Track NPC state to change dialogue based on game progress
|
||||
-- =============================================================================
|
||||
|
||||
print("Dialogue sequence examples completed!")
|
||||
177
src/features/editScene/lua-examples/entity_example.lua
Normal file
177
src/features/editScene/lua-examples/entity_example.lua
Normal file
@@ -0,0 +1,177 @@
|
||||
-- =============================================================================
|
||||
-- Entity Lua API Examples
|
||||
-- =============================================================================
|
||||
-- This file demonstrates how to create, manage, and query entities from Lua
|
||||
-- using the ecs.* Lua API.
|
||||
--
|
||||
-- Entities are the fundamental building blocks of the ECS world. Every object
|
||||
-- in the game world is an entity with a unique numeric ID.
|
||||
-- =============================================================================
|
||||
|
||||
-- =============================================================================
|
||||
-- Creating Entities
|
||||
-- =============================================================================
|
||||
|
||||
-- Create a new entity. Returns a numeric ID.
|
||||
local player = ecs.create_entity()
|
||||
print("Created entity with ID: " .. player)
|
||||
|
||||
-- Create multiple entities:
|
||||
local npc1 = ecs.create_entity()
|
||||
local npc2 = ecs.create_entity()
|
||||
local item = ecs.create_entity()
|
||||
|
||||
-- =============================================================================
|
||||
-- Entity Existence and Lifecycle
|
||||
-- =============================================================================
|
||||
|
||||
-- Check if an entity exists:
|
||||
if ecs.entity_exists(player) then
|
||||
print("Player entity exists")
|
||||
end
|
||||
|
||||
-- Check a non-existent entity:
|
||||
if not ecs.entity_exists(999999) then
|
||||
print("Entity 999999 does not exist")
|
||||
end
|
||||
|
||||
-- Destroy an entity:
|
||||
ecs.destroy_entity(npc2)
|
||||
if not ecs.entity_exists(npc2) then
|
||||
print("npc2 was destroyed")
|
||||
end
|
||||
|
||||
-- =============================================================================
|
||||
-- Entity Names
|
||||
-- =============================================================================
|
||||
|
||||
-- Set an entity's name:
|
||||
ecs.set_entity_name(player, "Hero")
|
||||
ecs.set_entity_name(npc1, "Villager_01")
|
||||
ecs.set_entity_name(item, "Health_Potion")
|
||||
|
||||
-- Get an entity's name:
|
||||
local name = ecs.get_entity_name(player)
|
||||
print("Player name: " .. name)
|
||||
|
||||
-- Look up an entity by name:
|
||||
local found = ecs.get_entity_by_name("Villager_01")
|
||||
if found then
|
||||
print("Found entity " .. found .. " with name 'Villager_01'")
|
||||
end
|
||||
|
||||
-- Look up a non-existent name:
|
||||
local not_found = ecs.get_entity_by_name("Nonexistent")
|
||||
if not_found == nil then
|
||||
print("'Nonexistent' not found (returns nil)")
|
||||
end
|
||||
|
||||
-- =============================================================================
|
||||
-- Entity Hierarchy (Parent/Children)
|
||||
-- =============================================================================
|
||||
|
||||
-- Create a parent-child hierarchy:
|
||||
local house = ecs.create_entity()
|
||||
ecs.set_entity_name(house, "House")
|
||||
|
||||
local door = ecs.create_entity()
|
||||
ecs.set_entity_name(door, "Door")
|
||||
|
||||
local window = ecs.create_entity()
|
||||
ecs.set_entity_name(window, "Window")
|
||||
|
||||
-- Check parent of a child (initially none):
|
||||
local p = ecs.parent(door)
|
||||
if p == nil then
|
||||
print("Door has no parent initially")
|
||||
end
|
||||
|
||||
-- Check children of a parent (initially none):
|
||||
local kids = ecs.children(house)
|
||||
print("House has " .. #kids .. " children initially")
|
||||
|
||||
-- =============================================================================
|
||||
-- Practical Example: Creating a Scene
|
||||
-- =============================================================================
|
||||
|
||||
-- Create a complete scene with entities:
|
||||
function create_scene()
|
||||
-- Create terrain
|
||||
local terrain = ecs.create_entity()
|
||||
ecs.set_entity_name(terrain, "Terrain")
|
||||
|
||||
-- Create lighting
|
||||
local sun = ecs.create_entity()
|
||||
ecs.set_entity_name(sun, "Sun")
|
||||
|
||||
-- Create buildings
|
||||
local buildings = {}
|
||||
for i = 1, 3 do
|
||||
local bldg = ecs.create_entity()
|
||||
ecs.set_entity_name(bldg, "Building_" .. i)
|
||||
table.insert(buildings, bldg)
|
||||
end
|
||||
|
||||
-- Create characters
|
||||
local characters = {}
|
||||
for i = 1, 5 do
|
||||
local char = ecs.create_entity()
|
||||
ecs.set_entity_name(char, "Character_" .. i)
|
||||
table.insert(characters, char)
|
||||
end
|
||||
|
||||
print("Scene created with:")
|
||||
print(" 1 terrain entity")
|
||||
print(" 1 sun entity")
|
||||
print(" " .. #buildings .. " buildings")
|
||||
print(" " .. #characters .. " characters")
|
||||
|
||||
return {
|
||||
terrain = terrain,
|
||||
sun = sun,
|
||||
buildings = buildings,
|
||||
characters = characters
|
||||
}
|
||||
end
|
||||
|
||||
local scene = create_scene()
|
||||
|
||||
-- =============================================================================
|
||||
-- Entity ID Properties
|
||||
-- =============================================================================
|
||||
|
||||
-- Entity IDs are positive integers:
|
||||
local e = ecs.create_entity()
|
||||
assert(type(e) == "number", "Entity ID should be a number")
|
||||
assert(e > 0, "Entity ID should be positive")
|
||||
|
||||
-- Each entity gets a unique ID:
|
||||
local ids = {}
|
||||
for i = 1, 10 do
|
||||
ids[i] = ecs.create_entity()
|
||||
end
|
||||
for i = 1, 10 do
|
||||
for j = i + 1, 10 do
|
||||
assert(ids[i] ~= ids[j], "IDs should be unique")
|
||||
end
|
||||
end
|
||||
print("All 10 entities have unique IDs")
|
||||
|
||||
-- Destroyed entity IDs are not reused immediately:
|
||||
local old_id = ecs.create_entity()
|
||||
ecs.destroy_entity(old_id)
|
||||
local new_id = ecs.create_entity()
|
||||
assert(new_id ~= old_id, "New entity should have different ID")
|
||||
print("Destroyed entity ID " .. old_id .. " is not reused")
|
||||
|
||||
-- =============================================================================
|
||||
-- Error Handling
|
||||
-- =============================================================================
|
||||
|
||||
-- These operations should not crash:
|
||||
ecs.destroy_entity(999999) -- destroying non-existent entity is safe
|
||||
ecs.set_entity_name(999999, "ghost") -- setting name on non-existent entity
|
||||
local n = ecs.get_entity_name(999999) -- returns empty string
|
||||
print("Name of non-existent entity: '" .. n .. "'")
|
||||
|
||||
print("Entity API examples completed successfully!")
|
||||
285
src/features/editScene/lua-examples/event_example.lua
Normal file
285
src/features/editScene/lua-examples/event_example.lua
Normal file
@@ -0,0 +1,285 @@
|
||||
-- =============================================================================
|
||||
-- Event Lua API Examples
|
||||
-- =============================================================================
|
||||
-- This file demonstrates how to subscribe to, send, and manage events from
|
||||
-- Lua using the ecs.* Lua API.
|
||||
--
|
||||
-- Events are a publish/subscribe mechanism. Any part of the game can send
|
||||
-- an event, and any subscriber can react to it. This decouples systems
|
||||
-- from each other.
|
||||
-- =============================================================================
|
||||
|
||||
-- =============================================================================
|
||||
-- Subscribing to Events
|
||||
-- =============================================================================
|
||||
|
||||
-- Subscribe to an event. Returns a subscription ID (number) that can be
|
||||
-- used to unsubscribe later.
|
||||
local sub_id = ecs.subscribe_event("hello", function(event, params)
|
||||
print("Received event: " .. event)
|
||||
end)
|
||||
|
||||
print("Subscribed to 'hello' with ID: " .. sub_id)
|
||||
|
||||
-- =============================================================================
|
||||
-- Sending Events
|
||||
-- =============================================================================
|
||||
|
||||
-- Send a simple event with no parameters:
|
||||
ecs.send_event("hello")
|
||||
|
||||
-- Send an event with parameters:
|
||||
ecs.send_event("hello", {
|
||||
values = { count = 42 },
|
||||
stringValues = { message = "Hello World!" }
|
||||
})
|
||||
|
||||
-- =============================================================================
|
||||
-- Event Callback Parameters
|
||||
-- =============================================================================
|
||||
|
||||
-- The callback receives two arguments:
|
||||
-- 1. event - the event name (string)
|
||||
-- 2. params - a table with the event data (or nil if no data was sent)
|
||||
|
||||
ecs.subscribe_event("player_damaged", function(event, params)
|
||||
print("Event: " .. event)
|
||||
if params then
|
||||
if params.values then
|
||||
print(" Damage: " .. (params.values.damage or 0))
|
||||
print(" Health remaining: " .. (params.values.health or 0))
|
||||
end
|
||||
if params.stringValues then
|
||||
print(" Source: " .. (params.stringValues.source or "unknown"))
|
||||
end
|
||||
if params.vec3Values then
|
||||
local pos = params.vec3Values.position
|
||||
if pos then
|
||||
print(" Position: " .. pos[1] .. ", " .. pos[2] .. ", " .. pos[3])
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
-- Send a damage event:
|
||||
ecs.send_event("player_damaged", {
|
||||
values = { damage = 25, health = 75 },
|
||||
stringValues = { source = "goblin_archer" },
|
||||
vec3Values = { position = { 10, 0, 20 } }
|
||||
})
|
||||
|
||||
-- =============================================================================
|
||||
-- Unsubscribing from Events
|
||||
-- =============================================================================
|
||||
|
||||
-- When you no longer need to listen to an event, unsubscribe:
|
||||
local temp_sub = ecs.subscribe_event("temporary", function()
|
||||
print("This should not be printed after unsubscribe")
|
||||
end)
|
||||
|
||||
ecs.send_event("temporary") -- callback fires
|
||||
|
||||
ecs.unsubscribe_event(temp_sub)
|
||||
|
||||
ecs.send_event("temporary") -- callback does NOT fire (unsubscribed)
|
||||
|
||||
-- =============================================================================
|
||||
-- Multiple Subscribers
|
||||
-- =============================================================================
|
||||
|
||||
-- Multiple subscribers can listen to the same event. All of them will be
|
||||
-- called when the event is sent.
|
||||
|
||||
local count_a = 0
|
||||
local count_b = 0
|
||||
|
||||
local sub_a = ecs.subscribe_event("multi", function()
|
||||
count_a = count_a + 1
|
||||
print("Subscriber A called (total: " .. count_a .. ")")
|
||||
end)
|
||||
|
||||
local sub_b = ecs.subscribe_event("multi", function()
|
||||
count_b = count_b + 1
|
||||
print("Subscriber B called (total: " .. count_b .. ")")
|
||||
end)
|
||||
|
||||
ecs.send_event("multi") -- both A and B fire
|
||||
ecs.send_event("multi") -- both fire again
|
||||
|
||||
-- =============================================================================
|
||||
-- Event Parameter Types
|
||||
-- =============================================================================
|
||||
|
||||
-- Events can carry various types of data in their params table:
|
||||
|
||||
ecs.subscribe_event("data_event", function(event, params)
|
||||
print("Received data_event with:")
|
||||
if params then
|
||||
-- Integer values:
|
||||
if params.values then
|
||||
for k, v in pairs(params.values) do
|
||||
print(" int " .. k .. " = " .. v)
|
||||
end
|
||||
end
|
||||
-- Float values:
|
||||
if params.floatValues then
|
||||
for k, v in pairs(params.floatValues) do
|
||||
print(" float " .. k .. " = " .. v)
|
||||
end
|
||||
end
|
||||
-- String values:
|
||||
if params.stringValues then
|
||||
for k, v in pairs(params.stringValues) do
|
||||
print(" string " .. k .. " = '" .. v .. "'")
|
||||
end
|
||||
end
|
||||
-- Vec3 values:
|
||||
if params.vec3Values then
|
||||
for k, v in pairs(params.vec3Values) do
|
||||
print(" vec3 " .. k .. " = (" .. v[1] .. ", " .. v[2] .. ", " .. v[3] .. ")")
|
||||
end
|
||||
end
|
||||
-- Bit flags:
|
||||
if params.bits ~= nil then
|
||||
print(" bits = " .. params.bits)
|
||||
end
|
||||
if params.mask ~= nil then
|
||||
print(" mask = " .. params.mask)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
-- Send an event with all parameter types:
|
||||
ecs.send_event("data_event", {
|
||||
values = { score = 100, level = 5, kills = 42 },
|
||||
floatValues = { speed = 1.5, health = 75.5 },
|
||||
stringValues = { name = "Hero", state = "exploring" },
|
||||
vec3Values = { position = { 10, 20, 30 }, velocity = { 1, 0, 0 } },
|
||||
bits = 5,
|
||||
mask = 7
|
||||
})
|
||||
|
||||
-- =============================================================================
|
||||
-- Practical Example: Game Event System
|
||||
-- =============================================================================
|
||||
|
||||
-- Create a simple game event system:
|
||||
|
||||
local event_handlers = {}
|
||||
|
||||
function register_game_handlers()
|
||||
-- Quest events:
|
||||
event_handlers.quest_complete = ecs.subscribe_event("quest_complete", function(event, params)
|
||||
local quest_name = params and params.stringValues and params.stringValues.quest_name or "unknown"
|
||||
local reward_xp = params and params.values and params.values.reward_xp or 0
|
||||
print("Quest completed: " .. quest_name .. " (+" .. reward_xp .. " XP)")
|
||||
end)
|
||||
|
||||
-- Combat events:
|
||||
event_handlers.enemy_killed = ecs.subscribe_event("enemy_killed", function(event, params)
|
||||
local enemy = params and params.stringValues and params.stringValues.enemy_type or "unknown"
|
||||
local xp = params and params.values and params.values.xp_reward or 0
|
||||
print("Killed " .. enemy .. " (+" .. xp .. " XP)")
|
||||
end)
|
||||
|
||||
-- Item events:
|
||||
event_handlers.item_picked_up = ecs.subscribe_event("item_picked_up", function(event, params)
|
||||
local item = params and params.stringValues and params.stringValues.item_name or "unknown"
|
||||
local count = params and params.values and params.values.count or 1
|
||||
print("Picked up " .. count .. "x " .. item)
|
||||
end)
|
||||
|
||||
-- Dialogue events:
|
||||
event_handlers.dialogue_started = ecs.subscribe_event("dialogue_started", function(event, params)
|
||||
local npc = params and params.stringValues and params.stringValues.npc_name or "unknown"
|
||||
print("Started dialogue with " .. npc)
|
||||
end)
|
||||
|
||||
-- Environment events:
|
||||
event_handlers.time_changed = ecs.subscribe_event("time_changed", function(event, params)
|
||||
local hour = params and params.values and params.values.hour or 0
|
||||
local minute = params and params.values and params.values.minute or 0
|
||||
print("Time changed to " .. hour .. ":" .. string.format("%02d", minute))
|
||||
end)
|
||||
|
||||
-- Player events:
|
||||
event_handlers.player_died = ecs.subscribe_event("player_died", function(event, params)
|
||||
local killer = params and params.stringValues and params.stringValues.killed_by or "unknown"
|
||||
print("Player was killed by " .. killer .. "!")
|
||||
end)
|
||||
|
||||
print("All game event handlers registered")
|
||||
end
|
||||
|
||||
register_game_handlers()
|
||||
|
||||
-- Simulate some game events:
|
||||
ecs.send_event("quest_complete", {
|
||||
stringValues = { quest_name = "The Lost Artifact" },
|
||||
values = { reward_xp = 500 }
|
||||
})
|
||||
|
||||
ecs.send_event("enemy_killed", {
|
||||
stringValues = { enemy_type = "Goblin Warrior" },
|
||||
values = { xp_reward = 50 }
|
||||
})
|
||||
|
||||
ecs.send_event("item_picked_up", {
|
||||
stringValues = { item_name = "Health Potion" },
|
||||
values = { count = 2 }
|
||||
})
|
||||
|
||||
ecs.send_event("dialogue_started", {
|
||||
stringValues = { npc_name = "Elder Marcus" }
|
||||
})
|
||||
|
||||
ecs.send_event("time_changed", {
|
||||
values = { hour = 18, minute = 30 }
|
||||
})
|
||||
|
||||
-- =============================================================================
|
||||
-- Cleanup: Unsubscribe All
|
||||
-- =============================================================================
|
||||
|
||||
-- When cleaning up, unsubscribe all registered handlers:
|
||||
function cleanup_event_handlers()
|
||||
for name, id in pairs(event_handlers) do
|
||||
ecs.unsubscribe_event(id)
|
||||
print("Unsubscribed from: " .. name)
|
||||
end
|
||||
end
|
||||
|
||||
-- Uncomment to clean up:
|
||||
-- cleanup_event_handlers()
|
||||
|
||||
-- =============================================================================
|
||||
-- Event API Reference
|
||||
-- =============================================================================
|
||||
--
|
||||
-- ecs.subscribe_event(event_name, callback)
|
||||
-- Subscribe to an event. Returns a subscription ID (number).
|
||||
-- Parameters:
|
||||
-- event_name - string, the name of the event to listen for
|
||||
-- callback - function(event, params), called when the event fires
|
||||
-- Returns: subscription ID (number)
|
||||
--
|
||||
-- ecs.unsubscribe_event(subscription_id)
|
||||
-- Unsubscribe from an event.
|
||||
-- Parameters:
|
||||
-- subscription_id - number, the ID returned by subscribe_event
|
||||
-- Safe to call with an invalid ID (no crash).
|
||||
--
|
||||
-- ecs.send_event(event_name, params)
|
||||
-- Send an event to all subscribers.
|
||||
-- Parameters:
|
||||
-- event_name - string, the name of the event to send
|
||||
-- params - optional table with event data:
|
||||
-- .values - table of integer key-value pairs
|
||||
-- .floatValues - table of float key-value pairs
|
||||
-- .stringValues - table of string key-value pairs
|
||||
-- .vec3Values - table of vec3 key-value pairs (each vec3 is {x, y, z})
|
||||
-- .bits - integer bit flags
|
||||
-- .mask - integer bit mask
|
||||
-- =============================================================================
|
||||
|
||||
print("Event API examples completed successfully!")
|
||||
576
src/features/editScene/lua/LuaActionApi.cpp
Normal file
576
src/features/editScene/lua/LuaActionApi.cpp
Normal file
@@ -0,0 +1,576 @@
|
||||
#include "LuaActionApi.hpp"
|
||||
#include "../components/ActionDatabase.hpp"
|
||||
#include "../components/GoapBlackboard.hpp"
|
||||
#include "../components/BehaviorTree.hpp"
|
||||
#include <OgreLogManager.h>
|
||||
#include <cstring>
|
||||
|
||||
namespace editScene
|
||||
{
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: push a BehaviorTreeNode as a Lua table
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static void pushBehaviorTree(lua_State *L, const BehaviorTreeNode &node)
|
||||
{
|
||||
lua_newtable(L);
|
||||
|
||||
lua_pushstring(L, node.type.c_str());
|
||||
lua_setfield(L, -2, "type");
|
||||
|
||||
if (!node.name.empty()) {
|
||||
lua_pushstring(L, node.name.c_str());
|
||||
lua_setfield(L, -2, "name");
|
||||
}
|
||||
|
||||
if (!node.params.empty()) {
|
||||
lua_pushstring(L, node.params.c_str());
|
||||
lua_setfield(L, -2, "params");
|
||||
}
|
||||
|
||||
if (!node.children.empty()) {
|
||||
lua_newtable(L);
|
||||
for (size_t i = 0; i < node.children.size(); i++) {
|
||||
pushBehaviorTree(L, node.children[i]);
|
||||
lua_rawseti(L, -2, (int)i + 1);
|
||||
}
|
||||
lua_setfield(L, -2, "children");
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: read a BehaviorTreeNode from a Lua table at given index
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static BehaviorTreeNode readBehaviorTree(lua_State *L, int idx)
|
||||
{
|
||||
BehaviorTreeNode node;
|
||||
|
||||
if (!lua_istable(L, idx))
|
||||
return node;
|
||||
|
||||
// type (required)
|
||||
lua_getfield(L, idx, "type");
|
||||
if (lua_isstring(L, -1))
|
||||
node.type = lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
|
||||
// name (optional)
|
||||
lua_getfield(L, idx, "name");
|
||||
if (lua_isstring(L, -1))
|
||||
node.name = lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
|
||||
// params (optional)
|
||||
lua_getfield(L, idx, "params");
|
||||
if (lua_isstring(L, -1))
|
||||
node.params = lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
|
||||
// children (optional array)
|
||||
lua_getfield(L, idx, "children");
|
||||
if (lua_istable(L, -1)) {
|
||||
lua_pushnil(L);
|
||||
while (lua_next(L, -2) != 0) {
|
||||
// key is numeric index, value is child table
|
||||
if (lua_istable(L, -1)) {
|
||||
node.children.push_back(
|
||||
readBehaviorTree(L, lua_gettop(L)));
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: push a GoapBlackboard as a Lua table
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static void pushBlackboard(lua_State *L, const GoapBlackboard &bb)
|
||||
{
|
||||
lua_newtable(L); // blackboard table
|
||||
|
||||
// Bits
|
||||
lua_newtable(L); // bits table
|
||||
for (int i = 0; i < 64; i++) {
|
||||
if (bb.hasBit(i)) {
|
||||
const char *name = GoapBlackboard::getBitName(i);
|
||||
const char *key = name ? name : "";
|
||||
lua_pushboolean(L, bb.getBit(i));
|
||||
lua_setfield(L, -2, key);
|
||||
}
|
||||
}
|
||||
lua_setfield(L, -2, "bits");
|
||||
|
||||
// Integer values
|
||||
lua_newtable(L);
|
||||
for (const auto &kv : bb.values) {
|
||||
lua_pushinteger(L, kv.second);
|
||||
lua_setfield(L, -2, kv.first.c_str());
|
||||
}
|
||||
lua_setfield(L, -2, "values");
|
||||
|
||||
// Float values
|
||||
lua_newtable(L);
|
||||
for (const auto &kv : bb.floatValues) {
|
||||
lua_pushnumber(L, kv.second);
|
||||
lua_setfield(L, -2, kv.first.c_str());
|
||||
}
|
||||
lua_setfield(L, -2, "floatValues");
|
||||
|
||||
// String values
|
||||
lua_newtable(L);
|
||||
for (const auto &kv : bb.stringValues) {
|
||||
lua_pushstring(L, kv.second.c_str());
|
||||
lua_setfield(L, -2, kv.first.c_str());
|
||||
}
|
||||
lua_setfield(L, -2, "stringValues");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: read a GoapBlackboard from a Lua table (optional, at given index)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static GoapBlackboard readBlackboard(lua_State *L, int idx)
|
||||
{
|
||||
GoapBlackboard bb;
|
||||
|
||||
if (!lua_istable(L, idx))
|
||||
return bb;
|
||||
|
||||
// Read bits
|
||||
lua_getfield(L, idx, "bits");
|
||||
if (lua_istable(L, -1)) {
|
||||
lua_pushnil(L);
|
||||
while (lua_next(L, -2) != 0) {
|
||||
// key is the bit name (string), value is boolean
|
||||
if (lua_isstring(L, -2) && lua_isboolean(L, -1)) {
|
||||
const char *name = lua_tostring(L, -2);
|
||||
bool val = lua_toboolean(L, -1) != 0;
|
||||
// Find bit index by name (auto-registers if new)
|
||||
int idx2 = GoapBlackboard::findBitByName(name);
|
||||
if (idx2 < 0) {
|
||||
// Find first free slot
|
||||
for (int i = 0; i < 64; i++) {
|
||||
if (GoapBlackboard::getBitName(
|
||||
i) == nullptr) {
|
||||
GoapBlackboard::setBitName(
|
||||
i, name);
|
||||
idx2 = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (idx2 >= 0)
|
||||
bb.setBit(idx2, val);
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
|
||||
// Read integer values
|
||||
lua_getfield(L, idx, "values");
|
||||
if (lua_istable(L, -1)) {
|
||||
lua_pushnil(L);
|
||||
while (lua_next(L, -2) != 0) {
|
||||
if (lua_isstring(L, -2) && lua_isinteger(L, -1))
|
||||
bb.values[lua_tostring(L, -2)] =
|
||||
(int)lua_tointeger(L, -1);
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
|
||||
// Read float values
|
||||
lua_getfield(L, idx, "floatValues");
|
||||
if (lua_istable(L, -1)) {
|
||||
lua_pushnil(L);
|
||||
while (lua_next(L, -2) != 0) {
|
||||
if (lua_isstring(L, -2) && lua_isnumber(L, -1))
|
||||
bb.floatValues[lua_tostring(L, -2)] =
|
||||
(float)lua_tonumber(L, -1);
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
|
||||
// Read string values
|
||||
lua_getfield(L, idx, "stringValues");
|
||||
if (lua_istable(L, -1)) {
|
||||
lua_pushnil(L);
|
||||
while (lua_next(L, -2) != 0) {
|
||||
if (lua_isstring(L, -2) && lua_isstring(L, -1))
|
||||
bb.stringValues[lua_tostring(L, -2)] =
|
||||
lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
|
||||
return bb;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.action_db.add_action(name, cost, preconds, effects, behaviorTree)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaAddAction(lua_State *L)
|
||||
{
|
||||
const char *name = luaL_checkstring(L, 1);
|
||||
int cost = (int)luaL_optinteger(L, 2, 1);
|
||||
|
||||
GoapAction action(name, cost);
|
||||
|
||||
// Optional preconditions table (arg 3)
|
||||
if (lua_gettop(L) >= 3 && lua_istable(L, 3))
|
||||
action.preconditions = readBlackboard(L, 3);
|
||||
|
||||
// Optional effects table (arg 4)
|
||||
if (lua_gettop(L) >= 4 && lua_istable(L, 4))
|
||||
action.effects = readBlackboard(L, 4);
|
||||
|
||||
// Optional behavior tree table (arg 5)
|
||||
if (lua_gettop(L) >= 5 && lua_istable(L, 5))
|
||||
action.behaviorTree = readBehaviorTree(L, 5);
|
||||
|
||||
ActionDatabase::getSingleton().addOrReplaceAction(action);
|
||||
|
||||
Ogre::LogManager::getSingleton().stream()
|
||||
<< "[Lua] Added action: " << name;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.action_db.add_goal(name, priority, target, condition)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaAddGoal(lua_State *L)
|
||||
{
|
||||
const char *name = luaL_checkstring(L, 1);
|
||||
int priority = (int)luaL_optinteger(L, 2, 1);
|
||||
|
||||
GoapGoal goal(name, priority);
|
||||
|
||||
// Optional target blackboard (arg 3)
|
||||
if (lua_gettop(L) >= 3 && lua_istable(L, 3))
|
||||
goal.target = readBlackboard(L, 3);
|
||||
|
||||
// Optional condition string (arg 4)
|
||||
if (lua_gettop(L) >= 4 && lua_isstring(L, 4))
|
||||
goal.condition = lua_tostring(L, 4);
|
||||
|
||||
ActionDatabase::getSingleton().addOrReplaceGoal(goal);
|
||||
|
||||
Ogre::LogManager::getSingleton().stream()
|
||||
<< "[Lua] Added goal: " << name;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.action_db.remove_action(name) -> bool
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaRemoveAction(lua_State *L)
|
||||
{
|
||||
const char *name = luaL_checkstring(L, 1);
|
||||
bool removed = ActionDatabase::getSingleton().removeAction(name);
|
||||
lua_pushboolean(L, removed);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.action_db.remove_goal(name) -> bool
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaRemoveGoal(lua_State *L)
|
||||
{
|
||||
const char *name = luaL_checkstring(L, 1);
|
||||
bool removed = ActionDatabase::getSingleton().removeGoal(name);
|
||||
lua_pushboolean(L, removed);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.action_db.find_action(name) -> table or nil
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaFindAction(lua_State *L)
|
||||
{
|
||||
const char *name = luaL_checkstring(L, 1);
|
||||
const GoapAction *action =
|
||||
ActionDatabase::getSingleton().findAction(name);
|
||||
|
||||
if (!action) {
|
||||
lua_pushnil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
lua_newtable(L);
|
||||
lua_pushstring(L, action->name.c_str());
|
||||
lua_setfield(L, -2, "name");
|
||||
lua_pushinteger(L, action->cost);
|
||||
lua_setfield(L, -2, "cost");
|
||||
|
||||
pushBlackboard(L, action->preconditions);
|
||||
lua_setfield(L, -2, "preconditions");
|
||||
|
||||
pushBlackboard(L, action->effects);
|
||||
lua_setfield(L, -2, "effects");
|
||||
|
||||
// Behavior tree
|
||||
pushBehaviorTree(L, action->behaviorTree);
|
||||
lua_setfield(L, -2, "behaviorTree");
|
||||
|
||||
// Behavior tree name (optional reference)
|
||||
if (!action->behaviorTreeName.empty()) {
|
||||
lua_pushstring(L, action->behaviorTreeName.c_str());
|
||||
lua_setfield(L, -2, "behaviorTreeName");
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.action_db.find_goal(name) -> table or nil
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaFindGoal(lua_State *L)
|
||||
{
|
||||
const char *name = luaL_checkstring(L, 1);
|
||||
const GoapGoal *goal = ActionDatabase::getSingleton().findGoal(name);
|
||||
|
||||
if (!goal) {
|
||||
lua_pushnil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
lua_newtable(L);
|
||||
lua_pushstring(L, goal->name.c_str());
|
||||
lua_setfield(L, -2, "name");
|
||||
lua_pushinteger(L, goal->priority);
|
||||
lua_setfield(L, -2, "priority");
|
||||
|
||||
pushBlackboard(L, goal->target);
|
||||
lua_setfield(L, -2, "target");
|
||||
|
||||
lua_pushstring(L, goal->condition.c_str());
|
||||
lua_setfield(L, -2, "condition");
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.action_db.list_actions() -> table of action names
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaListActions(lua_State *L)
|
||||
{
|
||||
const auto &db = ActionDatabase::getSingleton();
|
||||
lua_newtable(L);
|
||||
int idx = 1;
|
||||
for (const auto &action : db.actions) {
|
||||
lua_pushstring(L, action.name.c_str());
|
||||
lua_rawseti(L, -2, idx++);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.action_db.list_goals() -> table of goal names
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaListGoals(lua_State *L)
|
||||
{
|
||||
const auto &db = ActionDatabase::getSingleton();
|
||||
lua_newtable(L);
|
||||
int idx = 1;
|
||||
for (const auto &goal : db.goals) {
|
||||
lua_pushstring(L, goal.name.c_str());
|
||||
lua_rawseti(L, -2, idx++);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.action_db.clear() -> nil
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaClear(lua_State *L)
|
||||
{
|
||||
(void)L;
|
||||
ActionDatabase::getSingleton().clear();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.action_db.set_bit_name(index, name) -> nil
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaSetBitName(lua_State *L)
|
||||
{
|
||||
int index = (int)luaL_checkinteger(L, 1);
|
||||
const char *name = luaL_checkstring(L, 2);
|
||||
|
||||
if (index < 0 || index >= 64)
|
||||
luaL_error(L, "bit index must be 0-63, got %d", index);
|
||||
|
||||
GoapBlackboard::setBitName(index, name);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.action_db.find_bit_by_name(name) -> index or nil
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaFindBitByName(lua_State *L)
|
||||
{
|
||||
const char *name = luaL_checkstring(L, 1);
|
||||
int index = GoapBlackboard::findBitByName(name);
|
||||
if (index < 0) {
|
||||
lua_pushnil(L);
|
||||
} else {
|
||||
lua_pushinteger(L, index);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.action_db.get_bit_name(index) -> name or nil
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaGetBitName(lua_State *L)
|
||||
{
|
||||
int index = (int)luaL_checkinteger(L, 1);
|
||||
if (index < 0 || index >= 64) {
|
||||
lua_pushnil(L);
|
||||
return 1;
|
||||
}
|
||||
const char *name = GoapBlackboard::getBitName(index);
|
||||
if (name) {
|
||||
lua_pushstring(L, name);
|
||||
} else {
|
||||
lua_pushnil(L);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.action_db.list_bit_names() -> table of { index, name }
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaListBitNames(lua_State *L)
|
||||
{
|
||||
lua_newtable(L);
|
||||
int idx = 1;
|
||||
for (int i = 0; i < 64; i++) {
|
||||
const char *name = GoapBlackboard::getBitName(i);
|
||||
if (name) {
|
||||
lua_newtable(L);
|
||||
lua_pushinteger(L, i);
|
||||
lua_setfield(L, -2, "index");
|
||||
lua_pushstring(L, name);
|
||||
lua_setfield(L, -2, "name");
|
||||
lua_rawseti(L, -2, idx++);
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.action_db.auto_assign_bit(name) -> index
|
||||
// ---------------------------------------------------------------------------
|
||||
// Finds a bit by name, or auto-assigns the first free slot if not found.
|
||||
// Returns the bit index, or -1 if all 64 slots are full.
|
||||
|
||||
static int luaAutoAssignBit(lua_State *L)
|
||||
{
|
||||
const char *name = luaL_checkstring(L, 1);
|
||||
int index = GoapBlackboard::findBitByName(name);
|
||||
if (index >= 0) {
|
||||
lua_pushinteger(L, index);
|
||||
return 1;
|
||||
}
|
||||
// Find first free slot
|
||||
for (int i = 0; i < 64; i++) {
|
||||
if (GoapBlackboard::getBitName(i) == nullptr) {
|
||||
GoapBlackboard::setBitName(i, name);
|
||||
lua_pushinteger(L, i);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
lua_pushinteger(L, -1);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Registration
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void registerLuaActionApi(lua_State *L)
|
||||
{
|
||||
// Get or create the "ecs" global table
|
||||
lua_getglobal(L, "ecs");
|
||||
if (lua_isnil(L, -1)) {
|
||||
lua_pop(L, 1);
|
||||
lua_newtable(L);
|
||||
}
|
||||
|
||||
// Create the action_db sub-table
|
||||
lua_newtable(L);
|
||||
|
||||
lua_pushcfunction(L, luaAddAction);
|
||||
lua_setfield(L, -2, "add_action");
|
||||
|
||||
lua_pushcfunction(L, luaAddGoal);
|
||||
lua_setfield(L, -2, "add_goal");
|
||||
|
||||
lua_pushcfunction(L, luaRemoveAction);
|
||||
lua_setfield(L, -2, "remove_action");
|
||||
|
||||
lua_pushcfunction(L, luaRemoveGoal);
|
||||
lua_setfield(L, -2, "remove_goal");
|
||||
|
||||
lua_pushcfunction(L, luaFindAction);
|
||||
lua_setfield(L, -2, "find_action");
|
||||
|
||||
lua_pushcfunction(L, luaFindGoal);
|
||||
lua_setfield(L, -2, "find_goal");
|
||||
|
||||
lua_pushcfunction(L, luaListActions);
|
||||
lua_setfield(L, -2, "list_actions");
|
||||
|
||||
lua_pushcfunction(L, luaListGoals);
|
||||
lua_setfield(L, -2, "list_goals");
|
||||
|
||||
lua_pushcfunction(L, luaClear);
|
||||
lua_setfield(L, -2, "clear");
|
||||
|
||||
// Bit name management
|
||||
lua_pushcfunction(L, luaSetBitName);
|
||||
lua_setfield(L, -2, "set_bit_name");
|
||||
|
||||
lua_pushcfunction(L, luaFindBitByName);
|
||||
lua_setfield(L, -2, "find_bit_by_name");
|
||||
|
||||
lua_pushcfunction(L, luaGetBitName);
|
||||
lua_setfield(L, -2, "get_bit_name");
|
||||
|
||||
lua_pushcfunction(L, luaListBitNames);
|
||||
lua_setfield(L, -2, "list_bit_names");
|
||||
|
||||
lua_pushcfunction(L, luaAutoAssignBit);
|
||||
lua_setfield(L, -2, "auto_assign_bit");
|
||||
|
||||
// Set action_db as a field of ecs
|
||||
lua_setfield(L, -2, "action_db");
|
||||
|
||||
// Ensure ecs is global
|
||||
lua_setglobal(L, "ecs");
|
||||
}
|
||||
|
||||
} // namespace editScene
|
||||
101
src/features/editScene/lua/LuaActionApi.hpp
Normal file
101
src/features/editScene/lua/LuaActionApi.hpp
Normal file
@@ -0,0 +1,101 @@
|
||||
#ifndef EDITSCENE_LUA_ACTION_API_HPP
|
||||
#define EDITSCENE_LUA_ACTION_API_HPP
|
||||
#pragma once
|
||||
|
||||
#include <lua.hpp>
|
||||
|
||||
/**
|
||||
* @file LuaActionApi.hpp
|
||||
* @brief Lua API for the ActionDatabase singleton.
|
||||
*
|
||||
* Provides Lua functions to create, query, and manage GOAP actions
|
||||
* and goals in the global ActionDatabase singleton.
|
||||
*
|
||||
* Exposed Lua globals (under the "ecs" table):
|
||||
* ecs.action_db.add_action(name, cost, preconds, effects, behaviorTree) -> nil
|
||||
* ecs.action_db.add_goal(name, priority, target, condition) -> nil
|
||||
* ecs.action_db.remove_action(name) -> bool
|
||||
* ecs.action_db.remove_goal(name) -> bool
|
||||
* ecs.action_db.find_action(name) -> table or nil
|
||||
* ecs.action_db.find_goal(name) -> table or nil
|
||||
* ecs.action_db.list_actions() -> table of action names
|
||||
* ecs.action_db.list_goals() -> table of goal names
|
||||
* ecs.action_db.clear() -> nil
|
||||
*
|
||||
* Bit Name Management:
|
||||
* ecs.action_db.set_bit_name(index, name) -> nil
|
||||
* Assign a human-readable name to a bit slot (0-63).
|
||||
* Example: ecs.action_db.set_bit_name(0, "has_axe")
|
||||
*
|
||||
* ecs.action_db.find_bit_by_name(name) -> index or nil
|
||||
* Look up which bit index a name is assigned to.
|
||||
* Returns nil if the name hasn't been assigned yet.
|
||||
*
|
||||
* ecs.action_db.get_bit_name(index) -> name or nil
|
||||
* Get the name assigned to a bit slot, or nil if unassigned.
|
||||
*
|
||||
* ecs.action_db.list_bit_names() -> table of { index, name }
|
||||
* Returns an array of all assigned bit names with their indices.
|
||||
*
|
||||
* ecs.action_db.auto_assign_bit(name) -> index
|
||||
* Find a bit by name, or auto-assign the first free slot.
|
||||
* Returns the bit index, or -1 if all 64 slots are full.
|
||||
* This is the same logic used internally by readBlackboard()
|
||||
* when it encounters an unknown bit name in preconditions/effects.
|
||||
*
|
||||
* Bit Name Convention:
|
||||
* Bit names are global across the entire game session. They map
|
||||
* human-readable names (like "has_axe", "is_hungry") to bit indices
|
||||
* (0-63) used in GoapBlackboard preconditions and effects.
|
||||
*
|
||||
* You can define bits explicitly at startup:
|
||||
* ecs.action_db.set_bit_name(0, "has_axe")
|
||||
* ecs.action_db.set_bit_name(1, "has_wood")
|
||||
* ecs.action_db.set_bit_name(2, "is_hungry")
|
||||
*
|
||||
* Or let them be auto-assigned when you use them in actions:
|
||||
* ecs.action_db.add_action("chop_wood", 2,
|
||||
* { bits = { has_axe = true } }, -- "has_axe" auto-assigned if new
|
||||
* { bits = { has_wood = true } })
|
||||
*
|
||||
* Use auto_assign_bit() to explicitly reserve a name:
|
||||
* local idx = ecs.action_db.auto_assign_bit("my_flag")
|
||||
* -- idx is now the bit index for "my_flag"
|
||||
*
|
||||
* Use list_bit_names() to see all currently assigned names:
|
||||
* local bits = ecs.action_db.list_bit_names()
|
||||
* for _, b in ipairs(bits) do
|
||||
* print(b.index .. ": " .. b.name)
|
||||
* end
|
||||
*
|
||||
* Behavior Tree Table Format (arg 5 of add_action):
|
||||
* {
|
||||
* type = "sequence", -- node type: sequence, selector, invert, task, check, etc.
|
||||
* name = "optional_name", -- action/condition name, animation state, etc.
|
||||
* params = "optional_params", -- extra parameters (e.g. delay seconds, bit index)
|
||||
* children = { -- array of child nodes (for sequence/selector/invert)
|
||||
* { type = "task", name = "myAction" },
|
||||
* { type = "setAnimationState", name = "SM/Walk" },
|
||||
* { type = "delay", params = "2.0" }
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* See BehaviorTree.hpp for full list of node types.
|
||||
*/
|
||||
|
||||
namespace editScene
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Register all ActionDatabase-related Lua API functions.
|
||||
*
|
||||
* Adds action_db sub-table to the "ecs" global table.
|
||||
* Must be called after LuaState is constructed.
|
||||
*
|
||||
* @param L The Lua state.
|
||||
*/
|
||||
void registerLuaActionApi(lua_State *L);
|
||||
|
||||
} // namespace editScene
|
||||
|
||||
#endif // EDITSCENE_LUA_ACTION_API_HPP
|
||||
471
src/features/editScene/lua/LuaBehaviorTreeApi.cpp
Normal file
471
src/features/editScene/lua/LuaBehaviorTreeApi.cpp
Normal file
@@ -0,0 +1,471 @@
|
||||
#include "LuaBehaviorTreeApi.hpp"
|
||||
#include "LuaEntityApi.hpp"
|
||||
#include "../components/GoapBlackboard.hpp"
|
||||
#include <OgreLogManager.h>
|
||||
#include <unordered_map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace editScene
|
||||
{
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Global registry of Lua node handlers
|
||||
// ---------------------------------------------------------------------------
|
||||
// Maps node name -> Lua registry reference for the callback function.
|
||||
// The callback is stored as a Lua reference in the registry so it persists
|
||||
// across Lua state resets and can be called from C++.
|
||||
|
||||
static std::unordered_map<std::string, int> g_luaNodeHandlers;
|
||||
|
||||
// Global Lua state pointer, set during registerLuaBehaviorTreeApi().
|
||||
// Used by callLuaBehaviorTreeNode() which is invoked from the C++
|
||||
// behavior tree system without direct access to the Lua state.
|
||||
static lua_State *g_luaState = nullptr;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: push a GoapBlackboard as a Lua table (reused from LuaActionApi)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static void pushBlackboard(lua_State *L, const GoapBlackboard &bb)
|
||||
{
|
||||
lua_newtable(L); // blackboard table
|
||||
|
||||
// Bits
|
||||
lua_newtable(L); // bits table
|
||||
for (int i = 0; i < 64; i++) {
|
||||
if (bb.hasBit(i)) {
|
||||
const char *name = GoapBlackboard::getBitName(i);
|
||||
const char *key = name ? name : "";
|
||||
lua_pushboolean(L, bb.getBit(i));
|
||||
lua_setfield(L, -2, key);
|
||||
}
|
||||
}
|
||||
lua_setfield(L, -2, "bits");
|
||||
|
||||
// Integer values
|
||||
lua_newtable(L);
|
||||
for (const auto &kv : bb.values) {
|
||||
lua_pushinteger(L, kv.second);
|
||||
lua_setfield(L, -2, kv.first.c_str());
|
||||
}
|
||||
lua_setfield(L, -2, "values");
|
||||
|
||||
// Float values
|
||||
lua_newtable(L);
|
||||
for (const auto &kv : bb.floatValues) {
|
||||
lua_pushnumber(L, kv.second);
|
||||
lua_setfield(L, -2, kv.first.c_str());
|
||||
}
|
||||
lua_setfield(L, -2, "floatValues");
|
||||
|
||||
// String values
|
||||
lua_newtable(L);
|
||||
for (const auto &kv : bb.stringValues) {
|
||||
lua_pushstring(L, kv.second.c_str());
|
||||
lua_setfield(L, -2, kv.first.c_str());
|
||||
}
|
||||
lua_setfield(L, -2, "stringValues");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: parse "key=val,key2=val2" params into a Lua table
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static void pushParamsTable(lua_State *L, const std::string ¶ms)
|
||||
{
|
||||
lua_newtable(L); // params table
|
||||
|
||||
if (params.empty())
|
||||
return;
|
||||
|
||||
const char *s = params.c_str();
|
||||
while (*s) {
|
||||
// Skip whitespace
|
||||
while (*s == ' ' || *s == '\t')
|
||||
s++;
|
||||
if (!*s)
|
||||
break;
|
||||
|
||||
// Find key
|
||||
const char *keyStart = s;
|
||||
while (*s && *s != '=' && *s != ',')
|
||||
s++;
|
||||
std::string key(keyStart, static_cast<size_t>(s - keyStart));
|
||||
// Trim trailing spaces from key
|
||||
while (!key.empty() &&
|
||||
(key.back() == ' ' || key.back() == '\t'))
|
||||
key.pop_back();
|
||||
|
||||
if (*s != '=') {
|
||||
while (*s && *s != ',')
|
||||
s++;
|
||||
if (*s == ',')
|
||||
s++;
|
||||
continue;
|
||||
}
|
||||
s++; // skip '='
|
||||
|
||||
// Skip whitespace before value
|
||||
while (*s == ' ' || *s == '\t')
|
||||
s++;
|
||||
|
||||
// Find value end (next comma or end)
|
||||
const char *valStart = s;
|
||||
bool inQuotes = false;
|
||||
while (*s && (*s != ',' || inQuotes)) {
|
||||
if (*s == '"')
|
||||
inQuotes = !inQuotes;
|
||||
s++;
|
||||
}
|
||||
std::string val(valStart, static_cast<size_t>(s - valStart));
|
||||
// Trim trailing spaces from value
|
||||
while (!val.empty() &&
|
||||
(val.back() == ' ' || val.back() == '\t'))
|
||||
val.pop_back();
|
||||
|
||||
// Strip quotes if present
|
||||
if (val.size() >= 2 && val.front() == '"' &&
|
||||
val.back() == '"') {
|
||||
val = val.substr(1, val.size() - 2);
|
||||
lua_pushstring(L, val.c_str());
|
||||
lua_setfield(L, -2, key.c_str());
|
||||
} else {
|
||||
// Try numeric
|
||||
char *end = nullptr;
|
||||
long iVal = strtol(val.c_str(), &end, 10);
|
||||
if (end != val.c_str() && *end == '\0') {
|
||||
lua_pushinteger(L, (int)iVal);
|
||||
lua_setfield(L, -2, key.c_str());
|
||||
} else {
|
||||
// Try float
|
||||
end = nullptr;
|
||||
float fVal = strtof(val.c_str(), &end);
|
||||
if (end != val.c_str() && *end == '\0') {
|
||||
lua_pushnumber(L, fVal);
|
||||
lua_setfield(L, -2, key.c_str());
|
||||
} else {
|
||||
// Fallback to string
|
||||
lua_pushstring(L, val.c_str());
|
||||
lua_setfield(L, -2, key.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (*s == ',')
|
||||
s++;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public API: Call a registered Lua node handler
|
||||
// ---------------------------------------------------------------------------
|
||||
// Returns: 0 = success, 1 = failure, 2 = running, -1 = not found/error
|
||||
|
||||
int callLuaBehaviorTreeNode(void *L_, const std::string &nodeName,
|
||||
flecs::entity entity, const std::string ¶ms)
|
||||
{
|
||||
// Use the global Lua state if none was passed
|
||||
lua_State *L = static_cast<lua_State *>(L_);
|
||||
if (!L)
|
||||
L = g_luaState;
|
||||
if (!L)
|
||||
return -1;
|
||||
|
||||
auto it = g_luaNodeHandlers.find(nodeName);
|
||||
if (it == g_luaNodeHandlers.end())
|
||||
return -1;
|
||||
|
||||
int ref = it->second;
|
||||
|
||||
// Push the callback function
|
||||
lua_rawgeti(L, LUA_REGISTRYINDEX, ref);
|
||||
|
||||
// Push entity ID as first argument
|
||||
lua_pushinteger(L, luaEntityToId(entity));
|
||||
|
||||
// Push params table as second argument
|
||||
pushParamsTable(L, params);
|
||||
|
||||
// Call the function
|
||||
if (lua_pcall(L, 2, 1, 0) != LUA_OK) {
|
||||
Ogre::LogManager::getSingleton().stream()
|
||||
<< "[Lua BT] Error calling node '" << nodeName
|
||||
<< "': " << lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Read the result
|
||||
if (!lua_isstring(L, -1)) {
|
||||
lua_pop(L, 1);
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char *result = lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
|
||||
if (strcmp(result, "success") == 0)
|
||||
return 0;
|
||||
if (strcmp(result, "failure") == 0)
|
||||
return 1;
|
||||
if (strcmp(result, "running") == 0)
|
||||
return 2;
|
||||
|
||||
Ogre::LogManager::getSingleton().stream()
|
||||
<< "[Lua BT] Node '" << nodeName
|
||||
<< "' returned invalid result: " << result;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Convenience overload: call with raw entity ID (for tests)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
int callLuaBehaviorTreeNode(void *L_, const std::string &nodeName,
|
||||
uint64_t entityId, const std::string ¶ms)
|
||||
{
|
||||
// Use the global Lua state if none was passed
|
||||
lua_State *L = static_cast<lua_State *>(L_);
|
||||
if (!L)
|
||||
L = g_luaState;
|
||||
if (!L)
|
||||
return -1;
|
||||
|
||||
auto it = g_luaNodeHandlers.find(nodeName);
|
||||
if (it == g_luaNodeHandlers.end())
|
||||
return -1;
|
||||
|
||||
int ref = it->second;
|
||||
|
||||
// Push the callback function
|
||||
lua_rawgeti(L, LUA_REGISTRYINDEX, ref);
|
||||
|
||||
// Push entity ID as first argument
|
||||
lua_pushinteger(L, (lua_Integer)entityId);
|
||||
|
||||
// Push params table as second argument
|
||||
pushParamsTable(L, params);
|
||||
|
||||
// Call the function
|
||||
if (lua_pcall(L, 2, 1, 0) != LUA_OK) {
|
||||
Ogre::LogManager::getSingleton().stream()
|
||||
<< "[Lua BT] Error calling node '" << nodeName
|
||||
<< "': " << lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Read the result
|
||||
if (!lua_isstring(L, -1)) {
|
||||
lua_pop(L, 1);
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char *result = lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
|
||||
if (strcmp(result, "success") == 0)
|
||||
return 0;
|
||||
if (strcmp(result, "failure") == 0)
|
||||
return 1;
|
||||
if (strcmp(result, "running") == 0)
|
||||
return 2;
|
||||
|
||||
Ogre::LogManager::getSingleton().stream()
|
||||
<< "[Lua BT] Node '" << nodeName
|
||||
<< "' returned invalid result: " << result;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.behavior_tree.register_node(name, function)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaRegisterNode(lua_State *L)
|
||||
{
|
||||
const char *name = luaL_checkstring(L, 1);
|
||||
|
||||
if (!lua_isfunction(L, 2))
|
||||
luaL_error(L, "Expected function as second argument");
|
||||
|
||||
// Remove any existing handler with the same name
|
||||
auto it = g_luaNodeHandlers.find(name);
|
||||
if (it != g_luaNodeHandlers.end()) {
|
||||
luaL_unref(L, LUA_REGISTRYINDEX, it->second);
|
||||
}
|
||||
|
||||
// Store the function reference
|
||||
lua_pushvalue(L, 2);
|
||||
int ref = luaL_ref(L, LUA_REGISTRYINDEX);
|
||||
g_luaNodeHandlers[name] = ref;
|
||||
|
||||
Ogre::LogManager::getSingleton().stream()
|
||||
<< "[Lua BT] Registered node handler: " << name;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.behavior_tree.unregister_node(name) -> bool
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaUnregisterNode(lua_State *L)
|
||||
{
|
||||
const char *name = luaL_checkstring(L, 1);
|
||||
|
||||
auto it = g_luaNodeHandlers.find(name);
|
||||
if (it == g_luaNodeHandlers.end()) {
|
||||
lua_pushboolean(L, false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
luaL_unref(L, LUA_REGISTRYINDEX, it->second);
|
||||
g_luaNodeHandlers.erase(it);
|
||||
|
||||
lua_pushboolean(L, true);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.behavior_tree.list_nodes() -> table of names
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaListNodeHandlers(lua_State *L)
|
||||
{
|
||||
lua_newtable(L);
|
||||
int idx = 1;
|
||||
for (const auto &kv : g_luaNodeHandlers) {
|
||||
lua_pushstring(L, kv.first.c_str());
|
||||
lua_rawseti(L, -2, idx++);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.behavior_tree.create_node(type, name, params) -> table
|
||||
// ecs.behavior_tree.create_node(type, params) -> table
|
||||
// ecs.behavior_tree.create_node(luaHandlerName, params) -> table (backward compat)
|
||||
// ---------------------------------------------------------------------------
|
||||
// Creates a behavior tree node table suitable for use in action behavior trees.
|
||||
//
|
||||
// If the first argument matches a registered Lua node handler name, it creates
|
||||
// a luaTask node (backward compatible behavior).
|
||||
// Otherwise, the first argument is treated as the node type string.
|
||||
//
|
||||
// Examples:
|
||||
// ecs.behavior_tree.create_node("setAnimationState", "locomotion/walk")
|
||||
// -> { type = "setAnimationState", name = "locomotion/walk" }
|
||||
//
|
||||
// ecs.behavior_tree.create_node("delay", "2.0")
|
||||
// -> { type = "delay", params = "2.0" }
|
||||
//
|
||||
// ecs.behavior_tree.create_node("delay", "", "2.0")
|
||||
// -> { type = "delay", params = "2.0" }
|
||||
//
|
||||
// ecs.behavior_tree.create_node("say_hello", "message=Hi")
|
||||
// -> { type = "luaTask", name = "say_hello", params = "message=Hi" }
|
||||
|
||||
static int luaCreateNode(lua_State *L)
|
||||
{
|
||||
const char *arg1 = luaL_checkstring(L, 1);
|
||||
|
||||
// Check if arg1 is a registered Lua node handler name (backward compat)
|
||||
bool isLuaHandler = g_luaNodeHandlers.find(arg1) !=
|
||||
g_luaNodeHandlers.end();
|
||||
|
||||
// Save arg2 and arg3 before pushing the result table (which shifts indices)
|
||||
const char *arg2 = NULL;
|
||||
const char *arg3 = NULL;
|
||||
if (lua_gettop(L) >= 2 && lua_isstring(L, 2))
|
||||
arg2 = lua_tostring(L, 2);
|
||||
if (lua_gettop(L) >= 3 && lua_isstring(L, 3))
|
||||
arg3 = lua_tostring(L, 3);
|
||||
|
||||
lua_newtable(L);
|
||||
|
||||
if (isLuaHandler) {
|
||||
// Backward compatible: create a luaTask node
|
||||
lua_pushstring(L, "luaTask");
|
||||
lua_setfield(L, -2, "type");
|
||||
|
||||
lua_pushstring(L, arg1);
|
||||
lua_setfield(L, -2, "name");
|
||||
|
||||
if (arg2 && arg2[0]) {
|
||||
lua_pushstring(L, arg2);
|
||||
lua_setfield(L, -2, "params");
|
||||
}
|
||||
} else {
|
||||
// arg1 is the node type
|
||||
lua_pushstring(L, arg1);
|
||||
lua_setfield(L, -2, "type");
|
||||
|
||||
// arg2 is either name or params depending on the node type
|
||||
if (arg2 && arg2[0]) {
|
||||
lua_pushstring(L, arg2);
|
||||
lua_setfield(L, -2, "name");
|
||||
}
|
||||
|
||||
// arg3 is params (optional)
|
||||
if (arg3 && arg3[0]) {
|
||||
lua_pushstring(L, arg3);
|
||||
lua_setfield(L, -2, "params");
|
||||
}
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public C++ API: get list of registered Lua node names
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
std::vector<std::string> getRegisteredLuaNodeNames()
|
||||
{
|
||||
std::vector<std::string> names;
|
||||
names.reserve(g_luaNodeHandlers.size());
|
||||
for (const auto &kv : g_luaNodeHandlers)
|
||||
names.push_back(kv.first);
|
||||
return names;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Registration
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void registerLuaBehaviorTreeApi(lua_State *L)
|
||||
{
|
||||
// Store the Lua state globally so callLuaBehaviorTreeNode can use it
|
||||
g_luaState = L;
|
||||
|
||||
// Get or create the "ecs" global table
|
||||
lua_getglobal(L, "ecs");
|
||||
if (lua_isnil(L, -1)) {
|
||||
lua_pop(L, 1);
|
||||
lua_newtable(L);
|
||||
}
|
||||
|
||||
// Create the behavior_tree sub-table
|
||||
lua_newtable(L);
|
||||
|
||||
lua_pushcfunction(L, luaRegisterNode);
|
||||
lua_setfield(L, -2, "register_node");
|
||||
|
||||
lua_pushcfunction(L, luaUnregisterNode);
|
||||
lua_setfield(L, -2, "unregister_node");
|
||||
|
||||
lua_pushcfunction(L, luaListNodeHandlers);
|
||||
lua_setfield(L, -2, "list_nodes");
|
||||
|
||||
lua_pushcfunction(L, luaCreateNode);
|
||||
lua_setfield(L, -2, "create_node");
|
||||
|
||||
// Set behavior_tree as a field of ecs
|
||||
lua_setfield(L, -2, "behavior_tree");
|
||||
|
||||
// Ensure ecs is global
|
||||
lua_setglobal(L, "ecs");
|
||||
}
|
||||
|
||||
} // namespace editScene
|
||||
149
src/features/editScene/lua/LuaBehaviorTreeApi.hpp
Normal file
149
src/features/editScene/lua/LuaBehaviorTreeApi.hpp
Normal file
@@ -0,0 +1,149 @@
|
||||
#ifndef EDITSCENE_LUA_BEHAVIOR_TREE_API_HPP
|
||||
#define EDITSCENE_LUA_BEHAVIOR_TREE_API_HPP
|
||||
#pragma once
|
||||
|
||||
#include <lua.hpp>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <cstdint>
|
||||
#include <flecs.h>
|
||||
|
||||
/**
|
||||
* @file LuaBehaviorTreeApi.hpp
|
||||
* @brief Lua API for creating behavior tree nodes that run Lua functions.
|
||||
*
|
||||
* Provides Lua bindings to register named Lua functions as behavior tree
|
||||
* node handlers, and to create behavior tree nodes that invoke those
|
||||
* functions during tree evaluation.
|
||||
*
|
||||
* Exposed Lua globals (under the "ecs" table):
|
||||
* ecs.behavior_tree.register_node(name, function) -> nil
|
||||
* Register a Lua function as a behavior tree node handler.
|
||||
* The function receives (entity_id, params_table) and must return
|
||||
* "success", "failure", or "running".
|
||||
*
|
||||
* ecs.behavior_tree.unregister_node(name) -> bool
|
||||
* Remove a previously registered node handler.
|
||||
*
|
||||
* ecs.behavior_tree.list_nodes() -> table of registered node names
|
||||
* Returns an array of all registered node handler names.
|
||||
*
|
||||
* ecs.behavior_tree.create_node(type, name, params) -> table
|
||||
* Creates a behavior tree node table suitable for use in
|
||||
* ecs.action_db.add_action() behavior trees.
|
||||
*
|
||||
* If the first argument matches a registered Lua node handler name,
|
||||
* it creates a luaTask node (backward compatible).
|
||||
* Otherwise, the first argument is the node type string.
|
||||
*
|
||||
* Examples:
|
||||
* ecs.behavior_tree.create_node("setAnimationState", "locomotion/walk")
|
||||
* -> { type = "setAnimationState", name = "locomotion/walk" }
|
||||
*
|
||||
* ecs.behavior_tree.create_node("delay", "", "2.0")
|
||||
* -> { type = "delay", params = "2.0" }
|
||||
*
|
||||
* ecs.behavior_tree.create_node("say_hello", "message=Hi")
|
||||
* -> { type = "luaTask", name = "say_hello", params = "message=Hi" }
|
||||
*
|
||||
* Supported node types (see BehaviorTree.hpp for full list):
|
||||
* sequence, selector, invert, task, check, debugPrint,
|
||||
* setAnimationState, isAnimationEnded, setBit, checkBit,
|
||||
* setValue, checkValue, blackboardDump, delay, teleportToChild,
|
||||
* disablePhysics, enablePhysics, sendEvent, hasItem, hasItemByName,
|
||||
* countItem, pickupItem, dropItem, useItem, addItemToInventory,
|
||||
* luaTask
|
||||
*
|
||||
* Example:
|
||||
* -- Register a Lua node handler
|
||||
* ecs.behavior_tree.register_node("say_hello", function(entity_id, params)
|
||||
* print("Hello from entity " .. entity_id .. "! " .. (params.message or ""))
|
||||
* return "success"
|
||||
* end)
|
||||
*
|
||||
* -- Build a behavior tree using both built-in C++ nodes and Lua nodes
|
||||
* ecs.action_db.add_action("greet", 1, {}, {}, {
|
||||
* type = "sequence",
|
||||
* children = {
|
||||
* ecs.behavior_tree.create_node("setAnimationState", "locomotion/walk"),
|
||||
* ecs.behavior_tree.create_node("delay", "", "2.0"),
|
||||
* ecs.behavior_tree.create_node("setAnimationState", "locomotion/idle"),
|
||||
* ecs.behavior_tree.create_node("say_hello", "message=Hello World")
|
||||
* }
|
||||
* })
|
||||
*
|
||||
* -- A Lua node that runs for a while:
|
||||
* ecs.behavior_tree.register_node("wait_random", function(entity_id, params)
|
||||
* local bb = ecs.get_component(entity_id, "GoapBlackboard")
|
||||
* if not bb then return "failure" end
|
||||
*
|
||||
* local key = "wait_timer_" .. params.timer_key
|
||||
* local dt = ecs.get_delta_time()
|
||||
* bb.floatValues[key] = (bb.floatValues[key] or 0) + dt
|
||||
*
|
||||
* local duration = tonumber(params.duration) or 1.0
|
||||
* if bb.floatValues[key] >= duration then
|
||||
* bb.floatValues[key] = nil
|
||||
* ecs.set_component(entity_id, "GoapBlackboard", bb)
|
||||
* return "success"
|
||||
* end
|
||||
* ecs.set_component(entity_id, "GoapBlackboard", bb)
|
||||
* return "running"
|
||||
* end)
|
||||
*/
|
||||
|
||||
namespace editScene
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Register all behavior tree Lua API functions into the "ecs" table.
|
||||
*
|
||||
* Adds a behavior_tree sub-table to the "ecs" global table with functions
|
||||
* for registering Lua node handlers and creating behavior tree nodes.
|
||||
*
|
||||
* @param L The Lua state.
|
||||
*/
|
||||
void registerLuaBehaviorTreeApi(lua_State *L);
|
||||
|
||||
/**
|
||||
* @brief Get the list of registered Lua node handler names.
|
||||
*
|
||||
* Used by the behavior tree editor UI to show registered Lua nodes
|
||||
* in a dropdown instead of requiring manual entry.
|
||||
*
|
||||
* @return A vector of registered node handler names.
|
||||
*/
|
||||
std::vector<std::string> getRegisteredLuaNodeNames();
|
||||
|
||||
/**
|
||||
* @brief Call a registered Lua behavior tree node handler.
|
||||
*
|
||||
* This is the primary evaluation function used by the behavior tree system.
|
||||
* It looks up the registered handler by name, pushes entity and params
|
||||
* as arguments, calls the Lua function, and interprets the result.
|
||||
*
|
||||
* @param L_ Pointer to the Lua state (can be nullptr to use global state).
|
||||
* @param nodeName The name of the registered node handler.
|
||||
* @param entity The flecs entity executing this node.
|
||||
* @param params The params string (key=val,key2=val2 format).
|
||||
* @return 0 = success, 1 = failure, 2 = running, -1 = error/not found.
|
||||
*/
|
||||
int callLuaBehaviorTreeNode(void *L_, const std::string &nodeName,
|
||||
flecs::entity entity, const std::string ¶ms);
|
||||
|
||||
/**
|
||||
* @brief Convenience overload for calling a registered Lua node handler
|
||||
* with a raw entity ID (for use in tests or contexts without flecs).
|
||||
*
|
||||
* @param L_ Pointer to the Lua state (can be nullptr to use global state).
|
||||
* @param nodeName The name of the registered node handler.
|
||||
* @param entityId The raw entity ID.
|
||||
* @param params The params string (key=val,key2=val2 format).
|
||||
* @return 0 = success, 1 = failure, 2 = running, -1 = error/not found.
|
||||
*/
|
||||
int callLuaBehaviorTreeNode(void *L_, const std::string &nodeName,
|
||||
uint64_t entityId, const std::string ¶ms);
|
||||
|
||||
} // namespace editScene
|
||||
|
||||
#endif // EDITSCENE_LUA_BEHAVIOR_TREE_API_HPP
|
||||
1988
src/features/editScene/lua/LuaComponentApi.cpp
Normal file
1988
src/features/editScene/lua/LuaComponentApi.cpp
Normal file
File diff suppressed because it is too large
Load Diff
57
src/features/editScene/lua/LuaComponentApi.hpp
Normal file
57
src/features/editScene/lua/LuaComponentApi.hpp
Normal file
@@ -0,0 +1,57 @@
|
||||
#ifndef EDITSCENE_LUA_COMPONENT_API_HPP
|
||||
#define EDITSCENE_LUA_COMPONENT_API_HPP
|
||||
#pragma once
|
||||
|
||||
#include <lua.hpp>
|
||||
|
||||
/**
|
||||
* @file LuaComponentApi.hpp
|
||||
* @brief Lua API for adding, removing, and modifying ECS components.
|
||||
*
|
||||
* Provides a generic component API where Lua scripts can add, get, set,
|
||||
* and remove components on entities. Each component type is registered
|
||||
* with a name string and a set of field accessors.
|
||||
*
|
||||
* Exposed Lua globals (in the "ecs" table):
|
||||
* ecs.add_component(id, "ComponentName") -> nil
|
||||
* ecs.remove_component(id, "ComponentName") -> nil
|
||||
* ecs.has_component(id, "ComponentName") -> bool
|
||||
* ecs.get_component(id, "ComponentName") -> table (field -> value)
|
||||
* ecs.set_component(id, "ComponentName", table) -> nil
|
||||
* ecs.get_field(id, "ComponentName", "fieldName") -> value
|
||||
* ecs.set_field(id, "ComponentName", "fieldName", value) -> nil
|
||||
*
|
||||
* Supported component names (case-sensitive):
|
||||
* "EntityName", "Transform", "Renderable", "Light", "Camera",
|
||||
* "RigidBody", "PhysicsCollider", "Character", "CharacterSlots",
|
||||
* "AnimationTree", "AnimationTreeTemplate", "BehaviorTree",
|
||||
* "GoapBlackboard", "GoapAction", "GoapGoal", "ActionDatabase",
|
||||
* "ActionDebug", "SmartObject", "Actuator", "EventHandler",
|
||||
* "GoapPlanner", "GoapRunner", "PathFollowing", "NavMesh",
|
||||
* "NavMeshGeometrySource", "NavMeshAgent", "Item", "Inventory",
|
||||
* "Lod", "LodSettings", "StaticGeometry", "StaticGeometryMember",
|
||||
* "ProceduralTexture", "ProceduralMaterial", "Primitive",
|
||||
* "TriangleBuffer", "Sun", "Skybox", "WaterPlane", "WaterPhysics",
|
||||
* "BuoyancyInfo", "InWater", "StartupMenu", "Dialogue",
|
||||
* "PlayerController", "CellGrid", "Room", "ClearArea", "Roof",
|
||||
* "Lot", "District", "Town", "FurnitureTemplate", "PrefabInstance",
|
||||
* "EditorMarker", "GeneratedPhysicsTag", "ParentComponent",
|
||||
* "ModifiedComponent"
|
||||
*/
|
||||
|
||||
namespace editScene
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Register all component Lua API functions into the "ecs" global table.
|
||||
*
|
||||
* Adds functions for component manipulation (add, remove, has, get, set,
|
||||
* get_field, set_field) to the existing "ecs" table.
|
||||
*
|
||||
* @param L The Lua state.
|
||||
*/
|
||||
void registerLuaComponentApi(lua_State *L);
|
||||
|
||||
} // namespace editScene
|
||||
|
||||
#endif // EDITSCENE_LUA_COMPONENT_API_HPP
|
||||
236
src/features/editScene/lua/LuaEntityApi.cpp
Normal file
236
src/features/editScene/lua/LuaEntityApi.cpp
Normal file
@@ -0,0 +1,236 @@
|
||||
#include "LuaEntityApi.hpp"
|
||||
#include "components/EditorMarker.hpp"
|
||||
#include <OgreLogManager.h>
|
||||
#include <iostream>
|
||||
|
||||
namespace editScene
|
||||
{
|
||||
|
||||
// Global entity ID map
|
||||
LuaEntityIdMap g_luaEntityIdMap;
|
||||
|
||||
int luaEntityToId(flecs::entity e)
|
||||
{
|
||||
if (!e.is_valid())
|
||||
return -1;
|
||||
return g_luaEntityIdMap.addEntity(e);
|
||||
}
|
||||
|
||||
flecs::entity luaIdToEntity(int id)
|
||||
{
|
||||
return g_luaEntityIdMap.getEntity(id);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: get the Flecs world from the Lua registry
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static flecs::world getWorld(lua_State *L)
|
||||
{
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, "EditSceneFlecsWorld");
|
||||
OgreAssert(lua_islightuserdata(L, -1), "Flecs world not registered");
|
||||
flecs::world *world =
|
||||
static_cast<flecs::world *>(lua_touserdata(L, -1));
|
||||
lua_pop(L, 1);
|
||||
return *world;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.create_entity() -> int (entity ID)
|
||||
// Creates a new entity with EditorMarkerComponent and returns its Lua ID.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaCreateEntity(lua_State *L)
|
||||
{
|
||||
flecs::world world = getWorld(L);
|
||||
flecs::entity e = world.entity();
|
||||
// Add EditorMarkerComponent so it appears in the editor hierarchy
|
||||
// (the component is forward-declared; we use a tag approach)
|
||||
e.add<EditorMarkerComponent>();
|
||||
int id = g_luaEntityIdMap.addEntity(e);
|
||||
lua_pushinteger(L, id);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.destroy_entity(id) -> nil
|
||||
// Destroys an entity and removes it from the ID map.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaDestroyEntity(lua_State *L)
|
||||
{
|
||||
luaL_checktype(L, 1, LUA_TNUMBER);
|
||||
int id = lua_tointeger(L, 1);
|
||||
flecs::entity e = g_luaEntityIdMap.getEntity(id);
|
||||
if (e.is_alive()) {
|
||||
g_luaEntityIdMap.removeEntity(e);
|
||||
e.destruct();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.entity_exists(id) -> bool
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaEntityExists(lua_State *L)
|
||||
{
|
||||
luaL_checktype(L, 1, LUA_TNUMBER);
|
||||
int id = lua_tointeger(L, 1);
|
||||
lua_pushboolean(L, g_luaEntityIdMap.hasId(id) ? 1 : 0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.get_player_entity() -> int (entity ID) or nil
|
||||
// Looks up the entity named "player" in the Flecs world.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaGetPlayerEntity(lua_State *L)
|
||||
{
|
||||
flecs::world world = getWorld(L);
|
||||
flecs::entity e = world.lookup("player");
|
||||
if (e.is_valid()) {
|
||||
int id = g_luaEntityIdMap.addEntity(e);
|
||||
lua_pushinteger(L, id);
|
||||
} else {
|
||||
lua_pushnil(L);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.get_entity_by_name(name) -> int (entity ID) or nil
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaGetEntityByName(lua_State *L)
|
||||
{
|
||||
luaL_checktype(L, 1, LUA_TSTRING);
|
||||
const char *name = lua_tostring(L, 1);
|
||||
flecs::world world = getWorld(L);
|
||||
flecs::entity e = world.lookup(name);
|
||||
if (e.is_valid()) {
|
||||
int id = g_luaEntityIdMap.addEntity(e);
|
||||
lua_pushinteger(L, id);
|
||||
} else {
|
||||
lua_pushnil(L);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.set_entity_name(id, name) -> nil
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaSetEntityName(lua_State *L)
|
||||
{
|
||||
luaL_checktype(L, 1, LUA_TNUMBER);
|
||||
luaL_checktype(L, 2, LUA_TSTRING);
|
||||
int id = lua_tointeger(L, 1);
|
||||
const char *name = lua_tostring(L, 2);
|
||||
flecs::entity e = g_luaEntityIdMap.getEntity(id);
|
||||
e.set_name(name);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.get_entity_name(id) -> string or nil
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaGetEntityName(lua_State *L)
|
||||
{
|
||||
luaL_checktype(L, 1, LUA_TNUMBER);
|
||||
int id = lua_tointeger(L, 1);
|
||||
flecs::entity e = g_luaEntityIdMap.getEntity(id);
|
||||
const char *name = e.name();
|
||||
if (name && name[0]) {
|
||||
lua_pushstring(L, name);
|
||||
} else {
|
||||
lua_pushnil(L);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.parent(id) -> int (parent ID) or nil
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaGetParent(lua_State *L)
|
||||
{
|
||||
luaL_checktype(L, 1, LUA_TNUMBER);
|
||||
int id = lua_tointeger(L, 1);
|
||||
flecs::entity e = g_luaEntityIdMap.getEntity(id);
|
||||
flecs::entity parent = e.parent();
|
||||
if (parent.is_valid()) {
|
||||
int parentId = g_luaEntityIdMap.addEntity(parent);
|
||||
lua_pushinteger(L, parentId);
|
||||
} else {
|
||||
lua_pushnil(L);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.children(id) -> table of child IDs
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaGetChildren(lua_State *L)
|
||||
{
|
||||
luaL_checktype(L, 1, LUA_TNUMBER);
|
||||
int id = lua_tointeger(L, 1);
|
||||
flecs::entity e = g_luaEntityIdMap.getEntity(id);
|
||||
|
||||
lua_newtable(L); // result table
|
||||
int index = 1;
|
||||
e.children([&](flecs::entity child) {
|
||||
int childId = g_luaEntityIdMap.addEntity(child);
|
||||
lua_pushinteger(L, childId);
|
||||
lua_rawseti(L, -2, index);
|
||||
index++;
|
||||
});
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Register all entity API functions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void registerLuaEntityApi(lua_State *L)
|
||||
{
|
||||
// Create the "ecs" global table
|
||||
lua_newtable(L);
|
||||
|
||||
// Entity management
|
||||
lua_pushcfunction(L, luaCreateEntity);
|
||||
lua_setfield(L, -2, "create_entity");
|
||||
|
||||
lua_pushcfunction(L, luaDestroyEntity);
|
||||
lua_setfield(L, -2, "destroy_entity");
|
||||
|
||||
lua_pushcfunction(L, luaEntityExists);
|
||||
lua_setfield(L, -2, "entity_exists");
|
||||
|
||||
lua_pushcfunction(L, luaGetPlayerEntity);
|
||||
lua_setfield(L, -2, "get_player_entity");
|
||||
|
||||
lua_pushcfunction(L, luaGetEntityByName);
|
||||
lua_setfield(L, -2, "get_entity_by_name");
|
||||
|
||||
lua_pushcfunction(L, luaSetEntityName);
|
||||
lua_setfield(L, -2, "set_entity_name");
|
||||
|
||||
lua_pushcfunction(L, luaGetEntityName);
|
||||
lua_setfield(L, -2, "get_entity_name");
|
||||
|
||||
// Hierarchy
|
||||
lua_pushcfunction(L, luaGetParent);
|
||||
lua_setfield(L, -2, "parent");
|
||||
|
||||
lua_pushcfunction(L, luaGetChildren);
|
||||
lua_setfield(L, -2, "children");
|
||||
|
||||
// Set the global
|
||||
lua_setglobal(L, "ecs");
|
||||
}
|
||||
|
||||
} // namespace editScene
|
||||
126
src/features/editScene/lua/LuaEntityApi.hpp
Normal file
126
src/features/editScene/lua/LuaEntityApi.hpp
Normal file
@@ -0,0 +1,126 @@
|
||||
#ifndef EDITSCENE_LUA_ENTITY_API_HPP
|
||||
#define EDITSCENE_LUA_ENTITY_API_HPP
|
||||
#pragma once
|
||||
|
||||
#include <flecs.h>
|
||||
#include <lua.hpp>
|
||||
#include <Ogre.h>
|
||||
#include <unordered_map>
|
||||
|
||||
/**
|
||||
* @file LuaEntityApi.hpp
|
||||
* @brief Lua API for entity creation, destruction, and ID mapping.
|
||||
*
|
||||
* Provides a bidirectional mapping between Lua integer IDs and
|
||||
* Flecs entity handles. Lua scripts reference entities by integer
|
||||
* IDs rather than raw Flecs handles for safety and simplicity.
|
||||
*
|
||||
* Exposed Lua globals:
|
||||
* ecs.create_entity() -> int (entity ID)
|
||||
* ecs.destroy_entity(id) -> nil
|
||||
* ecs.entity_exists(id) -> bool
|
||||
* ecs.get_player_entity() -> int (entity ID)
|
||||
* ecs.get_entity_by_name(name) -> int (entity ID) or nil
|
||||
* ecs.set_entity_name(id, name)-> nil
|
||||
* ecs.get_entity_name(id) -> string or nil
|
||||
* ecs.parent(id) -> int (parent ID) or nil
|
||||
* ecs.children(id) -> table of child IDs
|
||||
*/
|
||||
|
||||
namespace editScene
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Global bidirectional mapping between Lua integer IDs and Flecs entities.
|
||||
*
|
||||
* This is a singleton-like global so that all Lua API functions can
|
||||
* access it without needing to pass it through every closure.
|
||||
*/
|
||||
struct LuaEntityIdMap {
|
||||
std::unordered_map<int, flecs::entity> id2entity;
|
||||
std::unordered_map<flecs::entity_t, int> entity2id;
|
||||
int nextId = 0;
|
||||
|
||||
/** @brief Get the next available integer ID. */
|
||||
int getNextId()
|
||||
{
|
||||
nextId++;
|
||||
return nextId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Add an entity to the map, returning its integer ID.
|
||||
* If the entity is already mapped, returns the existing ID.
|
||||
*/
|
||||
int addEntity(flecs::entity e)
|
||||
{
|
||||
if (entity2id.find(e.id()) != entity2id.end())
|
||||
return entity2id[e.id()];
|
||||
int id = getNextId();
|
||||
id2entity[id] = e;
|
||||
entity2id[e.id()] = id;
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the Flecs entity for an integer ID.
|
||||
* Asserts if the ID is not found or the entity is invalid.
|
||||
*/
|
||||
flecs::entity getEntity(int id)
|
||||
{
|
||||
auto it = id2entity.find(id);
|
||||
OgreAssert(it != id2entity.end(), "Invalid entity ID");
|
||||
OgreAssert(it->second.is_valid(), "Entity is no longer valid");
|
||||
return it->second;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Remove an entity from the map.
|
||||
*/
|
||||
void removeEntity(flecs::entity e)
|
||||
{
|
||||
auto it = entity2id.find(e.id());
|
||||
if (it != entity2id.end()) {
|
||||
id2entity.erase(it->second);
|
||||
entity2id.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if an integer ID is valid.
|
||||
*/
|
||||
bool hasId(int id) const
|
||||
{
|
||||
auto it = id2entity.find(id);
|
||||
return it != id2entity.end() && it->second.is_valid();
|
||||
}
|
||||
};
|
||||
|
||||
/** @brief Global entity ID map instance. */
|
||||
extern LuaEntityIdMap g_luaEntityIdMap;
|
||||
|
||||
/**
|
||||
* @brief Convert a Flecs entity to a Lua integer ID.
|
||||
* @return The integer ID, or -1 if the entity is invalid.
|
||||
*/
|
||||
int luaEntityToId(flecs::entity e);
|
||||
|
||||
/**
|
||||
* @brief Convert a Lua integer ID to a Flecs entity.
|
||||
* Asserts if the ID is invalid.
|
||||
*/
|
||||
flecs::entity luaIdToEntity(int id);
|
||||
|
||||
/**
|
||||
* @brief Register all entity-related Lua API functions into the global table.
|
||||
*
|
||||
* Creates the "ecs" global table (or adds to it) with entity management
|
||||
* functions. Must be called after LuaState is constructed.
|
||||
*
|
||||
* @param L The Lua state.
|
||||
*/
|
||||
void registerLuaEntityApi(lua_State *L);
|
||||
|
||||
} // namespace editScene
|
||||
|
||||
#endif // EDITSCENE_LUA_ENTITY_API_HPP
|
||||
320
src/features/editScene/lua/LuaEventApi.cpp
Normal file
320
src/features/editScene/lua/LuaEventApi.cpp
Normal file
@@ -0,0 +1,320 @@
|
||||
#include "LuaEventApi.hpp"
|
||||
#include "LuaEntityApi.hpp"
|
||||
#include "../systems/EventBus.hpp"
|
||||
#include "../components/GoapBlackboard.hpp"
|
||||
#include <OgreLogManager.h>
|
||||
#include <OgreVector3.h>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace editScene
|
||||
{
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Internal: subscription ID tracking for Lua-managed subscriptions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @brief Map from Lua subscription IDs to EventBus ListenerIds.
|
||||
*
|
||||
* When Lua subscribes to an event, we store the mapping so that
|
||||
* unsubscribe_event() can remove the correct listener.
|
||||
*/
|
||||
static std::unordered_map<int, EventBus::ListenerId> s_luaSubscriptions;
|
||||
static int s_nextLuaSubId = 1;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: push a GoapBlackboard as a Lua table
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @brief Push a GoapBlackboard as a Lua table with named fields.
|
||||
*
|
||||
* The resulting table has:
|
||||
* .bits -> integer
|
||||
* .mask -> integer
|
||||
* .values -> {string -> int}
|
||||
* .floatValues -> {string -> float}
|
||||
* .vec3Values -> {string -> {x, y, z}}
|
||||
* .stringValues -> {string -> string}
|
||||
*
|
||||
* @param L Lua state.
|
||||
* @param bb The GoapBlackboard to convert.
|
||||
*/
|
||||
static void pushGoapBlackboard(lua_State *L, const GoapBlackboard &bb)
|
||||
{
|
||||
lua_newtable(L);
|
||||
|
||||
// bits and mask
|
||||
lua_pushinteger(L, (lua_Integer)bb.bits);
|
||||
lua_setfield(L, -2, "bits");
|
||||
|
||||
lua_pushinteger(L, (lua_Integer)bb.mask);
|
||||
lua_setfield(L, -2, "mask");
|
||||
|
||||
// values (int map)
|
||||
lua_newtable(L);
|
||||
for (auto &kv : bb.values) {
|
||||
lua_pushinteger(L, kv.second);
|
||||
lua_setfield(L, -2, kv.first.c_str());
|
||||
}
|
||||
lua_setfield(L, -2, "values");
|
||||
|
||||
// floatValues
|
||||
lua_newtable(L);
|
||||
for (auto &kv : bb.floatValues) {
|
||||
lua_pushnumber(L, kv.second);
|
||||
lua_setfield(L, -2, kv.first.c_str());
|
||||
}
|
||||
lua_setfield(L, -2, "floatValues");
|
||||
|
||||
// vec3Values
|
||||
lua_newtable(L);
|
||||
for (auto &kv : bb.vec3Values) {
|
||||
lua_newtable(L);
|
||||
lua_pushnumber(L, kv.second.x);
|
||||
lua_rawseti(L, -2, 1);
|
||||
lua_pushnumber(L, kv.second.y);
|
||||
lua_rawseti(L, -2, 2);
|
||||
lua_pushnumber(L, kv.second.z);
|
||||
lua_rawseti(L, -2, 3);
|
||||
lua_setfield(L, -2, kv.first.c_str());
|
||||
}
|
||||
lua_setfield(L, -2, "vec3Values");
|
||||
|
||||
// stringValues
|
||||
lua_newtable(L);
|
||||
for (auto &kv : bb.stringValues) {
|
||||
lua_pushstring(L, kv.second.c_str());
|
||||
lua_setfield(L, -2, kv.first.c_str());
|
||||
}
|
||||
lua_setfield(L, -2, "stringValues");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Read a Lua table at the given index as a GoapBlackboard.
|
||||
*
|
||||
* Expects the same format as pushGoapBlackboard produces.
|
||||
*
|
||||
* @param L Lua state.
|
||||
* @param idx Stack index of the table.
|
||||
* @return GoapBlackboard populated from the table.
|
||||
*/
|
||||
static GoapBlackboard readGoapBlackboard(lua_State *L, int idx)
|
||||
{
|
||||
GoapBlackboard bb;
|
||||
|
||||
// bits
|
||||
lua_getfield(L, idx, "bits");
|
||||
if (lua_isnumber(L, -1))
|
||||
bb.bits = (uint64_t)lua_tointeger(L, -1);
|
||||
lua_pop(L, 1);
|
||||
|
||||
// mask
|
||||
lua_getfield(L, idx, "mask");
|
||||
if (lua_isnumber(L, -1))
|
||||
bb.mask = (uint64_t)lua_tointeger(L, -1);
|
||||
lua_pop(L, 1);
|
||||
|
||||
// values (int map)
|
||||
lua_getfield(L, idx, "values");
|
||||
if (lua_istable(L, -1)) {
|
||||
lua_pushnil(L);
|
||||
while (lua_next(L, -2) != 0) {
|
||||
if (lua_isstring(L, -2) && lua_isnumber(L, -1))
|
||||
bb.values[lua_tostring(L, -2)] =
|
||||
(int)lua_tointeger(L, -1);
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
|
||||
// floatValues
|
||||
lua_getfield(L, idx, "floatValues");
|
||||
if (lua_istable(L, -1)) {
|
||||
lua_pushnil(L);
|
||||
while (lua_next(L, -2) != 0) {
|
||||
if (lua_isstring(L, -2) && lua_isnumber(L, -1))
|
||||
bb.floatValues[lua_tostring(L, -2)] =
|
||||
(float)lua_tonumber(L, -1);
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
|
||||
// vec3Values
|
||||
lua_getfield(L, idx, "vec3Values");
|
||||
if (lua_istable(L, -1)) {
|
||||
lua_pushnil(L);
|
||||
while (lua_next(L, -2) != 0) {
|
||||
if (lua_isstring(L, -2) && lua_istable(L, -1)) {
|
||||
Ogre::Vector3 v;
|
||||
lua_rawgeti(L, -1, 1);
|
||||
v.x = (float)lua_tonumber(L, -1);
|
||||
lua_pop(L, 1);
|
||||
lua_rawgeti(L, -1, 2);
|
||||
v.y = (float)lua_tonumber(L, -1);
|
||||
lua_pop(L, 1);
|
||||
lua_rawgeti(L, -1, 3);
|
||||
v.z = (float)lua_tonumber(L, -1);
|
||||
lua_pop(L, 1);
|
||||
bb.vec3Values[lua_tostring(L, -2)] = v;
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
|
||||
// stringValues
|
||||
lua_getfield(L, idx, "stringValues");
|
||||
if (lua_istable(L, -1)) {
|
||||
lua_pushnil(L);
|
||||
while (lua_next(L, -2) != 0) {
|
||||
if (lua_isstring(L, -2) && lua_isstring(L, -1))
|
||||
bb.stringValues[lua_tostring(L, -2)] =
|
||||
lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
|
||||
return bb;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.send_event("eventName", [params_table])
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @brief Lua: ecs.send_event("eventName", [params_table]) -> nil
|
||||
*
|
||||
* Sends an event through the global EventBus. The optional params table
|
||||
* is converted to a GoapBlackboard payload.
|
||||
*
|
||||
* Usage:
|
||||
* ecs.send_event("collision")
|
||||
* ecs.send_event("collision", { entity_id = 42, damage = 10 })
|
||||
*/
|
||||
static int luaSendEvent(lua_State *L)
|
||||
{
|
||||
const char *eventName = luaL_checkstring(L, 1);
|
||||
|
||||
GoapBlackboard params;
|
||||
if (lua_gettop(L) >= 2 && lua_istable(L, 2)) {
|
||||
params = readGoapBlackboard(L, 2);
|
||||
}
|
||||
|
||||
EventBus::getInstance().send(eventName, params);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.subscribe_event("eventName", callback_fn) -> int (subscription id)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @brief Lua: ecs.subscribe_event("eventName", callback_fn) -> int
|
||||
*
|
||||
* Subscribes a Lua function to an event. The callback receives:
|
||||
* function(event_name, params_table)
|
||||
*
|
||||
* Returns a subscription ID that can be used with unsubscribe_event().
|
||||
*
|
||||
* Usage:
|
||||
* local sub_id = ecs.subscribe_event("collision", function(event, params)
|
||||
* print("Collision event received!")
|
||||
* print("entity_id: " .. params.values.entity_id)
|
||||
* end)
|
||||
*/
|
||||
static int luaSubscribeEvent(lua_State *L)
|
||||
{
|
||||
const char *eventName = luaL_checkstring(L, 1);
|
||||
luaL_checktype(L, 2, LUA_TFUNCTION);
|
||||
|
||||
// Create a reference to the Lua callback function
|
||||
lua_pushvalue(L, 2);
|
||||
int callbackRef = luaL_ref(L, LUA_REGISTRYINDEX);
|
||||
|
||||
// Subscribe to the EventBus with a C++ lambda that calls the Lua function
|
||||
EventBus::ListenerId listenerId = EventBus::getInstance().subscribe(
|
||||
eventName, [L, callbackRef](const Ogre::String &eventName,
|
||||
const GoapBlackboard ¶ms) {
|
||||
// Push the Lua callback function
|
||||
lua_rawgeti(L, LUA_REGISTRYINDEX, callbackRef);
|
||||
|
||||
// Push event name
|
||||
lua_pushstring(L, eventName.c_str());
|
||||
|
||||
// Push params as a Lua table
|
||||
pushGoapBlackboard(L, params);
|
||||
|
||||
// Call the Lua function (2 args, 0 results)
|
||||
if (lua_pcall(L, 2, 0, 0) != LUA_OK) {
|
||||
Ogre::LogManager::getSingleton().stream()
|
||||
<< "Lua event callback error: "
|
||||
<< lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
});
|
||||
|
||||
// Store the mapping from Lua subscription ID to EventBus listener ID
|
||||
int luaSubId = s_nextLuaSubId++;
|
||||
s_luaSubscriptions[luaSubId] = listenerId;
|
||||
|
||||
lua_pushinteger(L, luaSubId);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.unsubscribe_event(subscription_id) -> nil
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @brief Lua: ecs.unsubscribe_event(subscription_id) -> nil
|
||||
*
|
||||
* Unsubscribes a previously registered event subscription.
|
||||
*
|
||||
* Usage:
|
||||
* ecs.unsubscribe_event(sub_id)
|
||||
*/
|
||||
static int luaUnsubscribeEvent(lua_State *L)
|
||||
{
|
||||
int luaSubId = (int)luaL_checkinteger(L, 1);
|
||||
|
||||
auto it = s_luaSubscriptions.find(luaSubId);
|
||||
if (it != s_luaSubscriptions.end()) {
|
||||
EventBus::getInstance().unsubscribe(it->second);
|
||||
s_luaSubscriptions.erase(it);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public registration function
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void registerLuaEventApi(lua_State *L)
|
||||
{
|
||||
// Get or create the "ecs" global table
|
||||
lua_getglobal(L, "ecs");
|
||||
if (lua_isnil(L, -1)) {
|
||||
lua_newtable(L);
|
||||
lua_setglobal(L, "ecs");
|
||||
lua_getglobal(L, "ecs");
|
||||
}
|
||||
|
||||
// Register event functions
|
||||
lua_pushcfunction(L, luaSendEvent);
|
||||
lua_setfield(L, -2, "send_event");
|
||||
|
||||
lua_pushcfunction(L, luaSubscribeEvent);
|
||||
lua_setfield(L, -2, "subscribe_event");
|
||||
|
||||
lua_pushcfunction(L, luaUnsubscribeEvent);
|
||||
lua_setfield(L, -2, "unsubscribe_event");
|
||||
|
||||
// Pop the ecs table
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
|
||||
} // namespace editScene
|
||||
55
src/features/editScene/lua/LuaEventApi.hpp
Normal file
55
src/features/editScene/lua/LuaEventApi.hpp
Normal file
@@ -0,0 +1,55 @@
|
||||
#ifndef EDITSCENE_LUA_EVENT_API_HPP
|
||||
#define EDITSCENE_LUA_EVENT_API_HPP
|
||||
#pragma once
|
||||
|
||||
#include <lua.hpp>
|
||||
|
||||
/**
|
||||
* @file LuaEventApi.hpp
|
||||
* @brief Lua API for the EventBus system.
|
||||
*
|
||||
* Provides Lua bindings for the global EventBus singleton, allowing
|
||||
* Lua scripts to subscribe to events, send events, and manage
|
||||
* event subscriptions.
|
||||
*
|
||||
* The EventBus is a synchronous publish/subscribe system. Events are
|
||||
* identified by name strings. Payloads use GoapBlackboard, which
|
||||
* supports int, float, Vector3, and string values.
|
||||
*
|
||||
* Exposed Lua globals (in the "ecs" table):
|
||||
* ecs.send_event("eventName") -> nil
|
||||
* ecs.send_event("eventName", {key=value, ...}) -> nil
|
||||
* ecs.subscribe_event("eventName", callback_fn) -> int (subscription id)
|
||||
* ecs.unsubscribe_event(subscription_id) -> nil
|
||||
*
|
||||
* The callback function receives (event_name, params_table):
|
||||
* ecs.subscribe_event("collision", function(event, params)
|
||||
* print(event .. " occurred")
|
||||
* print("entity_id: " .. params.entity_id)
|
||||
* end)
|
||||
*
|
||||
* The params table contains the GoapBlackboard fields:
|
||||
* params.bits -> integer (bitfield)
|
||||
* params.mask -> integer (bitmask)
|
||||
* params.values -> table {string -> int}
|
||||
* params.floatValues -> table {string -> float}
|
||||
* params.vec3Values -> table {string -> {x, y, z}}
|
||||
* params.stringValues -> table {string -> string}
|
||||
*/
|
||||
|
||||
namespace editScene
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Register all event-related Lua API functions into the "ecs" table.
|
||||
*
|
||||
* Adds functions for event subscription, unsubscription, and sending.
|
||||
* Must be called after LuaState is constructed and the "ecs" table exists.
|
||||
*
|
||||
* @param L The Lua state.
|
||||
*/
|
||||
void registerLuaEventApi(lua_State *L);
|
||||
|
||||
} // namespace editScene
|
||||
|
||||
#endif // EDITSCENE_LUA_EVENT_API_HPP
|
||||
187
src/features/editScene/lua/LuaState.cpp
Normal file
187
src/features/editScene/lua/LuaState.cpp
Normal file
@@ -0,0 +1,187 @@
|
||||
#include "LuaState.hpp"
|
||||
#include <OgreResourceGroupManager.h>
|
||||
#include <OgreLogManager.h>
|
||||
#include <OgreDataStream.h>
|
||||
#include <iostream>
|
||||
|
||||
extern "C" {
|
||||
int luaopen_lpeg(lua_State *L);
|
||||
}
|
||||
|
||||
namespace editScene
|
||||
{
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Custom library loader: loads .lua files from OGRE resource groups
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
int LuaState::luaLibraryLoader(lua_State *L)
|
||||
{
|
||||
if (!lua_isstring(L, 1)) {
|
||||
luaL_error(
|
||||
L,
|
||||
"luaLibraryLoader: Expected string for first parameter");
|
||||
}
|
||||
|
||||
std::string libraryFile = lua_tostring(L, 1);
|
||||
|
||||
// Translate '.' to '/' for OGRE resource path compatibility
|
||||
while (libraryFile.find('.') != std::string::npos)
|
||||
libraryFile.replace(libraryFile.find('.'), 1, "/");
|
||||
|
||||
libraryFile += ".lua";
|
||||
|
||||
Ogre::DataStreamPtr stream =
|
||||
Ogre::ResourceGroupManager::getSingleton().openResource(
|
||||
libraryFile, "LuaScripts");
|
||||
Ogre::String script = stream->getAsString();
|
||||
if (luaL_loadbuffer(L, script.c_str(), script.length(),
|
||||
libraryFile.c_str())) {
|
||||
luaL_error(
|
||||
L,
|
||||
"Error loading library '%s' from resource archive.\n%s",
|
||||
libraryFile.c_str(), lua_tostring(L, -1));
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Install the custom loader into package.searchers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void LuaState::installLibraryLoader()
|
||||
{
|
||||
lua_getglobal(L, "table");
|
||||
lua_getfield(L, -1, "insert");
|
||||
lua_remove(L, -2); // table
|
||||
lua_getglobal(L, "package");
|
||||
lua_getfield(L, -1, "searchers");
|
||||
lua_remove(L, -2); // package
|
||||
lua_pushnumber(L, 1); // insert at position 1 (highest priority)
|
||||
lua_pushcfunction(L, luaLibraryLoader);
|
||||
if (lua_pcall(L, 3, 0, 0))
|
||||
Ogre::LogManager::getSingleton().stream() << lua_tostring(L, 1);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Constructor: create Lua state and open standard libraries
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
LuaState::LuaState()
|
||||
: L(luaL_newstate())
|
||||
{
|
||||
luaopen_base(L);
|
||||
luaopen_table(L);
|
||||
luaopen_package(L);
|
||||
luaL_requiref(L, "table", luaopen_table, 1);
|
||||
lua_pop(L, 1);
|
||||
luaL_requiref(L, "math", luaopen_math, 1);
|
||||
lua_pop(L, 1);
|
||||
luaL_requiref(L, "package", luaopen_package, 1);
|
||||
lua_pop(L, 1);
|
||||
luaL_requiref(L, "string", luaopen_string, 1);
|
||||
lua_pop(L, 1);
|
||||
luaL_requiref(L, "io", luaopen_io, 1);
|
||||
lua_pop(L, 1);
|
||||
luaL_requiref(L, "lpeg", luaopen_lpeg, 1);
|
||||
lua_pop(L, 1);
|
||||
|
||||
installLibraryLoader();
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Destructor
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
LuaState::~LuaState()
|
||||
{
|
||||
lua_close(L);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Handler registration
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
int LuaState::setupHandler()
|
||||
{
|
||||
luaL_checktype(L, 1, LUA_TFUNCTION);
|
||||
lua_pushvalue(L, 1);
|
||||
int ref = luaL_ref(L, LUA_REGISTRYINDEX);
|
||||
setupHandlers.push_back(ref);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Call all handlers with an event name (no entities)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
int LuaState::callHandler(const Ogre::String &event)
|
||||
{
|
||||
for (int ref : setupHandlers) {
|
||||
lua_rawgeti(L, LUA_REGISTRYINDEX, ref);
|
||||
lua_pushstring(L, event.c_str());
|
||||
lua_pushinteger(L, -1);
|
||||
lua_pushinteger(L, -1);
|
||||
if (lua_pcall(L, 3, 0, 0) != LUA_OK) {
|
||||
Ogre::LogManager::getSingleton().stream()
|
||||
<< lua_tostring(L, -1);
|
||||
OgreAssert(false, "Lua error");
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Call all handlers with an event name and two entities
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
int LuaState::callHandler(const Ogre::String &event, flecs::entity e1,
|
||||
flecs::entity e2)
|
||||
{
|
||||
// Entity IDs are mapped through the global idmap (see LuaEntityApi.cpp)
|
||||
extern int luaEntityToId(flecs::entity e);
|
||||
extern flecs::entity luaIdToEntity(int id);
|
||||
|
||||
for (int ref : setupHandlers) {
|
||||
lua_rawgeti(L, LUA_REGISTRYINDEX, ref);
|
||||
lua_pushstring(L, event.c_str());
|
||||
lua_pushinteger(L, luaEntityToId(e1));
|
||||
lua_pushinteger(L, luaEntityToId(e2));
|
||||
if (lua_pcall(L, 3, 0, 0) != LUA_OK) {
|
||||
Ogre::LogManager::getSingleton().stream()
|
||||
<< lua_tostring(L, -1);
|
||||
OgreAssert(false, "Lua error");
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Late setup: load data.lua and run initialisation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void LuaState::lateSetup()
|
||||
{
|
||||
Ogre::DataStreamPtr stream =
|
||||
Ogre::ResourceGroupManager::getSingleton().openResource(
|
||||
"data2.lua", "LuaScripts");
|
||||
std::cout << "stream: " << stream->getAsString() << "\n";
|
||||
if (luaL_dostring(L, stream->getAsString().c_str()) != LUA_OK) {
|
||||
std::cout << "error: " << lua_tostring(L, -1) << "\n";
|
||||
OgreAssert(false, "Script failure");
|
||||
}
|
||||
|
||||
const char *lua_code = "\n\
|
||||
function stuff()\n\
|
||||
return 4\n\
|
||||
end\n\
|
||||
x = stuff()\n\
|
||||
";
|
||||
luaL_dostring(L, lua_code);
|
||||
lua_getglobal(L, "x");
|
||||
int x = lua_tonumber(L, 1);
|
||||
std::cout << "lua: " << x << "\n";
|
||||
}
|
||||
|
||||
} // namespace editScene
|
||||
137
src/features/editScene/lua/LuaState.hpp
Normal file
137
src/features/editScene/lua/LuaState.hpp
Normal file
@@ -0,0 +1,137 @@
|
||||
#ifndef EDITSCENE_LUA_STATE_HPP
|
||||
#define EDITSCENE_LUA_STATE_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
#include <lua.hpp>
|
||||
#include <flecs.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
/**
|
||||
* @file LuaState.hpp
|
||||
* @brief Lua state management for the editScene editor.
|
||||
*
|
||||
* Manages the Lua virtual machine instance, library loading,
|
||||
* script loading from OGRE resource groups, and the custom
|
||||
* package.searchers entry that loads Lua modules from
|
||||
* the "LuaScripts" resource group.
|
||||
*
|
||||
* Usage:
|
||||
* LuaState lua;
|
||||
* lua.installLibraryLoader(); // Register custom searcher
|
||||
* lua.lateSetup(); // Load data.lua and run startup
|
||||
* lua.callHandler("event_name", e1, e2);
|
||||
*/
|
||||
|
||||
namespace editScene
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Manages a single lua_State instance.
|
||||
*
|
||||
* Opens standard Lua libraries (base, table, math, package, string, io)
|
||||
* plus LPEG. Installs a custom package.searchers entry that loads
|
||||
* .lua files from OGRE's "LuaScripts" resource group.
|
||||
*
|
||||
* Provides a handler/callback system: Lua scripts can register
|
||||
* callback functions via setup_handler(), and C++ code can invoke
|
||||
* them with event names and optional entity parameters.
|
||||
*/
|
||||
class LuaState {
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new Lua state.
|
||||
*
|
||||
* Opens all standard libraries and installs the custom
|
||||
* library loader for OGRE resource-based Lua modules.
|
||||
*/
|
||||
LuaState();
|
||||
|
||||
/**
|
||||
* @brief Destroy the Lua state and close the VM.
|
||||
*/
|
||||
~LuaState();
|
||||
|
||||
/**
|
||||
* @brief Get the underlying lua_State pointer.
|
||||
*/
|
||||
lua_State *getState() const
|
||||
{
|
||||
return L;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Install the custom library loader into package.searchers.
|
||||
*
|
||||
* Inserts luaLibraryLoader at position 1 of the searchers table
|
||||
* so it takes precedence over the default file-system loader.
|
||||
* This allows Lua's require() to load modules from OGRE resource
|
||||
* archives.
|
||||
*/
|
||||
void installLibraryLoader();
|
||||
|
||||
/**
|
||||
* @brief Late setup: load data.lua and run initialisation.
|
||||
*
|
||||
* Opens "data.lua" from the "LuaScripts" resource group and
|
||||
* executes it. Also runs a simple inline Lua test snippet.
|
||||
* Called once after the ECS world is fully initialised.
|
||||
*/
|
||||
void lateSetup();
|
||||
|
||||
/**
|
||||
* @brief Register a Lua callback function for event handling.
|
||||
*
|
||||
* Expects a Lua function on the stack (at index 1).
|
||||
* Stores a reference to it in the registry so it can be
|
||||
* called later via callHandler().
|
||||
*
|
||||
* @return int Reference index (stored internally).
|
||||
*/
|
||||
int setupHandler();
|
||||
|
||||
/**
|
||||
* @brief Call all registered handlers with an event name.
|
||||
*
|
||||
* @param event The event name string.
|
||||
* @return int 0 on success.
|
||||
*/
|
||||
int callHandler(const Ogre::String &event);
|
||||
|
||||
/**
|
||||
* @brief Call all registered handlers with an event name and
|
||||
* two entity IDs.
|
||||
*
|
||||
* Entity IDs are mapped through the global idmap (see LuaEntityApi).
|
||||
*
|
||||
* @param event The event name string.
|
||||
* @param e1 First entity (e.g. subject).
|
||||
* @param e2 Second entity (e.g. object).
|
||||
* @return int 0 on success.
|
||||
*/
|
||||
int callHandler(const Ogre::String &event, flecs::entity e1,
|
||||
flecs::entity e2);
|
||||
|
||||
private:
|
||||
lua_State *L;
|
||||
|
||||
/** Registry references for registered Lua callback functions. */
|
||||
std::vector<int> setupHandlers;
|
||||
|
||||
/**
|
||||
* @brief Custom Lua loader function for OGRE resource-based modules.
|
||||
*
|
||||
* Registered in package.searchers. Translates dots to path
|
||||
* separators, appends ".lua", and loads the file from the
|
||||
* "LuaScripts" OGRE resource group.
|
||||
*
|
||||
* @param L Lua state.
|
||||
* @return int 1 (pushes loaded chunk onto stack) or error.
|
||||
*/
|
||||
static int luaLibraryLoader(lua_State *L);
|
||||
};
|
||||
|
||||
} // namespace editScene
|
||||
|
||||
#endif // EDITSCENE_LUA_STATE_HPP
|
||||
@@ -790,10 +790,10 @@ public:
|
||||
node->_setDerivedOrientation(JoltPhysics::convert(q));
|
||||
}
|
||||
for (JPH::Character *ch : characters) {
|
||||
if (body_interface.IsAdded(ch->GetBodyID())) {
|
||||
JPH::BodyID bID = ch->GetBodyID();
|
||||
if (body_interface.IsAdded(bID)) {
|
||||
ch->PostSimulation(0.1f);
|
||||
Ogre::SceneNode *node =
|
||||
id2node[ch->GetBodyID()];
|
||||
Ogre::SceneNode *node = id2node[bID];
|
||||
if (node)
|
||||
node->_setDerivedPosition(
|
||||
JoltPhysics::convert(
|
||||
@@ -1633,6 +1633,13 @@ public:
|
||||
return it->second;
|
||||
return nullptr;
|
||||
}
|
||||
void setRootMotionCharacter(JPH::BodyID id, bool enabled)
|
||||
{
|
||||
(void)id;
|
||||
(void)enabled;
|
||||
/* No longer needed - root motion drives physics velocity,
|
||||
* and physics writes position back to scene node normally. */
|
||||
}
|
||||
};
|
||||
|
||||
void physics()
|
||||
@@ -1976,5 +1983,10 @@ JoltPhysicsWrapper::getSceneNodeFromBodyID(JPH::BodyID id) const
|
||||
return phys->getSceneNodeFromBodyID(id);
|
||||
}
|
||||
|
||||
void JoltPhysicsWrapper::setRootMotionCharacter(JPH::BodyID id, bool enabled)
|
||||
{
|
||||
phys->setRootMotionCharacter(id, enabled);
|
||||
}
|
||||
|
||||
template <>
|
||||
JoltPhysicsWrapper *Ogre::Singleton<JoltPhysicsWrapper>::msSingleton = 0;
|
||||
|
||||
@@ -230,5 +230,12 @@ public:
|
||||
bool bodyIsCharacter(JPH::BodyID id) const;
|
||||
void destroyCharacter(std::shared_ptr<JPH::Character> ch);
|
||||
Ogre::SceneNode *getSceneNodeFromBodyID(JPH::BodyID id) const;
|
||||
|
||||
/* Mark a character body as root-motion-driven.
|
||||
* When true, Physics::update() will NOT write the character's
|
||||
* position back to the scene node after the physics step,
|
||||
* because the scene node position is driven by root motion
|
||||
* from AnimationTreeSystem. */
|
||||
void setRootMotionCharacter(JPH::BodyID id, bool enabled);
|
||||
};
|
||||
#endif
|
||||
|
||||
538
src/features/editScene/systems/ActuatorSystem.cpp
Normal file
538
src/features/editScene/systems/ActuatorSystem.cpp
Normal file
@@ -0,0 +1,538 @@
|
||||
#include "ActuatorSystem.hpp"
|
||||
#include "../EditorApp.hpp"
|
||||
#include "BehaviorTreeSystem.hpp"
|
||||
#include "ItemSystem.hpp"
|
||||
#include "../components/Actuator.hpp"
|
||||
#include "../components/Item.hpp"
|
||||
#include "../components/Inventory.hpp"
|
||||
#include "../components/PlayerController.hpp"
|
||||
#include "../components/Transform.hpp"
|
||||
#include "../components/Character.hpp"
|
||||
#include "../components/ActionDatabase.hpp"
|
||||
#include "../components/EntityName.hpp"
|
||||
#include "../camera/EditorCamera.hpp"
|
||||
#include <OgreCamera.h>
|
||||
#include <OgreSceneNode.h>
|
||||
#include <OgreLogManager.h>
|
||||
#include <imgui.h>
|
||||
#include <cmath>
|
||||
|
||||
ActuatorSystem::ActuatorSystem(flecs::world &world,
|
||||
Ogre::SceneManager *sceneMgr,
|
||||
EditorApp *editorApp,
|
||||
BehaviorTreeSystem *btSystem)
|
||||
: m_world(world)
|
||||
, m_sceneMgr(sceneMgr)
|
||||
, m_editorApp(editorApp)
|
||||
, m_btSystem(btSystem)
|
||||
{
|
||||
}
|
||||
|
||||
ActuatorSystem::~ActuatorSystem() = default;
|
||||
|
||||
Ogre::Vector2 ActuatorSystem::projectToScreen(const Ogre::Vector3 &worldPoint)
|
||||
{
|
||||
if (!m_editorApp)
|
||||
return Ogre::Vector2(-1, -1);
|
||||
|
||||
EditorCamera *editorCam = m_editorApp->getEditorCamera();
|
||||
if (!editorCam)
|
||||
return Ogre::Vector2(-1, -1);
|
||||
Ogre::Camera *camera = editorCam->getCamera();
|
||||
if (!camera)
|
||||
return Ogre::Vector2(-1, -1);
|
||||
|
||||
ImVec2 vpSize = ImGui::GetMainViewport()->Size;
|
||||
float width = vpSize.x;
|
||||
float height = vpSize.y;
|
||||
|
||||
// Convert to camera space
|
||||
Ogre::Vector3 eyeSpacePoint = camera->getViewMatrix() * worldPoint;
|
||||
|
||||
// Project to clip space
|
||||
Ogre::Vector3 clipSpacePoint =
|
||||
camera->getProjectionMatrix() * eyeSpacePoint;
|
||||
if (clipSpacePoint.z < 0.0f)
|
||||
return Ogre::Vector2(-1, -1);
|
||||
|
||||
// Convert from clip space (-1 to 1) to screen space (0 to 1)
|
||||
float screenX = (clipSpacePoint.x / 2.0f) + 0.5f;
|
||||
float screenY = 1.0f - ((clipSpacePoint.y / 2.0f) + 0.5f);
|
||||
|
||||
// Map to actual pixel dimensions
|
||||
return Ogre::Vector2(screenX * width, screenY * height);
|
||||
}
|
||||
|
||||
bool ActuatorSystem::isInRange(const Ogre::Vector3 &charPos,
|
||||
const Ogre::Vector3 &objPos, float radius,
|
||||
float height)
|
||||
{
|
||||
Ogre::Vector3 diff = charPos - objPos;
|
||||
float xzDist = std::sqrt(diff.x * diff.x + diff.z * diff.z);
|
||||
if (xzDist > radius)
|
||||
return false;
|
||||
float yDiff = std::abs(diff.y);
|
||||
if (yDiff > height)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void ActuatorSystem::setPlayerInputLocked(bool locked)
|
||||
{
|
||||
m_world.query<PlayerControllerComponent>().each(
|
||||
[&](flecs::entity, PlayerControllerComponent &pc) {
|
||||
pc.inputLocked = locked;
|
||||
});
|
||||
}
|
||||
|
||||
void ActuatorSystem::executeAction(flecs::entity character,
|
||||
flecs::entity actuatorEntity,
|
||||
const Ogre::String &actionName)
|
||||
{
|
||||
if (!character.is_alive() || !actuatorEntity.is_alive())
|
||||
return;
|
||||
|
||||
if (!actuatorEntity.has<ActuatorComponent>())
|
||||
return;
|
||||
|
||||
auto &actuator = actuatorEntity.get_mut<ActuatorComponent>();
|
||||
actuator.isExecuting = true;
|
||||
|
||||
m_executingActuatorId = actuatorEntity.id();
|
||||
m_executingCharacterId = character.id();
|
||||
m_executingActionName = actionName;
|
||||
m_actionFirstFrame = true;
|
||||
|
||||
// Lock player input while action executes
|
||||
setPlayerInputLocked(true);
|
||||
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"[ActuatorSystem] Executing action: " + actionName);
|
||||
}
|
||||
|
||||
bool ActuatorSystem::isActionComplete(flecs::entity character, float deltaTime)
|
||||
{
|
||||
if (!m_btSystem || !character.is_alive())
|
||||
return true;
|
||||
|
||||
if (m_executingActionName.empty())
|
||||
return true;
|
||||
|
||||
// Look up the action in the singleton database to get its behavior tree
|
||||
ActionDatabase *db = ActionDatabase::getSingletonPtr();
|
||||
if (!db)
|
||||
return true;
|
||||
|
||||
const GoapAction *action = db->findAction(m_executingActionName);
|
||||
if (!action)
|
||||
return true;
|
||||
|
||||
// Evaluate the behavior tree directly (no ActionDebug)
|
||||
auto status = m_btSystem->evaluatePlayerAction(character.id(),
|
||||
action->behaviorTree,
|
||||
deltaTime,
|
||||
m_actionFirstFrame);
|
||||
m_actionFirstFrame = false;
|
||||
|
||||
return status != BehaviorTreeSystem::Status::running;
|
||||
}
|
||||
|
||||
void ActuatorSystem::drawActionMenu(flecs::entity actuatorEntity)
|
||||
{
|
||||
if (!actuatorEntity.is_alive() ||
|
||||
!actuatorEntity.has<ActuatorComponent>())
|
||||
return;
|
||||
|
||||
auto &actuator = actuatorEntity.get<ActuatorComponent>();
|
||||
|
||||
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
|
||||
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing,
|
||||
ImVec2(0.5f, 0.5f));
|
||||
ImGui::SetNextWindowSize(ImVec2(300, 0), ImGuiCond_Appearing);
|
||||
|
||||
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize |
|
||||
ImGuiWindowFlags_NoCollapse |
|
||||
ImGuiWindowFlags_AlwaysAutoResize;
|
||||
|
||||
if (ImGui::Begin("Select Action", nullptr, flags)) {
|
||||
ImGui::Text("Select an action:");
|
||||
ImGui::Separator();
|
||||
|
||||
for (const auto &name : actuator.actionNames) {
|
||||
if (name.empty())
|
||||
continue;
|
||||
if (ImGui::Button(
|
||||
name.c_str(),
|
||||
ImVec2(ImGui::GetContentRegionAvail().x,
|
||||
0))) {
|
||||
m_pendingActionName = name;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
if (ImGui::Button("Cancel", ImVec2(120, 0))) {
|
||||
m_menuOpen = false;
|
||||
m_menuActuatorId = 0;
|
||||
m_eHoldTime = 0.0f;
|
||||
m_eWasHeld = false;
|
||||
if (m_editorApp)
|
||||
m_editorApp->setWindowGrab(true);
|
||||
}
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
void ActuatorSystem::update(float deltaTime)
|
||||
{
|
||||
// Update cooldown timers for all actuators
|
||||
m_world.query<ActuatorComponent>().each(
|
||||
[&](flecs::entity, ActuatorComponent &actuator) {
|
||||
if (actuator.cooldownTimer > 0.0f) {
|
||||
actuator.cooldownTimer -= deltaTime;
|
||||
if (actuator.cooldownTimer < 0.0f)
|
||||
actuator.cooldownTimer = 0.0f;
|
||||
}
|
||||
});
|
||||
|
||||
// Only run in game mode while playing
|
||||
if (!m_editorApp ||
|
||||
m_editorApp->getGameMode() != EditorApp::GameMode::Game ||
|
||||
m_editorApp->getGamePlayState() !=
|
||||
EditorApp::GamePlayState::Playing)
|
||||
return;
|
||||
|
||||
// Check if an action is currently executing
|
||||
if (m_executingActuatorId != 0) {
|
||||
flecs::entity character =
|
||||
m_world.entity(m_executingCharacterId);
|
||||
if (isActionComplete(character, deltaTime)) {
|
||||
// Action finished - start cooldown
|
||||
flecs::entity actuator =
|
||||
m_world.entity(m_executingActuatorId);
|
||||
if (actuator.is_alive() &&
|
||||
actuator.has<ActuatorComponent>()) {
|
||||
auto &ac =
|
||||
actuator.get_mut<ActuatorComponent>();
|
||||
ac.isExecuting = false;
|
||||
float cooldown = 1.5f;
|
||||
m_world.query<PlayerControllerComponent>().each(
|
||||
[&](flecs::entity,
|
||||
PlayerControllerComponent &pc) {
|
||||
cooldown = pc.actuatorCooldown;
|
||||
});
|
||||
ac.cooldownTimer = cooldown;
|
||||
}
|
||||
m_executingActuatorId = 0;
|
||||
m_executingCharacterId = 0;
|
||||
m_executingActionName.clear();
|
||||
|
||||
// Unlock player input
|
||||
setPlayerInputLocked(false);
|
||||
}
|
||||
// Don't collect or draw anything while executing
|
||||
m_visibleActuators.clear();
|
||||
m_targetIndex = -1;
|
||||
m_labelText.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// Find player character
|
||||
flecs::entity playerCharacter = flecs::entity::null();
|
||||
float actuatorDistance = 25.0f;
|
||||
float actuatorCooldown = 1.5f;
|
||||
m_circleColor = ImVec4(0.0f, 0.4f, 1.0f, 1.0f);
|
||||
m_distantRadius = 8.0f;
|
||||
m_nearRadius = 14.0f;
|
||||
m_labelFontSize = 14.0f;
|
||||
|
||||
m_world.query<PlayerControllerComponent>().each([&](flecs::entity e,
|
||||
PlayerControllerComponent
|
||||
&pc) {
|
||||
(void)e;
|
||||
if (!playerCharacter.is_alive()) {
|
||||
m_world.query<EntityNameComponent>().each(
|
||||
[&](flecs::entity ec, EntityNameComponent &en) {
|
||||
if (!playerCharacter.is_alive() &&
|
||||
en.name == pc.targetCharacterName)
|
||||
playerCharacter = ec;
|
||||
});
|
||||
actuatorDistance = pc.actuatorDistance;
|
||||
actuatorCooldown = pc.actuatorCooldown;
|
||||
m_circleColor = ImVec4(pc.actuatorColor.x,
|
||||
pc.actuatorColor.y,
|
||||
pc.actuatorColor.z, 1.0f);
|
||||
m_distantRadius = pc.distantCircleRadius;
|
||||
m_nearRadius = pc.nearCircleRadius;
|
||||
m_labelFontSize = pc.actuatorLabelFontSize;
|
||||
}
|
||||
});
|
||||
|
||||
if (!playerCharacter.is_alive() ||
|
||||
!playerCharacter.has<TransformComponent>())
|
||||
return;
|
||||
|
||||
Ogre::Vector3 charPos = playerCharacter.get<TransformComponent>()
|
||||
.node->_getDerivedPosition();
|
||||
|
||||
// Collect actuators within distance
|
||||
m_visibleActuators.clear();
|
||||
std::vector<ScreenActuator> inRangeActuators;
|
||||
|
||||
// --- Collect ActuatorComponent entities ---
|
||||
m_world.query<ActuatorComponent, TransformComponent>().each(
|
||||
[&](flecs::entity e, ActuatorComponent &actuator,
|
||||
TransformComponent &trans) {
|
||||
// Skip if on cooldown or executing
|
||||
if (actuator.cooldownTimer > 0.0f ||
|
||||
actuator.isExecuting)
|
||||
return;
|
||||
|
||||
if (!trans.node)
|
||||
return;
|
||||
|
||||
Ogre::Vector3 objPos =
|
||||
trans.node->_getDerivedPosition();
|
||||
float dist = charPos.distance(objPos);
|
||||
if (dist > actuatorDistance)
|
||||
return;
|
||||
|
||||
Ogre::Vector2 screenPos = projectToScreen(objPos);
|
||||
if (screenPos.x < 0)
|
||||
return;
|
||||
|
||||
ScreenActuator sa;
|
||||
sa.entity = e;
|
||||
sa.screenPos = screenPos;
|
||||
sa.distance = dist;
|
||||
ImVec2 vpSize = ImGui::GetMainViewport()->Size;
|
||||
sa.distToScreenCenter =
|
||||
std::abs(screenPos.x - vpSize.x * 0.5f);
|
||||
|
||||
m_visibleActuators.push_back(sa);
|
||||
|
||||
if (isInRange(charPos, objPos, actuator.radius,
|
||||
actuator.height)) {
|
||||
inRangeActuators.push_back(sa);
|
||||
}
|
||||
});
|
||||
|
||||
// --- Collect ItemComponent entities (no ActuatorComponent) ---
|
||||
m_world.query<ItemComponent, TransformComponent>().each(
|
||||
[&](flecs::entity e, ItemComponent &item,
|
||||
TransformComponent &trans) {
|
||||
// Skip items that also have an ActuatorComponent
|
||||
// (those are handled above as actuators)
|
||||
if (e.has<ActuatorComponent>())
|
||||
return;
|
||||
|
||||
if (!trans.node)
|
||||
return;
|
||||
|
||||
Ogre::Vector3 objPos =
|
||||
trans.node->_getDerivedPosition();
|
||||
float dist = charPos.distance(objPos);
|
||||
if (dist > actuatorDistance)
|
||||
return;
|
||||
|
||||
Ogre::Vector2 screenPos = projectToScreen(objPos);
|
||||
if (screenPos.x < 0)
|
||||
return;
|
||||
|
||||
ScreenActuator sa;
|
||||
sa.entity = e;
|
||||
sa.screenPos = screenPos;
|
||||
sa.distance = dist;
|
||||
ImVec2 vpSize = ImGui::GetMainViewport()->Size;
|
||||
sa.distToScreenCenter =
|
||||
std::abs(screenPos.x - vpSize.x * 0.5f);
|
||||
|
||||
m_visibleActuators.push_back(sa);
|
||||
|
||||
// Items use a default pickup range (same as actuator defaults)
|
||||
if (isInRange(charPos, objPos, 1.5f, 1.8f)) {
|
||||
inRangeActuators.push_back(sa);
|
||||
}
|
||||
});
|
||||
|
||||
// Determine target actuator for interaction
|
||||
m_targetIndex = -1;
|
||||
if (!inRangeActuators.empty()) {
|
||||
size_t bestIdx = 0;
|
||||
for (size_t i = 1; i < inRangeActuators.size(); i++) {
|
||||
if (inRangeActuators[i].distToScreenCenter <
|
||||
inRangeActuators[bestIdx].distToScreenCenter) {
|
||||
bestIdx = i;
|
||||
} else if (inRangeActuators[i].distToScreenCenter ==
|
||||
inRangeActuators[bestIdx].distToScreenCenter) {
|
||||
if (inRangeActuators[i].screenPos.y >
|
||||
inRangeActuators[bestIdx].screenPos.y)
|
||||
bestIdx = i;
|
||||
}
|
||||
}
|
||||
// Find the index in m_visibleActuators
|
||||
for (size_t i = 0; i < m_visibleActuators.size(); i++) {
|
||||
if (m_visibleActuators[i].entity.id() ==
|
||||
inRangeActuators[bestIdx].entity.id()) {
|
||||
m_targetIndex = static_cast<int>(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build label text
|
||||
m_labelText.clear();
|
||||
if (m_targetIndex >= 0) {
|
||||
flecs::entity targetEntity =
|
||||
m_visibleActuators[m_targetIndex].entity;
|
||||
if (targetEntity.is_alive()) {
|
||||
if (targetEntity.has<ActuatorComponent>()) {
|
||||
auto &actuator =
|
||||
targetEntity.get<ActuatorComponent>();
|
||||
if (actuator.actionNames.size() == 1 &&
|
||||
!actuator.actionNames[0].empty()) {
|
||||
m_labelText =
|
||||
"E " + actuator.actionNames[0];
|
||||
} else if (actuator.actionNames.size() > 1) {
|
||||
m_labelText = "E";
|
||||
}
|
||||
} else if (targetEntity.has<ItemComponent>()) {
|
||||
// Show "E - Pick up [ItemName]" for items
|
||||
auto &item = targetEntity.get<ItemComponent>();
|
||||
m_labelText = "E Pick up " + item.itemName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle input
|
||||
GameInputState &input = m_editorApp->getGameInputState();
|
||||
|
||||
// Handle menu state first
|
||||
if (m_menuOpen) {
|
||||
flecs::entity menuActuator = m_world.entity(m_menuActuatorId);
|
||||
// Menu rendering happens in render()
|
||||
|
||||
if (!m_pendingActionName.empty()) {
|
||||
executeAction(playerCharacter, menuActuator,
|
||||
m_pendingActionName);
|
||||
m_pendingActionName.clear();
|
||||
m_menuOpen = false;
|
||||
m_menuActuatorId = 0;
|
||||
m_eHoldTime = 0.0f;
|
||||
m_eWasHeld = false;
|
||||
if (m_editorApp)
|
||||
m_editorApp->setWindowGrab(true);
|
||||
}
|
||||
|
||||
if (!input.e && m_eWasHeld) {
|
||||
m_menuOpen = false;
|
||||
m_menuActuatorId = 0;
|
||||
m_eHoldTime = 0.0f;
|
||||
m_eWasHeld = false;
|
||||
if (m_editorApp)
|
||||
m_editorApp->setWindowGrab(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle E key
|
||||
if (m_targetIndex >= 0 && input.e) {
|
||||
flecs::entity targetEntity =
|
||||
m_visibleActuators[m_targetIndex].entity;
|
||||
|
||||
if (targetEntity.has<ActuatorComponent>()) {
|
||||
auto &actuator = targetEntity.get<ActuatorComponent>();
|
||||
m_eHoldTime += deltaTime;
|
||||
|
||||
if (actuator.actionNames.size() == 1 &&
|
||||
!actuator.actionNames[0].empty()) {
|
||||
if (input.ePressed) {
|
||||
executeAction(playerCharacter,
|
||||
targetEntity,
|
||||
actuator.actionNames[0]);
|
||||
m_eHoldTime = 0.0f;
|
||||
}
|
||||
} else if (actuator.actionNames.size() > 1) {
|
||||
if (m_eHoldTime > 0.3f && !m_menuOpen) {
|
||||
m_menuOpen = true;
|
||||
m_menuActuatorId = targetEntity.id();
|
||||
m_eWasHeld = true;
|
||||
if (m_editorApp)
|
||||
m_editorApp->setWindowGrab(
|
||||
false);
|
||||
}
|
||||
}
|
||||
} else if (targetEntity.has<ItemComponent>() &&
|
||||
input.ePressed) {
|
||||
// Pick up item
|
||||
if (m_itemSystem) {
|
||||
m_itemSystem->pickupItem(playerCharacter,
|
||||
targetEntity);
|
||||
}
|
||||
m_eHoldTime = 0.0f;
|
||||
}
|
||||
} else {
|
||||
m_eHoldTime = 0.0f;
|
||||
m_eWasHeld = false;
|
||||
}
|
||||
}
|
||||
|
||||
void ActuatorSystem::render()
|
||||
{
|
||||
// Only run in game mode while playing
|
||||
if (!m_editorApp ||
|
||||
m_editorApp->getGameMode() != EditorApp::GameMode::Game ||
|
||||
m_editorApp->getGamePlayState() !=
|
||||
EditorApp::GamePlayState::Playing)
|
||||
return;
|
||||
|
||||
ImDrawList *drawList = ImGui::GetBackgroundDrawList();
|
||||
if (!drawList)
|
||||
return;
|
||||
|
||||
ImColor defaultColor(m_circleColor);
|
||||
ImColor targetColor(ImVec4(std::min(m_circleColor.x + 0.2f, 1.0f),
|
||||
std::min(m_circleColor.y + 0.3f, 1.0f),
|
||||
std::min(m_circleColor.z + 0.0f, 1.0f),
|
||||
1.0f));
|
||||
|
||||
for (size_t i = 0; i < m_visibleActuators.size(); i++) {
|
||||
bool isTarget = (static_cast<int>(i) == m_targetIndex);
|
||||
float circleRadius = isTarget ? m_nearRadius : m_distantRadius;
|
||||
ImColor circleCol = isTarget ? targetColor : defaultColor;
|
||||
|
||||
ImVec2 center(m_visibleActuators[i].screenPos.x,
|
||||
m_visibleActuators[i].screenPos.y);
|
||||
drawList->AddCircleFilled(center, circleRadius, circleCol);
|
||||
drawList->AddCircle(center, circleRadius,
|
||||
IM_COL32(255, 255, 255, 180), 0, 2.0f);
|
||||
}
|
||||
|
||||
// Draw label for target
|
||||
if (m_targetIndex >= 0 && !m_labelText.empty()) {
|
||||
float circleRadius = m_nearRadius;
|
||||
ImVec2 center(m_visibleActuators[m_targetIndex].screenPos.x,
|
||||
m_visibleActuators[m_targetIndex].screenPos.y);
|
||||
|
||||
ImFont *font = ImGui::GetFont();
|
||||
ImVec2 textSize = font->CalcTextSizeA(
|
||||
m_labelFontSize, FLT_MAX, -1.0f, m_labelText.c_str());
|
||||
|
||||
ImVec2 textPos(center.x - (textSize.x * 0.5f),
|
||||
center.y + circleRadius + 6.0f);
|
||||
|
||||
// Shadow
|
||||
drawList->AddText(font, m_labelFontSize,
|
||||
ImVec2(textPos.x + 1, textPos.y + 1),
|
||||
IM_COL32(0, 0, 0, 200), m_labelText.c_str());
|
||||
// Text
|
||||
drawList->AddText(font, m_labelFontSize, textPos,
|
||||
IM_COL32(255, 255, 255, 255),
|
||||
m_labelText.c_str());
|
||||
}
|
||||
|
||||
// Draw action menu if open
|
||||
if (m_menuOpen) {
|
||||
flecs::entity menuActuator = m_world.entity(m_menuActuatorId);
|
||||
drawActionMenu(menuActuator);
|
||||
}
|
||||
}
|
||||
98
src/features/editScene/systems/ActuatorSystem.hpp
Normal file
98
src/features/editScene/systems/ActuatorSystem.hpp
Normal file
@@ -0,0 +1,98 @@
|
||||
#ifndef EDITSCENE_ACTUATOR_SYSTEM_HPP
|
||||
#define EDITSCENE_ACTUATOR_SYSTEM_HPP
|
||||
#pragma once
|
||||
|
||||
#include <flecs.h>
|
||||
#include <Ogre.h>
|
||||
#include <imgui.h>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
class EditorApp;
|
||||
class BehaviorTreeSystem;
|
||||
class ItemSystem;
|
||||
|
||||
/**
|
||||
* System that handles player interaction with Actuator entities
|
||||
* and Item entities.
|
||||
*
|
||||
* In game mode:
|
||||
* - update() finds nearby actuators and items, handles input
|
||||
* - render() draws on-screen circle markers (called in preViewportUpdate after NewFrame)
|
||||
* - Shows "E - ActionName" (or just "E" for multi-action) when in reach
|
||||
* - Shows "E - Pick up [ItemName]" for items
|
||||
* - Handles E press / hold for action activation
|
||||
* - Manages per-actuator cooldowns
|
||||
* - Executes actions via BehaviorTreeSystem directly (no ActionDebug)
|
||||
* - Disables player controls during action execution
|
||||
*
|
||||
* Items (entities with ItemComponent but no ActuatorComponent) are
|
||||
* detected automatically and shown with a "Pick up" prompt. On E press,
|
||||
* the item is added to the player's inventory via ItemSystem.
|
||||
*/
|
||||
class ActuatorSystem {
|
||||
public:
|
||||
ActuatorSystem(flecs::world &world, Ogre::SceneManager *sceneMgr,
|
||||
EditorApp *editorApp, BehaviorTreeSystem *btSystem);
|
||||
~ActuatorSystem();
|
||||
|
||||
/**
|
||||
* Set the ItemSystem for item pickup handling.
|
||||
* Must be called after construction.
|
||||
*/
|
||||
void setItemSystem(ItemSystem *system)
|
||||
{
|
||||
m_itemSystem = system;
|
||||
}
|
||||
|
||||
void update(float deltaTime);
|
||||
void render();
|
||||
|
||||
private:
|
||||
struct ScreenActuator {
|
||||
flecs::entity entity;
|
||||
Ogre::Vector2 screenPos;
|
||||
float distance;
|
||||
float distToScreenCenter;
|
||||
};
|
||||
|
||||
Ogre::Vector2 projectToScreen(const Ogre::Vector3 &worldPoint);
|
||||
bool isInRange(const Ogre::Vector3 &charPos,
|
||||
const Ogre::Vector3 &objPos, float radius, float height);
|
||||
void executeAction(flecs::entity character,
|
||||
flecs::entity actuatorEntity,
|
||||
const Ogre::String &actionName);
|
||||
bool isActionComplete(flecs::entity character, float deltaTime);
|
||||
void drawActionMenu(flecs::entity actuatorEntity);
|
||||
void setPlayerInputLocked(bool locked);
|
||||
|
||||
flecs::world &m_world;
|
||||
Ogre::SceneManager *m_sceneMgr;
|
||||
EditorApp *m_editorApp;
|
||||
BehaviorTreeSystem *m_btSystem;
|
||||
ItemSystem *m_itemSystem = nullptr;
|
||||
|
||||
// Cached data between update() and render()
|
||||
std::vector<ScreenActuator> m_visibleActuators;
|
||||
int m_targetIndex = -1;
|
||||
Ogre::String m_labelText;
|
||||
ImVec4 m_circleColor;
|
||||
float m_distantRadius = 8.0f;
|
||||
float m_nearRadius = 14.0f;
|
||||
float m_labelFontSize = 14.0f;
|
||||
|
||||
// Multi-action menu state
|
||||
bool m_menuOpen = false;
|
||||
flecs::entity_t m_menuActuatorId = 0;
|
||||
float m_eHoldTime = 0.0f;
|
||||
bool m_eWasHeld = false;
|
||||
Ogre::String m_pendingActionName;
|
||||
|
||||
// Currently executing action state
|
||||
flecs::entity_t m_executingActuatorId = 0;
|
||||
flecs::entity_t m_executingCharacterId = 0;
|
||||
Ogre::String m_executingActionName;
|
||||
bool m_actionFirstFrame = false;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_ACTUATOR_SYSTEM_HPP
|
||||
@@ -300,6 +300,12 @@ void AnimationTreeSystem::update(float deltaTime)
|
||||
flecs::entity e,
|
||||
AnimationTreeComponent
|
||||
&at) {
|
||||
/* Reset root-motion velocity for all entities.
|
||||
* Root motion velocity will be set below only for
|
||||
* entities with active root motion. */
|
||||
if (e.has<CharacterComponent>())
|
||||
e.get_mut<CharacterComponent>().linearVelocity =
|
||||
Ogre::Vector3::ZERO;
|
||||
resolveTemplate(at);
|
||||
|
||||
if (at.dirty) {
|
||||
@@ -369,39 +375,58 @@ void AnimationTreeSystem::update(float deltaTime)
|
||||
info.ogreAnimState->addTime(data.timeDelta);
|
||||
float thisTime =
|
||||
info.ogreAnimState->getTimePosition();
|
||||
float length = info.ogreAnimState->getLength();
|
||||
bool loop = info.ogreAnimState->getLoop();
|
||||
|
||||
int loops = 0;
|
||||
if (loop && length > 0.0f) {
|
||||
loops = (int)std::round(
|
||||
(lastTime + data.timeDelta -
|
||||
thisTime) /
|
||||
length);
|
||||
}
|
||||
|
||||
if (at.useRootMotion && info.rootTrack) {
|
||||
Ogre::TransformKeyFrame tkf(nullptr,
|
||||
0.0f);
|
||||
info.rootTrack->getInterpolatedKeyFrame(
|
||||
lastTime, &tkf);
|
||||
Ogre::Vector3 lastPos =
|
||||
tkf.getTranslate();
|
||||
info.rootTrack->getInterpolatedKeyFrame(
|
||||
thisTime, &tkf);
|
||||
Ogre::Vector3 thisPos =
|
||||
tkf.getTranslate();
|
||||
Ogre::Vector3 delta =
|
||||
thisPos - lastPos +
|
||||
loops * info.loopTranslation;
|
||||
|
||||
Ogre::Vector3 delta;
|
||||
if (info.hasPrevRootPos) {
|
||||
/*
|
||||
* Compute delta from previous
|
||||
* root bone position. Detect
|
||||
* animation wrapping by checking
|
||||
* if time decreased (wrapped
|
||||
* around). When wrapping, add
|
||||
* the loop translation to
|
||||
* compensate for the jump from
|
||||
* end back to start.
|
||||
*/
|
||||
delta = thisPos -
|
||||
info.prevRootPos;
|
||||
if (thisTime < lastTime) {
|
||||
/* Animation wrapped */
|
||||
delta +=
|
||||
info.loopTranslation;
|
||||
}
|
||||
} else {
|
||||
delta = Ogre::Vector3::ZERO;
|
||||
}
|
||||
info.prevRootPos = thisPos;
|
||||
info.hasPrevRootPos = true;
|
||||
|
||||
totalRootMotion += delta * data.weight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (at.useRootMotion && sceneNode) {
|
||||
/*
|
||||
* Compute root motion velocity from the animation
|
||||
* displacement. Do NOT move the scene node directly -
|
||||
* the physics character's velocity drives movement,
|
||||
* and physics writes the position back to the scene
|
||||
* node naturally. This avoids jitter caused by
|
||||
* teleporting the physics character to match a
|
||||
* root-motion-driven scene node position.
|
||||
*/
|
||||
if (e.has<CharacterComponent>()) {
|
||||
auto &cc = e.get_mut<CharacterComponent>();
|
||||
cc.useRootMotion = true;
|
||||
if (deltaTime > 0.0000001f) {
|
||||
float safeDelta = Ogre::Math::Clamp(
|
||||
deltaTime, 0.005f, 0.99f);
|
||||
@@ -421,9 +446,6 @@ void AnimationTreeSystem::update(float deltaTime)
|
||||
cc.linearVelocity.y, -10.5f,
|
||||
10.0f);
|
||||
}
|
||||
} else {
|
||||
sceneNode->translate(totalRootMotion,
|
||||
Ogre::Node::TS_LOCAL);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -637,9 +659,18 @@ void AnimationTreeSystem::setStateInternal(flecs::entity e,
|
||||
auto itAnim = state.animations.find(
|
||||
animNode->animationName);
|
||||
if (itAnim != state.animations.end() &&
|
||||
itAnim->second.ogreAnimState)
|
||||
itAnim->second.ogreAnimState) {
|
||||
itAnim->second.ogreAnimState
|
||||
->setTimePosition(0.0f);
|
||||
/* Reset root motion tracking
|
||||
* so the first frame of the
|
||||
* new animation doesn't
|
||||
* produce a large delta from
|
||||
* the previous animation's
|
||||
* root position. */
|
||||
itAnim->second.hasPrevRootPos =
|
||||
false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,11 @@ private:
|
||||
Ogre::AnimationState *ogreAnimState = nullptr;
|
||||
Ogre::NodeAnimationTrack *rootTrack = nullptr;
|
||||
Ogre::Vector3 loopTranslation = Ogre::Vector3::ZERO;
|
||||
/* Previous root bone position for root motion delta
|
||||
* computation. Used to detect animation wrapping and
|
||||
* compute smooth deltas across loop boundaries. */
|
||||
Ogre::Vector3 prevRootPos = Ogre::Vector3::ZERO;
|
||||
bool hasPrevRootPos = false;
|
||||
};
|
||||
|
||||
struct FadeInfo {
|
||||
@@ -67,6 +72,11 @@ private:
|
||||
Ogre::Quaternion rootBindingOrientation;
|
||||
Ogre::Vector3 rootBindingScale;
|
||||
|
||||
/* Root motion unapply/reapply state */
|
||||
Ogre::Vector3 appliedRootTranslation = Ogre::Vector3::ZERO;
|
||||
Ogre::Quaternion appliedRootRotation =
|
||||
Ogre::Quaternion::IDENTITY;
|
||||
|
||||
std::unordered_map<Ogre::String, AnimationRuntimeInfo>
|
||||
animations;
|
||||
std::unordered_map<Ogre::String, FadeInfo> fadeStates;
|
||||
|
||||
@@ -2,21 +2,123 @@
|
||||
#include "AnimationTreeSystem.hpp"
|
||||
#include "CharacterSystem.hpp"
|
||||
#include "SmartObjectSystem.hpp"
|
||||
#include "ItemSystem.hpp"
|
||||
#include "EventBus.hpp"
|
||||
#include "../components/BehaviorTree.hpp"
|
||||
#include "../components/ActionDatabase.hpp"
|
||||
#include "../components/GoapBlackboard.hpp"
|
||||
#include "../components/SmartObject.hpp"
|
||||
#include "../components/Transform.hpp"
|
||||
#include "../components/EntityName.hpp"
|
||||
#include "../components/Relationship.hpp"
|
||||
#include "../components/Character.hpp"
|
||||
#include "../components/Item.hpp"
|
||||
#include "../components/Inventory.hpp"
|
||||
#include <OgreLogManager.h>
|
||||
#include <iostream>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
/* Forward declaration for Lua behavior tree node support.
|
||||
* The actual implementation is in lua/LuaBehaviorTreeApi.cpp.
|
||||
* Returns: 0 = success, 1 = failure, 2 = running, -1 = not found/error.
|
||||
* We use a function pointer approach to avoid a hard dependency on
|
||||
* the Lua headers in this translation unit.
|
||||
*
|
||||
* The lua_State pointer is passed as void* to avoid needing the Lua
|
||||
* headers in this translation unit. */
|
||||
namespace editScene
|
||||
{
|
||||
int callLuaBehaviorTreeNode(void *L, const std::string &nodeName,
|
||||
flecs::entity entity, const std::string ¶ms);
|
||||
}
|
||||
|
||||
static float g_epsilon = 0.0001f;
|
||||
|
||||
static bool parseValueString(const Ogre::String &str, int &outInt,
|
||||
float &outFloat, Ogre::Vector3 &outVec3,
|
||||
int &type);
|
||||
|
||||
/** Parse "key=val,key2=val2" params into a GoapBlackboard.
|
||||
* Values are auto-detected: int, float, vec3 (x,y,z), or quoted string. */
|
||||
static void parseEventParams(const Ogre::String &str, GoapBlackboard &out)
|
||||
{
|
||||
if (str.empty())
|
||||
return;
|
||||
|
||||
const char *s = str.c_str();
|
||||
while (*s) {
|
||||
// Skip whitespace
|
||||
while (*s == ' ' || *s == '\t')
|
||||
s++;
|
||||
if (!*s)
|
||||
break;
|
||||
|
||||
// Find key
|
||||
const char *keyStart = s;
|
||||
while (*s && *s != '=' && *s != ',')
|
||||
s++;
|
||||
Ogre::String key(keyStart, static_cast<size_t>(s - keyStart));
|
||||
// Trim trailing spaces from key
|
||||
while (!key.empty() &&
|
||||
(key.back() == ' ' || key.back() == '\t'))
|
||||
key.pop_back();
|
||||
|
||||
if (*s != '=') {
|
||||
while (*s && *s != ',')
|
||||
s++;
|
||||
if (*s == ',')
|
||||
s++;
|
||||
continue;
|
||||
}
|
||||
s++; // skip '='
|
||||
|
||||
// Skip whitespace before value
|
||||
while (*s == ' ' || *s == '\t')
|
||||
s++;
|
||||
|
||||
// Find value end (next comma or end)
|
||||
const char *valStart = s;
|
||||
bool inQuotes = false;
|
||||
while (*s && (*s != ',' || inQuotes)) {
|
||||
if (*s == '"')
|
||||
inQuotes = !inQuotes;
|
||||
s++;
|
||||
}
|
||||
Ogre::String val(valStart, static_cast<size_t>(s - valStart));
|
||||
// Trim trailing spaces from value
|
||||
while (!val.empty() &&
|
||||
(val.back() == ' ' || val.back() == '\t'))
|
||||
val.pop_back();
|
||||
|
||||
// Strip quotes if present
|
||||
if (val.size() >= 2 && val.front() == '"' &&
|
||||
val.back() == '"') {
|
||||
val = val.substr(1, val.size() - 2);
|
||||
out.setStringValue(key, val);
|
||||
} else {
|
||||
// Try int/float/vec3
|
||||
int iVal;
|
||||
float fVal;
|
||||
Ogre::Vector3 vVal;
|
||||
int vType;
|
||||
if (parseValueString(val, iVal, fVal, vVal, vType)) {
|
||||
if (vType == 0)
|
||||
out.setValue(key, iVal);
|
||||
else if (vType == 1)
|
||||
out.setFloatValue(key, fVal);
|
||||
else
|
||||
out.setVec3Value(key, vVal);
|
||||
} else {
|
||||
// Fallback to string
|
||||
out.setStringValue(key, val);
|
||||
}
|
||||
}
|
||||
|
||||
if (*s == ',')
|
||||
s++;
|
||||
}
|
||||
}
|
||||
|
||||
static bool parseBitIndex(const Ogre::String &str, int &out)
|
||||
{
|
||||
// Try numeric first
|
||||
@@ -234,6 +336,15 @@ BehaviorTreeSystem::evaluateNode(const BehaviorTreeNode &node, flecs::entity e,
|
||||
return Status::success;
|
||||
}
|
||||
|
||||
if (node.type == "sendEvent") {
|
||||
if (isNewlyActive(state, &node)) {
|
||||
GoapBlackboard params;
|
||||
parseEventParams(node.params, params);
|
||||
EventBus::getInstance().send(node.name, params);
|
||||
}
|
||||
return Status::success;
|
||||
}
|
||||
|
||||
if (node.type == "delay") {
|
||||
char key[32];
|
||||
snprintf(key, sizeof(key), "%p", (void *)&node);
|
||||
@@ -481,6 +592,267 @@ BehaviorTreeSystem::evaluateNode(const BehaviorTreeNode &node, flecs::entity e,
|
||||
return Status::success;
|
||||
}
|
||||
|
||||
/* --- Item / Inventory nodes --- */
|
||||
/* These nodes need an ItemSystem instance. We look it up via
|
||||
* the EditorApp singleton pattern (stored in SmartObjectSystem). */
|
||||
ItemSystem *itemSystem = nullptr;
|
||||
{
|
||||
SmartObjectSystem *soSystem = SmartObjectSystem::getInstance();
|
||||
if (soSystem)
|
||||
itemSystem = soSystem->getItemSystem();
|
||||
}
|
||||
|
||||
if (node.type == "hasItem") {
|
||||
if (!itemSystem || !e.has<InventoryComponent>())
|
||||
return Status::failure;
|
||||
return itemSystem->hasItem(e, node.name) ? Status::success :
|
||||
Status::failure;
|
||||
}
|
||||
|
||||
if (node.type == "hasItemByName") {
|
||||
if (!itemSystem || !e.has<InventoryComponent>())
|
||||
return Status::failure;
|
||||
return itemSystem->hasItemByName(e, node.name) ?
|
||||
Status::success :
|
||||
Status::failure;
|
||||
}
|
||||
|
||||
if (node.type == "countItem") {
|
||||
if (!itemSystem || !e.has<InventoryComponent>())
|
||||
return Status::failure;
|
||||
int required = 1;
|
||||
if (!node.params.empty()) {
|
||||
char *end = nullptr;
|
||||
long val = strtol(node.params.c_str(), &end, 10);
|
||||
if (end != node.params.c_str() && *end == '\0' &&
|
||||
val > 0)
|
||||
required = static_cast<int>(val);
|
||||
}
|
||||
int count = itemSystem->countItem(e, node.name);
|
||||
return count >= required ? Status::success : Status::failure;
|
||||
}
|
||||
|
||||
if (node.type == "pickupItem") {
|
||||
if (isNewlyActive(state, &node)) {
|
||||
if (!itemSystem || !e.has<InventoryComponent>())
|
||||
return Status::failure;
|
||||
|
||||
// Find nearest ItemComponent entity matching the
|
||||
// optional itemId filter (node.name)
|
||||
flecs::entity nearestItem = flecs::entity::null();
|
||||
float nearestDist = std::numeric_limits<float>::max();
|
||||
|
||||
Ogre::Vector3 charPos = Ogre::Vector3::ZERO;
|
||||
if (e.has<TransformComponent>()) {
|
||||
auto &trans = e.get<TransformComponent>();
|
||||
if (trans.node)
|
||||
charPos =
|
||||
trans.node
|
||||
->_getDerivedPosition();
|
||||
else
|
||||
charPos = trans.position;
|
||||
}
|
||||
|
||||
m_world.query<ItemComponent, TransformComponent>().each(
|
||||
[&](flecs::entity itemEntity,
|
||||
ItemComponent &itemComp,
|
||||
TransformComponent &itemTrans) {
|
||||
// Skip items that are in containers
|
||||
// (they have no TransformComponent in world space)
|
||||
if (!itemTrans.node &&
|
||||
itemTrans.position ==
|
||||
Ogre::Vector3::ZERO)
|
||||
return;
|
||||
if (!node.name.empty() &&
|
||||
itemComp.itemId != node.name)
|
||||
return;
|
||||
|
||||
Ogre::Vector3 itemPos =
|
||||
Ogre::Vector3::ZERO;
|
||||
if (itemTrans.node)
|
||||
itemPos =
|
||||
itemTrans.node
|
||||
->_getDerivedPosition();
|
||||
else
|
||||
itemPos = itemTrans.position;
|
||||
|
||||
float dist = charPos.distance(itemPos);
|
||||
// Default pickup radius of 1.5 units
|
||||
if (dist < nearestDist && dist < 1.5f) {
|
||||
nearestDist = dist;
|
||||
nearestItem = itemEntity;
|
||||
}
|
||||
});
|
||||
|
||||
if (nearestItem.is_alive()) {
|
||||
itemSystem->addItemEntityToInventory(
|
||||
e, nearestItem);
|
||||
std::cout << "[BT] pickupItem: picked up "
|
||||
<< nearestItem.id() << std::endl;
|
||||
return Status::success;
|
||||
}
|
||||
return Status::failure;
|
||||
}
|
||||
return Status::success;
|
||||
}
|
||||
|
||||
if (node.type == "dropItem") {
|
||||
if (isNewlyActive(state, &node)) {
|
||||
if (!itemSystem || !e.has<InventoryComponent>())
|
||||
return Status::failure;
|
||||
|
||||
int count = 1;
|
||||
if (!node.params.empty()) {
|
||||
char *end = nullptr;
|
||||
long val =
|
||||
strtol(node.params.c_str(), &end, 10);
|
||||
if (end != node.params.c_str() &&
|
||||
*end == '\0' && val > 0)
|
||||
count = static_cast<int>(val);
|
||||
}
|
||||
|
||||
// Find the item in inventory
|
||||
int slotIdx = -1;
|
||||
auto &inv = e.get<InventoryComponent>();
|
||||
for (int i = 0; i < (int)inv.slots.size(); i++) {
|
||||
if (!inv.slots[i].isEmpty() &&
|
||||
inv.slots[i].itemId == node.name) {
|
||||
slotIdx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (slotIdx < 0)
|
||||
return Status::failure;
|
||||
|
||||
// Get drop position (in front of character)
|
||||
Ogre::Vector3 dropPos = Ogre::Vector3::ZERO;
|
||||
if (e.has<TransformComponent>()) {
|
||||
auto &trans = e.get<TransformComponent>();
|
||||
if (trans.node) {
|
||||
dropPos =
|
||||
trans.node
|
||||
->_getDerivedPosition();
|
||||
Ogre::Vector3 forward =
|
||||
trans.node
|
||||
->_getDerivedOrientation() *
|
||||
Ogre::Vector3(0, 0, -1);
|
||||
forward.y = 0;
|
||||
forward.normalise();
|
||||
dropPos += forward * 1.5f;
|
||||
} else {
|
||||
dropPos = trans.position;
|
||||
}
|
||||
}
|
||||
|
||||
itemSystem->dropItem(e, slotIdx, dropPos, count);
|
||||
std::cout << "[BT] dropItem: dropped " << node.name
|
||||
<< " x" << count << std::endl;
|
||||
return Status::success;
|
||||
}
|
||||
return Status::success;
|
||||
}
|
||||
|
||||
if (node.type == "useItem") {
|
||||
if (isNewlyActive(state, &node)) {
|
||||
if (!itemSystem || !e.has<InventoryComponent>())
|
||||
return Status::failure;
|
||||
|
||||
// Find the item in inventory
|
||||
int slotIdx = -1;
|
||||
auto &inv = e.get<InventoryComponent>();
|
||||
for (int i = 0; i < (int)inv.slots.size(); i++) {
|
||||
if (!inv.slots[i].isEmpty() &&
|
||||
inv.slots[i].itemId == node.name) {
|
||||
slotIdx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (slotIdx < 0)
|
||||
return Status::failure;
|
||||
|
||||
itemSystem->useItem(e, e, slotIdx);
|
||||
std::cout << "[BT] useItem: used " << node.name
|
||||
<< std::endl;
|
||||
return Status::success;
|
||||
}
|
||||
return Status::success;
|
||||
}
|
||||
|
||||
if (node.type == "addItemToInventory") {
|
||||
if (isNewlyActive(state, &node)) {
|
||||
if (!itemSystem || !e.has<InventoryComponent>())
|
||||
return Status::failure;
|
||||
|
||||
// Parse params: "itemId,itemName,itemType,count,weight,value"
|
||||
Ogre::String itemId = node.name;
|
||||
Ogre::String itemName = node.name;
|
||||
Ogre::String itemType = "misc";
|
||||
int count = 1;
|
||||
float weight = 0.1f;
|
||||
int value = 1;
|
||||
|
||||
if (!node.params.empty()) {
|
||||
// Parse comma-separated values
|
||||
std::vector<Ogre::String> parts;
|
||||
const char *s = node.params.c_str();
|
||||
const char *start = s;
|
||||
while (*s) {
|
||||
if (*s == ',') {
|
||||
parts.push_back(Ogre::String(
|
||||
start,
|
||||
static_cast<size_t>(
|
||||
s - start)));
|
||||
start = s + 1;
|
||||
}
|
||||
s++;
|
||||
}
|
||||
if (s > start)
|
||||
parts.push_back(Ogre::String(
|
||||
start, static_cast<size_t>(
|
||||
s - start)));
|
||||
|
||||
if (parts.size() >= 1)
|
||||
itemName = parts[0];
|
||||
if (parts.size() >= 2)
|
||||
itemType = parts[1];
|
||||
if (parts.size() >= 3) {
|
||||
char *end = nullptr;
|
||||
long val = strtol(parts[2].c_str(),
|
||||
&end, 10);
|
||||
if (end != parts[2].c_str() &&
|
||||
*end == '\0' && val > 0)
|
||||
count = static_cast<int>(val);
|
||||
}
|
||||
if (parts.size() >= 4) {
|
||||
char *end = nullptr;
|
||||
float val =
|
||||
strtof(parts[3].c_str(), &end);
|
||||
if (end != parts[3].c_str() &&
|
||||
*end == '\0' && val >= 0.0f)
|
||||
weight = val;
|
||||
}
|
||||
if (parts.size() >= 5) {
|
||||
char *end = nullptr;
|
||||
long val = strtol(parts[4].c_str(),
|
||||
&end, 10);
|
||||
if (end != parts[4].c_str() &&
|
||||
*end == '\0' && val >= 0)
|
||||
value = static_cast<int>(val);
|
||||
}
|
||||
}
|
||||
|
||||
itemSystem->addItemToInventory(e, itemId, itemName,
|
||||
itemType, count, weight,
|
||||
value);
|
||||
std::cout << "[BT] addItemToInventory: added "
|
||||
<< itemName << " x" << count << std::endl;
|
||||
return Status::success;
|
||||
}
|
||||
return Status::success;
|
||||
}
|
||||
|
||||
/* --- Teleport to Smart Object child node --- */
|
||||
if (node.type == "teleportToChild") {
|
||||
if (isNewlyActive(state, &node)) {
|
||||
@@ -615,6 +987,23 @@ BehaviorTreeSystem::evaluateNode(const BehaviorTreeNode &node, flecs::entity e,
|
||||
return Status::success;
|
||||
}
|
||||
|
||||
/* --- Lua behavior tree node --- */
|
||||
if (node.type == "luaTask") {
|
||||
/* Call the Lua function via the forward-declared API.
|
||||
* Returns: 0 = success, 1 = failure, 2 = running, -1 = error/not found */
|
||||
int result = editScene::callLuaBehaviorTreeNode(
|
||||
nullptr, node.name, e, node.params);
|
||||
if (result == 0) return Status::success;
|
||||
if (result == 1)
|
||||
return Status::failure;
|
||||
if (result == 2)
|
||||
return Status::running;
|
||||
/* -1 or other: log error and return failure */
|
||||
std::cout << "[BT] luaTask: node '" << node.name
|
||||
<< "' not registered or error" << std::endl;
|
||||
return Status::failure;
|
||||
}
|
||||
|
||||
return Status::success;
|
||||
}
|
||||
|
||||
@@ -636,12 +1025,7 @@ void BehaviorTreeSystem::update(float deltaTime)
|
||||
});
|
||||
|
||||
/* --- ActionDebug test runs --- */
|
||||
ActionDatabase *db = nullptr;
|
||||
m_world.query<ActionDatabase>().each(
|
||||
[&](flecs::entity, ActionDatabase &database) {
|
||||
if (!db)
|
||||
db = &database;
|
||||
});
|
||||
ActionDatabase *db = ActionDatabase::getSingletonPtr();
|
||||
if (!db)
|
||||
return;
|
||||
|
||||
@@ -675,3 +1059,24 @@ void BehaviorTreeSystem::update(float deltaTime)
|
||||
debug.runTimer += deltaTime;
|
||||
});
|
||||
}
|
||||
|
||||
BehaviorTreeSystem::Status
|
||||
BehaviorTreeSystem::evaluatePlayerAction(flecs::entity_t id,
|
||||
const BehaviorTreeNode &root,
|
||||
float deltaTime, bool reset)
|
||||
{
|
||||
auto &state = m_playerActionStates[id];
|
||||
if (reset) {
|
||||
state.lastActiveLeaves.clear();
|
||||
state.firstRun = true;
|
||||
state.nodeTimers.clear();
|
||||
state.treeResult = Status::running;
|
||||
}
|
||||
state.currentActiveLeaves.clear();
|
||||
Status result =
|
||||
evaluateNode(root, m_world.entity(id), state, deltaTime);
|
||||
state.lastActiveLeaves = state.currentActiveLeaves;
|
||||
state.firstRun = false;
|
||||
state.treeResult = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -67,6 +67,14 @@ public:
|
||||
return defaultState;
|
||||
}
|
||||
|
||||
/** Evaluate a behavior tree directly for an entity.
|
||||
* Used by ActuatorSystem for player action execution.
|
||||
* Pass reset=true on the first call for a new action.
|
||||
* Returns running/success/failure. */
|
||||
Status evaluatePlayerAction(flecs::entity_t id,
|
||||
const BehaviorTreeNode &root, float deltaTime,
|
||||
bool reset);
|
||||
|
||||
private:
|
||||
Status evaluateNode(const BehaviorTreeNode &node, flecs::entity e,
|
||||
RunnerState &state, float deltaTime);
|
||||
@@ -84,6 +92,7 @@ private:
|
||||
|
||||
std::unordered_map<flecs::entity_t, RunnerState> m_runnerStates;
|
||||
std::unordered_map<flecs::entity_t, RunnerState> m_actionDebugStates;
|
||||
std::unordered_map<flecs::entity_t, RunnerState> m_playerActionStates;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_BEHAVIOR_TREE_SYSTEM_HPP
|
||||
|
||||
@@ -165,9 +165,10 @@ void CharacterSystem::setupEntity(flecs::entity e, CharacterComponent &cc)
|
||||
cc.hasFloor = false;
|
||||
std::cout << "CharacterSystem::setupEntity: entity=" << e.id()
|
||||
<< " nodePos=" << transform.node->_getDerivedPosition()
|
||||
<< " parent=" << (transform.node->getParent() ?
|
||||
transform.node->getParent()->getName() :
|
||||
"<root>")
|
||||
<< " parent="
|
||||
<< (transform.node->getParent() ?
|
||||
transform.node->getParent()->getName() :
|
||||
"<root>")
|
||||
<< " radius=" << cc.radius << " height=" << cc.height
|
||||
<< " dirty=" << cc.dirty << " hasFloor=" << cc.hasFloor
|
||||
<< std::endl;
|
||||
@@ -280,13 +281,24 @@ void CharacterSystem::update(float deltaTime)
|
||||
Ogre::Vector3 nodePos =
|
||||
state.sceneNode->_getDerivedPosition();
|
||||
|
||||
/* If scene node was moved externally (editor gizmo),
|
||||
* teleport character there */
|
||||
/*
|
||||
* Root motion drives physics velocity (AnimationTreeSystem
|
||||
* computes it from the animation displacement). The physics
|
||||
* character moves naturally via velocity, and physics writes
|
||||
* the position back to the scene node. No teleportation or
|
||||
* position sync needed - this avoids jitter.
|
||||
*
|
||||
* If the scene node was moved externally (editor gizmo),
|
||||
* teleport the physics character to match.
|
||||
*/
|
||||
Ogre::Vector3 diff = nodePos - charPos;
|
||||
if (diff.squaredLength() > 0.001f) {
|
||||
state.character->SetPosition(
|
||||
JoltPhysics::convert(nodePos));
|
||||
}
|
||||
/* Root motion velocity is applied via linear velocity
|
||||
* above. No special physics handling needed - physics
|
||||
* writes position back to scene node normally. */
|
||||
|
||||
/* Apply velocity via Jolt linear velocity.
|
||||
* Preserve physics-driven Y velocity when no explicit
|
||||
|
||||
224
src/features/editScene/systems/DialogueSystem.cpp
Normal file
224
src/features/editScene/systems/DialogueSystem.cpp
Normal file
@@ -0,0 +1,224 @@
|
||||
#include "DialogueSystem.hpp"
|
||||
#include "../EditorApp.hpp"
|
||||
#include "../components/DialogueComponent.hpp"
|
||||
#include "../systems/EventBus.hpp"
|
||||
#include "../components/GoapBlackboard.hpp"
|
||||
#include <imgui.h>
|
||||
#include <OgreFontManager.h>
|
||||
#include <OgreImGuiOverlay.h>
|
||||
#include <OgreLogManager.h>
|
||||
#include <OgreOverlayManager.h>
|
||||
|
||||
DialogueSystem::DialogueSystem(flecs::world &world,
|
||||
Ogre::SceneManager *sceneMgr,
|
||||
EditorApp *editorApp)
|
||||
: m_world(world)
|
||||
, m_sceneMgr(sceneMgr)
|
||||
, m_editorApp(editorApp)
|
||||
{
|
||||
// Subscribe to dialogue events
|
||||
EventBus::getInstance().subscribe(
|
||||
"dialogue_show",
|
||||
[this](const Ogre::String &, const GoapBlackboard ¶ms) {
|
||||
// Find the first entity with DialogueComponent
|
||||
m_world.query<DialogueComponent>().each([&](flecs::entity
|
||||
e,
|
||||
DialogueComponent
|
||||
&dc) {
|
||||
if (!dc.enabled)
|
||||
return;
|
||||
|
||||
Ogre::String text =
|
||||
params.getStringValue("text");
|
||||
if (text.empty())
|
||||
return;
|
||||
|
||||
// Parse choices from comma-separated
|
||||
// string
|
||||
std::vector<Ogre::String> choices;
|
||||
Ogre::String choicesStr =
|
||||
params.getStringValue("choices");
|
||||
if (!choicesStr.empty()) {
|
||||
Ogre::String::size_type start = 0;
|
||||
Ogre::String::size_type end;
|
||||
while ((end = choicesStr.find(",",
|
||||
start)) !=
|
||||
Ogre::String::npos) {
|
||||
choices.push_back(
|
||||
choicesStr.substr(
|
||||
start,
|
||||
end - start));
|
||||
start = end + 1;
|
||||
}
|
||||
if (start < choicesStr.length())
|
||||
choices.push_back(
|
||||
choicesStr.substr(
|
||||
start));
|
||||
}
|
||||
|
||||
Ogre::String speaker =
|
||||
params.getStringValue("speaker");
|
||||
|
||||
dc.show(text, choices, speaker);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
DialogueSystem::~DialogueSystem()
|
||||
{
|
||||
// EventBus subscriptions are managed externally
|
||||
}
|
||||
|
||||
void DialogueSystem::ensureFontLoaded(const Ogre::String &fontName,
|
||||
float fontSize)
|
||||
{
|
||||
if (m_fontLoaded && m_currentFontName == fontName &&
|
||||
m_currentFontSize == fontSize)
|
||||
return;
|
||||
|
||||
Ogre::ImGuiOverlay *overlay = m_editorApp->getImGuiOverlay();
|
||||
if (!overlay)
|
||||
return;
|
||||
|
||||
// Load the main dialogue font
|
||||
Ogre::FontPtr font;
|
||||
try {
|
||||
if (Ogre::FontManager::getSingleton().resourceExists(
|
||||
"DialogueFont", "General")) {
|
||||
Ogre::FontManager::getSingleton().remove("DialogueFont",
|
||||
"General");
|
||||
}
|
||||
font = Ogre::FontManager::getSingleton().create("DialogueFont",
|
||||
"General");
|
||||
font->setType(Ogre::FontType::FT_TRUETYPE);
|
||||
font->setSource(fontName);
|
||||
font->setTrueTypeSize(fontSize);
|
||||
font->setTrueTypeResolution(75);
|
||||
font->addCodePointRange(Ogre::Font::CodePointRange(32, 255));
|
||||
font->load();
|
||||
} catch (...) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"DialogueSystem: Failed to load font " + fontName);
|
||||
m_dialogueFont = nullptr;
|
||||
m_fontLoaded = false;
|
||||
return;
|
||||
}
|
||||
|
||||
m_dialogueFont = overlay->addFont("DialogueFont", "General");
|
||||
m_currentFontName = fontName;
|
||||
m_currentFontSize = fontSize;
|
||||
m_fontLoaded = true;
|
||||
}
|
||||
|
||||
void DialogueSystem::prepareFont()
|
||||
{
|
||||
if (!m_editorApp)
|
||||
return;
|
||||
|
||||
// Find an entity with DialogueComponent
|
||||
flecs::entity dialogueEntity = flecs::entity::null();
|
||||
m_world.query<DialogueComponent>().each(
|
||||
[&](flecs::entity e, DialogueComponent &) {
|
||||
if (!dialogueEntity.is_alive())
|
||||
dialogueEntity = e;
|
||||
});
|
||||
|
||||
if (dialogueEntity.is_alive()) {
|
||||
auto &dc = dialogueEntity.get_mut<DialogueComponent>();
|
||||
ensureFontLoaded(dc.fontName, dc.fontSize);
|
||||
}
|
||||
}
|
||||
|
||||
void DialogueSystem::update(float deltaTime)
|
||||
{
|
||||
(void)deltaTime;
|
||||
|
||||
if (!m_editorApp ||
|
||||
m_editorApp->getGameMode() != EditorApp::GameMode::Game ||
|
||||
m_editorApp->getGamePlayState() !=
|
||||
EditorApp::GamePlayState::Playing)
|
||||
return;
|
||||
|
||||
// Find an entity with DialogueComponent
|
||||
flecs::entity dialogueEntity = flecs::entity::null();
|
||||
m_world.query<DialogueComponent>().each(
|
||||
[&](flecs::entity e, DialogueComponent &) {
|
||||
if (!dialogueEntity.is_alive())
|
||||
dialogueEntity = e;
|
||||
});
|
||||
|
||||
if (!dialogueEntity.is_alive())
|
||||
return;
|
||||
|
||||
auto &dc = dialogueEntity.get_mut<DialogueComponent>();
|
||||
if (!dc.enabled || !dc.isActive())
|
||||
return;
|
||||
|
||||
renderDialogueBox(dc);
|
||||
}
|
||||
|
||||
void DialogueSystem::renderDialogueBox(DialogueComponent &dc)
|
||||
{
|
||||
ImVec2 size = ImGui::GetMainViewport()->Size;
|
||||
|
||||
float boxHeight = size.y * dc.boxHeightFraction;
|
||||
float boxY = size.y * dc.boxPositionFraction;
|
||||
|
||||
ImGui::SetNextWindowPos(ImVec2(0, boxY), ImGuiCond_Always);
|
||||
ImGui::SetNextWindowSize(ImVec2(size.x, boxHeight), ImGuiCond_Always);
|
||||
|
||||
// Semi-transparent background
|
||||
ImVec4 bgColor = ImVec4(0.0f, 0.0f, 0.0f, dc.backgroundOpacity);
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, bgColor);
|
||||
|
||||
ImGui::Begin(
|
||||
"DialogueBox", nullptr,
|
||||
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoDecoration |
|
||||
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoCollapse |
|
||||
ImGuiWindowFlags_NoFocusOnAppearing);
|
||||
|
||||
ImVec2 p = ImGui::GetCursorScreenPos();
|
||||
|
||||
// Speaker name (if provided)
|
||||
if (!dc.speaker.empty()) {
|
||||
if (m_speakerFont)
|
||||
ImGui::PushFont(m_speakerFont);
|
||||
ImGui::TextColored(ImVec4(0.8f, 0.8f, 1.0f, 1.0f), "%s",
|
||||
dc.speaker.c_str());
|
||||
if (m_speakerFont)
|
||||
ImGui::PopFont();
|
||||
ImGui::Spacing();
|
||||
}
|
||||
|
||||
// Narration text
|
||||
if (m_dialogueFont)
|
||||
ImGui::PushFont(m_dialogueFont);
|
||||
|
||||
ImGui::TextWrapped("%s", dc.text.c_str());
|
||||
|
||||
if (m_dialogueFont)
|
||||
ImGui::PopFont();
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
// Choices or click-to-progress
|
||||
if (dc.choices.empty()) {
|
||||
// No choices: click anywhere to progress
|
||||
ImGui::SetCursorScreenPos(p);
|
||||
if (ImGui::InvisibleButton("DialogueProgress",
|
||||
ImGui::GetWindowSize())) {
|
||||
dc.progress();
|
||||
}
|
||||
} else {
|
||||
// Choices: render as buttons
|
||||
for (int i = 0; i < (int)dc.choices.size(); i++) {
|
||||
if (ImGui::Button(dc.choices[i].c_str())) {
|
||||
dc.selectChoice(i + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
58
src/features/editScene/systems/DialogueSystem.hpp
Normal file
58
src/features/editScene/systems/DialogueSystem.hpp
Normal file
@@ -0,0 +1,58 @@
|
||||
#ifndef EDITSCENE_DIALOGUESYSTEM_HPP
|
||||
#define EDITSCENE_DIALOGUESYSTEM_HPP
|
||||
#pragma once
|
||||
|
||||
#include <flecs.h>
|
||||
#include <Ogre.h>
|
||||
#include <imgui.h>
|
||||
#include <memory>
|
||||
|
||||
#include "../components/DialogueComponent.hpp"
|
||||
|
||||
class EditorApp;
|
||||
|
||||
/**
|
||||
* System that renders the visual-novel style dialogue box in game mode.
|
||||
*
|
||||
* Only active when EditorApp is in GameMode::Game and
|
||||
* GamePlayState::Playing. The dialogue box is rendered at the bottom
|
||||
* of the screen, showing narration text and optional player choices.
|
||||
*
|
||||
* The dialogue can be triggered via:
|
||||
* 1. EventBus event "dialogue_show" with GoapBlackboard payload
|
||||
* 2. Direct API on DialogueComponent
|
||||
*/
|
||||
class DialogueSystem {
|
||||
public:
|
||||
DialogueSystem(flecs::world &world, Ogre::SceneManager *sceneMgr,
|
||||
EditorApp *editorApp);
|
||||
~DialogueSystem();
|
||||
|
||||
/**
|
||||
* Update and render the dialogue box.
|
||||
* Must be called inside an active ImGui frame.
|
||||
*/
|
||||
void update(float deltaTime);
|
||||
|
||||
/**
|
||||
* Pre-load the dialogue font before ImGui NewFrame().
|
||||
* Must be called outside an active ImGui frame (before NewFrame).
|
||||
*/
|
||||
void prepareFont();
|
||||
|
||||
private:
|
||||
void renderDialogueBox(DialogueComponent &dc);
|
||||
void ensureFontLoaded(const Ogre::String &fontName, float fontSize);
|
||||
|
||||
flecs::world &m_world;
|
||||
Ogre::SceneManager *m_sceneMgr;
|
||||
EditorApp *m_editorApp;
|
||||
|
||||
bool m_fontLoaded = false;
|
||||
Ogre::String m_currentFontName;
|
||||
float m_currentFontSize = 0.0f;
|
||||
ImFont *m_dialogueFont = nullptr;
|
||||
ImFont *m_speakerFont = nullptr;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_DIALOGUESYSTEM_HPP
|
||||
@@ -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,7 +36,14 @@
|
||||
#include "../components/GoapBlackboard.hpp"
|
||||
#include "../components/NavMesh.hpp"
|
||||
#include "../components/SmartObject.hpp"
|
||||
#include "../components/GoapPlanner.hpp"
|
||||
#include "../components/GoapRunner.hpp"
|
||||
#include "../components/PathFollowing.hpp"
|
||||
#include "../components/Actuator.hpp"
|
||||
#include "../components/EventHandler.hpp"
|
||||
#include "../components/PrefabInstance.hpp"
|
||||
#include "../components/Item.hpp"
|
||||
#include "../components/Inventory.hpp"
|
||||
|
||||
#include "../ui/TransformEditor.hpp"
|
||||
#include "../ui/RenderableEditor.hpp"
|
||||
@@ -87,11 +95,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 +116,62 @@ 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;
|
||||
}
|
||||
|
||||
@@ -281,6 +326,12 @@ void EditorUISystem::update(float deltaTime)
|
||||
renderPrefabBrowser();
|
||||
renderCursorPanel();
|
||||
|
||||
// Render Action Database singleton editor window
|
||||
if (m_showActionDatabaseSingleton) {
|
||||
m_actionDatabaseSingletonEditor.render(
|
||||
&m_showActionDatabaseSingleton);
|
||||
}
|
||||
|
||||
// Render FPS overlay
|
||||
renderFPSOverlay(deltaTime);
|
||||
}
|
||||
@@ -326,6 +377,27 @@ void EditorUISystem::renderHierarchyWindow()
|
||||
"Ctrl+O")) {
|
||||
showFileDialog(false);
|
||||
}
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem("Save Action DB",
|
||||
nullptr)) {
|
||||
ActionDatabase::saveToJson(
|
||||
"actions.json");
|
||||
}
|
||||
if (ImGui::MenuItem("Load Action DB",
|
||||
nullptr)) {
|
||||
ActionDatabase::loadFromJson(
|
||||
"actions.json");
|
||||
}
|
||||
if (ImGui::MenuItem("Reload Action DB",
|
||||
nullptr)) {
|
||||
// Reload from file, then re-sync
|
||||
// scene components on top
|
||||
ActionDatabase::getSingleton().clear();
|
||||
ActionDatabase::loadFromJson(
|
||||
"actions.json");
|
||||
ActionDatabase::reloadFromSceneComponents(
|
||||
m_world);
|
||||
}
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
@@ -343,6 +415,11 @@ void EditorUISystem::renderHierarchyWindow()
|
||||
if (ImGui::MenuItem("3D Cursor")) {
|
||||
m_showCursorPanel = true;
|
||||
}
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem(
|
||||
"Action Database (Singleton)")) {
|
||||
m_showActionDatabaseSingleton = true;
|
||||
}
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
@@ -561,10 +638,11 @@ void EditorUISystem::renderEntityNode(flecs::entity entity, int depth)
|
||||
indicators += " [Mat]";
|
||||
if (entity.has<AnimationTreeComponent>())
|
||||
indicators += " [Anim]";
|
||||
if (entity.has<ActionDatabase>())
|
||||
indicators += " [AI]";
|
||||
if (entity.has<ActionDatabaseComponent>())
|
||||
indicators += " [ActDB]";
|
||||
if (entity.has<ActionDebug>())
|
||||
indicators += " [Debug]";
|
||||
|
||||
if (entity.has<BehaviorTreeComponent>())
|
||||
indicators += " [BT]";
|
||||
if (entity.has<GoapBlackboard>())
|
||||
@@ -575,6 +653,8 @@ void EditorUISystem::renderEntityNode(flecs::entity entity, int depth)
|
||||
indicators += " [NavSrc]";
|
||||
if (entity.has<SmartObjectComponent>())
|
||||
indicators += " [SO]";
|
||||
if (entity.has<GoapPlannerComponent>())
|
||||
indicators += " [Planner]";
|
||||
|
||||
snprintf(label, sizeof(label), "%s%s##%llu", name.c_str(),
|
||||
indicators.c_str(), (unsigned long long)entity.id());
|
||||
@@ -952,14 +1032,15 @@ void EditorUISystem::renderComponentList(flecs::entity entity)
|
||||
componentCount++;
|
||||
}
|
||||
|
||||
// Render ActionDatabase if present
|
||||
if (entity.has<ActionDatabase>()) {
|
||||
auto &db = entity.get_mut<ActionDatabase>();
|
||||
m_componentRegistry.render<ActionDatabase>(entity, db);
|
||||
// Render ActionDatabaseComponent if present
|
||||
if (entity.has<ActionDatabaseComponent>()) {
|
||||
auto &db = entity.get_mut<ActionDatabaseComponent>();
|
||||
m_componentRegistry.render<ActionDatabaseComponent>(entity, db);
|
||||
componentCount++;
|
||||
}
|
||||
|
||||
// Render ActionDebug if present
|
||||
|
||||
if (entity.has<ActionDebug>()) {
|
||||
auto &debug = entity.get_mut<ActionDebug>();
|
||||
m_componentRegistry.render<ActionDebug>(entity, debug);
|
||||
@@ -998,6 +1079,57 @@ void EditorUISystem::renderComponentList(flecs::entity entity)
|
||||
componentCount++;
|
||||
}
|
||||
|
||||
// Render GoapPlanner if present
|
||||
if (entity.has<GoapPlannerComponent>()) {
|
||||
auto &planner = entity.get_mut<GoapPlannerComponent>();
|
||||
m_componentRegistry.render<GoapPlannerComponent>(entity,
|
||||
planner);
|
||||
componentCount++;
|
||||
}
|
||||
|
||||
// Render GoapRunner if present
|
||||
if (entity.has<GoapRunnerComponent>()) {
|
||||
auto &runner = entity.get_mut<GoapRunnerComponent>();
|
||||
m_componentRegistry.render<GoapRunnerComponent>(entity, runner);
|
||||
componentCount++;
|
||||
}
|
||||
|
||||
// Render PathFollowing if present
|
||||
if (entity.has<PathFollowingComponent>()) {
|
||||
auto &pf = entity.get_mut<PathFollowingComponent>();
|
||||
m_componentRegistry.render<PathFollowingComponent>(entity, pf);
|
||||
componentCount++;
|
||||
}
|
||||
|
||||
// Render Actuator if present
|
||||
if (entity.has<ActuatorComponent>()) {
|
||||
auto &actuator = entity.get_mut<ActuatorComponent>();
|
||||
m_componentRegistry.render<ActuatorComponent>(entity, actuator);
|
||||
componentCount++;
|
||||
}
|
||||
|
||||
// Render EventHandler if present
|
||||
if (entity.has<EventHandlerComponent>()) {
|
||||
auto &handler = entity.get_mut<EventHandlerComponent>();
|
||||
m_componentRegistry.render<EventHandlerComponent>(entity,
|
||||
handler);
|
||||
componentCount++;
|
||||
}
|
||||
|
||||
// Render Item if present
|
||||
if (entity.has<ItemComponent>()) {
|
||||
auto &item = entity.get_mut<ItemComponent>();
|
||||
m_componentRegistry.render<ItemComponent>(entity, item);
|
||||
componentCount++;
|
||||
}
|
||||
|
||||
// Render Inventory if present
|
||||
if (entity.has<InventoryComponent>()) {
|
||||
auto &inv = entity.get_mut<InventoryComponent>();
|
||||
m_componentRegistry.render<InventoryComponent>(entity, inv);
|
||||
componentCount++;
|
||||
}
|
||||
|
||||
// Show message if no components
|
||||
|
||||
if (componentCount == 0) {
|
||||
@@ -1741,6 +1873,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 +1936,48 @@ void EditorUISystem::renderCursorPanel()
|
||||
m_cursor3D->setVisible(visible);
|
||||
}
|
||||
|
||||
// Placement mode
|
||||
ImGui::Separator();
|
||||
|
||||
// Interaction mode radio buttons
|
||||
ImGui::Text("Interaction Mode:");
|
||||
int mode = static_cast<int>(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();
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "../ui/ComponentRegistry.hpp"
|
||||
#include "../ui/ActionDatabaseSingletonEditor.hpp"
|
||||
#include "../components/EntityName.hpp"
|
||||
#include "../gizmo/Gizmo.hpp"
|
||||
#include "../gizmo/Cursor3D.hpp"
|
||||
@@ -16,6 +17,7 @@
|
||||
class EditorPhysicsSystem;
|
||||
class BuoyancySystem;
|
||||
class NormalDebugSystem;
|
||||
class EditorCamera;
|
||||
|
||||
namespace Ogre
|
||||
{
|
||||
@@ -142,6 +144,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 +277,30 @@ private:
|
||||
std::vector<std::string> 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;
|
||||
|
||||
// Action Database singleton editor state
|
||||
bool m_showActionDatabaseSingleton = false;
|
||||
ActionDatabaseSingletonEditor m_actionDatabaseSingletonEditor;
|
||||
|
||||
// Queries
|
||||
flecs::query<EntityNameComponent> m_nameQuery;
|
||||
|
||||
78
src/features/editScene/systems/EventBus.cpp
Normal file
78
src/features/editScene/systems/EventBus.cpp
Normal file
@@ -0,0 +1,78 @@
|
||||
#include "EventBus.hpp"
|
||||
|
||||
EventBus &EventBus::getInstance()
|
||||
{
|
||||
static EventBus instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
EventBus::ListenerId EventBus::subscribe(const Ogre::String &eventName,
|
||||
Callback cb)
|
||||
{
|
||||
ListenerId id = m_nextId++;
|
||||
m_listeners[eventName].push_back({ id, std::move(cb) });
|
||||
m_idToEvent[id] = eventName;
|
||||
return id;
|
||||
}
|
||||
|
||||
void EventBus::unsubscribe(ListenerId id)
|
||||
{
|
||||
auto it = m_idToEvent.find(id);
|
||||
if (it == m_idToEvent.end())
|
||||
return;
|
||||
|
||||
const Ogre::String &eventName = it->second;
|
||||
auto lit = m_listeners.find(eventName);
|
||||
if (lit != m_listeners.end()) {
|
||||
auto &vec = lit->second;
|
||||
for (auto vit = vec.begin(); vit != vec.end(); ++vit) {
|
||||
if (vit->id == id) {
|
||||
vec.erase(vit);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (vec.empty())
|
||||
m_listeners.erase(lit);
|
||||
}
|
||||
m_idToEvent.erase(it);
|
||||
}
|
||||
|
||||
void EventBus::send(const Ogre::String &eventName,
|
||||
const GoapBlackboard ¶ms)
|
||||
{
|
||||
auto lit = m_listeners.find(eventName);
|
||||
if (lit == m_listeners.end())
|
||||
return;
|
||||
|
||||
// Copy the listener list in case a callback mutates subscriptions
|
||||
auto listeners = lit->second;
|
||||
for (const auto &listener : listeners) {
|
||||
if (listener.callback)
|
||||
listener.callback(eventName, params);
|
||||
}
|
||||
}
|
||||
|
||||
void EventBus::send(const Ogre::String &eventName,
|
||||
const Ogre::String ¶mName, int value)
|
||||
{
|
||||
GoapBlackboard params;
|
||||
params.setValue(paramName, value);
|
||||
send(eventName, params);
|
||||
}
|
||||
|
||||
void EventBus::send(const Ogre::String &eventName,
|
||||
const Ogre::String ¶mName, float value)
|
||||
{
|
||||
GoapBlackboard params;
|
||||
params.setFloatValue(paramName, value);
|
||||
send(eventName, params);
|
||||
}
|
||||
|
||||
void EventBus::send(const Ogre::String &eventName,
|
||||
const Ogre::String ¶mName,
|
||||
const Ogre::Vector3 &value)
|
||||
{
|
||||
GoapBlackboard params;
|
||||
params.setVec3Value(paramName, value);
|
||||
send(eventName, params);
|
||||
}
|
||||
66
src/features/editScene/systems/EventBus.hpp
Normal file
66
src/features/editScene/systems/EventBus.hpp
Normal file
@@ -0,0 +1,66 @@
|
||||
#ifndef EDITSCENE_EVENT_BUS_HPP
|
||||
#define EDITSCENE_EVENT_BUS_HPP
|
||||
#pragma once
|
||||
|
||||
#include "../components/GoapBlackboard.hpp"
|
||||
#include <Ogre.h>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
/**
|
||||
* Global synchronous event bus.
|
||||
*
|
||||
* Subscribers register callbacks by event name. When an event is sent,
|
||||
* all matching callbacks are invoked immediately in the send() call.
|
||||
*
|
||||
* Payload is a GoapBlackboard — reuses the existing int/float/vec3/bit/string
|
||||
* storage with zero RTTI overhead.
|
||||
*/
|
||||
class EventBus {
|
||||
public:
|
||||
using ListenerId = uint64_t;
|
||||
using Callback =
|
||||
std::function<void(const Ogre::String &eventName,
|
||||
const GoapBlackboard ¶ms)>;
|
||||
|
||||
static EventBus &getInstance();
|
||||
|
||||
/** Subscribe to an event. Returns a handle for unsubscribe(). */
|
||||
ListenerId subscribe(const Ogre::String &eventName, Callback cb);
|
||||
|
||||
/** Unsubscribe by handle. Safe to call with invalid id. */
|
||||
void unsubscribe(ListenerId id);
|
||||
|
||||
/** Send an event with a GoapBlackboard payload. */
|
||||
void send(const Ogre::String &eventName,
|
||||
const GoapBlackboard ¶ms = {});
|
||||
|
||||
/** Convenience: send event with a single int param. */
|
||||
void send(const Ogre::String &eventName,
|
||||
const Ogre::String ¶mName, int value);
|
||||
|
||||
/** Convenience: send event with a single float param. */
|
||||
void send(const Ogre::String &eventName,
|
||||
const Ogre::String ¶mName, float value);
|
||||
|
||||
/** Convenience: send event with a single Vec3 param. */
|
||||
void send(const Ogre::String &eventName,
|
||||
const Ogre::String ¶mName,
|
||||
const Ogre::Vector3 &value);
|
||||
|
||||
private:
|
||||
EventBus() = default;
|
||||
|
||||
struct Listener {
|
||||
ListenerId id;
|
||||
Callback callback;
|
||||
};
|
||||
|
||||
ListenerId m_nextId = 1;
|
||||
std::unordered_map<Ogre::String, std::vector<Listener>> m_listeners;
|
||||
std::unordered_map<ListenerId, Ogre::String> m_idToEvent;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_EVENT_BUS_HPP
|
||||
221
src/features/editScene/systems/EventHandlerSystem.cpp
Normal file
221
src/features/editScene/systems/EventHandlerSystem.cpp
Normal file
@@ -0,0 +1,221 @@
|
||||
#include "EventHandlerSystem.hpp"
|
||||
#include "BehaviorTreeSystem.hpp"
|
||||
#include "../components/EventHandler.hpp"
|
||||
#include "../components/ActionDatabase.hpp"
|
||||
#include "../components/GoapBlackboard.hpp"
|
||||
#include <OgreLogManager.h>
|
||||
|
||||
EventHandlerSystem::EventHandlerSystem(flecs::world &world,
|
||||
BehaviorTreeSystem *btSystem)
|
||||
: m_world(world)
|
||||
, m_btSystem(btSystem)
|
||||
{
|
||||
}
|
||||
|
||||
EventHandlerSystem::~EventHandlerSystem()
|
||||
{
|
||||
// Unsubscribe all remaining listeners
|
||||
for (auto &pair : m_subscriptions) {
|
||||
EventBus::getInstance().unsubscribe(pair.second);
|
||||
}
|
||||
m_subscriptions.clear();
|
||||
}
|
||||
|
||||
void EventHandlerSystem::subscribeEntity(
|
||||
flecs::entity e, const EventHandlerComponent &handler)
|
||||
{
|
||||
if (!handler.enabled || handler.eventName.empty())
|
||||
return;
|
||||
|
||||
flecs::entity_t id = e.id();
|
||||
if (m_subscriptions.find(id) != m_subscriptions.end())
|
||||
return;
|
||||
|
||||
EventBus::ListenerId lid = EventBus::getInstance().subscribe(
|
||||
handler.eventName,
|
||||
[this, id, handler](const Ogre::String &,
|
||||
const GoapBlackboard ¶ms) {
|
||||
this->onEvent(id, handler.eventName, params);
|
||||
});
|
||||
|
||||
m_subscriptions[id] = lid;
|
||||
}
|
||||
|
||||
void EventHandlerSystem::unsubscribeEntity(flecs::entity_t id)
|
||||
{
|
||||
auto it = m_subscriptions.find(id);
|
||||
if (it == m_subscriptions.end())
|
||||
return;
|
||||
|
||||
EventBus::getInstance().unsubscribe(it->second);
|
||||
m_subscriptions.erase(it);
|
||||
|
||||
// If there is an active handler, abort it and clean up
|
||||
auto activeIt = m_activeHandlers.find(id);
|
||||
if (activeIt != m_activeHandlers.end()) {
|
||||
auto keysIt = m_injectedKeys.find(id);
|
||||
if (keysIt != m_injectedKeys.end()) {
|
||||
flecs::entity e = m_world.entity(id);
|
||||
if (e.is_alive())
|
||||
removeParams(e, keysIt->second);
|
||||
m_injectedKeys.erase(keysIt);
|
||||
}
|
||||
m_activeHandlers.erase(activeIt);
|
||||
}
|
||||
}
|
||||
|
||||
void EventHandlerSystem::onEvent(flecs::entity_t entityId,
|
||||
const Ogre::String &eventName,
|
||||
const GoapBlackboard ¶ms)
|
||||
{
|
||||
(void)eventName;
|
||||
|
||||
flecs::entity e = m_world.entity(entityId);
|
||||
if (!e.is_alive() || !e.has<EventHandlerComponent>())
|
||||
return;
|
||||
|
||||
auto &handler = e.get<EventHandlerComponent>();
|
||||
if (!handler.enabled || handler.actionName.empty())
|
||||
return;
|
||||
|
||||
// If already handling an event, abort the previous one first
|
||||
auto activeIt = m_activeHandlers.find(entityId);
|
||||
if (activeIt != m_activeHandlers.end()) {
|
||||
auto keysIt = m_injectedKeys.find(entityId);
|
||||
if (keysIt != m_injectedKeys.end()) {
|
||||
removeParams(e, keysIt->second);
|
||||
m_injectedKeys.erase(keysIt);
|
||||
}
|
||||
m_activeHandlers.erase(activeIt);
|
||||
}
|
||||
|
||||
// Start new handler
|
||||
ActiveHandler ah;
|
||||
ah.entityId = entityId;
|
||||
ah.actionName = handler.actionName;
|
||||
ah.eventParams = params;
|
||||
ah.firstFrame = true;
|
||||
m_activeHandlers[entityId] = ah;
|
||||
|
||||
// Inject params immediately so the first update() tick sees them
|
||||
std::unordered_set<std::string> injected;
|
||||
injectParams(e, params, injected);
|
||||
m_injectedKeys[entityId] = std::move(injected);
|
||||
}
|
||||
|
||||
void EventHandlerSystem::injectParams(
|
||||
flecs::entity e, const GoapBlackboard ¶ms,
|
||||
std::unordered_set<std::string> &outKeys)
|
||||
{
|
||||
if (!e.has<GoapBlackboard>())
|
||||
e.set<GoapBlackboard>({});
|
||||
|
||||
auto &bb = e.get_mut<GoapBlackboard>();
|
||||
|
||||
for (const auto &pair : params.values) {
|
||||
bb.setValue(pair.first, pair.second);
|
||||
outKeys.insert(pair.first);
|
||||
}
|
||||
for (const auto &pair : params.floatValues) {
|
||||
bb.setFloatValue(pair.first, pair.second);
|
||||
outKeys.insert(pair.first);
|
||||
}
|
||||
for (const auto &pair : params.vec3Values) {
|
||||
bb.setVec3Value(pair.first, pair.second);
|
||||
outKeys.insert(pair.first);
|
||||
}
|
||||
for (const auto &pair : params.stringValues) {
|
||||
bb.setStringValue(pair.first, pair.second);
|
||||
outKeys.insert(pair.first);
|
||||
}
|
||||
// Bits are not injected individually; they are part of the event
|
||||
// payload semantics but merging bits globally is risky.
|
||||
// Instead, event bits are NOT auto-injected. If needed, use
|
||||
// setBit nodes inside the handler BT.
|
||||
}
|
||||
|
||||
void EventHandlerSystem::removeParams(
|
||||
flecs::entity e,
|
||||
const std::unordered_set<std::string> &keys)
|
||||
{
|
||||
if (!e.has<GoapBlackboard>())
|
||||
return;
|
||||
|
||||
auto &bb = e.get_mut<GoapBlackboard>();
|
||||
for (const auto &key : keys) {
|
||||
bb.removeValue(key);
|
||||
bb.removeFloatValue(key);
|
||||
bb.removeVec3Value(key);
|
||||
bb.removeStringValue(key);
|
||||
}
|
||||
}
|
||||
|
||||
void EventHandlerSystem::update(float deltaTime)
|
||||
{
|
||||
if (!m_btSystem)
|
||||
return;
|
||||
|
||||
// --- Sync subscriptions with current entities ---
|
||||
std::unordered_set<flecs::entity_t> currentEntities;
|
||||
m_world.query<EventHandlerComponent>().each(
|
||||
[&](flecs::entity e, EventHandlerComponent &handler) {
|
||||
currentEntities.insert(e.id());
|
||||
if (handler.enabled) {
|
||||
subscribeEntity(e, handler);
|
||||
} else {
|
||||
unsubscribeEntity(e.id());
|
||||
}
|
||||
});
|
||||
|
||||
// Unsubscribe entities that lost their component
|
||||
std::vector<flecs::entity_t> toRemove;
|
||||
for (auto &pair : m_subscriptions) {
|
||||
if (currentEntities.find(pair.first) == currentEntities.end())
|
||||
toRemove.push_back(pair.first);
|
||||
}
|
||||
for (flecs::entity_t id : toRemove)
|
||||
unsubscribeEntity(id);
|
||||
|
||||
// --- Tick active handlers ---
|
||||
std::vector<flecs::entity_t> completedHandlers;
|
||||
for (auto &pair : m_activeHandlers) {
|
||||
flecs::entity_t id = pair.first;
|
||||
ActiveHandler &ah = pair.second;
|
||||
|
||||
// Look up the action in the singleton database
|
||||
ActionDatabase *db = ActionDatabase::getSingletonPtr();
|
||||
if (!db) {
|
||||
completedHandlers.push_back(id);
|
||||
continue;
|
||||
}
|
||||
|
||||
const GoapAction *action = db->findAction(ah.actionName);
|
||||
if (!action) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"[EventHandlerSystem] Action not found: " +
|
||||
ah.actionName);
|
||||
completedHandlers.push_back(id);
|
||||
continue;
|
||||
}
|
||||
|
||||
auto status = m_btSystem->evaluatePlayerAction(
|
||||
id, action->behaviorTree, deltaTime, ah.firstFrame);
|
||||
ah.firstFrame = false;
|
||||
|
||||
if (status != BehaviorTreeSystem::Status::running) {
|
||||
completedHandlers.push_back(id);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Clean up completed handlers ---
|
||||
for (flecs::entity_t id : completedHandlers) {
|
||||
flecs::entity e = m_world.entity(id);
|
||||
auto keysIt = m_injectedKeys.find(id);
|
||||
if (keysIt != m_injectedKeys.end()) {
|
||||
if (e.is_alive())
|
||||
removeParams(e, keysIt->second);
|
||||
m_injectedKeys.erase(keysIt);
|
||||
}
|
||||
m_activeHandlers.erase(id);
|
||||
}
|
||||
}
|
||||
64
src/features/editScene/systems/EventHandlerSystem.hpp
Normal file
64
src/features/editScene/systems/EventHandlerSystem.hpp
Normal file
@@ -0,0 +1,64 @@
|
||||
#ifndef EDITSCENE_EVENT_HANDLER_SYSTEM_HPP
|
||||
#define EDITSCENE_EVENT_HANDLER_SYSTEM_HPP
|
||||
#pragma once
|
||||
|
||||
#include <flecs.h>
|
||||
#include <Ogre.h>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include "EventBus.hpp"
|
||||
#include "../components/GoapBlackboard.hpp"
|
||||
|
||||
class BehaviorTreeSystem;
|
||||
class EditorApp;
|
||||
|
||||
/**
|
||||
* System that executes behavior trees in response to events.
|
||||
*
|
||||
* For each entity with EventHandlerComponent, subscribes to the
|
||||
* specified event. When the event fires, copies parameters into the
|
||||
* entity's GoapBlackboard and runs the referenced action's behavior
|
||||
* tree. Cleans up injected parameters when the tree completes.
|
||||
*/
|
||||
class EventHandlerSystem {
|
||||
public:
|
||||
EventHandlerSystem(flecs::world &world, BehaviorTreeSystem *btSystem);
|
||||
~EventHandlerSystem();
|
||||
|
||||
void update(float deltaTime);
|
||||
|
||||
private:
|
||||
struct ActiveHandler {
|
||||
flecs::entity_t entityId;
|
||||
Ogre::String actionName;
|
||||
GoapBlackboard eventParams;
|
||||
bool firstFrame = true;
|
||||
};
|
||||
|
||||
void subscribeEntity(flecs::entity e,
|
||||
const class EventHandlerComponent &handler);
|
||||
void unsubscribeEntity(flecs::entity_t id);
|
||||
void onEvent(flecs::entity_t entityId,
|
||||
const Ogre::String &eventName,
|
||||
const GoapBlackboard ¶ms);
|
||||
void injectParams(flecs::entity e, const GoapBlackboard ¶ms,
|
||||
std::unordered_set<std::string> &outKeys);
|
||||
void removeParams(flecs::entity e,
|
||||
const std::unordered_set<std::string> &keys);
|
||||
|
||||
flecs::world &m_world;
|
||||
BehaviorTreeSystem *m_btSystem;
|
||||
|
||||
// Per-entity event subscription
|
||||
std::unordered_map<flecs::entity_t, EventBus::ListenerId> m_subscriptions;
|
||||
|
||||
// Per-entity active handler (one at a time per entity)
|
||||
std::unordered_map<flecs::entity_t, ActiveHandler> m_activeHandlers;
|
||||
|
||||
// Keys injected into blackboard per active handler
|
||||
std::unordered_map<flecs::entity_t, std::unordered_set<std::string>> m_injectedKeys;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_EVENT_HANDLER_SYSTEM_HPP
|
||||
261
src/features/editScene/systems/GoapPlannerSystem.cpp
Normal file
261
src/features/editScene/systems/GoapPlannerSystem.cpp
Normal file
@@ -0,0 +1,261 @@
|
||||
#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 <OgreLogManager.h>
|
||||
#include <queue>
|
||||
#include <functional>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace {
|
||||
|
||||
struct PlannerNode {
|
||||
GoapBlackboard state;
|
||||
std::vector<Ogre::String> 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<uint64_t> 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<std::string>{}(pair.first) + 0x9e3779b9 +
|
||||
(h << 6) + (h >> 2);
|
||||
h ^= std::hash<int>{}(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<const GoapAction *> GoapPlannerSystem::resolveActions(
|
||||
const GoapPlannerComponent &planner, const ActionDatabase *db)
|
||||
{
|
||||
std::vector<const GoapAction *> 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 singleton
|
||||
const ActionDatabase *db = ActionDatabase::getSingletonPtr();
|
||||
|
||||
// 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<const GoapAction *> 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<PlannerNode, std::vector<PlannerNode>,
|
||||
NodeCompare>
|
||||
openSet(cmp);
|
||||
|
||||
// Closed set: map from state hash -> best cost seen for that state
|
||||
std::unordered_map<size_t, int> closedSet;
|
||||
|
||||
// Track found plans to avoid duplicates
|
||||
std::unordered_set<std::string> 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<GoapPlannerComponent, GoapBlackboard>()
|
||||
.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);
|
||||
});
|
||||
}
|
||||
68
src/features/editScene/systems/GoapPlannerSystem.hpp
Normal file
68
src/features/editScene/systems/GoapPlannerSystem.hpp
Normal file
@@ -0,0 +1,68 @@
|
||||
#ifndef EDITSCENE_GOAP_PLANNER_SYSTEM_HPP
|
||||
#define EDITSCENE_GOAP_PLANNER_SYSTEM_HPP
|
||||
#pragma once
|
||||
|
||||
#include <flecs.h>
|
||||
#include <Ogre.h>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <queue>
|
||||
#include <unordered_set>
|
||||
|
||||
// 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<const GoapAction *>
|
||||
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
|
||||
312
src/features/editScene/systems/GoapRunnerSystem.cpp
Normal file
312
src/features/editScene/systems/GoapRunnerSystem.cpp
Normal file
@@ -0,0 +1,312 @@
|
||||
#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 <OgreLogManager.h>
|
||||
#include <cmath>
|
||||
|
||||
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<TransformComponent>())
|
||||
return flecs::entity::null();
|
||||
|
||||
Ogre::Vector3 charPos =
|
||||
character.get<TransformComponent>().node
|
||||
->_getDerivedPosition();
|
||||
|
||||
flecs::entity nearest = flecs::entity::null();
|
||||
float bestDist = maxDistance;
|
||||
|
||||
m_world.query<SmartObjectComponent, TransformComponent>()
|
||||
.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<GoapPlannerComponent>() || !e.has<GoapRunnerComponent>())
|
||||
return false;
|
||||
|
||||
auto &planner = e.get_mut<GoapPlannerComponent>();
|
||||
auto &runner = e.get_mut<GoapRunnerComponent>();
|
||||
|
||||
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<PathFollowingComponent>()) {
|
||||
auto &pf = e.get_mut<PathFollowingComponent>();
|
||||
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 singleton
|
||||
ActionDatabase *db = ActionDatabase::getSingletonPtr();
|
||||
|
||||
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<ActionDebug>()) {
|
||||
e.set<ActionDebug>({});
|
||||
m_managedActionDebugs.insert(e.id());
|
||||
}
|
||||
auto &debug = e.get_mut<ActionDebug>();
|
||||
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<PathFollowingComponent>()) {
|
||||
auto &pf = e.get_mut<PathFollowingComponent>();
|
||||
auto &soTrans = so.get<TransformComponent>();
|
||||
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<NavMeshComponent>()
|
||||
.each([&](flecs::entity ne,
|
||||
NavMeshComponent &) {
|
||||
if (!navmeshEntity.is_alive())
|
||||
navmeshEntity = ne;
|
||||
});
|
||||
if (navmeshEntity.is_alive()) {
|
||||
auto &charTrans =
|
||||
e.get<TransformComponent>();
|
||||
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<TransformComponent>();
|
||||
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<GoapPlannerComponent, GoapRunnerComponent>()
|
||||
.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<PathFollowingComponent>()) {
|
||||
auto &pf = e.get<PathFollowingComponent>();
|
||||
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<GoapBlackboard>()) {
|
||||
ActionDatabase *db = ActionDatabase::getSingletonPtr();
|
||||
if (db) {
|
||||
const GoapAction *action =
|
||||
db->findAction(
|
||||
runner.currentActionName);
|
||||
if (action) {
|
||||
auto &bb = e.get_mut<GoapBlackboard>();
|
||||
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<ActionDebug>()) {
|
||||
auto &debug = e.get_mut<ActionDebug>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
68
src/features/editScene/systems/GoapRunnerSystem.hpp
Normal file
68
src/features/editScene/systems/GoapRunnerSystem.hpp
Normal file
@@ -0,0 +1,68 @@
|
||||
#ifndef EDITSCENE_GOAP_RUNNER_SYSTEM_HPP
|
||||
#define EDITSCENE_GOAP_RUNNER_SYSTEM_HPP
|
||||
#pragma once
|
||||
|
||||
#include <flecs.h>
|
||||
#include <Ogre.h>
|
||||
#include <unordered_set>
|
||||
|
||||
// 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<flecs::entity_t> m_managedActionDebugs;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_GOAP_RUNNER_SYSTEM_HPP
|
||||
359
src/features/editScene/systems/ItemSystem.cpp
Normal file
359
src/features/editScene/systems/ItemSystem.cpp
Normal file
@@ -0,0 +1,359 @@
|
||||
#include "ItemSystem.hpp"
|
||||
#include "../EditorApp.hpp"
|
||||
#include "BehaviorTreeSystem.hpp"
|
||||
#include "../components/Item.hpp"
|
||||
#include "../components/Inventory.hpp"
|
||||
#include "../components/Transform.hpp"
|
||||
#include "../components/EntityName.hpp"
|
||||
#include "../components/ActionDatabase.hpp"
|
||||
#include <OgreSceneNode.h>
|
||||
#include <OgreLogManager.h>
|
||||
#include <cmath>
|
||||
|
||||
ItemSystem::ItemSystem(flecs::world &world, Ogre::SceneManager *sceneMgr,
|
||||
EditorApp *editorApp, BehaviorTreeSystem *btSystem)
|
||||
: m_world(world)
|
||||
, m_sceneMgr(sceneMgr)
|
||||
, m_editorApp(editorApp)
|
||||
, m_btSystem(btSystem)
|
||||
{
|
||||
}
|
||||
|
||||
ItemSystem::~ItemSystem() = default;
|
||||
|
||||
// --- Inventory manipulation API ---
|
||||
|
||||
bool ItemSystem::addItemToInventory(flecs::entity inventoryEntity,
|
||||
const Ogre::String &itemId,
|
||||
const Ogre::String &itemName,
|
||||
const Ogre::String &itemType, int stackSize,
|
||||
float weight, int value,
|
||||
const Ogre::String &useActionName)
|
||||
{
|
||||
if (!inventoryEntity.is_alive() ||
|
||||
!inventoryEntity.has<InventoryComponent>())
|
||||
return false;
|
||||
|
||||
auto &inv = inventoryEntity.get_mut<InventoryComponent>();
|
||||
|
||||
// Try to stack with existing items of the same itemId
|
||||
if (!itemId.empty()) {
|
||||
for (int i = 0; i < (int)inv.slots.size(); i++) {
|
||||
auto &slot = inv.slots[i];
|
||||
if (!slot.isEmpty() && slot.itemId == itemId &&
|
||||
slot.stackSize < slot.maxStackSize) {
|
||||
int space = slot.maxStackSize - slot.stackSize;
|
||||
int add = std::min(space, stackSize);
|
||||
slot.stackSize += add;
|
||||
stackSize -= add;
|
||||
if (stackSize <= 0) {
|
||||
inv.recalculateWeight();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find empty slot for remaining items
|
||||
while (stackSize > 0) {
|
||||
int slotIdx = inv.findEmptySlot();
|
||||
if (slotIdx < 0)
|
||||
return false; // Inventory full
|
||||
|
||||
// Ensure slots vector is large enough
|
||||
while ((int)inv.slots.size() <= slotIdx)
|
||||
inv.slots.emplace_back();
|
||||
|
||||
auto &slot = inv.slots[slotIdx];
|
||||
slot.itemEntity = 0;
|
||||
slot.itemId = itemId;
|
||||
slot.itemName = itemName;
|
||||
slot.itemType = itemType;
|
||||
slot.maxStackSize = 99;
|
||||
slot.weight = weight;
|
||||
slot.value = value;
|
||||
slot.useActionName = useActionName;
|
||||
|
||||
int add = std::min(stackSize, slot.maxStackSize);
|
||||
slot.stackSize = add;
|
||||
stackSize -= add;
|
||||
}
|
||||
|
||||
inv.recalculateWeight();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ItemSystem::addItemEntityToInventory(flecs::entity inventoryEntity,
|
||||
flecs::entity itemEntity)
|
||||
{
|
||||
if (!inventoryEntity.is_alive() ||
|
||||
!inventoryEntity.has<InventoryComponent>())
|
||||
return false;
|
||||
if (!itemEntity.is_alive() || !itemEntity.has<ItemComponent>())
|
||||
return false;
|
||||
|
||||
auto &item = itemEntity.get_mut<ItemComponent>();
|
||||
|
||||
// Add to inventory
|
||||
bool result = addItemToInventory(inventoryEntity, item.itemId,
|
||||
item.itemName, item.itemType,
|
||||
item.stackSize, item.weight,
|
||||
item.value, item.useActionName);
|
||||
|
||||
if (result) {
|
||||
// Hide the world entity
|
||||
if (itemEntity.has<TransformComponent>()) {
|
||||
auto &trans = itemEntity.get_mut<TransformComponent>();
|
||||
if (trans.node)
|
||||
trans.node->setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
int ItemSystem::removeItemFromInventory(flecs::entity inventoryEntity,
|
||||
const Ogre::String &itemId, int count)
|
||||
{
|
||||
if (!inventoryEntity.is_alive() ||
|
||||
!inventoryEntity.has<InventoryComponent>())
|
||||
return 0;
|
||||
|
||||
auto &inv = inventoryEntity.get_mut<InventoryComponent>();
|
||||
int removed = 0;
|
||||
|
||||
for (int i = (int)inv.slots.size() - 1; i >= 0 && count > 0; i--) {
|
||||
auto &slot = inv.slots[i];
|
||||
if (slot.isEmpty() || slot.itemId != itemId)
|
||||
continue;
|
||||
|
||||
int remove = std::min(count, slot.stackSize);
|
||||
slot.stackSize -= remove;
|
||||
count -= remove;
|
||||
removed += remove;
|
||||
|
||||
if (slot.stackSize <= 0)
|
||||
slot.clear();
|
||||
}
|
||||
|
||||
if (removed > 0)
|
||||
inv.recalculateWeight();
|
||||
|
||||
return removed;
|
||||
}
|
||||
|
||||
bool ItemSystem::removeItemFromSlot(flecs::entity inventoryEntity,
|
||||
int slotIndex, int count)
|
||||
{
|
||||
if (!inventoryEntity.is_alive() ||
|
||||
!inventoryEntity.has<InventoryComponent>())
|
||||
return false;
|
||||
|
||||
auto &inv = inventoryEntity.get_mut<InventoryComponent>();
|
||||
if (slotIndex < 0 || slotIndex >= (int)inv.slots.size())
|
||||
return false;
|
||||
|
||||
auto &slot = inv.slots[slotIndex];
|
||||
if (slot.isEmpty())
|
||||
return false;
|
||||
|
||||
int remove = std::min(count, slot.stackSize);
|
||||
slot.stackSize -= remove;
|
||||
|
||||
if (slot.stackSize <= 0)
|
||||
slot.clear();
|
||||
|
||||
inv.recalculateWeight();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ItemSystem::transferItem(flecs::entity fromInventory, int slotIndex,
|
||||
flecs::entity toInventory, int count)
|
||||
{
|
||||
if (!fromInventory.is_alive() ||
|
||||
!fromInventory.has<InventoryComponent>())
|
||||
return false;
|
||||
if (!toInventory.is_alive() || !toInventory.has<InventoryComponent>())
|
||||
return false;
|
||||
|
||||
auto &fromInv = fromInventory.get_mut<InventoryComponent>();
|
||||
if (slotIndex < 0 || slotIndex >= (int)fromInv.slots.size())
|
||||
return false;
|
||||
|
||||
auto &slot = fromInv.slots[slotIndex];
|
||||
if (slot.isEmpty())
|
||||
return false;
|
||||
|
||||
int transferCount = std::min(count, slot.stackSize);
|
||||
|
||||
// Add to target inventory
|
||||
bool added = addItemToInventory(toInventory, slot.itemId, slot.itemName,
|
||||
slot.itemType, transferCount,
|
||||
slot.weight, slot.value,
|
||||
slot.useActionName);
|
||||
|
||||
if (!added)
|
||||
return false;
|
||||
|
||||
// Remove from source
|
||||
slot.stackSize -= transferCount;
|
||||
if (slot.stackSize <= 0)
|
||||
slot.clear();
|
||||
|
||||
fromInv.recalculateWeight();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ItemSystem::hasItem(flecs::entity inventoryEntity,
|
||||
const Ogre::String &itemId) const
|
||||
{
|
||||
if (!inventoryEntity.is_alive() ||
|
||||
!inventoryEntity.has<InventoryComponent>())
|
||||
return false;
|
||||
|
||||
return inventoryEntity.get<InventoryComponent>().hasItem(itemId);
|
||||
}
|
||||
|
||||
bool ItemSystem::hasItemByName(flecs::entity inventoryEntity,
|
||||
const Ogre::String &itemName) const
|
||||
{
|
||||
if (!inventoryEntity.is_alive() ||
|
||||
!inventoryEntity.has<InventoryComponent>())
|
||||
return false;
|
||||
|
||||
return inventoryEntity.get<InventoryComponent>().hasItemByName(
|
||||
itemName);
|
||||
}
|
||||
|
||||
int ItemSystem::countItem(flecs::entity inventoryEntity,
|
||||
const Ogre::String &itemId) const
|
||||
{
|
||||
if (!inventoryEntity.is_alive() ||
|
||||
!inventoryEntity.has<InventoryComponent>())
|
||||
return 0;
|
||||
|
||||
return inventoryEntity.get<InventoryComponent>().countItem(itemId);
|
||||
}
|
||||
|
||||
bool ItemSystem::dropItem(flecs::entity inventoryEntity, int slotIndex,
|
||||
const Ogre::Vector3 &worldPosition, int count)
|
||||
{
|
||||
if (!inventoryEntity.is_alive() ||
|
||||
!inventoryEntity.has<InventoryComponent>())
|
||||
return false;
|
||||
|
||||
auto &inv = inventoryEntity.get_mut<InventoryComponent>();
|
||||
if (slotIndex < 0 || slotIndex >= (int)inv.slots.size())
|
||||
return false;
|
||||
|
||||
auto &slot = inv.slots[slotIndex];
|
||||
if (slot.isEmpty())
|
||||
return false;
|
||||
|
||||
int dropCount = std::min(count, slot.stackSize);
|
||||
|
||||
// If the item has a world entity reference, show it
|
||||
if (slot.itemEntity != 0) {
|
||||
flecs::entity itemEntity = m_world.entity(slot.itemEntity);
|
||||
if (itemEntity.is_alive() && itemEntity.has<ItemComponent>()) {
|
||||
auto &item = itemEntity.get_mut<ItemComponent>();
|
||||
item.stackSize = dropCount;
|
||||
|
||||
if (itemEntity.has<TransformComponent>()) {
|
||||
auto &trans =
|
||||
itemEntity.get_mut<TransformComponent>();
|
||||
trans.position = worldPosition;
|
||||
if (trans.node) {
|
||||
trans.node->setPosition(worldPosition);
|
||||
trans.node->setVisible(true);
|
||||
}
|
||||
trans.markChanged();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Create a new world entity for the dropped item
|
||||
flecs::entity itemEntity = m_world.entity();
|
||||
itemEntity.set<ItemComponent>(
|
||||
ItemComponent(slot.itemName, slot.itemType));
|
||||
auto &item = itemEntity.get_mut<ItemComponent>();
|
||||
item.itemId = slot.itemId;
|
||||
item.stackSize = dropCount;
|
||||
item.weight = slot.weight;
|
||||
item.value = slot.value;
|
||||
item.useActionName = slot.useActionName;
|
||||
|
||||
// Create a scene node for the dropped item
|
||||
Ogre::SceneNode *node =
|
||||
m_sceneMgr->getRootSceneNode()->createChildSceneNode(
|
||||
worldPosition);
|
||||
TransformComponent trans;
|
||||
trans.node = node;
|
||||
trans.position = worldPosition;
|
||||
itemEntity.set<TransformComponent>(trans);
|
||||
|
||||
EntityNameComponent nameComp;
|
||||
nameComp.name = slot.itemName + "_dropped";
|
||||
itemEntity.set<EntityNameComponent>(nameComp);
|
||||
}
|
||||
|
||||
// Remove from inventory
|
||||
slot.stackSize -= dropCount;
|
||||
if (slot.stackSize <= 0)
|
||||
slot.clear();
|
||||
|
||||
inv.recalculateWeight();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ItemSystem::useItem(flecs::entity characterEntity,
|
||||
flecs::entity inventoryEntity, int slotIndex)
|
||||
{
|
||||
if (!inventoryEntity.is_alive() ||
|
||||
!inventoryEntity.has<InventoryComponent>())
|
||||
return false;
|
||||
|
||||
auto &inv = inventoryEntity.get<InventoryComponent>();
|
||||
if (slotIndex < 0 || slotIndex >= (int)inv.slots.size())
|
||||
return false;
|
||||
|
||||
const auto &slot = inv.slots[slotIndex];
|
||||
if (slot.isEmpty() || slot.useActionName.empty())
|
||||
return false;
|
||||
|
||||
// Execute the use action via behavior tree
|
||||
if (m_btSystem && characterEntity.is_alive()) {
|
||||
// Look up the action in the singleton database
|
||||
ActionDatabase *db = ActionDatabase::getSingletonPtr();
|
||||
|
||||
if (db) {
|
||||
const GoapAction *action =
|
||||
db->findAction(slot.useActionName);
|
||||
if (action) {
|
||||
m_btSystem->evaluatePlayerAction(
|
||||
characterEntity.id(),
|
||||
action->behaviorTree, 0.016f, true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ItemSystem::pickupItem(flecs::entity characterEntity,
|
||||
flecs::entity itemEntity)
|
||||
{
|
||||
if (!characterEntity.is_alive() ||
|
||||
!characterEntity.has<InventoryComponent>())
|
||||
return false;
|
||||
if (!itemEntity.is_alive() || !itemEntity.has<ItemComponent>())
|
||||
return false;
|
||||
|
||||
bool result = addItemEntityToInventory(characterEntity, itemEntity);
|
||||
if (result) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"[ItemSystem] Picked up: " +
|
||||
itemEntity.get<ItemComponent>().itemName);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
88
src/features/editScene/systems/ItemSystem.hpp
Normal file
88
src/features/editScene/systems/ItemSystem.hpp
Normal file
@@ -0,0 +1,88 @@
|
||||
#ifndef EDITSCENE_ITEM_SYSTEM_HPP
|
||||
#define EDITSCENE_ITEM_SYSTEM_HPP
|
||||
#pragma once
|
||||
|
||||
#include <flecs.h>
|
||||
#include <Ogre.h>
|
||||
#include <string>
|
||||
|
||||
class EditorApp;
|
||||
class BehaviorTreeSystem;
|
||||
|
||||
/**
|
||||
* System that handles item pickup, drop, use, and inventory management.
|
||||
*
|
||||
* Provides a pure API for inventory operations. Proximity detection
|
||||
* for player pickup is handled by ActuatorSystem (which detects
|
||||
* entities with ItemComponent and shows "E - Pick up" prompts).
|
||||
*
|
||||
* For AI characters, behavior tree nodes provide inventory access.
|
||||
*/
|
||||
class ItemSystem {
|
||||
public:
|
||||
ItemSystem(flecs::world &world, Ogre::SceneManager *sceneMgr,
|
||||
EditorApp *editorApp, BehaviorTreeSystem *btSystem);
|
||||
~ItemSystem();
|
||||
|
||||
// --- Inventory manipulation API ---
|
||||
|
||||
/** Add an item to an inventory by itemId. Creates a new slot. */
|
||||
bool addItemToInventory(flecs::entity inventoryEntity,
|
||||
const Ogre::String &itemId,
|
||||
const Ogre::String &itemName,
|
||||
const Ogre::String &itemType, int stackSize = 1,
|
||||
float weight = 0.1f, int value = 1,
|
||||
const Ogre::String &useActionName = "");
|
||||
|
||||
/** Add an item entity (ItemComponent) to an inventory. */
|
||||
bool addItemEntityToInventory(flecs::entity inventoryEntity,
|
||||
flecs::entity itemEntity);
|
||||
|
||||
/** Remove items from inventory by itemId. Returns number removed. */
|
||||
int removeItemFromInventory(flecs::entity inventoryEntity,
|
||||
const Ogre::String &itemId, int count = 1);
|
||||
|
||||
/** Remove items from inventory by slot index. */
|
||||
bool removeItemFromSlot(flecs::entity inventoryEntity, int slotIndex,
|
||||
int count = 1);
|
||||
|
||||
/** Transfer items between two inventories. */
|
||||
bool transferItem(flecs::entity fromInventory, int slotIndex,
|
||||
flecs::entity toInventory, int count = 1);
|
||||
|
||||
/** Check if an inventory has at least one of a specific itemId. */
|
||||
bool hasItem(flecs::entity inventoryEntity,
|
||||
const Ogre::String &itemId) const;
|
||||
|
||||
/** Check if an inventory has at least one of a specific itemName. */
|
||||
bool hasItemByName(flecs::entity inventoryEntity,
|
||||
const Ogre::String &itemName) const;
|
||||
|
||||
/** Count how many of a specific itemId are in an inventory. */
|
||||
int countItem(flecs::entity inventoryEntity,
|
||||
const Ogre::String &itemId) const;
|
||||
|
||||
/** Drop an item from inventory into the world at a position. */
|
||||
bool dropItem(flecs::entity inventoryEntity, int slotIndex,
|
||||
const Ogre::Vector3 &worldPosition, int count = 1);
|
||||
|
||||
/** Use an item from inventory (executes its use action). */
|
||||
bool useItem(flecs::entity characterEntity,
|
||||
flecs::entity inventoryEntity, int slotIndex);
|
||||
|
||||
/**
|
||||
* Pick up a world item entity into a character's inventory.
|
||||
* Called by ActuatorSystem when player presses E near an item.
|
||||
* Returns true if the item was picked up.
|
||||
*/
|
||||
bool pickupItem(flecs::entity characterEntity,
|
||||
flecs::entity itemEntity);
|
||||
|
||||
private:
|
||||
flecs::world &m_world;
|
||||
Ogre::SceneManager *m_sceneMgr;
|
||||
EditorApp *m_editorApp;
|
||||
BehaviorTreeSystem *m_btSystem;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_ITEM_SYSTEM_HPP
|
||||
216
src/features/editScene/systems/PathFollowingSystem.cpp
Normal file
216
src/features/editScene/systems/PathFollowingSystem.cpp
Normal file
@@ -0,0 +1,216 @@
|
||||
#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 <OgreLogManager.h>
|
||||
#include <cmath>
|
||||
|
||||
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<TransformComponent>()) {
|
||||
auto &trans = e.get<TransformComponent>();
|
||||
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<TransformComponent>())
|
||||
return;
|
||||
auto &trans = e.get_mut<TransformComponent>();
|
||||
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<CharacterSlotsComponent>()) {
|
||||
auto &slots = e.get<CharacterSlotsComponent>();
|
||||
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<PathFollowingComponent>() || !m_animTreeSystem)
|
||||
return;
|
||||
|
||||
auto &path = e.get<PathFollowingComponent>();
|
||||
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<NavMeshComponent>().each(
|
||||
[&](flecs::entity e, NavMeshComponent &) {
|
||||
if (!navmeshEntity.is_alive())
|
||||
navmeshEntity = e;
|
||||
});
|
||||
|
||||
m_world.query<PathFollowingComponent, CharacterComponent,
|
||||
TransformComponent>()
|
||||
.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) {
|
||||
if (!cc.useRootMotion)
|
||||
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<Ogre::Vector3> 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) {
|
||||
if (!cc.useRootMotion)
|
||||
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";
|
||||
}
|
||||
|
||||
/* Only set velocity if root motion is not
|
||||
* active. When root motion is active,
|
||||
* AnimationTreeSystem already computed the
|
||||
* velocity from the animation displacement. */
|
||||
if (!cc.useRootMotion)
|
||||
cc.linearVelocity = toTarget * speed;
|
||||
|
||||
// Rotate character to face movement direction
|
||||
rotateTowards(e, toTarget, deltaTime);
|
||||
} else {
|
||||
if (!cc.useRootMotion)
|
||||
cc.linearVelocity = Ogre::Vector3::ZERO;
|
||||
}
|
||||
|
||||
applyLocomotionState(e);
|
||||
});
|
||||
}
|
||||
45
src/features/editScene/systems/PathFollowingSystem.hpp
Normal file
45
src/features/editScene/systems/PathFollowingSystem.hpp
Normal file
@@ -0,0 +1,45 @@
|
||||
#ifndef EDITSCENE_PATH_FOLLOWING_SYSTEM_HPP
|
||||
#define EDITSCENE_PATH_FOLLOWING_SYSTEM_HPP
|
||||
#pragma once
|
||||
|
||||
#include <flecs.h>
|
||||
#include <Ogre.h>
|
||||
|
||||
// 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
|
||||
@@ -164,9 +164,9 @@ void PlayerControllerSystem::updateTPSCamera(PlayerControllerComponent &pc,
|
||||
state.faceHidden = false;
|
||||
}
|
||||
|
||||
// Read mouse input
|
||||
// Read mouse input (skip if input locked by action)
|
||||
GameInputState &input = m_editorApp->getGameInputState();
|
||||
if (input.mouseMoved) {
|
||||
if (!pc.inputLocked && input.mouseMoved) {
|
||||
state.yaw -= input.mouseDeltaX * pc.mouseSensitivity;
|
||||
state.pitch -= input.mouseDeltaY * pc.mouseSensitivity;
|
||||
// Clamp pitch
|
||||
@@ -277,9 +277,9 @@ void PlayerControllerSystem::updateFPSCamera(PlayerControllerComponent &pc,
|
||||
|
||||
camNode->setPosition(boneWorldPos + offset);
|
||||
|
||||
// Apply mouse look
|
||||
// Apply mouse look (skip if input locked by action)
|
||||
GameInputState &input = m_editorApp->getGameInputState();
|
||||
if (input.mouseMoved) {
|
||||
if (!pc.inputLocked && input.mouseMoved) {
|
||||
state.yaw -= input.mouseDeltaX * pc.mouseSensitivity;
|
||||
state.pitch -= input.mouseDeltaY * pc.mouseSensitivity;
|
||||
if (state.pitch > 89.0f)
|
||||
@@ -302,8 +302,11 @@ void PlayerControllerSystem::updateLocomotion(PlayerControllerComponent &pc,
|
||||
if (!state.targetEntity.has<CharacterComponent>())
|
||||
return;
|
||||
|
||||
// Skip locomotion if input is locked by an executing action
|
||||
if (pc.inputLocked)
|
||||
return;
|
||||
|
||||
GameInputState &input = m_editorApp->getGameInputState();
|
||||
auto &cc = state.targetEntity.get_mut<CharacterComponent>();
|
||||
|
||||
// Get camera yaw for relative movement
|
||||
Ogre::Quaternion yawRot(Ogre::Degree(state.yaw), Ogre::Vector3::UNIT_Y);
|
||||
@@ -318,28 +321,26 @@ void PlayerControllerSystem::updateLocomotion(PlayerControllerComponent &pc,
|
||||
if (right.squaredLength() > 0.0001f)
|
||||
right.normalise();
|
||||
|
||||
Ogre::Vector3 desiredVel = Ogre::Vector3::ZERO;
|
||||
Ogre::Vector3 desiredDir = Ogre::Vector3::ZERO;
|
||||
if (input.w)
|
||||
desiredVel += forward;
|
||||
desiredDir += forward;
|
||||
if (input.s)
|
||||
desiredVel -= forward;
|
||||
desiredDir -= forward;
|
||||
if (input.a)
|
||||
desiredVel -= right;
|
||||
desiredDir -= right;
|
||||
if (input.d)
|
||||
desiredVel += right;
|
||||
desiredDir += right;
|
||||
|
||||
bool isMoving = desiredVel.squaredLength() > 0.0001f;
|
||||
float speed = input.shift ? 5.0f : 2.5f;
|
||||
bool isMoving = desiredDir.squaredLength() > 0.0001f;
|
||||
|
||||
if (isMoving) {
|
||||
desiredVel.normalise();
|
||||
cc.linearVelocity = desiredVel * speed;
|
||||
desiredDir.normalise();
|
||||
|
||||
// Rotate character to face movement direction
|
||||
auto &transform =
|
||||
state.targetEntity.get_mut<TransformComponent>();
|
||||
if (transform.node) {
|
||||
Ogre::Vector3 flatForward = desiredVel;
|
||||
Ogre::Vector3 flatForward = desiredDir;
|
||||
flatForward.y = 0;
|
||||
if (flatForward.squaredLength() > 0.0001f) {
|
||||
flatForward.normalise();
|
||||
@@ -354,8 +355,6 @@ void PlayerControllerSystem::updateLocomotion(PlayerControllerComponent &pc,
|
||||
targetRot, true));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cc.linearVelocity = Ogre::Vector3::ZERO;
|
||||
}
|
||||
|
||||
// Update animation state
|
||||
|
||||
@@ -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()) :
|
||||
"<root>") +
|
||||
std::to_string(parent.id()) :
|
||||
"<root>") +
|
||||
" nodeParent=" +
|
||||
(parentNode != m_sceneMgr->getRootSceneNode() ?
|
||||
parentNode->getName() :
|
||||
"<root>"));
|
||||
parentNode->getName() :
|
||||
"<root>"));
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -28,9 +28,15 @@
|
||||
#include "../components/WaterPlane.hpp"
|
||||
#include "../components/Sun.hpp"
|
||||
#include "../components/Skybox.hpp"
|
||||
#include "../components/EventHandler.hpp"
|
||||
#include "../components/ActionDatabase.hpp"
|
||||
#include "../components/ActionDebug.hpp"
|
||||
#include "../components/SmartObject.hpp"
|
||||
#include "../components/Actuator.hpp"
|
||||
#include "../components/GoapPlanner.hpp"
|
||||
#include "../components/Item.hpp"
|
||||
#include "../components/Inventory.hpp"
|
||||
#include "../components/PathFollowing.hpp"
|
||||
#include "../components/BehaviorTree.hpp"
|
||||
#include "../components/GoapBlackboard.hpp"
|
||||
#include "../components/NavMesh.hpp"
|
||||
@@ -68,6 +74,9 @@ bool SceneSerializer::saveToFile(const std::string &filepath)
|
||||
}
|
||||
});
|
||||
|
||||
// Save ActionDatabase singleton at scene level
|
||||
scene["actionDatabase"] = serializeActionDatabase();
|
||||
|
||||
// Write to file
|
||||
std::ofstream file(filepath);
|
||||
if (!file.is_open()) {
|
||||
@@ -115,6 +124,11 @@ bool SceneSerializer::loadFromFile(const std::string &filepath,
|
||||
}
|
||||
}
|
||||
|
||||
// Load ActionDatabase singleton at scene level
|
||||
if (scene.contains("actionDatabase")) {
|
||||
deserializeActionDatabase(scene["actionDatabase"]);
|
||||
}
|
||||
|
||||
// Clear entity map for new load
|
||||
m_entityMap.clear();
|
||||
|
||||
@@ -289,19 +303,37 @@ nlohmann::json SceneSerializer::serializeEntity(flecs::entity entity)
|
||||
json["waterPlane"] = serializeWaterPlane(entity);
|
||||
}
|
||||
|
||||
if (entity.has<ActionDatabase>()) {
|
||||
json["actionDatabase"] = serializeActionDatabase(entity);
|
||||
}
|
||||
// ActionDatabase is now a singleton, serialized at scene level
|
||||
if (entity.has<ActionDebug>()) {
|
||||
json["actionDebug"] = serializeActionDebug(entity);
|
||||
}
|
||||
if (entity.has<PathFollowingComponent>()) {
|
||||
json["pathFollowing"] = serializePathFollowing(entity);
|
||||
}
|
||||
if (entity.has<SmartObjectComponent>()) {
|
||||
json["smartObject"] = serializeSmartObject(entity);
|
||||
}
|
||||
if (entity.has<ActuatorComponent>()) {
|
||||
json["actuator"] = serializeActuator(entity);
|
||||
}
|
||||
if (entity.has<EventHandlerComponent>()) {
|
||||
json["eventHandler"] = serializeEventHandler(entity);
|
||||
}
|
||||
if (entity.has<GoapPlannerComponent>()) {
|
||||
json["goapPlanner"] = serializeGoapPlanner(entity);
|
||||
}
|
||||
if (entity.has<BehaviorTreeComponent>()) {
|
||||
json["behaviorTree"] = serializeBehaviorTree(entity);
|
||||
}
|
||||
|
||||
if (entity.has<ItemComponent>()) {
|
||||
json["item"] = serializeItem(entity);
|
||||
}
|
||||
|
||||
if (entity.has<InventoryComponent>()) {
|
||||
json["inventory"] = serializeInventory(entity);
|
||||
}
|
||||
|
||||
if (entity.has<PrefabInstanceComponent>()) {
|
||||
json["prefabInstance"] = serializePrefabInstance(entity);
|
||||
}
|
||||
@@ -489,19 +521,37 @@ void SceneSerializer::deserializeEntity(const nlohmann::json &json,
|
||||
deserializeWaterPlane(entity, json["waterPlane"]);
|
||||
}
|
||||
|
||||
if (json.contains("actionDatabase")) {
|
||||
deserializeActionDatabase(entity, json["actionDatabase"]);
|
||||
}
|
||||
// ActionDatabase is now a singleton, deserialized at scene level
|
||||
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("actuator")) {
|
||||
deserializeActuator(entity, json["actuator"]);
|
||||
}
|
||||
if (json.contains("eventHandler")) {
|
||||
deserializeEventHandler(entity, json["eventHandler"]);
|
||||
}
|
||||
if (json.contains("goapPlanner")) {
|
||||
deserializeGoapPlanner(entity, json["goapPlanner"]);
|
||||
}
|
||||
if (json.contains("behaviorTree")) {
|
||||
deserializeBehaviorTree(entity, json["behaviorTree"]);
|
||||
}
|
||||
|
||||
if (json.contains("item")) {
|
||||
deserializeItem(entity, json["item"]);
|
||||
}
|
||||
|
||||
if (json.contains("inventory")) {
|
||||
deserializeInventory(entity, json["inventory"]);
|
||||
}
|
||||
|
||||
if (json.contains("sun")) {
|
||||
deserializeSun(entity, json["sun"]);
|
||||
}
|
||||
@@ -542,14 +592,13 @@ void SceneSerializer::deserializeEntityFirstPass(const nlohmann::json &json,
|
||||
entity.child_of(parent);
|
||||
}
|
||||
|
||||
deserializeEntityComponents(entity, json, parent, uiSystem,
|
||||
true, true, true);
|
||||
deserializeEntityComponents(entity, json, parent, uiSystem, true, true,
|
||||
true);
|
||||
}
|
||||
|
||||
void SceneSerializer::deserializeEntityComponents(
|
||||
flecs::entity entity, const nlohmann::json &json,
|
||||
flecs::entity parent, EditorUISystem *uiSystem,
|
||||
bool processTransform, bool processName,
|
||||
flecs::entity entity, const nlohmann::json &json, flecs::entity parent,
|
||||
EditorUISystem *uiSystem, bool processTransform, bool processName,
|
||||
bool addEditorMarker)
|
||||
{
|
||||
if (processName) {
|
||||
@@ -603,8 +652,7 @@ void SceneSerializer::deserializeEntityComponents(
|
||||
}
|
||||
|
||||
if (json.contains("proceduralTexture")) {
|
||||
deserializeProceduralTexture(entity,
|
||||
json["proceduralTexture"]);
|
||||
deserializeProceduralTexture(entity, json["proceduralTexture"]);
|
||||
}
|
||||
|
||||
if (json.contains("proceduralMaterial")) {
|
||||
@@ -638,8 +686,7 @@ void SceneSerializer::deserializeEntityComponents(
|
||||
}
|
||||
|
||||
if (json.contains("playerController")) {
|
||||
deserializePlayerController(entity,
|
||||
json["playerController"]);
|
||||
deserializePlayerController(entity, json["playerController"]);
|
||||
}
|
||||
|
||||
if (json.contains("triangleBuffer")) {
|
||||
@@ -737,8 +784,7 @@ void SceneSerializer::deserializeEntityComponents(
|
||||
deserializeRoof(entity, json["roof"]);
|
||||
}
|
||||
if (json.contains("furnitureTemplate")) {
|
||||
deserializeFurnitureTemplate(entity,
|
||||
json["furnitureTemplate"]);
|
||||
deserializeFurnitureTemplate(entity, json["furnitureTemplate"]);
|
||||
}
|
||||
if (json.contains("clearArea")) {
|
||||
deserializeClearArea(entity, json["clearArea"]);
|
||||
@@ -749,8 +795,8 @@ void SceneSerializer::deserializeEntityComponents(
|
||||
}
|
||||
|
||||
if (json.contains("navMeshGeometrySource")) {
|
||||
deserializeNavMeshGeometrySource(
|
||||
entity, json["navMeshGeometrySource"]);
|
||||
deserializeNavMeshGeometrySource(entity,
|
||||
json["navMeshGeometrySource"]);
|
||||
}
|
||||
|
||||
if (json.contains("buoyancyInfo")) {
|
||||
@@ -765,19 +811,37 @@ void SceneSerializer::deserializeEntityComponents(
|
||||
deserializeWaterPlane(entity, json["waterPlane"]);
|
||||
}
|
||||
|
||||
if (json.contains("actionDatabase")) {
|
||||
deserializeActionDatabase(entity, json["actionDatabase"]);
|
||||
}
|
||||
// ActionDatabase is now a singleton, deserialized at scene level
|
||||
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("actuator")) {
|
||||
deserializeActuator(entity, json["actuator"]);
|
||||
if (json.contains("eventHandler")) {
|
||||
deserializeEventHandler(entity, json["eventHandler"]);
|
||||
}
|
||||
}
|
||||
if (json.contains("goapPlanner")) {
|
||||
deserializeGoapPlanner(entity, json["goapPlanner"]);
|
||||
}
|
||||
if (json.contains("behaviorTree")) {
|
||||
deserializeBehaviorTree(entity, json["behaviorTree"]);
|
||||
}
|
||||
|
||||
if (json.contains("item")) {
|
||||
deserializeItem(entity, json["item"]);
|
||||
}
|
||||
|
||||
if (json.contains("inventory")) {
|
||||
deserializeInventory(entity, json["inventory"]);
|
||||
}
|
||||
|
||||
if (json.contains("sun")) {
|
||||
deserializeSun(entity, json["sun"]);
|
||||
}
|
||||
@@ -820,8 +884,8 @@ nlohmann::json SceneSerializer::serializePrefabInstance(flecs::entity entity)
|
||||
return json;
|
||||
}
|
||||
|
||||
void SceneSerializer::deserializePrefabInstance(
|
||||
flecs::entity entity, const nlohmann::json &json)
|
||||
void SceneSerializer::deserializePrefabInstance(flecs::entity entity,
|
||||
const nlohmann::json &json)
|
||||
{
|
||||
PrefabInstanceComponent prefab;
|
||||
prefab.prefabPath = json.value("prefabPath", "");
|
||||
@@ -852,8 +916,8 @@ bool SceneSerializer::savePrefab(flecs::entity rootEntity,
|
||||
}
|
||||
}
|
||||
|
||||
flecs::entity SceneSerializer::loadPrefabForEdit(
|
||||
const std::string &filepath, EditorUISystem *uiSystem)
|
||||
flecs::entity SceneSerializer::loadPrefabForEdit(const std::string &filepath,
|
||||
EditorUISystem *uiSystem)
|
||||
{
|
||||
try {
|
||||
std::ifstream file(filepath);
|
||||
@@ -878,9 +942,8 @@ flecs::entity SceneSerializer::loadPrefabForEdit(
|
||||
}
|
||||
|
||||
deserializeEntityComponents(entity, prefabJson,
|
||||
flecs::entity::null(),
|
||||
uiSystem, true, true,
|
||||
true);
|
||||
flecs::entity::null(), uiSystem,
|
||||
true, true, true);
|
||||
|
||||
return entity;
|
||||
} catch (const std::exception &e) {
|
||||
@@ -918,17 +981,16 @@ bool SceneSerializer::instantiatePrefab(flecs::entity instanceEntity,
|
||||
// is the world-space override.
|
||||
// Skip name — the instance entity keeps its own name.
|
||||
flecs::entity parent = instanceEntity.parent();
|
||||
deserializeEntityComponents(instanceEntity, prefabJson,
|
||||
parent, uiSystem, false,
|
||||
false, false);
|
||||
deserializeEntityComponents(instanceEntity, prefabJson, parent,
|
||||
uiSystem, false, false, false);
|
||||
|
||||
// Restore main scene entity map
|
||||
m_entityMap = savedMap;
|
||||
|
||||
return true;
|
||||
} catch (const std::exception &e) {
|
||||
m_lastError = std::string("Prefab instantiate error: ") +
|
||||
e.what();
|
||||
m_lastError =
|
||||
std::string("Prefab instantiate error: ") + e.what();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -2837,6 +2899,13 @@ nlohmann::json SceneSerializer::serializePlayerController(flecs::entity entity)
|
||||
json["swimIdleState"] = pc.swimIdleState;
|
||||
json["swimState"] = pc.swimState;
|
||||
json["swimFastState"] = pc.swimFastState;
|
||||
json["actuatorDistance"] = pc.actuatorDistance;
|
||||
json["actuatorCooldown"] = pc.actuatorCooldown;
|
||||
json["actuatorColor"] = { pc.actuatorColor.x, pc.actuatorColor.y,
|
||||
pc.actuatorColor.z };
|
||||
json["distantCircleRadius"] = pc.distantCircleRadius;
|
||||
json["nearCircleRadius"] = pc.nearCircleRadius;
|
||||
json["actuatorLabelFontSize"] = pc.actuatorLabelFontSize;
|
||||
return json;
|
||||
}
|
||||
|
||||
@@ -2873,6 +2942,25 @@ void SceneSerializer::deserializePlayerController(flecs::entity entity,
|
||||
pc.swimIdleState = json.value("swimIdleState", pc.swimIdleState);
|
||||
pc.swimState = json.value("swimState", pc.swimState);
|
||||
pc.swimFastState = json.value("swimFastState", pc.swimFastState);
|
||||
pc.actuatorDistance =
|
||||
json.value("actuatorDistance", pc.actuatorDistance);
|
||||
pc.actuatorCooldown =
|
||||
json.value("actuatorCooldown", pc.actuatorCooldown);
|
||||
if (json.contains("actuatorColor") &&
|
||||
json["actuatorColor"].is_array() &&
|
||||
json["actuatorColor"].size() >= 3) {
|
||||
pc.actuatorColor = Ogre::Vector3(json["actuatorColor"][0],
|
||||
json["actuatorColor"][1],
|
||||
json["actuatorColor"][2]);
|
||||
}
|
||||
pc.distantCircleRadius =
|
||||
json.value("distantCircleRadius", pc.distantCircleRadius);
|
||||
pc.nearCircleRadius =
|
||||
json.value("nearCircleRadius", pc.nearCircleRadius);
|
||||
pc.actuatorLabelFontSize =
|
||||
json.value("actuatorLabelFontSize", pc.actuatorLabelFontSize);
|
||||
// inputLocked is runtime-only, always reset to false on load
|
||||
pc.inputLocked = false;
|
||||
entity.set<PlayerControllerComponent>(pc);
|
||||
}
|
||||
|
||||
@@ -3172,6 +3260,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 +3290,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 +3353,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 +3371,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"]);
|
||||
@@ -3304,9 +3398,12 @@ static void deserializeGoapGoal(GoapGoal &goal, const nlohmann::json &json)
|
||||
goal.condition = json.value("condition", "");
|
||||
}
|
||||
|
||||
nlohmann::json SceneSerializer::serializeActionDatabase(flecs::entity entity)
|
||||
nlohmann::json SceneSerializer::serializeActionDatabase()
|
||||
{
|
||||
const ActionDatabase &db = entity.get<ActionDatabase>();
|
||||
const ActionDatabase *dbPtr = ActionDatabase::getSingletonPtr();
|
||||
if (!dbPtr)
|
||||
return nlohmann::json();
|
||||
const ActionDatabase &db = *dbPtr;
|
||||
nlohmann::json json;
|
||||
|
||||
json["actions"] = nlohmann::json::array();
|
||||
@@ -3334,16 +3431,17 @@ nlohmann::json SceneSerializer::serializeActionDatabase(flecs::entity entity)
|
||||
return json;
|
||||
}
|
||||
|
||||
void SceneSerializer::deserializeActionDatabase(flecs::entity entity,
|
||||
const nlohmann::json &json)
|
||||
void SceneSerializer::deserializeActionDatabase(const nlohmann::json &json)
|
||||
{
|
||||
ActionDatabase db;
|
||||
ActionDatabase *db = ActionDatabase::getSingletonPtr();
|
||||
if (!db)
|
||||
return;
|
||||
|
||||
if (json.contains("actions") && json["actions"].is_array()) {
|
||||
for (const auto &actionJson : json["actions"]) {
|
||||
GoapAction action;
|
||||
deserializeGoapAction(action, actionJson);
|
||||
db.actions.push_back(action);
|
||||
db->addOrReplaceAction(action);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3351,7 +3449,7 @@ void SceneSerializer::deserializeActionDatabase(flecs::entity entity,
|
||||
for (const auto &goalJson : json["goals"]) {
|
||||
GoapGoal goal;
|
||||
deserializeGoapGoal(goal, goalJson);
|
||||
db.goals.push_back(goal);
|
||||
db->addOrReplaceGoal(goal);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3364,8 +3462,6 @@ void SceneSerializer::deserializeActionDatabase(flecs::entity entity,
|
||||
entry["name"].get<std::string>());
|
||||
}
|
||||
}
|
||||
|
||||
entity.set<ActionDatabase>(db);
|
||||
}
|
||||
|
||||
nlohmann::json SceneSerializer::serializeActionDebug(flecs::entity entity)
|
||||
@@ -3377,10 +3473,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<PathFollowingComponent>();
|
||||
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,10 +3493,9 @@ 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;
|
||||
}
|
||||
|
||||
@@ -3407,11 +3507,16 @@ void SceneSerializer::deserializeActionDebug(flecs::entity entity,
|
||||
deserializeGoapBlackboard(debug.blackboard, json["blackboard"]);
|
||||
debug.selectedActionName = json.value("selectedActionName", "");
|
||||
debug.selectedGoalName = json.value("selectedGoalName", "");
|
||||
entity.set<ActionDebug>(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 +3532,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 +3550,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<ActionDebug>(debug);
|
||||
pf.walkSpeed = json.value("walkSpeed", 2.5f);
|
||||
pf.runSpeed = json.value("runSpeed", 5.0f);
|
||||
pf.useRootMotion = json.value("useRootMotion", true);
|
||||
entity.set<PathFollowingComponent>(pf);
|
||||
}
|
||||
|
||||
nlohmann::json SceneSerializer::serializeSmartObject(flecs::entity entity)
|
||||
@@ -3507,6 +3592,46 @@ void SceneSerializer::deserializeSmartObject(flecs::entity entity,
|
||||
entity.set<SmartObjectComponent>(so);
|
||||
}
|
||||
|
||||
nlohmann::json SceneSerializer::serializeGoapPlanner(flecs::entity entity)
|
||||
{
|
||||
const GoapPlannerComponent &planner =
|
||||
entity.get<GoapPlannerComponent>();
|
||||
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<GoapPlannerComponent>(planner);
|
||||
}
|
||||
|
||||
nlohmann::json SceneSerializer::serializeBehaviorTree(flecs::entity entity)
|
||||
{
|
||||
const BehaviorTreeComponent &bt = entity.get<BehaviorTreeComponent>();
|
||||
@@ -3587,3 +3712,140 @@ void SceneSerializer::deserializeNavMeshGeometrySource(
|
||||
src.include = json.value("include", true);
|
||||
entity.set<NavMeshGeometrySource>(src);
|
||||
}
|
||||
|
||||
nlohmann::json SceneSerializer::serializeActuator(flecs::entity entity)
|
||||
{
|
||||
const ActuatorComponent &actuator = entity.get<ActuatorComponent>();
|
||||
nlohmann::json json;
|
||||
json["radius"] = actuator.radius;
|
||||
json["height"] = actuator.height;
|
||||
json["actionNames"] = actuator.actionNames;
|
||||
return json;
|
||||
}
|
||||
|
||||
void SceneSerializer::deserializeActuator(flecs::entity entity,
|
||||
const nlohmann::json &json)
|
||||
{
|
||||
ActuatorComponent actuator;
|
||||
actuator.radius = json.value("radius", 1.5f);
|
||||
actuator.height = json.value("height", 1.8f);
|
||||
if (json.contains("actionNames") && json["actionNames"].is_array()) {
|
||||
actuator.actionNames.clear();
|
||||
for (const auto &name : json["actionNames"]) {
|
||||
if (name.is_string())
|
||||
actuator.actionNames.push_back(name);
|
||||
}
|
||||
}
|
||||
entity.set<ActuatorComponent>(actuator);
|
||||
}
|
||||
|
||||
nlohmann::json SceneSerializer::serializeEventHandler(flecs::entity entity)
|
||||
{
|
||||
const EventHandlerComponent &handler =
|
||||
entity.get<EventHandlerComponent>();
|
||||
nlohmann::json json;
|
||||
json["eventName"] = handler.eventName;
|
||||
json["actionName"] = handler.actionName;
|
||||
json["enabled"] = handler.enabled;
|
||||
return json;
|
||||
}
|
||||
|
||||
void SceneSerializer::deserializeEventHandler(flecs::entity entity,
|
||||
const nlohmann::json &json)
|
||||
{
|
||||
EventHandlerComponent handler;
|
||||
handler.eventName = json.value("eventName", handler.eventName);
|
||||
handler.actionName = json.value("actionName", handler.actionName);
|
||||
handler.enabled = json.value("enabled", handler.enabled);
|
||||
entity.set<EventHandlerComponent>(handler);
|
||||
}
|
||||
|
||||
nlohmann::json SceneSerializer::serializeItem(flecs::entity entity)
|
||||
{
|
||||
const ItemComponent &item = entity.get<ItemComponent>();
|
||||
nlohmann::json json;
|
||||
json["itemName"] = item.itemName;
|
||||
json["itemType"] = item.itemType;
|
||||
json["itemId"] = item.itemId;
|
||||
json["stackSize"] = item.stackSize;
|
||||
json["maxStackSize"] = item.maxStackSize;
|
||||
json["weight"] = item.weight;
|
||||
json["value"] = item.value;
|
||||
json["useActionName"] = item.useActionName;
|
||||
return json;
|
||||
}
|
||||
|
||||
nlohmann::json SceneSerializer::serializeInventory(flecs::entity entity)
|
||||
{
|
||||
const InventoryComponent &inv = entity.get<InventoryComponent>();
|
||||
nlohmann::json json;
|
||||
json["maxSlots"] = inv.maxSlots;
|
||||
json["maxWeight"] = inv.maxWeight;
|
||||
json["isContainer"] = inv.isContainer;
|
||||
json["isOpen"] = inv.isOpen;
|
||||
|
||||
nlohmann::json slotsJson = nlohmann::json::array();
|
||||
for (const auto &slot : inv.slots) {
|
||||
nlohmann::json slotJson;
|
||||
slotJson["itemEntity"] = (uint64_t)slot.itemEntity;
|
||||
slotJson["itemName"] = slot.itemName;
|
||||
slotJson["itemType"] = slot.itemType;
|
||||
slotJson["itemId"] = slot.itemId;
|
||||
slotJson["stackSize"] = slot.stackSize;
|
||||
slotJson["maxStackSize"] = slot.maxStackSize;
|
||||
slotJson["weight"] = slot.weight;
|
||||
slotJson["value"] = slot.value;
|
||||
slotJson["useActionName"] = slot.useActionName;
|
||||
slotsJson.push_back(slotJson);
|
||||
}
|
||||
json["slots"] = slotsJson;
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
void SceneSerializer::deserializeItem(flecs::entity entity,
|
||||
const nlohmann::json &json)
|
||||
{
|
||||
ItemComponent item;
|
||||
item.itemName = json.value("itemName", "Item");
|
||||
item.itemType = json.value("itemType", "misc");
|
||||
item.itemId = json.value("itemId", "");
|
||||
item.stackSize = json.value("stackSize", 1);
|
||||
item.maxStackSize = json.value("maxStackSize", 99);
|
||||
item.weight = json.value("weight", 0.1f);
|
||||
item.value = json.value("value", 1);
|
||||
item.useActionName = json.value("useActionName", "");
|
||||
entity.set<ItemComponent>(item);
|
||||
}
|
||||
|
||||
void SceneSerializer::deserializeInventory(flecs::entity entity,
|
||||
const nlohmann::json &json)
|
||||
{
|
||||
InventoryComponent inv;
|
||||
inv.maxSlots = json.value("maxSlots", 20);
|
||||
inv.maxWeight = json.value("maxWeight", 50.0f);
|
||||
inv.isContainer = json.value("isContainer", false);
|
||||
inv.isOpen = json.value("isOpen", false);
|
||||
|
||||
inv.slots.clear();
|
||||
if (json.contains("slots") && json["slots"].is_array()) {
|
||||
for (const auto &slotJson : json["slots"]) {
|
||||
InventorySlot slot;
|
||||
slot.itemEntity = (flecs::entity_t)slotJson.value(
|
||||
"itemEntity", (uint64_t)0);
|
||||
slot.itemName = slotJson.value("itemName", "");
|
||||
slot.itemType = slotJson.value("itemType", "");
|
||||
slot.itemId = slotJson.value("itemId", "");
|
||||
slot.stackSize = slotJson.value("stackSize", 0);
|
||||
slot.maxStackSize = slotJson.value("maxStackSize", 99);
|
||||
slot.weight = slotJson.value("weight", 0.1f);
|
||||
slot.value = slotJson.value("value", 1);
|
||||
slot.useActionName =
|
||||
slotJson.value("useActionName", "");
|
||||
inv.slots.push_back(slot);
|
||||
}
|
||||
}
|
||||
|
||||
inv.recalculateWeight();
|
||||
entity.set<InventoryComponent>(inv);
|
||||
}
|
||||
|
||||
@@ -40,8 +40,7 @@ public:
|
||||
/**
|
||||
* Save an entity subtree as a prefab JSON file.
|
||||
*/
|
||||
bool savePrefab(flecs::entity rootEntity,
|
||||
const std::string &filepath);
|
||||
bool savePrefab(flecs::entity rootEntity, const std::string &filepath);
|
||||
|
||||
/**
|
||||
* Instantiate a prefab onto an existing entity.
|
||||
@@ -205,17 +204,35 @@ private:
|
||||
void deserializeSkybox(flecs::entity entity,
|
||||
const nlohmann::json &json);
|
||||
|
||||
// Item/Inventory serialization
|
||||
nlohmann::json serializeItem(flecs::entity entity);
|
||||
nlohmann::json serializeInventory(flecs::entity entity);
|
||||
void deserializeItem(flecs::entity entity, const nlohmann::json &json);
|
||||
void deserializeInventory(flecs::entity entity,
|
||||
const nlohmann::json &json);
|
||||
|
||||
// AI/GOAP serialization
|
||||
nlohmann::json serializeActionDatabase(flecs::entity entity);
|
||||
nlohmann::json serializeActionDatabase();
|
||||
nlohmann::json serializeActionDebug(flecs::entity entity);
|
||||
nlohmann::json serializePathFollowing(flecs::entity entity);
|
||||
nlohmann::json serializeSmartObject(flecs::entity entity);
|
||||
nlohmann::json serializeActuator(flecs::entity entity);
|
||||
nlohmann::json serializeEventHandler(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 deserializeActionDatabase(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 deserializeActuator(flecs::entity entity,
|
||||
const nlohmann::json &json);
|
||||
void deserializeEventHandler(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);
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -19,6 +20,20 @@
|
||||
#include <cmath>
|
||||
#include <unordered_set>
|
||||
|
||||
/**
|
||||
* Check if a character entity has root motion enabled in its
|
||||
* AnimationTreeComponent. When root motion is active, the
|
||||
* AnimationTreeSystem computes linearVelocity from animation
|
||||
* root bone displacement, so SmartObjectSystem should NOT
|
||||
* overwrite it with direct movement velocity.
|
||||
*/
|
||||
static bool hasRootMotion(flecs::entity e)
|
||||
{
|
||||
if (!e.has<AnimationTreeComponent>())
|
||||
return false;
|
||||
return e.get<AnimationTreeComponent>().useRootMotion;
|
||||
}
|
||||
|
||||
SmartObjectSystem *SmartObjectSystem::s_instance = nullptr;
|
||||
|
||||
SmartObjectSystem::SmartObjectSystem(flecs::world &world,
|
||||
@@ -115,11 +130,10 @@ void SmartObjectSystem::setLocomotionState(flecs::entity e,
|
||||
if (!e.has<AnimationTreeComponent>())
|
||||
return;
|
||||
|
||||
// Look up the path following state in ActionDebug
|
||||
if (e.has<ActionDebug>()) {
|
||||
auto &debug = e.get<ActionDebug>();
|
||||
const PathFollowingState *pfState =
|
||||
debug.findPathState(animName);
|
||||
// Look up the path following state in PathFollowingComponent
|
||||
if (e.has<PathFollowingComponent>()) {
|
||||
auto &pf = e.get<PathFollowingComponent>();
|
||||
const PathFollowingState *pfState = pf.findState(animName);
|
||||
if (pfState) {
|
||||
// Apply ALL state machine name -> state name pairs
|
||||
for (const auto &pair : pfState->stateMachineStates) {
|
||||
@@ -142,13 +156,8 @@ bool SmartObjectSystem::testSmartObjectAction(flecs::entity character,
|
||||
if (!character.is_alive() || !smartObject.is_alive())
|
||||
return false;
|
||||
|
||||
// Find the action in the database
|
||||
ActionDatabase *db = nullptr;
|
||||
m_world.query<ActionDatabase>().each(
|
||||
[&](flecs::entity, ActionDatabase &database) {
|
||||
if (!db)
|
||||
db = &database;
|
||||
});
|
||||
// Find the action in the singleton database
|
||||
ActionDatabase *db = ActionDatabase::getSingletonPtr();
|
||||
if (!db)
|
||||
return false;
|
||||
|
||||
@@ -174,6 +183,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
|
||||
@@ -184,13 +203,8 @@ void SmartObjectSystem::update(float deltaTime)
|
||||
navmeshEntity = e;
|
||||
});
|
||||
|
||||
// Find the action database
|
||||
ActionDatabase *db = nullptr;
|
||||
m_world.query<ActionDatabase>().each(
|
||||
[&](flecs::entity, ActionDatabase &database) {
|
||||
if (!db)
|
||||
db = &database;
|
||||
});
|
||||
// Get the action database singleton
|
||||
ActionDatabase *db = ActionDatabase::getSingletonPtr();
|
||||
|
||||
// Determine if we're in game mode. In game mode, player-controlled
|
||||
// characters are managed by PlayerControllerSystem and should NOT
|
||||
@@ -550,16 +564,16 @@ void SmartObjectSystem::update(float deltaTime)
|
||||
float speed = 2.5f; // walk speed
|
||||
|
||||
// Use run speed if far away
|
||||
if (e.has<ActionDebug>()) {
|
||||
auto &debug =
|
||||
e.get<ActionDebug>();
|
||||
if (e.has<PathFollowingComponent>()) {
|
||||
auto &pf = e.get<
|
||||
PathFollowingComponent>();
|
||||
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");
|
||||
}
|
||||
@@ -567,12 +581,19 @@ void SmartObjectSystem::update(float deltaTime)
|
||||
setLocomotionState(e, "walk");
|
||||
}
|
||||
|
||||
cc.linearVelocity = toTarget * speed;
|
||||
// When root motion is enabled, the
|
||||
// AnimationTreeSystem already computes
|
||||
// linearVelocity from animation root
|
||||
// bone displacement. Do NOT overwrite
|
||||
// it with direct movement velocity.
|
||||
if (!hasRootMotion(e))
|
||||
cc.linearVelocity =
|
||||
toTarget * speed;
|
||||
|
||||
// Rotate character to face movement
|
||||
// direction (Y plane only)
|
||||
rotateTowards(e, toTarget, deltaTime);
|
||||
} else {
|
||||
} else if (!hasRootMotion(e)) {
|
||||
cc.linearVelocity = Ogre::Vector3::ZERO;
|
||||
}
|
||||
return;
|
||||
@@ -631,167 +652,213 @@ void SmartObjectSystem::update(float deltaTime)
|
||||
});
|
||||
|
||||
// Also process characters with ActionDebug (for editor testing)
|
||||
m_world.query<CharacterComponent, TransformComponent, ActionDebug>()
|
||||
.each([&](flecs::entity e, CharacterComponent &cc,
|
||||
TransformComponent &trans, ActionDebug &debug) {
|
||||
(void)trans;
|
||||
(void)debug;
|
||||
m_world.query<CharacterComponent, TransformComponent, ActionDebug>().each([&](flecs::entity
|
||||
e,
|
||||
CharacterComponent
|
||||
&cc,
|
||||
TransformComponent
|
||||
&trans,
|
||||
ActionDebug
|
||||
&debug) {
|
||||
(void)trans;
|
||||
(void)debug;
|
||||
|
||||
// Skip player-controlled characters - they are managed
|
||||
// by PlayerControllerSystem, not by SmartObjectSystem AI.
|
||||
if (playerCharacterIds.find(e.id()) !=
|
||||
playerCharacterIds.end())
|
||||
return;
|
||||
// Skip player-controlled characters - they are managed
|
||||
// by PlayerControllerSystem, not by SmartObjectSystem AI.
|
||||
if (playerCharacterIds.find(e.id()) != playerCharacterIds.end())
|
||||
return;
|
||||
|
||||
auto &state = m_states[e.id()];
|
||||
auto &state = m_states[e.id()];
|
||||
|
||||
// When the behavior tree is running (via ActionDebug),
|
||||
// do NOT override animation states - the behavior tree's
|
||||
// setAnimationState nodes should be the sole controller.
|
||||
if (debug.isRunning &&
|
||||
!debug.currentActionName.empty()) {
|
||||
// Behavior tree controls animations, skip
|
||||
// locomotion state override
|
||||
} else if (state.state == State::Idle) {
|
||||
setLocomotionState(e, "idle");
|
||||
// When the behavior tree is running (via ActionDebug),
|
||||
// do NOT override animation states - the behavior tree's
|
||||
// setAnimationState nodes should be the sole controller.
|
||||
if (debug.isRunning && !debug.currentActionName.empty()) {
|
||||
// Behavior tree controls animations, skip
|
||||
// locomotion state override
|
||||
} else if (state.state == State::Idle) {
|
||||
setLocomotionState(e, "idle");
|
||||
return;
|
||||
}
|
||||
|
||||
GoapBlackboard &bb = debug.blackboard;
|
||||
|
||||
// --- State: Pathfinding ---
|
||||
if (state.state == State::Pathfinding) {
|
||||
if (!state.target.smartObject.is_alive()) {
|
||||
std::cout
|
||||
<< "[SO] Pathfinding: target dead, going idle"
|
||||
<< std::endl;
|
||||
state.state = State::Idle;
|
||||
return;
|
||||
}
|
||||
|
||||
GoapBlackboard &bb = debug.blackboard;
|
||||
Ogre::Vector3 charPos = getEntityPosition(e);
|
||||
Ogre::Vector3 objPos =
|
||||
getEntityPosition(state.target.smartObject);
|
||||
|
||||
// --- State: Pathfinding ---
|
||||
if (state.state == State::Pathfinding) {
|
||||
if (!state.target.smartObject.is_alive()) {
|
||||
auto &soComp = state.target.smartObject
|
||||
.get<SmartObjectComponent>();
|
||||
if (isInRange(charPos, objPos, soComp.radius,
|
||||
soComp.height)) {
|
||||
std::cout
|
||||
<< "[SO] Pathfinding: already in range, executing"
|
||||
<< std::endl;
|
||||
state.state = State::Executing;
|
||||
state.target.isExecuting = true;
|
||||
state.target.executionTimer = 0.0f;
|
||||
cc.linearVelocity = Ogre::Vector3::ZERO;
|
||||
return;
|
||||
}
|
||||
|
||||
if (navmeshEntity.is_alive() && m_navSystem) {
|
||||
state.target.path.clear();
|
||||
state.target.pathIndex = 0;
|
||||
bool found = m_navSystem->findPath(
|
||||
navmeshEntity, charPos, objPos,
|
||||
state.target.path);
|
||||
if (found && !state.target.path.empty()) {
|
||||
std::cout
|
||||
<< "[SO] Pathfinding: target dead, going idle"
|
||||
<< std::endl;
|
||||
state.state = State::Idle;
|
||||
return;
|
||||
}
|
||||
|
||||
Ogre::Vector3 charPos = getEntityPosition(e);
|
||||
Ogre::Vector3 objPos = getEntityPosition(
|
||||
state.target.smartObject);
|
||||
|
||||
auto &soComp =
|
||||
state.target.smartObject
|
||||
.get<SmartObjectComponent>();
|
||||
if (isInRange(charPos, objPos, soComp.radius,
|
||||
soComp.height)) {
|
||||
std::cout
|
||||
<< "[SO] Pathfinding: already in range, executing"
|
||||
<< std::endl;
|
||||
state.state = State::Executing;
|
||||
state.target.isExecuting = true;
|
||||
state.target.executionTimer = 0.0f;
|
||||
cc.linearVelocity = Ogre::Vector3::ZERO;
|
||||
return;
|
||||
}
|
||||
|
||||
if (navmeshEntity.is_alive() && m_navSystem) {
|
||||
state.target.path.clear();
|
||||
state.target.pathIndex = 0;
|
||||
bool found = m_navSystem->findPath(
|
||||
navmeshEntity, charPos, objPos,
|
||||
state.target.path);
|
||||
if (found &&
|
||||
!state.target.path.empty()) {
|
||||
std::cout
|
||||
<< "[SO] Pathfinding: path found with "
|
||||
<< state.target.path
|
||||
.size()
|
||||
<< " waypoints"
|
||||
<< std::endl;
|
||||
state.state = State::Moving;
|
||||
} else {
|
||||
std::cout
|
||||
<< "[SO] Pathfinding: no path found, going idle"
|
||||
<< std::endl;
|
||||
state.state = State::Idle;
|
||||
cc.linearVelocity =
|
||||
Ogre::Vector3::ZERO;
|
||||
}
|
||||
<< "[SO] Pathfinding: path found with "
|
||||
<< state.target.path.size()
|
||||
<< " waypoints" << std::endl;
|
||||
state.state = State::Moving;
|
||||
} else {
|
||||
std::cout
|
||||
<< "[SO] Pathfinding: no navmesh, going direct"
|
||||
<< std::endl;
|
||||
state.target.path.clear();
|
||||
state.target.path.push_back(objPos);
|
||||
state.target.pathIndex = 0;
|
||||
state.state = State::Moving;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// --- State: Moving ---
|
||||
if (state.state == State::Moving) {
|
||||
if (!state.target.smartObject.is_alive() ||
|
||||
state.target.path.empty()) {
|
||||
std::cout
|
||||
<< "[SO] Moving: target dead or path empty, idle"
|
||||
<< "[SO] Pathfinding: no path found, going idle"
|
||||
<< std::endl;
|
||||
state.state = State::Idle;
|
||||
cc.linearVelocity = Ogre::Vector3::ZERO;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
std::cout
|
||||
<< "[SO] Pathfinding: no navmesh, going direct"
|
||||
<< std::endl;
|
||||
state.target.path.clear();
|
||||
state.target.path.push_back(objPos);
|
||||
state.target.pathIndex = 0;
|
||||
state.state = State::Moving;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Ogre::Vector3 charPos = getEntityPosition(e);
|
||||
Ogre::Vector3 objPos = getEntityPosition(
|
||||
state.target.smartObject);
|
||||
// --- State: Moving ---
|
||||
if (state.state == State::Moving) {
|
||||
if (!state.target.smartObject.is_alive() ||
|
||||
state.target.path.empty()) {
|
||||
std::cout
|
||||
<< "[SO] Moving: target dead or path empty, idle"
|
||||
<< std::endl;
|
||||
state.state = State::Idle;
|
||||
cc.linearVelocity = Ogre::Vector3::ZERO;
|
||||
return;
|
||||
}
|
||||
|
||||
auto &soComp =
|
||||
state.target.smartObject
|
||||
.get<SmartObjectComponent>();
|
||||
Ogre::Vector3 charPos = getEntityPosition(e);
|
||||
Ogre::Vector3 objPos =
|
||||
getEntityPosition(state.target.smartObject);
|
||||
|
||||
// Always check range first
|
||||
if (isInRange(charPos, objPos, soComp.radius,
|
||||
soComp.height)) {
|
||||
auto &soComp = state.target.smartObject
|
||||
.get<SmartObjectComponent>();
|
||||
|
||||
// Always check range first
|
||||
if (isInRange(charPos, objPos, soComp.radius,
|
||||
soComp.height)) {
|
||||
std::cout << "[SO] Moving: in range, executing"
|
||||
<< std::endl;
|
||||
state.state = State::Executing;
|
||||
state.target.isExecuting = true;
|
||||
state.target.executionTimer = 0.0f;
|
||||
cc.linearVelocity = Ogre::Vector3::ZERO;
|
||||
return;
|
||||
}
|
||||
|
||||
state.target.pathRecalcTimer += deltaTime;
|
||||
if (state.target.pathRecalcTimer > 2.0f) {
|
||||
state.target.pathRecalcTimer = 0.0f;
|
||||
if (navmeshEntity.is_alive() && m_navSystem) {
|
||||
std::vector<Ogre::Vector3> newPath;
|
||||
bool found = m_navSystem->findPath(
|
||||
navmeshEntity, charPos, objPos,
|
||||
newPath);
|
||||
if (found && !newPath.empty()) {
|
||||
state.target.path = newPath;
|
||||
state.target.pathIndex = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if path is exhausted
|
||||
if (state.target.pathIndex >=
|
||||
(int)state.target.path.size()) {
|
||||
float distToObj = charPos.distance(objPos);
|
||||
std::cout
|
||||
<< "[SO] Moving: path exhausted, dist="
|
||||
<< distToObj
|
||||
<< " radius*2=" << soComp.radius * 2.0f
|
||||
<< std::endl;
|
||||
// If close enough, teleport to range
|
||||
// and execute
|
||||
if (distToObj < soComp.radius * 2.0f) {
|
||||
std::cout
|
||||
<< "[SO] Moving: in range, executing"
|
||||
<< "[SO] Moving: close enough, teleporting"
|
||||
<< std::endl;
|
||||
Ogre::Vector3 dirToObj =
|
||||
objPos - charPos;
|
||||
dirToObj.y = 0;
|
||||
float xzDist = dirToObj.length();
|
||||
if (xzDist > 0.01f) {
|
||||
dirToObj /= xzDist;
|
||||
Ogre::Vector3 newPos =
|
||||
objPos -
|
||||
dirToObj *
|
||||
(soComp.radius *
|
||||
0.5f);
|
||||
newPos.y = charPos.y;
|
||||
if (trans.node) {
|
||||
trans.node->setPosition(
|
||||
newPos);
|
||||
}
|
||||
trans.position = newPos;
|
||||
}
|
||||
state.state = State::Executing;
|
||||
state.target.isExecuting = true;
|
||||
state.target.executionTimer = 0.0f;
|
||||
cc.linearVelocity = Ogre::Vector3::ZERO;
|
||||
return;
|
||||
}
|
||||
// Too far - recalculate path
|
||||
std::cout << "[SO] Moving: too far, recalc path"
|
||||
<< std::endl;
|
||||
state.target.path.clear();
|
||||
state.target.path.push_back(objPos);
|
||||
state.target.pathIndex = 0;
|
||||
}
|
||||
|
||||
state.target.pathRecalcTimer += deltaTime;
|
||||
if (state.target.pathRecalcTimer > 2.0f) {
|
||||
state.target.pathRecalcTimer = 0.0f;
|
||||
if (navmeshEntity.is_alive() &&
|
||||
m_navSystem) {
|
||||
std::vector<Ogre::Vector3>
|
||||
newPath;
|
||||
bool found =
|
||||
m_navSystem->findPath(
|
||||
navmeshEntity,
|
||||
charPos, objPos,
|
||||
newPath);
|
||||
if (found && !newPath.empty()) {
|
||||
state.target.path =
|
||||
newPath;
|
||||
state.target.pathIndex =
|
||||
0;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ogre::Vector3 targetPos =
|
||||
state.target.path[state.target.pathIndex];
|
||||
Ogre::Vector3 toTarget = targetPos - charPos;
|
||||
|
||||
// Check if path is exhausted
|
||||
// Waypoint arrival threshold - generous since
|
||||
// waypoints are just guide points
|
||||
const float WAYPOINT_THRESHOLD = 1.0f;
|
||||
if (toTarget.length() < WAYPOINT_THRESHOLD) {
|
||||
state.target.pathIndex++;
|
||||
if (state.target.pathIndex >=
|
||||
(int)state.target.path.size()) {
|
||||
// Path exhausted - teleport to
|
||||
// range and execute if close
|
||||
// enough
|
||||
float distToObj =
|
||||
charPos.distance(objPos);
|
||||
std::cout
|
||||
<< "[SO] Moving: path exhausted, dist="
|
||||
<< "[SO] Moving: last waypoint reached, dist="
|
||||
<< distToObj << " radius*2="
|
||||
<< soComp.radius * 2.0f
|
||||
<< std::endl;
|
||||
// If close enough, teleport to range
|
||||
// and execute
|
||||
if (distToObj < soComp.radius * 2.0f) {
|
||||
std::cout
|
||||
<< "[SO] Moving: close enough, teleporting"
|
||||
<< "[SO] Moving: close enough at waypoint, teleporting"
|
||||
<< std::endl;
|
||||
Ogre::Vector3 dirToObj =
|
||||
objPos - charPos;
|
||||
@@ -822,207 +889,134 @@ void SmartObjectSystem::update(float deltaTime)
|
||||
}
|
||||
// Too far - recalculate path
|
||||
std::cout
|
||||
<< "[SO] Moving: too far, recalc path"
|
||||
<< "[SO] Moving: too far at waypoint, recalc"
|
||||
<< std::endl;
|
||||
state.target.path.clear();
|
||||
state.target.path.push_back(objPos);
|
||||
state.target.pathIndex = 0;
|
||||
cc.linearVelocity = Ogre::Vector3::ZERO;
|
||||
return;
|
||||
}
|
||||
|
||||
Ogre::Vector3 targetPos =
|
||||
targetPos =
|
||||
state.target
|
||||
.path[state.target.pathIndex];
|
||||
Ogre::Vector3 toTarget = targetPos - charPos;
|
||||
|
||||
// Waypoint arrival threshold - generous since
|
||||
// waypoints are just guide points
|
||||
const float WAYPOINT_THRESHOLD = 1.0f;
|
||||
if (toTarget.length() < WAYPOINT_THRESHOLD) {
|
||||
state.target.pathIndex++;
|
||||
if (state.target.pathIndex >=
|
||||
(int)state.target.path.size()) {
|
||||
// Path exhausted - teleport to
|
||||
// range and execute if close
|
||||
// enough
|
||||
float distToObj =
|
||||
charPos.distance(
|
||||
objPos);
|
||||
std::cout
|
||||
<< "[SO] Moving: last waypoint reached, dist="
|
||||
<< distToObj
|
||||
<< " radius*2="
|
||||
<< soComp.radius * 2.0f
|
||||
<< std::endl;
|
||||
if (distToObj <
|
||||
soComp.radius * 2.0f) {
|
||||
std::cout
|
||||
<< "[SO] Moving: close enough at waypoint, teleporting"
|
||||
<< std::endl;
|
||||
Ogre::Vector3 dirToObj =
|
||||
objPos -
|
||||
charPos;
|
||||
dirToObj.y = 0;
|
||||
float xzDist =
|
||||
dirToObj.length();
|
||||
if (xzDist > 0.01f) {
|
||||
dirToObj /=
|
||||
xzDist;
|
||||
Ogre::Vector3 newPos =
|
||||
objPos -
|
||||
dirToObj *
|
||||
(soComp.radius *
|
||||
0.5f);
|
||||
newPos.y =
|
||||
charPos.y;
|
||||
if (trans.node) {
|
||||
trans.node
|
||||
->setPosition(
|
||||
newPos);
|
||||
}
|
||||
trans.position =
|
||||
newPos;
|
||||
}
|
||||
state.state =
|
||||
State::Executing;
|
||||
state.target
|
||||
.isExecuting =
|
||||
true;
|
||||
state.target
|
||||
.executionTimer =
|
||||
0.0f;
|
||||
cc.linearVelocity =
|
||||
Ogre::Vector3::
|
||||
ZERO;
|
||||
return;
|
||||
}
|
||||
// Too far - recalculate path
|
||||
std::cout
|
||||
<< "[SO] Moving: too far at waypoint, recalc"
|
||||
<< std::endl;
|
||||
state.target.path.clear();
|
||||
state.target.path.push_back(
|
||||
objPos);
|
||||
state.target.pathIndex = 0;
|
||||
cc.linearVelocity =
|
||||
Ogre::Vector3::ZERO;
|
||||
return;
|
||||
}
|
||||
targetPos =
|
||||
state.target.path
|
||||
[state.target.pathIndex];
|
||||
toTarget = targetPos - charPos;
|
||||
}
|
||||
|
||||
toTarget.y = 0;
|
||||
if (toTarget.squaredLength() > 0.0001f) {
|
||||
toTarget.normalise();
|
||||
|
||||
float distToTarget =
|
||||
(charPos - objPos).length();
|
||||
float speed = debug.walkSpeed;
|
||||
|
||||
if (distToTarget >
|
||||
debug.walkSpeed * 3.0f) {
|
||||
speed = debug.runSpeed;
|
||||
setLocomotionState(e, "run");
|
||||
} else {
|
||||
speed = debug.walkSpeed;
|
||||
setLocomotionState(e, "walk");
|
||||
}
|
||||
|
||||
cc.linearVelocity = toTarget * speed;
|
||||
|
||||
// Rotate character to face movement
|
||||
// direction (Y plane only)
|
||||
rotateTowards(e, toTarget, deltaTime);
|
||||
} else {
|
||||
cc.linearVelocity = Ogre::Vector3::ZERO;
|
||||
}
|
||||
return;
|
||||
toTarget = targetPos - charPos;
|
||||
}
|
||||
|
||||
// --- State: Executing ---
|
||||
if (state.state == State::Executing) {
|
||||
cc.linearVelocity = Ogre::Vector3::ZERO;
|
||||
toTarget.y = 0;
|
||||
if (toTarget.squaredLength() > 0.0001f) {
|
||||
toTarget.normalise();
|
||||
|
||||
// When the behavior tree is running (via ActionDebug),
|
||||
// do NOT override animation states - the behavior tree's
|
||||
// setAnimationState nodes should be the sole controller.
|
||||
if (!debug.isRunning ||
|
||||
debug.currentActionName.empty()) {
|
||||
setLocomotionState(e, "idle");
|
||||
float distToTarget =
|
||||
(charPos - objPos).length();
|
||||
float speed = 2.5f;
|
||||
if (e.has<PathFollowingComponent>()) {
|
||||
auto &pf =
|
||||
e.get<PathFollowingComponent>();
|
||||
speed = pf.walkSpeed;
|
||||
if (distToTarget >
|
||||
pf.walkSpeed * 3.0f) {
|
||||
speed = pf.runSpeed;
|
||||
setLocomotionState(e, "run");
|
||||
} else {
|
||||
speed = pf.walkSpeed;
|
||||
setLocomotionState(e, "walk");
|
||||
}
|
||||
} else {
|
||||
setLocomotionState(e, "walk");
|
||||
}
|
||||
|
||||
if (state.target.isExecuting && db) {
|
||||
const GoapAction *action =
|
||||
db->findAction(
|
||||
state.target.actionName);
|
||||
if (action) {
|
||||
// Kick off the action's
|
||||
// behavior tree via
|
||||
// BehaviorTreeSystem's
|
||||
// ActionDebug path.
|
||||
// Only reset runTimer on the
|
||||
// first frame of execution
|
||||
// (when isRunning is false) so
|
||||
// BehaviorTreeSystem re-initializes
|
||||
// the runner state for a fresh
|
||||
// execution. Do NOT reset every
|
||||
// frame or isNewlyActive will
|
||||
// fire repeatedly.
|
||||
if (!debug.isRunning) {
|
||||
debug.runTimer = 0.0f;
|
||||
}
|
||||
debug.isRunning = true;
|
||||
debug.currentActionName =
|
||||
state.target.actionName;
|
||||
// When root motion is enabled, the
|
||||
// AnimationTreeSystem already computes
|
||||
// linearVelocity from animation root
|
||||
// bone displacement. Do NOT overwrite
|
||||
// it with direct movement velocity.
|
||||
if (!hasRootMotion(e))
|
||||
cc.linearVelocity = toTarget * speed;
|
||||
|
||||
// Check if the behavior tree
|
||||
// has finished (sequence
|
||||
// completed or failed).
|
||||
// Only check completion when
|
||||
// runTimer > 0 (at least one
|
||||
// frame has passed since
|
||||
// starting) to avoid
|
||||
// immediately detecting
|
||||
// completion on the same frame
|
||||
// the tree was started.
|
||||
auto &btState =
|
||||
m_btSystem
|
||||
->getActionDebugState(
|
||||
e.id());
|
||||
if (debug.runTimer > 0.0f &&
|
||||
btState.treeResult !=
|
||||
BehaviorTreeSystem::
|
||||
Status::running) {
|
||||
// Behavior tree
|
||||
// completed - stop
|
||||
// evaluation
|
||||
debug.isRunning = false;
|
||||
debug.currentActionName
|
||||
.clear();
|
||||
// Rotate character to face movement
|
||||
// direction (Y plane only)
|
||||
rotateTowards(e, toTarget, deltaTime);
|
||||
} else if (!hasRootMotion(e)) {
|
||||
cc.linearVelocity = Ogre::Vector3::ZERO;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
bb.apply(
|
||||
action->effects);
|
||||
state.state =
|
||||
State::Idle;
|
||||
state.target
|
||||
.isExecuting =
|
||||
false;
|
||||
debug.lastResult =
|
||||
"Smart object action '" +
|
||||
state.target
|
||||
.actionName +
|
||||
"' completed";
|
||||
}
|
||||
} else {
|
||||
// --- State: Executing ---
|
||||
if (state.state == State::Executing) {
|
||||
cc.linearVelocity = Ogre::Vector3::ZERO;
|
||||
|
||||
// When the behavior tree is running (via ActionDebug),
|
||||
// do NOT override animation states - the behavior tree's
|
||||
// setAnimationState nodes should be the sole controller.
|
||||
if (!debug.isRunning ||
|
||||
debug.currentActionName.empty()) {
|
||||
setLocomotionState(e, "idle");
|
||||
}
|
||||
|
||||
if (state.target.isExecuting && db) {
|
||||
const GoapAction *action =
|
||||
db->findAction(state.target.actionName);
|
||||
if (action) {
|
||||
// Kick off the action's
|
||||
// behavior tree via
|
||||
// BehaviorTreeSystem's
|
||||
// ActionDebug path.
|
||||
// Only reset runTimer on the
|
||||
// first frame of execution
|
||||
// (when isRunning is false) so
|
||||
// BehaviorTreeSystem re-initializes
|
||||
// the runner state for a fresh
|
||||
// execution. Do NOT reset every
|
||||
// frame or isNewlyActive will
|
||||
// fire repeatedly.
|
||||
if (!debug.isRunning) {
|
||||
debug.runTimer = 0.0f;
|
||||
}
|
||||
debug.isRunning = true;
|
||||
debug.currentActionName =
|
||||
state.target.actionName;
|
||||
|
||||
// Check if the behavior tree
|
||||
// has finished (sequence
|
||||
// completed or failed).
|
||||
// Only check completion when
|
||||
// runTimer > 0 (at least one
|
||||
// frame has passed since
|
||||
// starting) to avoid
|
||||
// immediately detecting
|
||||
// completion on the same frame
|
||||
// the tree was started.
|
||||
auto &btState =
|
||||
m_btSystem->getActionDebugState(
|
||||
e.id());
|
||||
if (debug.runTimer > 0.0f &&
|
||||
btState.treeResult !=
|
||||
BehaviorTreeSystem::Status::
|
||||
running) {
|
||||
// Behavior tree
|
||||
// completed - stop
|
||||
// evaluation
|
||||
debug.isRunning = false;
|
||||
debug.currentActionName.clear();
|
||||
|
||||
bb.apply(action->effects);
|
||||
state.state = State::Idle;
|
||||
state.target.isExecuting =
|
||||
false;
|
||||
debug.lastResult =
|
||||
"Smart object action '" +
|
||||
state.target.actionName +
|
||||
"' completed";
|
||||
}
|
||||
} else {
|
||||
state.state = State::Idle;
|
||||
state.target.isExecuting = false;
|
||||
}
|
||||
} else {
|
||||
state.state = State::Idle;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
class NavMeshSystem;
|
||||
class BehaviorTreeSystem;
|
||||
class AnimationTreeSystem;
|
||||
class ItemSystem;
|
||||
class EditorApp;
|
||||
|
||||
/**
|
||||
@@ -55,6 +56,22 @@ public:
|
||||
m_editorApp = app;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the ItemSystem for behavior tree item/inventory nodes.
|
||||
*/
|
||||
void setItemSystem(ItemSystem *system)
|
||||
{
|
||||
m_itemSystem = system;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ItemSystem for behavior tree item/inventory nodes.
|
||||
*/
|
||||
ItemSystem *getItemSystem() const
|
||||
{
|
||||
return m_itemSystem;
|
||||
}
|
||||
|
||||
void update(float deltaTime);
|
||||
|
||||
/**
|
||||
@@ -65,6 +82,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;
|
||||
@@ -114,6 +138,7 @@ private:
|
||||
BehaviorTreeSystem *m_btSystem;
|
||||
AnimationTreeSystem *m_animTreeSystem;
|
||||
EditorApp *m_editorApp = nullptr;
|
||||
ItemSystem *m_itemSystem = nullptr;
|
||||
|
||||
std::unordered_map<flecs::entity_t, CharacterState> m_states;
|
||||
|
||||
|
||||
136
src/features/editScene/tests/Ogre.h
Normal file
136
src/features/editScene/tests/Ogre.h
Normal file
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* @file Ogre.h
|
||||
* @brief Stub Ogre.h for standalone tests.
|
||||
*
|
||||
* Provides minimal Ogre type aliases needed to compile
|
||||
* ActionDatabase, GoapBlackboard, GoapGoal, GoapAction,
|
||||
* and BehaviorTree components without the full Ogre SDK.
|
||||
*
|
||||
* Only for use in standalone tests (action_db_lua_test).
|
||||
*/
|
||||
|
||||
#ifndef OGRE_STUB_H
|
||||
#define OGRE_STUB_H
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <sstream>
|
||||
#include <cassert>
|
||||
|
||||
namespace Ogre
|
||||
{
|
||||
|
||||
// String is just std::string
|
||||
using String = std::string;
|
||||
|
||||
// Minimal Vector3 for GoapBlackboard
|
||||
struct Vector3 {
|
||||
float x, y, z;
|
||||
Vector3()
|
||||
: x(0)
|
||||
, y(0)
|
||||
, z(0)
|
||||
{
|
||||
}
|
||||
Vector3(float x_, float y_, float z_)
|
||||
: x(x_)
|
||||
, y(y_)
|
||||
, z(z_)
|
||||
{
|
||||
}
|
||||
static const Vector3 ZERO;
|
||||
|
||||
// Member operator== defined inline inside the struct body
|
||||
// This ensures it's found by ADL for std::pair::operator==
|
||||
bool operator==(const Vector3 &other) const
|
||||
{
|
||||
return x == other.x && y == other.y && z == other.z;
|
||||
}
|
||||
};
|
||||
|
||||
inline const Vector3 Vector3::ZERO(0, 0, 0);
|
||||
|
||||
// Minimal StringConverter stub for GoapBlackboard::dump()
|
||||
struct StringConverter {
|
||||
static String toString(int val)
|
||||
{
|
||||
return std::to_string(val);
|
||||
}
|
||||
static String toString(float val)
|
||||
{
|
||||
return std::to_string(val);
|
||||
}
|
||||
};
|
||||
|
||||
// Minimal LogManager stub (used by LuaActionApi and ActionDatabase)
|
||||
class LogManager {
|
||||
public:
|
||||
static LogManager &getSingleton()
|
||||
{
|
||||
static LogManager instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
class Stream {
|
||||
public:
|
||||
template <typename T> Stream &operator<<(const T &)
|
||||
{
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
|
||||
Stream stream()
|
||||
{
|
||||
return Stream();
|
||||
}
|
||||
|
||||
void logMessage(const String &, int = 0, bool = false)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
// Minimal ResourceGroupManager stub (used by ActionDatabase save/load)
|
||||
class ResourceGroupManager {
|
||||
public:
|
||||
// LocationList is a vector of shared_ptr to Location
|
||||
struct Location {
|
||||
struct Archive {
|
||||
String getName() const
|
||||
{
|
||||
return "";
|
||||
}
|
||||
};
|
||||
// Use raw pointer to avoid shared_ptr dependency
|
||||
Archive *archive;
|
||||
};
|
||||
using LocationList = std::vector<Location>;
|
||||
|
||||
static ResourceGroupManager &getSingleton()
|
||||
{
|
||||
static ResourceGroupManager instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
const LocationList &getResourceLocationList(const String &) const
|
||||
{
|
||||
static LocationList empty;
|
||||
return empty;
|
||||
}
|
||||
};
|
||||
|
||||
// OgreAssert macro (used by LuaEntityApi.hpp)
|
||||
#ifndef OgreAssert
|
||||
#define OgreAssert(expr, msg) \
|
||||
do { \
|
||||
if (!(expr)) { \
|
||||
fprintf(stderr, "OgreAssert failed: %s\n", msg); \
|
||||
assert(expr); \
|
||||
} \
|
||||
} while (0)
|
||||
#endif
|
||||
|
||||
} // namespace Ogre
|
||||
|
||||
#endif // OGRE_STUB_H
|
||||
14
src/features/editScene/tests/OgreLogManager.h
Normal file
14
src/features/editScene/tests/OgreLogManager.h
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* @file OgreLogManager.h
|
||||
* @brief Stub OgreLogManager.h for standalone tests.
|
||||
*
|
||||
* Provides Ogre::LogManager for LuaActionApi.cpp compilation
|
||||
* in standalone test builds.
|
||||
*/
|
||||
|
||||
#ifndef OGRE_LOG_MANAGER_H
|
||||
#define OGRE_LOG_MANAGER_H
|
||||
|
||||
#include "Ogre.h"
|
||||
|
||||
#endif // OGRE_LOG_MANAGER_H
|
||||
895
src/features/editScene/tests/action_db_lua_test.cpp
Normal file
895
src/features/editScene/tests/action_db_lua_test.cpp
Normal file
@@ -0,0 +1,895 @@
|
||||
/**
|
||||
* @file action_db_lua_test.cpp
|
||||
* @brief Compile-time test for ActionDatabase Lua API.
|
||||
*
|
||||
* This test creates a Lua state, registers the ActionDatabase singleton
|
||||
* and the Lua action API, then runs Lua scripts that create actions
|
||||
* (including with behavior trees), goals, queries them, and verifies
|
||||
* the results match expectations.
|
||||
*
|
||||
* Build with:
|
||||
* g++ -std=c++17 -I. -I../.. -I../../lua/lua-5.4.8/src \
|
||||
* action_db_lua_test.cpp \
|
||||
* ../components/ActionDatabase.cpp \
|
||||
* ../components/GoapBlackboard.cpp \
|
||||
* ../components/GoapGoal.cpp \
|
||||
* ../components/GoapAction.cpp \
|
||||
* ../../lua/lua-5.4.8/src/liblua.a \
|
||||
* -o action_db_lua_test -lm
|
||||
*
|
||||
* Or via CMake (see CMakeLists.txt in this directory).
|
||||
*/
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// Ogre stub (provides Ogre::String, Ogre::Vector3, Ogre::LogManager)
|
||||
// Must be included before any component headers that use Ogre types.
|
||||
#include "ogre_stub.h"
|
||||
|
||||
// Include Lua
|
||||
extern "C" {
|
||||
#include <lua.h>
|
||||
#include <lauxlib.h>
|
||||
#include <lualib.h>
|
||||
}
|
||||
|
||||
// Include the components we need
|
||||
#include "../components/ActionDatabase.hpp"
|
||||
#include "../components/GoapBlackboard.hpp"
|
||||
#include "../components/GoapAction.hpp"
|
||||
#include "../components/GoapGoal.hpp"
|
||||
#include "../components/BehaviorTree.hpp"
|
||||
|
||||
// Forward declare the registration function
|
||||
namespace editScene
|
||||
{
|
||||
void registerLuaActionApi(lua_State *L);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int testCount = 0;
|
||||
static int passCount = 0;
|
||||
|
||||
#define TEST(name) \
|
||||
do { \
|
||||
testCount++; \
|
||||
printf(" TEST %d: %s ... ", testCount, name); \
|
||||
} while (0)
|
||||
|
||||
#define PASS() \
|
||||
do { \
|
||||
passCount++; \
|
||||
printf("PASS\n"); \
|
||||
} while (0)
|
||||
|
||||
#define FAIL(msg) \
|
||||
do { \
|
||||
printf("FAIL: %s\n", msg); \
|
||||
return 1; \
|
||||
} while (0)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: run a Lua string and check for errors
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static bool runLua(lua_State *L, const char *code)
|
||||
{
|
||||
if (luaL_dostring(L, code) != LUA_OK) {
|
||||
fprintf(stderr, "Lua error: %s\n", lua_tostring(L, -1));
|
||||
lua_pop(L, 1);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 1: Basic action creation and lookup
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int testBasicAction(lua_State *L)
|
||||
{
|
||||
TEST("create and find a simple action");
|
||||
|
||||
// Clear any previous state
|
||||
ActionDatabase::getSingleton().clear();
|
||||
|
||||
// Create action via Lua
|
||||
bool ok = runLua(
|
||||
L,
|
||||
"ecs.action_db.add_action('test_action', 5, "
|
||||
" { bits = { has_axe = true }, values = { stamina = 10 } }, "
|
||||
" { bits = { has_wood = true }, values = { stamina = -5 } }"
|
||||
")");
|
||||
if (!ok)
|
||||
FAIL("failed to run Lua");
|
||||
|
||||
// Verify via C++ API
|
||||
const GoapAction *action =
|
||||
ActionDatabase::getSingleton().findAction("test_action");
|
||||
if (!action)
|
||||
FAIL("action not found");
|
||||
if (action->name != "test_action")
|
||||
FAIL("wrong name");
|
||||
if (action->cost != 5)
|
||||
FAIL("wrong cost");
|
||||
|
||||
// Verify preconditions
|
||||
if (!action->preconditions.getBit(0))
|
||||
FAIL("has_axe bit not set");
|
||||
if (action->preconditions.values.at("stamina") != 10)
|
||||
FAIL("wrong stamina precondition");
|
||||
|
||||
// Verify effects
|
||||
if (!action->effects.getBit(1))
|
||||
FAIL("has_wood bit not set");
|
||||
if (action->effects.values.at("stamina") != -5)
|
||||
FAIL("wrong stamina effect");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 2: Action with behavior tree
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int testActionWithBehaviorTree(lua_State *L)
|
||||
{
|
||||
TEST("create action with behavior tree");
|
||||
|
||||
ActionDatabase::getSingleton().clear();
|
||||
|
||||
bool ok = runLua(L,
|
||||
"ecs.action_db.add_action('wave', 1, {}, {}, {"
|
||||
" type = 'sequence',"
|
||||
" children = {"
|
||||
" { type = 'setAnimationState', name = 'SM/Wave' },"
|
||||
" { type = 'delay', params = '2.0' },"
|
||||
" { type = 'setAnimationState', name = 'SM/Idle' }"
|
||||
" }"
|
||||
"})");
|
||||
if (!ok)
|
||||
FAIL("failed to run Lua");
|
||||
|
||||
const GoapAction *action =
|
||||
ActionDatabase::getSingleton().findAction("wave");
|
||||
if (!action)
|
||||
FAIL("action not found");
|
||||
|
||||
// Verify behavior tree structure
|
||||
const BehaviorTreeNode &bt = action->behaviorTree;
|
||||
if (bt.type != "sequence")
|
||||
FAIL("expected sequence root");
|
||||
if (bt.children.size() != 3)
|
||||
FAIL("expected 3 children");
|
||||
|
||||
if (bt.children[0].type != "setAnimationState")
|
||||
FAIL("child 0 wrong type");
|
||||
if (bt.children[0].name != "SM/Wave")
|
||||
FAIL("child 0 wrong name");
|
||||
|
||||
if (bt.children[1].type != "delay")
|
||||
FAIL("child 1 wrong type");
|
||||
if (bt.children[1].params != "2.0")
|
||||
FAIL("child 1 wrong params");
|
||||
|
||||
if (bt.children[2].type != "setAnimationState")
|
||||
FAIL("child 2 wrong type");
|
||||
if (bt.children[2].name != "SM/Idle")
|
||||
FAIL("child 2 wrong name");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 3: Nested behavior tree (selector with sequences)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int testNestedBehaviorTree(lua_State *L)
|
||||
{
|
||||
TEST("create action with nested behavior tree");
|
||||
|
||||
ActionDatabase::getSingleton().clear();
|
||||
|
||||
bool ok = runLua(
|
||||
L,
|
||||
"ecs.action_db.add_action('chop_tree', 3,"
|
||||
" { bits = { near_tree = true }, values = { stamina = 15 } },"
|
||||
" { bits = { has_wood = true }, values = { stamina = -8 } },"
|
||||
" {"
|
||||
" type = 'selector',"
|
||||
" children = {"
|
||||
" {"
|
||||
" type = 'sequence',"
|
||||
" children = {"
|
||||
" { type = 'checkBit', name = 'has_axe', params = '1' },"
|
||||
" { type = 'setAnimationState', name = 'SM/Chop' },"
|
||||
" { type = 'delay', params = '3.0' }"
|
||||
" }"
|
||||
" },"
|
||||
" {"
|
||||
" type = 'sequence',"
|
||||
" children = {"
|
||||
" { type = 'debugPrint', name = 'No axe!' },"
|
||||
" { type = 'setAnimationState', name = 'SM/Pickup' }"
|
||||
" }"
|
||||
" }"
|
||||
" }"
|
||||
" }"
|
||||
")");
|
||||
if (!ok)
|
||||
FAIL("failed to run Lua");
|
||||
|
||||
const GoapAction *action =
|
||||
ActionDatabase::getSingleton().findAction("chop_tree");
|
||||
if (!action)
|
||||
FAIL("action not found");
|
||||
|
||||
const BehaviorTreeNode &bt = action->behaviorTree;
|
||||
if (bt.type != "selector")
|
||||
FAIL("expected selector root");
|
||||
if (bt.children.size() != 2)
|
||||
FAIL("expected 2 children");
|
||||
|
||||
// First child: sequence with 3 nodes
|
||||
const BehaviorTreeNode &seq1 = bt.children[0];
|
||||
if (seq1.type != "sequence")
|
||||
FAIL("child 0 should be sequence");
|
||||
if (seq1.children.size() != 3)
|
||||
FAIL("seq1 expected 3 children");
|
||||
if (seq1.children[0].type != "checkBit")
|
||||
FAIL("seq1 child 0 wrong type");
|
||||
if (seq1.children[0].name != "has_axe")
|
||||
FAIL("seq1 child 0 wrong name");
|
||||
if (seq1.children[0].params != "1")
|
||||
FAIL("seq1 child 0 wrong params");
|
||||
|
||||
// Second child: sequence with 2 nodes
|
||||
const BehaviorTreeNode &seq2 = bt.children[1];
|
||||
if (seq2.type != "sequence")
|
||||
FAIL("child 1 should be sequence");
|
||||
if (seq2.children.size() != 2)
|
||||
FAIL("seq2 expected 2 children");
|
||||
if (seq2.children[0].type != "debugPrint")
|
||||
FAIL("seq2 child 0 wrong type");
|
||||
if (seq2.children[0].name != "No axe!")
|
||||
FAIL("seq2 child 0 wrong name");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 4: Goal creation and lookup
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int testGoal(lua_State *L)
|
||||
{
|
||||
TEST("create and find a goal");
|
||||
|
||||
ActionDatabase::getSingleton().clear();
|
||||
|
||||
bool ok = runLua(L, "ecs.action_db.add_goal('test_goal', 80,"
|
||||
" { values = { health = 100, stamina = 80 } },"
|
||||
" 'health < 50 || stamina < 30'"
|
||||
")");
|
||||
if (!ok)
|
||||
FAIL("failed to run Lua");
|
||||
|
||||
const GoapGoal *goal =
|
||||
ActionDatabase::getSingleton().findGoal("test_goal");
|
||||
if (!goal)
|
||||
FAIL("goal not found");
|
||||
if (goal->name != "test_goal")
|
||||
FAIL("wrong name");
|
||||
if (goal->priority != 80)
|
||||
FAIL("wrong priority");
|
||||
if (goal->condition != "health < 50 || stamina < 30")
|
||||
FAIL("wrong condition");
|
||||
if (goal->target.values.at("health") != 100)
|
||||
FAIL("wrong health target");
|
||||
if (goal->target.values.at("stamina") != 80)
|
||||
FAIL("wrong stamina target");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 5: Action replacement (same name)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int testActionReplacement(lua_State *L)
|
||||
{
|
||||
TEST("replace action with same name");
|
||||
|
||||
ActionDatabase::getSingleton().clear();
|
||||
|
||||
// Create initial action
|
||||
runLua(L,
|
||||
"ecs.action_db.add_action('replace_me', 1, "
|
||||
"{ bits = { old_flag = true } }, { bits = { old_done = true } })");
|
||||
|
||||
// Verify initial
|
||||
const GoapAction *first =
|
||||
ActionDatabase::getSingleton().findAction("replace_me");
|
||||
if (!first)
|
||||
FAIL("initial action not found");
|
||||
if (first->cost != 1)
|
||||
FAIL("initial cost wrong");
|
||||
|
||||
// Replace with new definition
|
||||
runLua(L,
|
||||
"ecs.action_db.add_action('replace_me', 99, "
|
||||
"{ bits = { new_flag = true } }, { bits = { new_done = true } })");
|
||||
|
||||
// Verify replacement
|
||||
const GoapAction *second =
|
||||
ActionDatabase::getSingleton().findAction("replace_me");
|
||||
if (!second)
|
||||
FAIL("replaced action not found");
|
||||
if (second->cost != 99)
|
||||
FAIL("replaced cost wrong");
|
||||
if (second->preconditions.getBit(0))
|
||||
FAIL("old precondition still present");
|
||||
|
||||
// Should only be one action with this name
|
||||
int count = 0;
|
||||
for (const auto &a : ActionDatabase::getSingleton().actions) {
|
||||
if (a.name == "replace_me")
|
||||
count++;
|
||||
}
|
||||
if (count != 1)
|
||||
FAIL("expected exactly one action with this name");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 6: Remove action
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int testRemoveAction(lua_State *L)
|
||||
{
|
||||
TEST("remove action");
|
||||
|
||||
ActionDatabase::getSingleton().clear();
|
||||
|
||||
runLua(L, "ecs.action_db.add_action('to_remove', 1)");
|
||||
runLua(L, "ecs.action_db.add_action('to_keep', 2)");
|
||||
|
||||
if (!ActionDatabase::getSingleton().findAction("to_remove"))
|
||||
FAIL("action should exist before removal");
|
||||
|
||||
// Remove via Lua
|
||||
bool ok = runLua(L,
|
||||
"local r = ecs.action_db.remove_action('to_remove');"
|
||||
"assert(r == true, 'remove should return true')");
|
||||
if (!ok)
|
||||
FAIL("failed to run Lua remove");
|
||||
|
||||
if (ActionDatabase::getSingleton().findAction("to_remove"))
|
||||
FAIL("action should not exist after removal");
|
||||
if (!ActionDatabase::getSingleton().findAction("to_keep"))
|
||||
FAIL("other action should still exist");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 7: List actions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int testListActions(lua_State *L)
|
||||
{
|
||||
TEST("list actions");
|
||||
|
||||
ActionDatabase::getSingleton().clear();
|
||||
|
||||
runLua(L, "ecs.action_db.add_action('alpha', 1)");
|
||||
runLua(L, "ecs.action_db.add_action('beta', 2)");
|
||||
runLua(L, "ecs.action_db.add_action('gamma', 3)");
|
||||
|
||||
bool ok = runLua(
|
||||
L, "local list = ecs.action_db.list_actions();"
|
||||
"assert(#list == 3, 'expected 3 actions, got ' .. #list);"
|
||||
"assert(list[1] == 'alpha', 'expected alpha first');"
|
||||
"assert(list[2] == 'beta', 'expected beta second');"
|
||||
"assert(list[3] == 'gamma', 'expected gamma third')");
|
||||
if (!ok)
|
||||
FAIL("list actions assertion failed");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 8: Find action via Lua (returns table)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int testFindActionLua(lua_State *L)
|
||||
{
|
||||
TEST("find action from Lua returns table");
|
||||
|
||||
ActionDatabase::getSingleton().clear();
|
||||
|
||||
runLua(L, "ecs.action_db.add_action('find_me', 7,"
|
||||
" { values = { x = 42 } },"
|
||||
" { values = { y = 99 } },"
|
||||
" { type = 'task', name = 'do_something' }"
|
||||
")");
|
||||
|
||||
bool ok = runLua(
|
||||
L,
|
||||
"local a = ecs.action_db.find_action('find_me');"
|
||||
"assert(a ~= nil, 'action should exist');"
|
||||
"assert(a.name == 'find_me', 'wrong name');"
|
||||
"assert(a.cost == 7, 'wrong cost');"
|
||||
"assert(a.preconditions.values.x == 42, 'wrong precond');"
|
||||
"assert(a.effects.values.y == 99, 'wrong effects');"
|
||||
"assert(a.behaviorTree ~= nil, 'should have behaviorTree');"
|
||||
"assert(a.behaviorTree.type == 'task', 'wrong bt type');"
|
||||
"assert(a.behaviorTree.name == 'do_something', 'wrong bt name')");
|
||||
if (!ok)
|
||||
FAIL("find action Lua assertions failed");
|
||||
|
||||
// Test nil for non-existent
|
||||
ok = runLua(L, "local a = ecs.action_db.find_action('nonexistent');"
|
||||
"assert(a == nil, 'nonexistent should be nil')");
|
||||
if (!ok)
|
||||
FAIL("nonexistent action should be nil");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 9: Clear all
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int testClear(lua_State *L)
|
||||
{
|
||||
TEST("clear all actions and goals");
|
||||
|
||||
ActionDatabase::getSingleton().clear();
|
||||
|
||||
runLua(L, "ecs.action_db.add_action('a1', 1)");
|
||||
runLua(L, "ecs.action_db.add_action('a2', 2)");
|
||||
runLua(L, "ecs.action_db.add_goal('g1', 10)");
|
||||
|
||||
if (ActionDatabase::getSingleton().actions.size() != 2)
|
||||
FAIL("expected 2 actions before clear");
|
||||
if (ActionDatabase::getSingleton().goals.size() != 1)
|
||||
FAIL("expected 1 goal before clear");
|
||||
|
||||
runLua(L, "ecs.action_db.clear()");
|
||||
|
||||
if (ActionDatabase::getSingleton().actions.size() != 0)
|
||||
FAIL("expected 0 actions after clear");
|
||||
if (ActionDatabase::getSingleton().goals.size() != 0)
|
||||
FAIL("expected 0 goals after clear");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 10: Action with inventory behavior tree nodes
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int testInventoryBehaviorTree(lua_State *L)
|
||||
{
|
||||
TEST("action with inventory behavior tree nodes");
|
||||
|
||||
ActionDatabase::getSingleton().clear();
|
||||
|
||||
bool ok = runLua(
|
||||
L, "ecs.action_db.add_action('gather_wood', 2,"
|
||||
" { bits = { near_forest = true } },"
|
||||
" { values = { wood_count = 3 } },"
|
||||
" {"
|
||||
" type = 'sequence',"
|
||||
" children = {"
|
||||
" { type = 'setAnimationState', name = 'SM/Gather' },"
|
||||
" { type = 'delay', params = '2.0' },"
|
||||
" { type = 'addItemToInventory',"
|
||||
" params = 'wood,Firewood,material,3,1.0,0' },"
|
||||
" { type = 'setAnimationState', name = 'SM/Idle' }"
|
||||
" }"
|
||||
" }"
|
||||
")");
|
||||
if (!ok)
|
||||
FAIL("failed to run Lua");
|
||||
|
||||
const GoapAction *action =
|
||||
ActionDatabase::getSingleton().findAction("gather_wood");
|
||||
if (!action)
|
||||
FAIL("action not found");
|
||||
|
||||
const BehaviorTreeNode &bt = action->behaviorTree;
|
||||
if (bt.type != "sequence")
|
||||
FAIL("expected sequence");
|
||||
if (bt.children.size() != 4)
|
||||
FAIL("expected 4 children");
|
||||
if (bt.children[2].type != "addItemToInventory")
|
||||
FAIL("wrong node type");
|
||||
if (bt.children[2].params != "wood,Firewood,material,3,1.0,0")
|
||||
FAIL("wrong inventory params");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 11: Action with teleport/disablePhysics behavior tree
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int testPhysicsBehaviorTree(lua_State *L)
|
||||
{
|
||||
TEST("action with physics behavior tree nodes");
|
||||
|
||||
ActionDatabase::getSingleton().clear();
|
||||
|
||||
bool ok = runLua(
|
||||
L, "ecs.action_db.add_action('sit_on_chair', 1,"
|
||||
" { bits = { near_chair = true } },"
|
||||
" { bits = { is_sitting = true } },"
|
||||
" {"
|
||||
" type = 'sequence',"
|
||||
" children = {"
|
||||
" { type = 'teleportToChild', name = 'SitTarget' },"
|
||||
" { type = 'disablePhysics' },"
|
||||
" { type = 'setAnimationState', name = 'SM/Sit' },"
|
||||
" { type = 'delay', params = '5.0' },"
|
||||
" { type = 'setAnimationState', name = 'SM/Stand' },"
|
||||
" { type = 'enablePhysics' }"
|
||||
" }"
|
||||
" }"
|
||||
")");
|
||||
if (!ok)
|
||||
FAIL("failed to run Lua");
|
||||
|
||||
const GoapAction *action =
|
||||
ActionDatabase::getSingleton().findAction("sit_on_chair");
|
||||
if (!action)
|
||||
FAIL("action not found");
|
||||
|
||||
const BehaviorTreeNode &bt = action->behaviorTree;
|
||||
if (bt.children.size() != 6)
|
||||
FAIL("expected 6 children");
|
||||
if (bt.children[0].type != "teleportToChild")
|
||||
FAIL("wrong child 0 type");
|
||||
if (bt.children[0].name != "SitTarget")
|
||||
FAIL("wrong child 0 name");
|
||||
if (bt.children[1].type != "disablePhysics")
|
||||
FAIL("wrong child 1 type");
|
||||
if (bt.children[4].type != "setAnimationState")
|
||||
FAIL("wrong child 4 type");
|
||||
if (bt.children[4].name != "SM/Stand")
|
||||
FAIL("wrong child 4 name");
|
||||
if (bt.children[5].type != "enablePhysics")
|
||||
FAIL("wrong child 5 type");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 12: Action with blackboard check nodes
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int testBlackboardCheckBehaviorTree(lua_State *L)
|
||||
{
|
||||
TEST("action with blackboard check behavior tree nodes");
|
||||
|
||||
ActionDatabase::getSingleton().clear();
|
||||
|
||||
bool ok = runLua(
|
||||
L,
|
||||
"ecs.action_db.add_action('travel_to_market', 5,"
|
||||
" { bits = { is_awake = true }, values = { energy = 20 } },"
|
||||
" { bits = { at_market = true }, values = { energy = -15 } },"
|
||||
" {"
|
||||
" type = 'sequence',"
|
||||
" children = {"
|
||||
" { type = 'checkValue', name = 'energy', params = '>= 20' },"
|
||||
" { type = 'setAnimationState', name = 'SM/Walk' },"
|
||||
" { type = 'delay', params = '5.0' },"
|
||||
" { type = 'setAnimationState', name = 'SM/Idle' },"
|
||||
" { type = 'setBit', name = 'at_market', params = '1' },"
|
||||
" { type = 'setBit', name = 'at_home', params = '0' }"
|
||||
" }"
|
||||
" }"
|
||||
")");
|
||||
if (!ok)
|
||||
FAIL("failed to run Lua");
|
||||
|
||||
const GoapAction *action =
|
||||
ActionDatabase::getSingleton().findAction("travel_to_market");
|
||||
if (!action)
|
||||
FAIL("action not found");
|
||||
|
||||
const BehaviorTreeNode &bt = action->behaviorTree;
|
||||
if (bt.children.size() != 6)
|
||||
FAIL("expected 6 children");
|
||||
if (bt.children[0].type != "checkValue")
|
||||
FAIL("wrong child 0 type");
|
||||
if (bt.children[0].name != "energy")
|
||||
FAIL("wrong child 0 name");
|
||||
if (bt.children[0].params != ">= 20")
|
||||
FAIL("wrong child 0 params");
|
||||
if (bt.children[4].type != "setBit")
|
||||
FAIL("wrong child 4 type");
|
||||
if (bt.children[4].name != "at_market")
|
||||
FAIL("wrong child 4 name");
|
||||
if (bt.children[4].params != "1")
|
||||
FAIL("wrong child 4 params");
|
||||
if (bt.children[5].type != "setBit")
|
||||
FAIL("wrong child 5 type");
|
||||
if (bt.children[5].name != "at_home")
|
||||
FAIL("wrong child 5 name");
|
||||
if (bt.children[5].params != "0")
|
||||
FAIL("wrong child 5 params");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 13: Set and get bit names from Lua
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int testSetGetBitName(lua_State *L)
|
||||
{
|
||||
TEST("set and get bit names from Lua");
|
||||
|
||||
// Clear any previous bit names
|
||||
for (int i = 0; i < 64; i++)
|
||||
GoapBlackboard::setBitName(i, "");
|
||||
|
||||
// Set bit names via Lua
|
||||
bool ok = runLua(L, "ecs.action_db.set_bit_name(0, 'has_axe');"
|
||||
"ecs.action_db.set_bit_name(1, 'has_wood');"
|
||||
"ecs.action_db.set_bit_name(5, 'is_hungry')");
|
||||
if (!ok)
|
||||
FAIL("failed to set bit names");
|
||||
|
||||
// Verify via C++ API
|
||||
const char *name0 = GoapBlackboard::getBitName(0);
|
||||
if (!name0 || strcmp(name0, "has_axe") != 0)
|
||||
FAIL("bit 0 should be 'has_axe'");
|
||||
|
||||
const char *name1 = GoapBlackboard::getBitName(1);
|
||||
if (!name1 || strcmp(name1, "has_wood") != 0)
|
||||
FAIL("bit 1 should be 'has_wood'");
|
||||
|
||||
const char *name5 = GoapBlackboard::getBitName(5);
|
||||
if (!name5 || strcmp(name5, "is_hungry") != 0)
|
||||
FAIL("bit 5 should be 'is_hungry'");
|
||||
|
||||
// Verify unset bits are null
|
||||
if (GoapBlackboard::getBitName(2) != nullptr)
|
||||
FAIL("bit 2 should be unset");
|
||||
|
||||
// Verify via Lua get_bit_name
|
||||
ok = runLua(
|
||||
L,
|
||||
"local n0 = ecs.action_db.get_bit_name(0);"
|
||||
"assert(n0 == 'has_axe', 'expected has_axe, got ' .. tostring(n0));"
|
||||
"local n1 = ecs.action_db.get_bit_name(1);"
|
||||
"assert(n1 == 'has_wood', 'expected has_wood, got ' .. tostring(n1));"
|
||||
"local n5 = ecs.action_db.get_bit_name(5);"
|
||||
"assert(n5 == 'is_hungry', 'expected is_hungry, got ' .. tostring(n5));"
|
||||
"local n2 = ecs.action_db.get_bit_name(2);"
|
||||
"assert(n2 == nil, 'bit 2 should be nil, got ' .. tostring(n2))");
|
||||
if (!ok)
|
||||
FAIL("get_bit_name assertions failed");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 14: Find bit by name from Lua
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int testFindBitByName(lua_State *L)
|
||||
{
|
||||
TEST("find bit by name from Lua");
|
||||
|
||||
// Clear and set known bits
|
||||
for (int i = 0; i < 64; i++)
|
||||
GoapBlackboard::setBitName(i, "");
|
||||
GoapBlackboard::setBitName(3, "near_tree");
|
||||
GoapBlackboard::setBitName(7, "near_fire");
|
||||
|
||||
bool ok = runLua(
|
||||
L,
|
||||
"local idx3 = ecs.action_db.find_bit_by_name('near_tree');"
|
||||
"assert(idx3 == 3, 'expected 3, got ' .. tostring(idx3));"
|
||||
"local idx7 = ecs.action_db.find_bit_by_name('near_fire');"
|
||||
"assert(idx7 == 7, 'expected 7, got ' .. tostring(idx7));"
|
||||
"local nilIdx = ecs.action_db.find_bit_by_name('nonexistent');"
|
||||
"assert(nilIdx == nil, 'expected nil, got ' .. tostring(nilIdx))");
|
||||
if (!ok)
|
||||
FAIL("find_bit_by_name assertions failed");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 15: Auto-assign bit from Lua
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int testAutoAssignBit(lua_State *L)
|
||||
{
|
||||
TEST("auto-assign bit from Lua");
|
||||
|
||||
// Clear all bits
|
||||
for (int i = 0; i < 64; i++)
|
||||
GoapBlackboard::setBitName(i, "");
|
||||
|
||||
// Auto-assign a new name
|
||||
bool ok = runLua(
|
||||
L,
|
||||
"local idx = ecs.action_db.auto_assign_bit('my_flag');"
|
||||
"assert(idx == 0, 'expected first free slot 0, got ' .. tostring(idx));"
|
||||
"local name = ecs.action_db.get_bit_name(0);"
|
||||
"assert(name == 'my_flag', 'expected my_flag, got ' .. tostring(name))");
|
||||
if (!ok)
|
||||
FAIL("first auto_assign failed");
|
||||
|
||||
// Auto-assign another - should get slot 1
|
||||
ok = runLua(
|
||||
L,
|
||||
"local idx = ecs.action_db.auto_assign_bit('other_flag');"
|
||||
"assert(idx == 1, 'expected slot 1, got ' .. tostring(idx))");
|
||||
if (!ok)
|
||||
FAIL("second auto_assign failed");
|
||||
|
||||
// Auto-assign an already-existing name - should return existing index
|
||||
ok = runLua(
|
||||
L,
|
||||
"local idx = ecs.action_db.auto_assign_bit('my_flag');"
|
||||
"assert(idx == 0, 'expected existing slot 0, got ' .. tostring(idx))");
|
||||
if (!ok)
|
||||
FAIL("re-assign existing name failed");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 16: List bit names from Lua
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int testListBitNames(lua_State *L)
|
||||
{
|
||||
TEST("list bit names from Lua");
|
||||
|
||||
// Clear and set known bits
|
||||
for (int i = 0; i < 64; i++)
|
||||
GoapBlackboard::setBitName(i, "");
|
||||
GoapBlackboard::setBitName(0, "alpha");
|
||||
GoapBlackboard::setBitName(5, "beta");
|
||||
GoapBlackboard::setBitName(10, "gamma");
|
||||
|
||||
bool ok = runLua(
|
||||
L,
|
||||
"local bits = ecs.action_db.list_bit_names();"
|
||||
"assert(#bits == 3, 'expected 3 bits, got ' .. #bits);"
|
||||
"assert(bits[1].index == 0 and bits[1].name == 'alpha', 'wrong bit 0');"
|
||||
"assert(bits[2].index == 5 and bits[2].name == 'beta', 'wrong bit 5');"
|
||||
"assert(bits[3].index == 10 and bits[3].name == 'gamma', 'wrong bit 10')");
|
||||
if (!ok)
|
||||
FAIL("list_bit_names assertions failed");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 17: Bit names auto-assigned when used in action preconditions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int testAutoAssignInAction(lua_State *L)
|
||||
{
|
||||
TEST("bit names auto-assigned in action preconditions");
|
||||
|
||||
// Clear all bits
|
||||
for (int i = 0; i < 64; i++)
|
||||
GoapBlackboard::setBitName(i, "");
|
||||
|
||||
// Create an action with a bit name that hasn't been defined yet
|
||||
bool ok = runLua(L, "ecs.action_db.add_action('test_auto_bit', 1,"
|
||||
" { bits = { brand_new_bit = true } },"
|
||||
" { bits = { another_new_bit = true } })");
|
||||
if (!ok)
|
||||
FAIL("failed to create action with auto-assigned bits");
|
||||
|
||||
// The bits should have been auto-assigned
|
||||
int idx1 = GoapBlackboard::findBitByName("brand_new_bit");
|
||||
if (idx1 < 0)
|
||||
FAIL("brand_new_bit should have been auto-assigned");
|
||||
if (idx1 != 0)
|
||||
FAIL("brand_new_bit should be at slot 0");
|
||||
|
||||
int idx2 = GoapBlackboard::findBitByName("another_new_bit");
|
||||
if (idx2 < 0)
|
||||
FAIL("another_new_bit should have been auto-assigned");
|
||||
if (idx2 != 1)
|
||||
FAIL("another_new_bit should be at slot 1");
|
||||
|
||||
// Verify the action's preconditions use the correct bit
|
||||
const GoapAction *action =
|
||||
ActionDatabase::getSingleton().findAction("test_auto_bit");
|
||||
if (!action)
|
||||
FAIL("action not found");
|
||||
if (!action->preconditions.getBit(idx1))
|
||||
FAIL("precondition bit should be set");
|
||||
if (!action->effects.getBit(idx2))
|
||||
FAIL("effect bit should be set");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
int main()
|
||||
{
|
||||
printf("ActionDatabase Lua API Tests\n");
|
||||
printf("============================\n\n");
|
||||
|
||||
// Create Lua state
|
||||
lua_State *L = luaL_newstate();
|
||||
if (!L) {
|
||||
fprintf(stderr, "Failed to create Lua state\n");
|
||||
return 1;
|
||||
}
|
||||
luaL_openlibs(L);
|
||||
|
||||
// Register the action API
|
||||
editScene::registerLuaActionApi(L);
|
||||
|
||||
// Run tests
|
||||
int failures = 0;
|
||||
failures += testBasicAction(L);
|
||||
failures += testActionWithBehaviorTree(L);
|
||||
failures += testNestedBehaviorTree(L);
|
||||
failures += testGoal(L);
|
||||
failures += testActionReplacement(L);
|
||||
failures += testRemoveAction(L);
|
||||
failures += testListActions(L);
|
||||
failures += testFindActionLua(L);
|
||||
failures += testClear(L);
|
||||
failures += testInventoryBehaviorTree(L);
|
||||
failures += testPhysicsBehaviorTree(L);
|
||||
failures += testBlackboardCheckBehaviorTree(L);
|
||||
failures += testSetGetBitName(L);
|
||||
failures += testFindBitByName(L);
|
||||
failures += testAutoAssignBit(L);
|
||||
failures += testListBitNames(L);
|
||||
failures += testAutoAssignInAction(L);
|
||||
|
||||
// Cleanup
|
||||
lua_close(L);
|
||||
|
||||
printf("\nResults: %d/%d passed, %d failed\n", passCount, testCount,
|
||||
failures);
|
||||
|
||||
return failures > 0 ? 1 : 0;
|
||||
}
|
||||
1337
src/features/editScene/tests/behavior_tree_lua_test.cpp
Normal file
1337
src/features/editScene/tests/behavior_tree_lua_test.cpp
Normal file
File diff suppressed because it is too large
Load Diff
1909
src/features/editScene/tests/component_lua_test.cpp
Normal file
1909
src/features/editScene/tests/component_lua_test.cpp
Normal file
File diff suppressed because it is too large
Load Diff
27
src/features/editScene/tests/components/EditorMarker.hpp
Normal file
27
src/features/editScene/tests/components/EditorMarker.hpp
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* @file EditorMarker.hpp
|
||||
* @brief Stub for standalone tests.
|
||||
*
|
||||
* Provides the EditorMarkerComponent tag used by LuaEntityApi.cpp.
|
||||
* In the real build, this is defined in the editScene components.
|
||||
*/
|
||||
|
||||
#ifndef EDITSCENE_COMPONENTS_EDITORMARKER_HPP
|
||||
#define EDITSCENE_COMPONENTS_EDITORMARKER_HPP
|
||||
|
||||
#include <flecs.h>
|
||||
|
||||
namespace editScene
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Tag component marking entities as editor-managed.
|
||||
*
|
||||
* In the real build, this is a Flecs tag. For tests, we define
|
||||
* it as an empty struct so LuaEntityApi.cpp can compile.
|
||||
*/
|
||||
struct EditorMarkerComponent {};
|
||||
|
||||
} // namespace editScene
|
||||
|
||||
#endif // EDITSCENE_COMPONENTS_EDITORMARKER_HPP
|
||||
403
src/features/editScene/tests/entity_lua_test.cpp
Normal file
403
src/features/editScene/tests/entity_lua_test.cpp
Normal file
@@ -0,0 +1,403 @@
|
||||
/**
|
||||
* @file entity_lua_test.cpp
|
||||
* @brief Standalone test for the Lua Entity API.
|
||||
*
|
||||
* Tests entity creation, destruction, naming, hierarchy (parent/children),
|
||||
* and entity lookup functions exposed via the ecs.* Lua API.
|
||||
*
|
||||
* Build with:
|
||||
* g++ -std=c++17 -I. -I../.. -I../../lua/lua-5.4.8/src \
|
||||
* entity_lua_test.cpp \
|
||||
* ../lua/LuaEntityApi.cpp \
|
||||
* ../../lua/lua-5.4.8/src/liblua.a \
|
||||
* -o entity_lua_test -lm
|
||||
*
|
||||
* Or via CMake (see CMakeLists.txt in this directory).
|
||||
*/
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// Ogre stub (provides Ogre::String, Ogre::Vector3, Ogre::LogManager)
|
||||
#include "ogre_stub.h"
|
||||
|
||||
// Include Lua
|
||||
extern "C" {
|
||||
#include <lua.h>
|
||||
#include <lauxlib.h>
|
||||
#include <lualib.h>
|
||||
}
|
||||
|
||||
// Flecs stub for standalone testing
|
||||
// We provide minimal Flecs types needed by LuaEntityApi
|
||||
namespace flecs
|
||||
{
|
||||
|
||||
// Minimal entity wrapper
|
||||
struct entity {
|
||||
uint64_t m_id = 0;
|
||||
bool m_valid = false;
|
||||
|
||||
entity()
|
||||
: m_id(0)
|
||||
, m_valid(false)
|
||||
{
|
||||
}
|
||||
explicit entity(uint64_t id)
|
||||
: m_id(id)
|
||||
, m_valid(true)
|
||||
{
|
||||
}
|
||||
|
||||
uint64_t id() const
|
||||
{
|
||||
return m_id;
|
||||
}
|
||||
bool is_valid() const
|
||||
{
|
||||
return m_valid;
|
||||
}
|
||||
bool is_alive() const
|
||||
{
|
||||
return m_valid;
|
||||
}
|
||||
const char *name() const
|
||||
{
|
||||
return "";
|
||||
}
|
||||
void set_name(const char *)
|
||||
{
|
||||
}
|
||||
void destruct()
|
||||
{
|
||||
m_valid = false;
|
||||
}
|
||||
|
||||
entity parent() const
|
||||
{
|
||||
return entity();
|
||||
}
|
||||
|
||||
template <typename Func> void children(Func) const
|
||||
{
|
||||
}
|
||||
|
||||
template <typename T> void add()
|
||||
{
|
||||
}
|
||||
|
||||
template <typename T> bool has() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
template <typename T> const T *get() const
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
template <typename T> void set(const T &)
|
||||
{
|
||||
}
|
||||
|
||||
bool operator==(const entity &other) const
|
||||
{
|
||||
return m_id == other.m_id;
|
||||
}
|
||||
};
|
||||
|
||||
using entity_t = uint64_t;
|
||||
|
||||
// Minimal world stub
|
||||
struct world {
|
||||
entity make_entity()
|
||||
{
|
||||
return entity(nextId++);
|
||||
}
|
||||
entity lookup(const char *)
|
||||
{
|
||||
return entity();
|
||||
}
|
||||
|
||||
static world &get()
|
||||
{
|
||||
static world w;
|
||||
return w;
|
||||
}
|
||||
|
||||
private:
|
||||
uint64_t nextId = 1000;
|
||||
};
|
||||
|
||||
} // namespace flecs
|
||||
|
||||
// Forward declare the registration function
|
||||
namespace editScene
|
||||
{
|
||||
void registerLuaEntityApi(lua_State *L);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int testCount = 0;
|
||||
static int passCount = 0;
|
||||
|
||||
#define TEST(name) \
|
||||
do { \
|
||||
testCount++; \
|
||||
printf(" TEST %d: %s ... ", testCount, name); \
|
||||
} while (0)
|
||||
|
||||
#define PASS() \
|
||||
do { \
|
||||
passCount++; \
|
||||
printf("PASS\n"); \
|
||||
} while (0)
|
||||
|
||||
#define FAIL(msg) \
|
||||
do { \
|
||||
printf("FAIL: %s\n", msg); \
|
||||
return 1; \
|
||||
} while (0)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: run a Lua string and check for errors
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static bool runLua(lua_State *L, const char *code)
|
||||
{
|
||||
if (luaL_dostring(L, code) != LUA_OK) {
|
||||
fprintf(stderr, "Lua error: %s\n", lua_tostring(L, -1));
|
||||
lua_pop(L, 1);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 1: Create entity
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int testCreateEntity(lua_State *L)
|
||||
{
|
||||
TEST("create entity returns integer ID");
|
||||
|
||||
bool ok = runLua(
|
||||
L,
|
||||
"local id = ecs.create_entity();"
|
||||
"assert(type(id) == 'number', 'expected number, got ' .. type(id));"
|
||||
"assert(id > 0, 'expected positive ID, got ' .. tostring(id))");
|
||||
if (!ok)
|
||||
FAIL("create entity assertion failed");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 2: Entity exists
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int testEntityExists(lua_State *L)
|
||||
{
|
||||
TEST("entity_exists returns correct values");
|
||||
|
||||
bool ok = runLua(
|
||||
L,
|
||||
"local id = ecs.create_entity();"
|
||||
"assert(ecs.entity_exists(id) == true, 'entity should exist');"
|
||||
"assert(ecs.entity_exists(999999) == false, 'fake entity should not exist')");
|
||||
if (!ok)
|
||||
FAIL("entity exists assertion failed");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 3: Destroy entity
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int testDestroyEntity(lua_State *L)
|
||||
{
|
||||
TEST("destroy entity");
|
||||
|
||||
bool ok = runLua(
|
||||
L,
|
||||
"local id = ecs.create_entity();"
|
||||
"assert(ecs.entity_exists(id) == true, 'entity should exist before destroy');"
|
||||
"ecs.destroy_entity(id);"
|
||||
"assert(ecs.entity_exists(id) == false, 'entity should not exist after destroy')");
|
||||
if (!ok)
|
||||
FAIL("destroy entity assertion failed");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 4: Set and get entity name
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int testEntityName(lua_State *L)
|
||||
{
|
||||
TEST("set and get entity name");
|
||||
|
||||
bool ok = runLua(
|
||||
L,
|
||||
"local id = ecs.create_entity();"
|
||||
"ecs.set_entity_name(id, 'test_hero');"
|
||||
"local name = ecs.get_entity_name(id);"
|
||||
"assert(name == 'test_hero', 'expected test_hero, got ' .. tostring(name))");
|
||||
if (!ok)
|
||||
FAIL("entity name assertion failed");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 5: Get entity by name
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int testGetEntityByName(lua_State *L)
|
||||
{
|
||||
TEST("get entity by name");
|
||||
|
||||
bool ok = runLua(
|
||||
L,
|
||||
"local id = ecs.create_entity();"
|
||||
"ecs.set_entity_name(id, 'findable_entity');"
|
||||
"local found = ecs.get_entity_by_name('findable_entity');"
|
||||
"assert(found == id, 'expected same ID, got ' .. tostring(found));"
|
||||
"local not_found = ecs.get_entity_by_name('nonexistent');"
|
||||
"assert(not_found == nil, 'expected nil for nonexistent')");
|
||||
if (!ok)
|
||||
FAIL("get entity by name assertion failed");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 6: Parent and children hierarchy
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int testHierarchy(lua_State *L)
|
||||
{
|
||||
TEST("parent and children hierarchy");
|
||||
|
||||
bool ok = runLua(
|
||||
L,
|
||||
"local parent = ecs.create_entity();"
|
||||
"local child = ecs.create_entity();"
|
||||
"ecs.set_entity_name(parent, 'parent_entity');"
|
||||
"ecs.set_entity_name(child, 'child_entity');"
|
||||
"local p = ecs.parent(child);"
|
||||
"assert(p == nil, 'child should have no parent initially');"
|
||||
"local kids = ecs.children(parent);"
|
||||
"assert(type(kids) == 'table', 'children should return a table');"
|
||||
"assert(#kids == 0, 'parent should have no children initially')");
|
||||
if (!ok)
|
||||
FAIL("hierarchy assertion failed");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 7: Multiple entity creation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int testMultipleEntities(lua_State *L)
|
||||
{
|
||||
TEST("create multiple entities with unique IDs");
|
||||
|
||||
bool ok = runLua(
|
||||
L,
|
||||
"local ids = {};"
|
||||
"for i = 1, 5 do"
|
||||
" ids[i] = ecs.create_entity();"
|
||||
"end;"
|
||||
"for i = 1, 5 do"
|
||||
" for j = i+1, 5 do"
|
||||
" assert(ids[i] ~= ids[j], 'IDs should be unique');"
|
||||
" end;"
|
||||
"end;"
|
||||
"for i = 1, 5 do"
|
||||
" assert(ecs.entity_exists(ids[i]) == true, 'entity should exist');"
|
||||
"end");
|
||||
if (!ok)
|
||||
FAIL("multiple entities assertion failed");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 8: Destroy and recreate (ID reuse)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int testDestroyRecreate(lua_State *L)
|
||||
{
|
||||
TEST("destroy and recreate entity");
|
||||
|
||||
bool ok = runLua(
|
||||
L,
|
||||
"local id1 = ecs.create_entity();"
|
||||
"ecs.destroy_entity(id1);"
|
||||
"assert(ecs.entity_exists(id1) == false, 'destroyed entity should not exist');"
|
||||
"local id2 = ecs.create_entity();"
|
||||
"assert(ecs.entity_exists(id2) == true, 'new entity should exist');"
|
||||
"assert(id2 ~= id1, 'new entity should have different ID')");
|
||||
if (!ok)
|
||||
FAIL("destroy recreate assertion failed");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
int main()
|
||||
{
|
||||
printf("Entity Lua API Tests\n");
|
||||
printf("====================\n\n");
|
||||
|
||||
// Create Lua state
|
||||
lua_State *L = luaL_newstate();
|
||||
if (!L) {
|
||||
fprintf(stderr, "Failed to create Lua state\n");
|
||||
return 1;
|
||||
}
|
||||
luaL_openlibs(L);
|
||||
|
||||
// Register the entity API
|
||||
editScene::registerLuaEntityApi(L);
|
||||
|
||||
// Run tests
|
||||
int failures = 0;
|
||||
failures += testCreateEntity(L);
|
||||
failures += testEntityExists(L);
|
||||
failures += testDestroyEntity(L);
|
||||
failures += testEntityName(L);
|
||||
failures += testGetEntityByName(L);
|
||||
failures += testHierarchy(L);
|
||||
failures += testMultipleEntities(L);
|
||||
failures += testDestroyRecreate(L);
|
||||
|
||||
// Cleanup
|
||||
lua_close(L);
|
||||
|
||||
printf("\nResults: %d/%d passed, %d failed\n", passCount, testCount,
|
||||
failures);
|
||||
|
||||
return failures > 0 ? 1 : 0;
|
||||
}
|
||||
375
src/features/editScene/tests/event_lua_test.cpp
Normal file
375
src/features/editScene/tests/event_lua_test.cpp
Normal file
@@ -0,0 +1,375 @@
|
||||
/**
|
||||
* @file event_lua_test.cpp
|
||||
* @brief Standalone test for the Lua Event API.
|
||||
*
|
||||
* Tests event subscription, unsubscription, and sending functions
|
||||
* exposed via the ecs.* Lua API.
|
||||
*
|
||||
* Build with:
|
||||
* g++ -std=c++17 -I. -I../.. -I../../lua/lua-5.4.8/src \
|
||||
* event_lua_test.cpp \
|
||||
* ../lua/LuaEventApi.cpp \
|
||||
* ../lua/LuaEntityApi.cpp \
|
||||
* ../systems/EventBus.cpp \
|
||||
* ../components/GoapBlackboard.cpp \
|
||||
* ../../lua/lua-5.4.8/src/liblua.a \
|
||||
* -o event_lua_test -lm
|
||||
*
|
||||
* Or via CMake (see CMakeLists.txt in this directory).
|
||||
*/
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// Ogre stub (provides Ogre::String, Ogre::Vector3, Ogre::LogManager)
|
||||
#include "ogre_stub.h"
|
||||
|
||||
// Include Lua
|
||||
extern "C" {
|
||||
#include <lua.h>
|
||||
#include <lauxlib.h>
|
||||
#include <lualib.h>
|
||||
}
|
||||
|
||||
// Include the components we need
|
||||
#include "../systems/EventBus.hpp"
|
||||
#include "../components/GoapBlackboard.hpp"
|
||||
|
||||
// Forward declare the registration function
|
||||
namespace editScene
|
||||
{
|
||||
void registerLuaEventApi(lua_State *L);
|
||||
void registerLuaEntityApi(lua_State *L);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int testCount = 0;
|
||||
static int passCount = 0;
|
||||
|
||||
#define TEST(name) \
|
||||
do { \
|
||||
testCount++; \
|
||||
printf(" TEST %d: %s ... ", testCount, name); \
|
||||
} while (0)
|
||||
|
||||
#define PASS() \
|
||||
do { \
|
||||
passCount++; \
|
||||
printf("PASS\n"); \
|
||||
} while (0)
|
||||
|
||||
#define FAIL(msg) \
|
||||
do { \
|
||||
printf("FAIL: %s\n", msg); \
|
||||
return 1; \
|
||||
} while (0)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: run a Lua string and check for errors
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static bool runLua(lua_State *L, const char *code)
|
||||
{
|
||||
if (luaL_dostring(L, code) != LUA_OK) {
|
||||
fprintf(stderr, "Lua error: %s\n", lua_tostring(L, -1));
|
||||
lua_pop(L, 1);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 1: Send a simple event
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int testSendSimpleEvent(lua_State *L)
|
||||
{
|
||||
TEST("send a simple event");
|
||||
|
||||
bool ok = runLua(L, "ecs.send_event('test_event')");
|
||||
if (!ok)
|
||||
FAIL("failed to send simple event");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 2: Subscribe and receive an event
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int testSubscribeAndReceive(lua_State *L)
|
||||
{
|
||||
TEST("subscribe and receive an event");
|
||||
|
||||
// Track whether the callback was called
|
||||
bool ok = runLua(
|
||||
L,
|
||||
"local received = false;"
|
||||
"local sub_id = ecs.subscribe_event('hello', function(event, params)"
|
||||
" received = true;"
|
||||
"end);"
|
||||
"assert(sub_id ~= nil, 'subscription ID should not be nil');"
|
||||
"assert(type(sub_id) == 'number', 'subscription ID should be a number');"
|
||||
"ecs.send_event('hello');"
|
||||
"assert(received == true, 'callback should have been called')");
|
||||
if (!ok)
|
||||
FAIL("subscribe and receive assertion failed");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 3: Subscribe with event name and params
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int testSubscribeWithParams(lua_State *L)
|
||||
{
|
||||
TEST("subscribe with event name and params");
|
||||
|
||||
bool ok = runLua(
|
||||
L,
|
||||
"local received_event = nil;"
|
||||
"local sub_id = ecs.subscribe_event('collision', function(event, params)"
|
||||
" received_event = event;"
|
||||
"end);"
|
||||
"ecs.send_event('collision', { values = { damage = 10 } });"
|
||||
"assert(received_event == 'collision', 'expected collision event')");
|
||||
if (!ok)
|
||||
FAIL("subscribe with params assertion failed");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 4: Unsubscribe from an event
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int testUnsubscribe(lua_State *L)
|
||||
{
|
||||
TEST("unsubscribe from an event");
|
||||
|
||||
bool ok = runLua(
|
||||
L,
|
||||
"local call_count = 0;"
|
||||
"local sub_id = ecs.subscribe_event('temp', function(event, params)"
|
||||
" call_count = call_count + 1;"
|
||||
"end);"
|
||||
"ecs.send_event('temp');"
|
||||
"assert(call_count == 1, 'should have been called once');"
|
||||
"ecs.unsubscribe_event(sub_id);"
|
||||
"ecs.send_event('temp');"
|
||||
"assert(call_count == 1, 'should not have been called after unsubscribe')");
|
||||
if (!ok)
|
||||
FAIL("unsubscribe assertion failed");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 5: Multiple subscribers to same event
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int testMultipleSubscribers(lua_State *L)
|
||||
{
|
||||
TEST("multiple subscribers to same event");
|
||||
|
||||
bool ok = runLua(
|
||||
L,
|
||||
"local count1 = 0; local count2 = 0;"
|
||||
"local s1 = ecs.subscribe_event('multi', function() count1 = count1 + 1; end);"
|
||||
"local s2 = ecs.subscribe_event('multi', function() count2 = count2 + 1; end);"
|
||||
"ecs.send_event('multi');"
|
||||
"assert(count1 == 1, 'subscriber 1 should have been called');"
|
||||
"assert(count2 == 1, 'subscriber 2 should have been called');"
|
||||
"ecs.send_event('multi');"
|
||||
"assert(count1 == 2, 'subscriber 1 should have been called twice');"
|
||||
"assert(count2 == 2, 'subscriber 2 should have been called twice')");
|
||||
if (!ok)
|
||||
FAIL("multiple subscribers assertion failed");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 6: Send event with blackboard params
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int testEventWithBlackboardParams(lua_State *L)
|
||||
{
|
||||
TEST("send event with blackboard params");
|
||||
|
||||
bool ok = runLua(
|
||||
L,
|
||||
"local result = nil;"
|
||||
"ecs.subscribe_event('data_event', function(event, params)"
|
||||
" result = params;"
|
||||
"end);"
|
||||
"ecs.send_event('data_event', {"
|
||||
" values = { score = 42, level = 5 },"
|
||||
" floatValues = { speed = 1.5 },"
|
||||
" stringValues = { name = 'hero' }"
|
||||
"});"
|
||||
"assert(result ~= nil, 'params should not be nil');"
|
||||
"assert(result.values.score == 42, 'expected score 42');"
|
||||
"assert(result.values.level == 5, 'expected level 5');"
|
||||
"assert(result.floatValues.speed == 1.5, 'expected speed 1.5');"
|
||||
"assert(result.stringValues.name == 'hero', 'expected name hero')");
|
||||
if (!ok)
|
||||
FAIL("event with blackboard params assertion failed");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 7: Send event with vec3 params
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int testEventWithVec3Params(lua_State *L)
|
||||
{
|
||||
TEST("send event with vec3 params");
|
||||
|
||||
bool ok = runLua(
|
||||
L,
|
||||
"local result = nil;"
|
||||
"ecs.subscribe_event('move', function(event, params)"
|
||||
" result = params;"
|
||||
"end);"
|
||||
"ecs.send_event('move', {"
|
||||
" vec3Values = { position = { 10, 20, 30 }, velocity = { 1, 0, 0 } }"
|
||||
"});"
|
||||
"assert(result ~= nil, 'params should not be nil');"
|
||||
"assert(result.vec3Values.position[1] == 10, 'expected pos.x=10');"
|
||||
"assert(result.vec3Values.position[2] == 20, 'expected pos.y=20');"
|
||||
"assert(result.vec3Values.position[3] == 30, 'expected pos.z=30');"
|
||||
"assert(result.vec3Values.velocity[1] == 1, 'expected vel.x=1')");
|
||||
if (!ok)
|
||||
FAIL("event with vec3 params assertion failed");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 8: Send event with bits and mask
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int testEventWithBits(lua_State *L)
|
||||
{
|
||||
TEST("send event with bits and mask");
|
||||
|
||||
bool ok = runLua(
|
||||
L, "local result = nil;"
|
||||
"ecs.subscribe_event('flag_event', function(event, params)"
|
||||
" result = params;"
|
||||
"end);"
|
||||
"ecs.send_event('flag_event', {"
|
||||
" bits = 5,"
|
||||
" mask = 7"
|
||||
"});"
|
||||
"assert(result ~= nil, 'params should not be nil');"
|
||||
"assert(result.bits == 5, 'expected bits=5');"
|
||||
"assert(result.mask == 7, 'expected mask=7')");
|
||||
if (!ok)
|
||||
FAIL("event with bits assertion failed");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 9: Multiple events with different names
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int testMultipleEvents(lua_State *L)
|
||||
{
|
||||
TEST("multiple events with different names");
|
||||
|
||||
bool ok = runLua(
|
||||
L,
|
||||
"local events = {};"
|
||||
"ecs.subscribe_event('event_a', function() table.insert(events, 'a'); end);"
|
||||
"ecs.subscribe_event('event_b', function() table.insert(events, 'b'); end);"
|
||||
"ecs.send_event('event_a');"
|
||||
"ecs.send_event('event_b');"
|
||||
"ecs.send_event('event_a');"
|
||||
"assert(#events == 3, 'expected 3 events, got ' .. #events);"
|
||||
"assert(events[1] == 'a', 'expected a');"
|
||||
"assert(events[2] == 'b', 'expected b');"
|
||||
"assert(events[3] == 'a', 'expected a')");
|
||||
if (!ok)
|
||||
FAIL("multiple events assertion failed");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 10: Unsubscribe invalid ID (should not crash)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int testUnsubscribeInvalid(lua_State *L)
|
||||
{
|
||||
TEST("unsubscribe invalid ID (should not crash)");
|
||||
|
||||
bool ok = runLua(L, "ecs.unsubscribe_event(99999);");
|
||||
if (!ok)
|
||||
FAIL("unsubscribe invalid ID should not error");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
int main()
|
||||
{
|
||||
printf("Event Lua API Tests\n");
|
||||
printf("===================\n\n");
|
||||
|
||||
// Create Lua state
|
||||
lua_State *L = luaL_newstate();
|
||||
if (!L) {
|
||||
fprintf(stderr, "Failed to create Lua state\n");
|
||||
return 1;
|
||||
}
|
||||
luaL_openlibs(L);
|
||||
|
||||
// Register the entity and event APIs
|
||||
editScene::registerLuaEntityApi(L);
|
||||
editScene::registerLuaEventApi(L);
|
||||
|
||||
// Run tests
|
||||
int failures = 0;
|
||||
failures += testSendSimpleEvent(L);
|
||||
failures += testSubscribeAndReceive(L);
|
||||
failures += testSubscribeWithParams(L);
|
||||
failures += testUnsubscribe(L);
|
||||
failures += testMultipleSubscribers(L);
|
||||
failures += testEventWithBlackboardParams(L);
|
||||
failures += testEventWithVec3Params(L);
|
||||
failures += testEventWithBits(L);
|
||||
failures += testMultipleEvents(L);
|
||||
failures += testUnsubscribeInvalid(L);
|
||||
|
||||
// Cleanup
|
||||
lua_close(L);
|
||||
|
||||
printf("\nResults: %d/%d passed, %d failed\n", passCount, testCount,
|
||||
failures);
|
||||
|
||||
return failures > 0 ? 1 : 0;
|
||||
}
|
||||
576
src/features/editScene/tests/lua_test_stubs.cpp
Normal file
576
src/features/editScene/tests/lua_test_stubs.cpp
Normal file
@@ -0,0 +1,576 @@
|
||||
/**
|
||||
* @file lua_test_stubs.cpp
|
||||
* @brief Stub implementations of Lua API registration functions for standalone tests.
|
||||
*
|
||||
* These stubs provide minimal implementations that work with the
|
||||
* flecs stubs and ogre_stub.h defined in the test files.
|
||||
* They are used instead of the real Lua API .cpp files which
|
||||
* require the full OGRE SDK.
|
||||
*
|
||||
* The stubs maintain state (entity IDs, names, components) so that
|
||||
* the tests can verify correct behavior.
|
||||
*/
|
||||
|
||||
#include "ogre_stub.h"
|
||||
#include <lua.hpp>
|
||||
#include <cstdio>
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shared component storage (used by both entity and component API stubs)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
namespace editScene
|
||||
{
|
||||
|
||||
// A value can be a number, string, boolean, array of numbers, array of strings,
|
||||
// or a nested table (map of string->value)
|
||||
struct ComponentFieldValue {
|
||||
enum Type { NIL, NUMBER, STRING, BOOLEAN, NUM_ARRAY, STR_ARRAY, TABLE };
|
||||
Type type = NIL;
|
||||
double numVal = 0;
|
||||
std::string strVal;
|
||||
bool boolVal = false;
|
||||
std::vector<double> numArr;
|
||||
std::vector<std::string> strArr;
|
||||
std::map<std::string, ComponentFieldValue> tableVal;
|
||||
};
|
||||
|
||||
using ComponentData = std::unordered_map<std::string, ComponentFieldValue>;
|
||||
std::unordered_map<int, std::unordered_map<std::string, ComponentData> >
|
||||
s_components;
|
||||
|
||||
} // namespace editScene
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Stub: LuaEntityApi
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
namespace editScene
|
||||
{
|
||||
|
||||
// Entity state for stubs
|
||||
struct EntityState {
|
||||
bool alive;
|
||||
std::string name;
|
||||
};
|
||||
|
||||
static std::unordered_map<int, EntityState> s_entities;
|
||||
static int s_nextId = 1000;
|
||||
|
||||
static int createEntity()
|
||||
{
|
||||
int id = s_nextId++;
|
||||
s_entities[id] = { true, "" };
|
||||
// Auto-add EditorMarker component (matching real behavior)
|
||||
s_components[id]["EditorMarker"];
|
||||
return id;
|
||||
}
|
||||
|
||||
static void destroyEntity(int id)
|
||||
{
|
||||
auto it = s_entities.find(id);
|
||||
if (it != s_entities.end())
|
||||
it->second.alive = false;
|
||||
}
|
||||
|
||||
static bool entityExists(int id)
|
||||
{
|
||||
auto it = s_entities.find(id);
|
||||
return it != s_entities.end() && it->second.alive;
|
||||
}
|
||||
|
||||
static void setEntityName(int id, const std::string &name)
|
||||
{
|
||||
auto it = s_entities.find(id);
|
||||
if (it != s_entities.end())
|
||||
it->second.name = name;
|
||||
}
|
||||
|
||||
static std::string getEntityName(int id)
|
||||
{
|
||||
auto it = s_entities.find(id);
|
||||
if (it != s_entities.end())
|
||||
return it->second.name;
|
||||
return "";
|
||||
}
|
||||
|
||||
static int findEntityByName(const std::string &name)
|
||||
{
|
||||
for (auto &[id, state] : s_entities) {
|
||||
if (state.alive && state.name == name)
|
||||
return id;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void registerLuaEntityApi(lua_State *L)
|
||||
{
|
||||
// Create the "ecs" global table
|
||||
lua_newtable(L);
|
||||
|
||||
// create_entity
|
||||
lua_pushcfunction(L, [](lua_State *L) -> int {
|
||||
int id = createEntity();
|
||||
lua_pushinteger(L, id);
|
||||
return 1;
|
||||
});
|
||||
lua_setfield(L, -2, "create_entity");
|
||||
|
||||
// destroy_entity
|
||||
lua_pushcfunction(L, [](lua_State *L) -> int {
|
||||
int id = lua_tointeger(L, 1);
|
||||
destroyEntity(id);
|
||||
return 0;
|
||||
});
|
||||
lua_setfield(L, -2, "destroy_entity");
|
||||
|
||||
// entity_exists
|
||||
lua_pushcfunction(L, [](lua_State *L) -> int {
|
||||
int id = lua_tointeger(L, 1);
|
||||
lua_pushboolean(L, entityExists(id) ? 1 : 0);
|
||||
return 1;
|
||||
});
|
||||
lua_setfield(L, -2, "entity_exists");
|
||||
|
||||
// get_player_entity
|
||||
lua_pushcfunction(L, [](lua_State *L) -> int {
|
||||
int id = findEntityByName("player");
|
||||
if (id >= 0) {
|
||||
lua_pushinteger(L, id);
|
||||
} else {
|
||||
lua_pushnil(L);
|
||||
}
|
||||
return 1;
|
||||
});
|
||||
lua_setfield(L, -2, "get_player_entity");
|
||||
|
||||
// get_entity_by_name
|
||||
lua_pushcfunction(L, [](lua_State *L) -> int {
|
||||
const char *name = lua_tostring(L, 1);
|
||||
int id = findEntityByName(name ? name : "");
|
||||
if (id >= 0) {
|
||||
lua_pushinteger(L, id);
|
||||
} else {
|
||||
lua_pushnil(L);
|
||||
}
|
||||
return 1;
|
||||
});
|
||||
lua_setfield(L, -2, "get_entity_by_name");
|
||||
|
||||
// set_entity_name
|
||||
lua_pushcfunction(L, [](lua_State *L) -> int {
|
||||
int id = lua_tointeger(L, 1);
|
||||
const char *name = lua_tostring(L, 2);
|
||||
setEntityName(id, name ? name : "");
|
||||
return 0;
|
||||
});
|
||||
lua_setfield(L, -2, "set_entity_name");
|
||||
|
||||
// get_entity_name
|
||||
lua_pushcfunction(L, [](lua_State *L) -> int {
|
||||
int id = lua_tointeger(L, 1);
|
||||
std::string name = getEntityName(id);
|
||||
if (!name.empty()) {
|
||||
lua_pushstring(L, name.c_str());
|
||||
} else {
|
||||
lua_pushnil(L);
|
||||
}
|
||||
return 1;
|
||||
});
|
||||
lua_setfield(L, -2, "get_entity_name");
|
||||
|
||||
// parent
|
||||
lua_pushcfunction(L, [](lua_State *L) -> int {
|
||||
lua_pushnil(L);
|
||||
return 1;
|
||||
});
|
||||
lua_setfield(L, -2, "parent");
|
||||
|
||||
// children
|
||||
lua_pushcfunction(L, [](lua_State *L) -> int {
|
||||
lua_newtable(L);
|
||||
return 1;
|
||||
});
|
||||
lua_setfield(L, -2, "children");
|
||||
|
||||
lua_setglobal(L, "ecs");
|
||||
}
|
||||
|
||||
} // namespace editScene
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Stub: LuaComponentApi
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
namespace editScene
|
||||
{
|
||||
|
||||
static ComponentData &getOrCreateComponent(int entityId,
|
||||
const std::string &compName)
|
||||
{
|
||||
return s_components[entityId][compName];
|
||||
}
|
||||
|
||||
static bool hasComponent(int entityId, const std::string &compName)
|
||||
{
|
||||
auto eit = s_components.find(entityId);
|
||||
if (eit == s_components.end())
|
||||
return false;
|
||||
return eit->second.find(compName) != eit->second.end();
|
||||
}
|
||||
|
||||
static void removeComponent(int entityId, const std::string &compName)
|
||||
{
|
||||
auto eit = s_components.find(entityId);
|
||||
if (eit != s_components.end())
|
||||
eit->second.erase(compName);
|
||||
}
|
||||
|
||||
// Forward declaration
|
||||
static void pushFieldValueToLua(lua_State *L, const ComponentFieldValue &fv);
|
||||
|
||||
static void pushFieldValueToLua(lua_State *L, const ComponentFieldValue &fv)
|
||||
{
|
||||
switch (fv.type) {
|
||||
case ComponentFieldValue::NIL:
|
||||
lua_pushnil(L);
|
||||
break;
|
||||
case ComponentFieldValue::NUMBER:
|
||||
lua_pushnumber(L, fv.numVal);
|
||||
break;
|
||||
case ComponentFieldValue::STRING:
|
||||
lua_pushstring(L, fv.strVal.c_str());
|
||||
break;
|
||||
case ComponentFieldValue::BOOLEAN:
|
||||
lua_pushboolean(L, fv.boolVal ? 1 : 0);
|
||||
break;
|
||||
case ComponentFieldValue::NUM_ARRAY: {
|
||||
lua_newtable(L);
|
||||
for (size_t i = 0; i < fv.numArr.size(); i++) {
|
||||
lua_pushnumber(L, fv.numArr[i]);
|
||||
lua_rawseti(L, -2, i + 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ComponentFieldValue::STR_ARRAY: {
|
||||
lua_newtable(L);
|
||||
for (size_t i = 0; i < fv.strArr.size(); i++) {
|
||||
lua_pushstring(L, fv.strArr[i].c_str());
|
||||
lua_rawseti(L, -2, i + 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ComponentFieldValue::TABLE: {
|
||||
lua_newtable(L);
|
||||
for (auto &[k, v] : fv.tableVal) {
|
||||
pushFieldValueToLua(L, v);
|
||||
lua_setfield(L, -2, k.c_str());
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Forward declaration
|
||||
static ComponentFieldValue readLuaValue(lua_State *L, int idx);
|
||||
|
||||
static ComponentFieldValue readLuaValue(lua_State *L, int idx)
|
||||
{
|
||||
ComponentFieldValue fv;
|
||||
// Convert to absolute index to be safe with stack changes
|
||||
int absIdx = lua_absindex(L, idx);
|
||||
int type = lua_type(L, absIdx);
|
||||
if (type == LUA_TNIL) {
|
||||
fv.type = ComponentFieldValue::NIL;
|
||||
} else if (type == LUA_TNUMBER) {
|
||||
fv.type = ComponentFieldValue::NUMBER;
|
||||
fv.numVal = lua_tonumber(L, absIdx);
|
||||
} else if (type == LUA_TSTRING) {
|
||||
fv.type = ComponentFieldValue::STRING;
|
||||
fv.strVal = lua_tostring(L, absIdx);
|
||||
} else if (type == LUA_TBOOLEAN) {
|
||||
fv.type = ComponentFieldValue::BOOLEAN;
|
||||
fv.boolVal = lua_toboolean(L, absIdx) != 0;
|
||||
} else if (type == LUA_TTABLE) {
|
||||
// Check if it's an array (all integer keys) or a map (string keys)
|
||||
bool isArray = true;
|
||||
bool isStringArray = false;
|
||||
bool isNumArray = false;
|
||||
int maxKey = 0;
|
||||
|
||||
lua_pushnil(L);
|
||||
while (lua_next(L, absIdx) != 0) {
|
||||
if (lua_type(L, -2) == LUA_TNUMBER) {
|
||||
int k = lua_tointeger(L, -2);
|
||||
if (k > maxKey)
|
||||
maxKey = k;
|
||||
if (lua_type(L, -1) == LUA_TSTRING)
|
||||
isStringArray = true;
|
||||
else if (lua_type(L, -1) == LUA_TNUMBER)
|
||||
isNumArray = true;
|
||||
} else {
|
||||
isArray = false;
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
|
||||
if (isArray && maxKey > 0) {
|
||||
if (isStringArray) {
|
||||
fv.type = ComponentFieldValue::STR_ARRAY;
|
||||
for (int i = 1; i <= maxKey; i++) {
|
||||
lua_rawgeti(L, absIdx, i);
|
||||
if (lua_type(L, -1) == LUA_TSTRING)
|
||||
fv.strArr.push_back(
|
||||
lua_tostring(L, -1));
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
} else if (isNumArray) {
|
||||
fv.type = ComponentFieldValue::NUM_ARRAY;
|
||||
for (int i = 1; i <= maxKey; i++) {
|
||||
lua_rawgeti(L, absIdx, i);
|
||||
if (lua_type(L, -1) == LUA_TNUMBER)
|
||||
fv.numArr.push_back(
|
||||
lua_tonumber(L, -1));
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fv.type = ComponentFieldValue::TABLE;
|
||||
lua_pushnil(L);
|
||||
while (lua_next(L, absIdx) != 0) {
|
||||
if (lua_type(L, -2) == LUA_TSTRING) {
|
||||
const char *key = lua_tostring(L, -2);
|
||||
if (key)
|
||||
fv.tableVal[key] =
|
||||
readLuaValue(L, -1);
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
return fv;
|
||||
}
|
||||
|
||||
void registerLuaComponentApi(lua_State *L)
|
||||
{
|
||||
// Get or create the "ecs" global table
|
||||
lua_getglobal(L, "ecs");
|
||||
if (lua_isnil(L, -1)) {
|
||||
lua_pop(L, 1);
|
||||
lua_newtable(L);
|
||||
}
|
||||
|
||||
// has_component
|
||||
lua_pushcfunction(L, [](lua_State *L) -> int {
|
||||
int entityId = lua_tointeger(L, 1);
|
||||
const char *compName = lua_tostring(L, 2);
|
||||
lua_pushboolean(L, hasComponent(entityId,
|
||||
compName ? compName : "") ?
|
||||
1 :
|
||||
0);
|
||||
return 1;
|
||||
});
|
||||
lua_setfield(L, -2, "has_component");
|
||||
|
||||
// add_component
|
||||
lua_pushcfunction(L, [](lua_State *L) -> int {
|
||||
int entityId = lua_tointeger(L, 1);
|
||||
const char *compName = lua_tostring(L, 2);
|
||||
if (compName)
|
||||
getOrCreateComponent(entityId, compName);
|
||||
return 0;
|
||||
});
|
||||
lua_setfield(L, -2, "add_component");
|
||||
|
||||
// remove_component
|
||||
lua_pushcfunction(L, [](lua_State *L) -> int {
|
||||
int entityId = lua_tointeger(L, 1);
|
||||
const char *compName = lua_tostring(L, 2);
|
||||
if (compName)
|
||||
removeComponent(entityId, compName);
|
||||
return 0;
|
||||
});
|
||||
lua_setfield(L, -2, "remove_component");
|
||||
|
||||
// get_component
|
||||
lua_pushcfunction(L, [](lua_State *L) -> int {
|
||||
int entityId = lua_tointeger(L, 1);
|
||||
const char *compName = lua_tostring(L, 2);
|
||||
if (!compName || !hasComponent(entityId, compName)) {
|
||||
lua_pushnil(L);
|
||||
return 1;
|
||||
}
|
||||
ComponentData &cd = s_components[entityId][compName];
|
||||
lua_newtable(L);
|
||||
for (auto &[k, v] : cd) {
|
||||
pushFieldValueToLua(L, v);
|
||||
lua_setfield(L, -2, k.c_str());
|
||||
}
|
||||
return 1;
|
||||
});
|
||||
lua_setfield(L, -2, "get_component");
|
||||
|
||||
// set_component
|
||||
lua_pushcfunction(L, [](lua_State *L) -> int {
|
||||
int entityId = lua_tointeger(L, 1);
|
||||
const char *compName = lua_tostring(L, 2);
|
||||
if (!compName)
|
||||
return 0;
|
||||
ComponentData &cd = getOrCreateComponent(entityId, compName);
|
||||
if (lua_istable(L, 3)) {
|
||||
cd.clear();
|
||||
lua_pushnil(L);
|
||||
while (lua_next(L, 3) != 0) {
|
||||
if (lua_type(L, -2) == LUA_TSTRING) {
|
||||
const char *key = lua_tostring(L, -2);
|
||||
if (key)
|
||||
cd[key] = readLuaValue(L, -1);
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
lua_setfield(L, -2, "set_component");
|
||||
|
||||
// get_field
|
||||
lua_pushcfunction(L, [](lua_State *L) -> int {
|
||||
int entityId = lua_tointeger(L, 1);
|
||||
const char *compName = lua_tostring(L, 2);
|
||||
const char *fieldName = lua_tostring(L, 3);
|
||||
if (!compName || !fieldName ||
|
||||
!hasComponent(entityId, compName)) {
|
||||
lua_pushnil(L);
|
||||
return 1;
|
||||
}
|
||||
ComponentData &cd = s_components[entityId][compName];
|
||||
auto it = cd.find(fieldName);
|
||||
if (it != cd.end()) {
|
||||
pushFieldValueToLua(L, it->second);
|
||||
} else {
|
||||
lua_pushnil(L);
|
||||
}
|
||||
return 1;
|
||||
});
|
||||
lua_setfield(L, -2, "get_field");
|
||||
|
||||
// set_field
|
||||
lua_pushcfunction(L, [](lua_State *L) -> int {
|
||||
int entityId = lua_tointeger(L, 1);
|
||||
const char *compName = lua_tostring(L, 2);
|
||||
const char *fieldName = lua_tostring(L, 3);
|
||||
if (!compName || !fieldName)
|
||||
return 0;
|
||||
ComponentData &cd = getOrCreateComponent(entityId, compName);
|
||||
cd[fieldName] = readLuaValue(L, 4);
|
||||
return 0;
|
||||
});
|
||||
lua_setfield(L, -2, "set_field");
|
||||
|
||||
lua_setglobal(L, "ecs");
|
||||
}
|
||||
|
||||
} // namespace editScene
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Stub: LuaEventApi
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
namespace editScene
|
||||
{
|
||||
|
||||
// Event subscription storage
|
||||
struct EventSubscription {
|
||||
int id;
|
||||
std::string eventName;
|
||||
int callbackRef; // Lua registry reference
|
||||
};
|
||||
|
||||
static std::vector<EventSubscription> s_subscriptions;
|
||||
static int s_nextSubId = 1;
|
||||
|
||||
void registerLuaEventApi(lua_State *L)
|
||||
{
|
||||
// Get or create the "ecs" global table
|
||||
lua_getglobal(L, "ecs");
|
||||
if (lua_isnil(L, -1)) {
|
||||
lua_pop(L, 1);
|
||||
lua_newtable(L);
|
||||
}
|
||||
|
||||
// subscribe_event
|
||||
lua_pushcfunction(L, [](lua_State *L) -> int {
|
||||
const char *eventName = lua_tostring(L, 1);
|
||||
if (!eventName || !lua_isfunction(L, 2)) {
|
||||
lua_pushnil(L);
|
||||
return 1;
|
||||
}
|
||||
// Store callback in Lua registry
|
||||
lua_pushvalue(L, 2);
|
||||
int ref = luaL_ref(L, LUA_REGISTRYINDEX);
|
||||
|
||||
int subId = s_nextSubId++;
|
||||
s_subscriptions.push_back({ subId, eventName, ref });
|
||||
lua_pushinteger(L, subId);
|
||||
return 1;
|
||||
});
|
||||
lua_setfield(L, -2, "subscribe_event");
|
||||
|
||||
// unsubscribe_event
|
||||
lua_pushcfunction(L, [](lua_State *L) -> int {
|
||||
int subId = lua_tointeger(L, 1);
|
||||
for (auto it = s_subscriptions.begin();
|
||||
it != s_subscriptions.end(); ++it) {
|
||||
if (it->id == subId) {
|
||||
luaL_unref(L, LUA_REGISTRYINDEX,
|
||||
it->callbackRef);
|
||||
s_subscriptions.erase(it);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
lua_setfield(L, -2, "unsubscribe_event");
|
||||
|
||||
// send_event
|
||||
lua_pushcfunction(L, [](lua_State *L) -> int {
|
||||
const char *eventName = lua_tostring(L, 1);
|
||||
if (!eventName)
|
||||
return 0;
|
||||
|
||||
// Call all matching subscriptions
|
||||
for (auto &sub : s_subscriptions) {
|
||||
if (sub.eventName == eventName) {
|
||||
// Push callback
|
||||
lua_rawgeti(L, LUA_REGISTRYINDEX,
|
||||
sub.callbackRef);
|
||||
// Push event name
|
||||
lua_pushstring(L, eventName);
|
||||
// Push params (table or nil)
|
||||
if (lua_istable(L, 2)) {
|
||||
lua_pushvalue(L, 2);
|
||||
} else {
|
||||
lua_pushnil(L);
|
||||
}
|
||||
// Call callback(event, params)
|
||||
if (lua_pcall(L, 2, 0, 0) != LUA_OK) {
|
||||
fprintf(stderr,
|
||||
"Event callback error: %s\n",
|
||||
lua_tostring(L, -1));
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
lua_setfield(L, -2, "send_event");
|
||||
|
||||
lua_setglobal(L, "ecs");
|
||||
}
|
||||
|
||||
} // namespace editScene
|
||||
88
src/features/editScene/tests/ogre_stub.h
Normal file
88
src/features/editScene/tests/ogre_stub.h
Normal file
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* @file ogre_stub.h
|
||||
* @brief Minimal Ogre stub for standalone tests.
|
||||
*
|
||||
* Provides just enough Ogre type aliases to compile the
|
||||
* ActionDatabase, GoapBlackboard, GoapGoal, GoapAction,
|
||||
* and BehaviorTree components without the full Ogre SDK.
|
||||
*
|
||||
* Only for use in standalone tests (action_db_lua_test).
|
||||
*/
|
||||
|
||||
#ifndef OGRE_STUB_H
|
||||
#define OGRE_STUB_H
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
|
||||
namespace Ogre
|
||||
{
|
||||
|
||||
// String is just std::string
|
||||
using String = std::string;
|
||||
|
||||
// Minimal Vector3 for GoapBlackboard
|
||||
struct Vector3 {
|
||||
float x, y, z;
|
||||
Vector3()
|
||||
: x(0)
|
||||
, y(0)
|
||||
, z(0)
|
||||
{
|
||||
}
|
||||
Vector3(float x_, float y_, float z_)
|
||||
: x(x_)
|
||||
, y(y_)
|
||||
, z(z_)
|
||||
{
|
||||
}
|
||||
static const Vector3 ZERO;
|
||||
|
||||
// Member operator== needed by std::pair::operator== when comparing
|
||||
// unordered_map<string, Vector3> in GoapBlackboard::operator==
|
||||
bool operator==(const Vector3 &other) const
|
||||
{
|
||||
return x == other.x && y == other.y && z == other.z;
|
||||
}
|
||||
};
|
||||
|
||||
inline const Vector3 Vector3::ZERO(0, 0, 0);
|
||||
|
||||
// Minimal LogManager stub (used by LuaActionApi)
|
||||
class LogManager {
|
||||
public:
|
||||
static LogManager &getSingleton()
|
||||
{
|
||||
static LogManager instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
class Stream {
|
||||
public:
|
||||
template <typename T> Stream &operator<<(const T &)
|
||||
{
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
|
||||
Stream stream()
|
||||
{
|
||||
return Stream();
|
||||
}
|
||||
};
|
||||
|
||||
// OgreAssert macro (used by LuaEntityApi.hpp)
|
||||
#ifndef OgreAssert
|
||||
#define OgreAssert(expr, msg) \
|
||||
do { \
|
||||
if (!(expr)) { \
|
||||
fprintf(stderr, "OgreAssert failed: %s\n", msg); \
|
||||
assert(expr); \
|
||||
} \
|
||||
} while (0)
|
||||
#endif
|
||||
|
||||
} // namespace Ogre
|
||||
|
||||
#endif // OGRE_STUB_H
|
||||
@@ -4,7 +4,7 @@
|
||||
#include <imgui.h>
|
||||
|
||||
bool ActionDatabaseEditor::renderComponent(flecs::entity entity,
|
||||
ActionDatabase &db)
|
||||
ActionDatabaseComponent &db)
|
||||
{
|
||||
bool modified = false;
|
||||
(void)entity;
|
||||
@@ -14,15 +14,18 @@ bool ActionDatabaseEditor::renderComponent(flecs::entity entity,
|
||||
ImGuiTreeNodeFlags_DefaultOpen))
|
||||
modified |= renderBitNames();
|
||||
|
||||
if (ImGui::CollapsingHeader("Actions",
|
||||
ImGuiTreeNodeFlags_DefaultOpen))
|
||||
if (ImGui::CollapsingHeader("Actions", ImGuiTreeNodeFlags_DefaultOpen))
|
||||
renderActions(db);
|
||||
|
||||
if (ImGui::CollapsingHeader("Goals",
|
||||
ImGuiTreeNodeFlags_DefaultOpen))
|
||||
if (ImGui::CollapsingHeader("Goals", ImGuiTreeNodeFlags_DefaultOpen))
|
||||
renderGoals(db);
|
||||
|
||||
ImGui::PopID();
|
||||
|
||||
// Sync to singleton if modified
|
||||
if (modified)
|
||||
db.syncToSingleton();
|
||||
|
||||
return modified;
|
||||
}
|
||||
|
||||
@@ -53,7 +56,7 @@ bool ActionDatabaseEditor::renderBitNames()
|
||||
return changed;
|
||||
}
|
||||
|
||||
void ActionDatabaseEditor::renderActions(ActionDatabase &db)
|
||||
void ActionDatabaseEditor::renderActions(ActionDatabaseComponent &db)
|
||||
{
|
||||
ImGui::Indent();
|
||||
|
||||
@@ -71,6 +74,7 @@ void ActionDatabaseEditor::renderActions(ActionDatabase &db)
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("X")) {
|
||||
db.actions.erase(db.actions.begin() + i);
|
||||
db.syncToSingleton();
|
||||
if (m_selectedAction == (int)i)
|
||||
m_selectedAction = -1;
|
||||
else if (m_selectedAction > (int)i)
|
||||
@@ -86,18 +90,20 @@ void ActionDatabaseEditor::renderActions(ActionDatabase &db)
|
||||
snprintf(newName, sizeof(newName), "Action_%zu",
|
||||
db.actions.size());
|
||||
db.actions.emplace_back(newName);
|
||||
db.syncToSingleton();
|
||||
m_selectedAction = (int)db.actions.size() - 1;
|
||||
}
|
||||
|
||||
if (m_selectedAction >= 0 &&
|
||||
m_selectedAction < (int)db.actions.size()) {
|
||||
renderActionEditor(db.actions[m_selectedAction]);
|
||||
db.syncToSingleton();
|
||||
}
|
||||
|
||||
ImGui::Unindent();
|
||||
}
|
||||
|
||||
void ActionDatabaseEditor::renderGoals(ActionDatabase &db)
|
||||
void ActionDatabaseEditor::renderGoals(ActionDatabaseComponent &db)
|
||||
{
|
||||
ImGui::Indent();
|
||||
|
||||
@@ -115,6 +121,7 @@ void ActionDatabaseEditor::renderGoals(ActionDatabase &db)
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("X")) {
|
||||
db.goals.erase(db.goals.begin() + i);
|
||||
db.syncToSingleton();
|
||||
if (m_selectedGoal == (int)i)
|
||||
m_selectedGoal = -1;
|
||||
else if (m_selectedGoal > (int)i)
|
||||
@@ -127,15 +134,15 @@ void ActionDatabaseEditor::renderGoals(ActionDatabase &db)
|
||||
|
||||
if (ImGui::Button("Add Goal")) {
|
||||
char newName[64];
|
||||
snprintf(newName, sizeof(newName), "Goal_%zu",
|
||||
db.goals.size());
|
||||
snprintf(newName, sizeof(newName), "Goal_%zu", db.goals.size());
|
||||
db.goals.emplace_back(newName);
|
||||
db.syncToSingleton();
|
||||
m_selectedGoal = (int)db.goals.size() - 1;
|
||||
}
|
||||
|
||||
if (m_selectedGoal >= 0 &&
|
||||
m_selectedGoal < (int)db.goals.size()) {
|
||||
if (m_selectedGoal >= 0 && m_selectedGoal < (int)db.goals.size()) {
|
||||
renderGoalEditor(db.goals[m_selectedGoal]);
|
||||
db.syncToSingleton();
|
||||
}
|
||||
|
||||
ImGui::Unindent();
|
||||
@@ -168,11 +175,42 @@ void ActionDatabaseEditor::renderActionEditor(GoapAction &action)
|
||||
}
|
||||
|
||||
if (ImGui::TreeNode("Preconditions")) {
|
||||
GoapBlackboardEditor::render(action.preconditions,
|
||||
"precond");
|
||||
GoapBlackboardEditor::render(action.preconditions, "precond");
|
||||
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();
|
||||
|
||||
@@ -6,21 +6,25 @@
|
||||
#include "../components/ActionDatabase.hpp"
|
||||
|
||||
/**
|
||||
* Editor for ActionDatabase component.
|
||||
* Editor for ActionDatabaseComponent.
|
||||
*
|
||||
* Allows editing the global list of GOAP actions and goals.
|
||||
* Edits the component data and syncs to the ActionDatabase singleton.
|
||||
*/
|
||||
class ActionDatabaseEditor : public ComponentEditor<ActionDatabase> {
|
||||
class ActionDatabaseEditor : public ComponentEditor<ActionDatabaseComponent> {
|
||||
public:
|
||||
const char *getName() const override { return "Action Database"; }
|
||||
const char *getName() const override
|
||||
{
|
||||
return "Action Database";
|
||||
}
|
||||
|
||||
protected:
|
||||
bool renderComponent(flecs::entity entity,
|
||||
ActionDatabase &db) override;
|
||||
ActionDatabaseComponent &db) override;
|
||||
|
||||
private:
|
||||
void renderActions(ActionDatabase &db);
|
||||
void renderGoals(ActionDatabase &db);
|
||||
void renderActions(ActionDatabaseComponent &db);
|
||||
void renderGoals(ActionDatabaseComponent &db);
|
||||
void renderActionEditor(GoapAction &action);
|
||||
void renderGoalEditor(GoapGoal &goal);
|
||||
bool renderBitNames();
|
||||
|
||||
236
src/features/editScene/ui/ActionDatabaseSingletonEditor.cpp
Normal file
236
src/features/editScene/ui/ActionDatabaseSingletonEditor.cpp
Normal file
@@ -0,0 +1,236 @@
|
||||
#include "ActionDatabaseSingletonEditor.hpp"
|
||||
#include "GoapBlackboardEditor.hpp"
|
||||
#include "InlineBehaviorTreeEditor.hpp"
|
||||
#include <imgui.h>
|
||||
|
||||
void ActionDatabaseSingletonEditor::render(bool *open)
|
||||
{
|
||||
if (!ImGui::Begin("Action Database (Singleton)", open)) {
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
if (ImGui::CollapsingHeader("Bit Names",
|
||||
ImGuiTreeNodeFlags_DefaultOpen))
|
||||
renderBitNames();
|
||||
|
||||
if (ImGui::CollapsingHeader("Actions", ImGuiTreeNodeFlags_DefaultOpen))
|
||||
renderActions();
|
||||
|
||||
if (ImGui::CollapsingHeader("Goals", ImGuiTreeNodeFlags_DefaultOpen))
|
||||
renderGoals();
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
void ActionDatabaseSingletonEditor::renderBitNames()
|
||||
{
|
||||
bool changed = false;
|
||||
ImGui::Indent();
|
||||
ImGui::Text("Name bits for use in preconditions/effects:");
|
||||
ImGui::TextDisabled("(empty slots show as numbers in blackboards)");
|
||||
|
||||
for (int i = 0; i < 64; i++) {
|
||||
const char *name = GoapBlackboard::getBitName(i);
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf), "%s", name ? name : "");
|
||||
|
||||
ImGui::PushID(i);
|
||||
char label[16];
|
||||
snprintf(label, sizeof(label), "%2d", i);
|
||||
ImGui::SetNextItemWidth(-FLT_MIN);
|
||||
if (ImGui::InputText(label, buf, sizeof(buf))) {
|
||||
GoapBlackboard::setBitName(i, buf);
|
||||
changed = true;
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
ImGui::Unindent();
|
||||
(void)changed;
|
||||
}
|
||||
|
||||
void ActionDatabaseSingletonEditor::renderActions()
|
||||
{
|
||||
ActionDatabase &db = ActionDatabase::getSingleton();
|
||||
ImGui::Indent();
|
||||
|
||||
for (size_t i = 0; i < db.actions.size(); i++) {
|
||||
ImGui::PushID((int)i);
|
||||
bool isSelected = (m_selectedAction == (int)i);
|
||||
|
||||
char label[256];
|
||||
snprintf(label, sizeof(label), "%s (cost: %d)",
|
||||
db.actions[i].name.c_str(), db.actions[i].cost);
|
||||
|
||||
if (ImGui::Selectable(label, isSelected))
|
||||
m_selectedAction = isSelected ? -1 : (int)i;
|
||||
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("X")) {
|
||||
db.actions.erase(db.actions.begin() + i);
|
||||
if (m_selectedAction == (int)i)
|
||||
m_selectedAction = -1;
|
||||
else if (m_selectedAction > (int)i)
|
||||
m_selectedAction--;
|
||||
ImGui::PopID();
|
||||
break;
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
if (ImGui::Button("Add Action")) {
|
||||
char newName[64];
|
||||
snprintf(newName, sizeof(newName), "Action_%zu",
|
||||
db.actions.size());
|
||||
db.actions.emplace_back(newName);
|
||||
m_selectedAction = (int)db.actions.size() - 1;
|
||||
}
|
||||
|
||||
if (m_selectedAction >= 0 &&
|
||||
m_selectedAction < (int)db.actions.size()) {
|
||||
renderActionEditor(db.actions[m_selectedAction]);
|
||||
}
|
||||
|
||||
ImGui::Unindent();
|
||||
}
|
||||
|
||||
void ActionDatabaseSingletonEditor::renderGoals()
|
||||
{
|
||||
ActionDatabase &db = ActionDatabase::getSingleton();
|
||||
ImGui::Indent();
|
||||
|
||||
for (size_t i = 0; i < db.goals.size(); i++) {
|
||||
ImGui::PushID((int)i + 10000);
|
||||
bool isSelected = (m_selectedGoal == (int)i);
|
||||
|
||||
char label[256];
|
||||
snprintf(label, sizeof(label), "%s (priority: %d)",
|
||||
db.goals[i].name.c_str(), db.goals[i].priority);
|
||||
|
||||
if (ImGui::Selectable(label, isSelected))
|
||||
m_selectedGoal = isSelected ? -1 : (int)i;
|
||||
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("X")) {
|
||||
db.goals.erase(db.goals.begin() + i);
|
||||
if (m_selectedGoal == (int)i)
|
||||
m_selectedGoal = -1;
|
||||
else if (m_selectedGoal > (int)i)
|
||||
m_selectedGoal--;
|
||||
ImGui::PopID();
|
||||
break;
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
if (ImGui::Button("Add Goal")) {
|
||||
char newName[64];
|
||||
snprintf(newName, sizeof(newName), "Goal_%zu", db.goals.size());
|
||||
db.goals.emplace_back(newName);
|
||||
m_selectedGoal = (int)db.goals.size() - 1;
|
||||
}
|
||||
|
||||
if (m_selectedGoal >= 0 && m_selectedGoal < (int)db.goals.size()) {
|
||||
renderGoalEditor(db.goals[m_selectedGoal]);
|
||||
}
|
||||
|
||||
ImGui::Unindent();
|
||||
}
|
||||
|
||||
void ActionDatabaseSingletonEditor::renderActionEditor(GoapAction &action)
|
||||
{
|
||||
ImGui::Separator();
|
||||
ImGui::Text("Action: %s", action.name.c_str());
|
||||
|
||||
char nameBuf[256];
|
||||
snprintf(nameBuf, sizeof(nameBuf), "%s", action.name.c_str());
|
||||
if (ImGui::InputText("Name", nameBuf, sizeof(nameBuf)))
|
||||
action.name = nameBuf;
|
||||
|
||||
if (ImGui::InputInt("Cost", &action.cost))
|
||||
action.cost = action.cost < 0 ? 0 : action.cost;
|
||||
|
||||
char btNameBuf[256];
|
||||
snprintf(btNameBuf, sizeof(btNameBuf), "%s",
|
||||
action.behaviorTreeName.c_str());
|
||||
if (ImGui::InputText("Behavior Tree Name", btNameBuf,
|
||||
sizeof(btNameBuf)))
|
||||
action.behaviorTreeName = btNameBuf;
|
||||
|
||||
if (ImGui::TreeNode("Behavior Tree")) {
|
||||
if (InlineBehaviorTreeEditor::render(action.behaviorTree))
|
||||
/* modified */;
|
||||
ImGui::TreePop();
|
||||
}
|
||||
|
||||
if (ImGui::TreeNode("Preconditions")) {
|
||||
GoapBlackboardEditor::render(action.preconditions, "precond");
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
void ActionDatabaseSingletonEditor::renderGoalEditor(GoapGoal &goal)
|
||||
{
|
||||
ImGui::Separator();
|
||||
ImGui::Text("Goal: %s", goal.name.c_str());
|
||||
|
||||
char nameBuf[256];
|
||||
snprintf(nameBuf, sizeof(nameBuf), "%s", goal.name.c_str());
|
||||
if (ImGui::InputText("Name", nameBuf, sizeof(nameBuf)))
|
||||
goal.name = nameBuf;
|
||||
|
||||
if (ImGui::InputInt("Priority", &goal.priority))
|
||||
goal.priority = goal.priority < 0 ? 0 : goal.priority;
|
||||
|
||||
char condBuf[512];
|
||||
snprintf(condBuf, sizeof(condBuf), "%s", goal.condition.c_str());
|
||||
if (ImGui::InputText("Condition", condBuf, sizeof(condBuf)))
|
||||
goal.condition = condBuf;
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip(
|
||||
"Optional expression: health > 20 && hunger < 50");
|
||||
}
|
||||
|
||||
if (ImGui::TreeNode("Target Blackboard")) {
|
||||
GoapBlackboardEditor::render(goal.target, "target");
|
||||
ImGui::TreePop();
|
||||
}
|
||||
}
|
||||
38
src/features/editScene/ui/ActionDatabaseSingletonEditor.hpp
Normal file
38
src/features/editScene/ui/ActionDatabaseSingletonEditor.hpp
Normal file
@@ -0,0 +1,38 @@
|
||||
#ifndef EDITSCENE_ACTION_DATABASE_SINGLETON_EDITOR_HPP
|
||||
#define EDITSCENE_ACTION_DATABASE_SINGLETON_EDITOR_HPP
|
||||
#pragma once
|
||||
|
||||
#include "../components/ActionDatabase.hpp"
|
||||
|
||||
/**
|
||||
* Editor for the ActionDatabase singleton.
|
||||
*
|
||||
* Unlike ActionDatabaseEditor (which edits the ActionDatabaseComponent on a
|
||||
* scene entity), this editor works directly on the global ActionDatabase
|
||||
* singleton. It is opened from the editor menu and renders in its own
|
||||
* ImGui window.
|
||||
*/
|
||||
class ActionDatabaseSingletonEditor {
|
||||
public:
|
||||
ActionDatabaseSingletonEditor() = default;
|
||||
|
||||
/**
|
||||
* Render the singleton editor window.
|
||||
* Call this inside an ImGui frame.
|
||||
*
|
||||
* @param open Pointer to bool controlling window visibility.
|
||||
*/
|
||||
void render(bool *open);
|
||||
|
||||
private:
|
||||
void renderBitNames();
|
||||
void renderActions();
|
||||
void renderGoals();
|
||||
void renderActionEditor(GoapAction &action);
|
||||
void renderGoalEditor(GoapGoal &goal);
|
||||
|
||||
int m_selectedAction = -1;
|
||||
int m_selectedGoal = -1;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_ACTION_DATABASE_SINGLETON_EDITOR_HPP
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user